Elasticsearch(ES)核心用法与实战技巧分享
一、ES常用场景介绍
我们多个项目中用到Elasticsearch,今天聚焦ES的核心知识点、高频实操技巧,尤其是深度分页这类易踩坑点,帮助大家快速上手、高效使用ES,后续也能一起交流深化。
在正式讲解实操前,先快速梳理ES的常用业务场景------结合我们日常开发经验,ES并非万能,但在以下4个场景中,能大幅提升效率、解决传统数据库无法解决的痛点,也是我们部门后续可能重点用到的方向:
-
日志检索:项目日志海量存储,快速检索异常日志、过滤时间范围、聚合日志级别,排查问题效率提升10倍以上;
-
业务全文搜索:商品搜索、用户搜索、文章检索等,支持中文分词、模糊匹配,解决MySQL like查询效率低、无法分词的痛点;
-
数据聚合分析:统计各分类数据、日志级别分布、用户行为指标等,快速生成统计结果,无需复杂SQL;
-
海量数据快速查询:千万级、亿级数据检索,响应时间控制在毫秒级,远超传统关系型数据库。
后续的实操、案例讲解,也会围绕这些场景展开,确保大家学完就能落地到实际工作中。
二、ES核心基础回顾
了解完常用场景,我们快速过一遍ES的核心概念------不用太深入底层,重点记"工作中会用到、会接触到"的内容,避免大家被复杂理论绕晕,也为后续实操打下基础。
首先快速过一遍ES的核心概念,不用太深入底层,重点记"工作中会用到、会接触到"的内容,避免大家被复杂理论绕晕。
1. 核心定位
ES是一款分布式、高可用、实时的全文搜索引擎,核心优势是"快"(检索速度)、"灵"(支持多类型检索)、"易扩展"(分布式架构),区别于MySQL等关系型数据库------MySQL适合结构化查询,ES适合全文检索、模糊匹配、海量数据快速检索(比如日志、商品搜索)。
2. 核心概念(对应MySQL,好记不混淆)
用大家熟悉的MySQL类比,快速对应ES的核心组件,不用死记硬背:
-
索引(Index):对应MySQL的"数据库",是一组具有相似结构的文档集合(比如"user_index"存储用户数据,"log_index"存储日志数据);
-
文档(Document):对应MySQL的"行",是ES中最小的数据单元,以JSON格式存储(贴合我们日常开发的JSON数据结构);
-
字段(Field):对应MySQL的"列",是文档中的属性(比如用户文档的name、age、phone字段);
-
映射(Mapping):对应MySQL的"表结构",定义文档中每个字段的类型(text、keyword、int等)、分词器等规则(核心,后续实操会重点讲);
-
分片(Shard):ES分布式存储的核心,将一个索引拆分多个分片,分布在不同节点,实现负载均衡(避免单节点压力过大);
-
副本(Replica):分片的备份,用于故障恢复和提升查询性能(工作中一般至少设置1个副本,保证高可用)。
3. 核心工作流程(极简版)
写入流程:客户端提交数据(JSON文档)→ 路由到对应分片 → 写入主分片 → 同步到副本分片 → 返回写入成功;
检索流程:客户端提交查询请求 → 广播到所有相关分片(主分片/副本分片)→ 各分片返回查询结果 → 协调节点聚合结果 → 返回给客户端。
三、工作高频实操(重点环节)
这部分是今天的核心,聚焦我们日常开发中"必用、常用"的操作,结合实操命令(简化,避免复杂语法),额外补充工作中易踩坑的深度分页知识,大家课后可以直接复制测试,快速落地到项目中。
这部分是今天的核心,聚焦我们日常开发中"必用、常用"的操作,结合实操命令(简化,避免复杂语法),大家课后可以直接复制测试,快速落地到项目中。
1. 索引设计(重中之重,决定后续检索性能)
索引设计是ES使用的基础,设计不合理会导致检索慢、存储冗余、查询结果异常,结合10年项目经验,重点讲3个核心点:
(1)映射(Mapping)设计技巧
核心原则:"字段类型精准匹配,避免过度冗余",重点关注2个高频字段类型:
-
text类型:用于全文检索(比如商品名称、文章内容、日志详情),会被分词器拆分(比如"苹果手机"拆分为"苹果""手机"),支持模糊匹配、关键词检索、高亮显示;避坑:不支持排序和聚合,需排序/聚合需搭配keyword子字段。
-
keyword类型:用于精确匹配(比如用户ID、订单号、状态、分类),不分词,支持精确查询、排序、聚合(比如统计不同订单状态的数量);避坑:不支持全文检索,长文本(超过1024字符)不建议使用,占用存储空间大。
-
数值类型(int/long/double/float):用于存储数值(价格、年龄、数量、ID编号),支持范围查询(gt/lt/gte/lte)、排序、聚合;选型建议:整数用int/long(根据数值范围选择,避免浪费空间),小数用double,不建议用float(精度不足)。
-
date类型:用于存储时间(创建时间、更新时间、日志时间),支持范围查询、排序,需指定格式(默认ISO格式,实操中常用yyyy-MM-dd HH:mm:ss);避坑:插入数据时格式需与映射中一致,否则会插入失败或被识别为字符串。
-
boolean类型:用于存储布尔值(是否有效、是否删除、是否审核通过),取值为true/false,支持精确查询、过滤;优势:存储占用小,查询效率高,无需复杂转换。
-
array类型:用于存储数组(比如用户标签、商品规格、多值属性),支持包含查询(查询包含某个元素的文档);避坑:数组内所有元素类型需一致(比如全是string、全是int),否则会导致查询异常。
-
object类型:用于存储嵌套对象(比如用户的地址信息、商品的详情属性),支持嵌套查询;注意:object类型查询会扁平化处理,复杂嵌套建议用nested类型(需单独配置)。
-
ip类型:用于存储IP地址(客户端IP、服务器IP),支持精确查询、范围查询(比如查询某个IP段的日志);优势:比用keyword存储更节省空间,支持IP段检索,无需手动处理IP格式。
补充说明:字段类型选择核心原则------"按需选型,最小占用",无需追求复杂类型,匹配业务场景即可;比如存储用户手机号,用keyword(无需分词、需精确查询),无需用text或其他类型,避免浪费资源和查询异常。
避坑点:不要把所有字段都设为text类型(比如订单号设为text,会被分词,导致精确查询失败);也不要滥用keyword类型(比如长文本设为keyword,占用大量存储空间)。
实操示例(简化命令,重点看映射规则):
json
// 创建商品索引,定义映射
PUT /product_index
{
"mappings": {
"properties": {
"product_id": {"type": "keyword"}, // 精确匹配,订单号/商品ID
"product_name": {"type": "text", "analyzer": "ik_max_word"}, // 全文检索,中文分词(IK分词器,工作中最常用)
"price": {"type": "double"}, // 数值类型,支持排序、范围查询
"category": {"type": "keyword"}, // 精确匹配,用于聚合(统计各分类商品数量)
"create_time": {"type": "date", "format": "yyyy-MM-dd HH:mm:ss"} // 日期类型,支持范围查询
}
}
}
(2)索引分片与副本设置
核心原则:结合数据量设置,避免分片过多或过少(分片过多,节点压力大;分片过少,无法实现负载均衡)。
实操建议:
-
小数据量(比如日志、小业务数据,小于1000万条):主分片设为3个,副本设为1个(总共6个分片,满足高可用和性能需求);
-
大数据量(大于1亿条):主分片设为5-8个,副本设为1-2个,后续根据数据增长动态扩容。
2. 高频查询操作(工作中80%场景会用到)
ES的查询语法较多,重点讲4个高频场景,结合实操命令,简化复杂语法,大家重点记"场景对应命令",课后可以直接复用。
(1)精确查询(keyword类型专用,比如根据ID、状态查询)
json
// 示例:查询商品ID为1001的商品
GET /product_index/_search
{
"query": {
"term": {
"product_id": "1001" // term查询,精确匹配keyword类型
}
}
}
(2)全文检索(text类型专用,比如搜索商品名称)
json
// 示例:搜索包含"苹果"的商品(IK分词器拆分,匹配"苹果手机""苹果电脑"等)
GET /product_index/_search
{
"query": {
"match": {
"product_name": "苹果" // match查询,全文检索text类型
}
}
}
(3)范围查询(日期、数值类型,比如查询价格区间、时间段数据)
json
// 示例:查询价格在3000-5000之间,且2024年1月1日后创建的商品
GET /product_index/_search
{
"query": {
"bool": {
"must": [
{"range": {"price": {"gte": 3000, "lte": 5000}} // gte>=,lte<=
],
"filter": [
{"range": {"create_time": {"gte": "2024-01-01 00:00:00"}}}
]
}
}
}
(4)聚合查询(统计分析,比如统计各分类商品数量)
json
// 示例:统计每个商品分类的商品数量
GET /product_index/_search
{
"size": 0, // 不返回具体文档,只返回聚合结果
"aggs": {
"category_count": {
"terms": {
"field": "category", // 聚合字段,必须是keyword类型
"size": 10 // 显示前10个分类
}
}
}
}
3. 数据批量操作(提升效率,避免循环单条操作)
工作中经常需要批量插入、批量删除数据,单条操作效率极低,重点讲批量插入(bulk命令):
json
// 批量插入3条商品数据
POST /_bulk
{"index":{"_index":"product_index","_id":"1001"}}
{"product_id":"1001","product_name":"苹果15手机","price":5999,"category":"手机","create_time":"2024-05-01 10:00:00"}
{"index":{"_index":"product_index","_id":"1002"}}
{"product_id":"1002","product_name":"华为Mate 60","price":6999,"category":"手机","create_time":"2024-05-02 10:00:00"}
{"index":{"_index":"product_index","_id":"1003"}}
{"product_id":"1003","product_name":"苹果笔记本电脑","price":9999,"category":"电脑","create_time":"2024-05-03 10:00:00"}
4. 深度分页讲解(新高频避坑点)
工作中涉及大量数据分页(比如分页查询10000条以后的数据),用常规分页方式会出现"查询缓慢、内存溢出、数据丢失"等问题,这就是ES深度分页的痛点,结合实战讲清"问题原因+3种解决方案+适用场景",都是大家项目中会直接用到的。
(1)深度分页痛点解析
常规分页方式(from+size):比如查询第1000页、每页10条数据(from=9990,size=10),ES会在所有分片上查询前9990+10条数据,然后筛选出最后10条返回,随着from增大,查询的数据量呈指数增长,导致节点内存占用过高、查询速度急剧下降,甚至返回超时。
实操反面示例(不推荐用于深度分页):
json
// 常规分页:查询第1000页,每页10条(from=9990,size=10),深度分页时极慢
GET /product_index/_search
{
"from": 9990, // 跳过前9990条数据
"size": 10, // 每页显示10条
"query": {
"match_all": {}
}
}
(2)三种深度分页解决方案(实战首选)
方案1:scroll滚动分页(推荐用于"全量导出数据"场景,比如导出10万条以上数据)
核心原理:创建一个滚动会话(scroll),记录当前查询的位置,后续每次分页都从这个位置继续查询,不需要重复查询前面的数据,大幅提升效率;缺点是不支持"跳页"(比如直接从第1页跳到第1000页),只适合连续分页导出。
实操示例(完整流程,可直接复用):
json
// 1. 创建滚动会话,设置会话有效期为1分钟(scroll=1m),查询第一页数据
GET /product_index/_search?scroll=1m
{
"size": 10, // 每页10条,不写from
"query": {
"match_all": {}
}
}
// 2. 响应结果中会返回scroll_id(滚动会话ID),用该ID查询下一页
// 3. 查询下一页,每次都携带scroll_id和会话有效期
GET /_search/scroll
{
"scroll": "1m", // 续期会话,避免超时
"scroll_id": "上一步返回的scroll_id" // 滚动会话ID
}
// 4. 数据查询完成后,手动删除scroll会话(释放资源,避免内存泄漏)
DELETE /_search/scroll/上一步返回的scroll_id
方案2:search_after分页(推荐用于"业务分页"场景,比如前端分页查询,支持连续分页)
核心原理:基于上一页的最后一条数据的某个"唯一排序字段"(比如id、create_time),作为下一页的查询条件,避免使用from,每次只查询当前页的数据,效率极高;缺点是同样不支持跳页,适合前端"上一页/下一页"连续分页,不适合直接跳转到指定页。
实操示例(基于create_time+product_id排序,保证唯一):
json
// 1. 查询第一页数据,指定排序字段(必须是唯一字段组合,避免数据重复/丢失)
GET /product_index/_search
{
"size": 10,
"query": {
"match_all": {}
},
"sort": [
{"create_time": {"order": "desc"}}, // 先按创建时间降序
{"product_id": {"order": "desc"}} // 再按商品ID降序,保证唯一
]
}
// 2. 假设第一页最后一条数据的sort值为:["2024-05-03 10:00:00", "1003"]
// 3. 查询第二页,用search_after指定上一页最后一条的sort值
GET /product_index/_search
{
"size": 10,
"query": {
"match_all": {}
},
"sort": [
{"create_time": {"order": "desc"}},
{"product_id": {"order": "desc"}}
],
"search_after": ["2024-05-03 10:00:00", "1003"] // 上一页最后一条的sort值
}
方案3:游标分页(基于业务字段,推荐用于"有明确筛选条件的深度分页")
核心原理:利用业务中唯一且有序的字段(比如id、订单号),通过范围查询实现分页,比如上一页最后一条数据的id是1000,下一页就查询id>1000的前10条数据,效率最高,且实现简单;缺点是需要业务字段满足"唯一、有序"。
实操示例(基于product_id分页,最简洁):
json
// 1. 查询第一页,id从小到大,取前10条
GET /product_index/_search
{
"size": 10,
"query": {
"range": {
"product_id": {
"gt": 0 // 大于0的id
}
}
},
"sort": [{"product_id": {"order": "asc"}}]
}
// 2. 假设第一页最后一条id是10,查询第二页(id>10,取前10条)
GET /product_index/_search
{
"size": 10,
"query": {
"range": {
"product_id": {
"gt": 10 // 大于上一页最后一条的id
}
}
},
"sort": [{"product_id": {"order": "asc"}}]
}
(3)三种方案对比(快速选型,避免踩坑)
-
scroll滚动分页:适用场景=全量导出数据(比如导出日志、导出订单数据),不支持跳页,支持海量数据;
-
search_after分页:适用场景=前端业务连续分页(上一页/下一页),不支持跳页,效率最高,推荐优先使用;
-
游标分页:适用场景=有唯一有序业务字段(id、订单号),支持简单跳页(需手动计算id范围),实现最简单。
避坑点:深度分页禁止使用from+size,数据量越大,性能差距越明显;优先选择search_after或游标分页,根据业务场景灵活选型。
四、实战场景落地(结合业务)
结合我们部门可能涉及的场景(日志检索、业务全文搜索),讲2个典型案例,把前面的实操串联起来,大家可以对应到自己的项目中。
场景1:日志检索(最常用,排查问题高效)
痛点:项目日志量大,用传统方式排查问题慢,ES可以快速检索日志内容、过滤时间范围、聚合异常日志。
落地方案:
-
创建日志索引(log_index),映射字段:log_id(keyword)、content(text,存储日志内容)、level(keyword,日志级别:INFO/WARN/ERROR)、create_time(date)、service_name(keyword,服务名称);
-
项目中通过Logback/Log4j将日志输出到ES(配置简单,课后可以分享配置文件);
-
排查问题时,通过日志级别、服务名称、时间范围、关键词检索(比如检索"NullPointerException"),快速定位异常日志。
场景2:业务全文搜索(比如商品搜索、用户搜索)
痛点:MySQL的like查询效率低,不支持分词检索(比如搜索"苹果",无法匹配"苹果手机"),ES可以实现高效全文检索+排序+过滤。
落地方案(结合前面的索引设计):
-
创建业务索引(比如product_index),合理设计映射(text/keyword区分);
-
业务系统中,写入商品数据时同步到ES(通过代码调用ES API,或通过消息队列异步同步,避免影响主业务);
-
前端搜索框输入关键词,后端调用ES的match查询,结合范围、排序条件,返回查询结果(比如根据价格排序、过滤分类)。
五、常见问题与避坑技巧
结合我们项目中踩过的坑,总结6个高频问题,每个问题对应"问题现象+原因+解决方案",大家可以直接避开,提升工作效率。
1. 问题1:检索速度慢
现象:查询ES需要几秒甚至十几秒,影响业务使用;
原因:1. 索引分片过多/过少;2. 字段类型设置错误(比如text类型用于精确查询);3. 查询语句不合理(没有过滤条件,全量检索);4. 没有设置副本,查询压力集中在主分片;
解决方案:优化分片数量、修正字段类型、优化查询语句(增加filter过滤条件)、设置1-2个副本、对高频查询字段建立索引。
2. 问题2:查询结果不准确
现象:搜索关键词,返回的结果不匹配,或漏查数据;
原因:1. 字段类型错误(比如text类型误设为keyword,无法分词检索);2. 分词器选择不当(没有用中文分词器,中文被拆分为单个字);3. 数据没有同步到ES(写入业务库后,未同步到ES);
解决方案:修正字段类型、使用IK中文分词器(工作中最常用,支持中文分词、自定义词典)、确保业务数据与ES数据同步(异步同步+重试机制)。
3. 问题3:ES集群不稳定,节点宕机
现象:ES节点宕机,导致查询/写入失败;
原因:1. 没有设置副本,主分片宕机后无法恢复;2. 节点资源不足(内存、CPU占用过高);3. 集群配置不合理;
解决方案:每个主分片至少设置1个副本、给ES节点分配足够的内存(建议8G以上,ES依赖内存提升性能)、定期监控节点状态(用Kibana监控,课后可以简单演示)。
4. 问题4:数据存储占用过大
现象:ES占用大量磁盘空间,导致磁盘满;
原因:1. 索引没有设置过期策略(比如日志数据,不需要长期存储);2. 字段冗余,存储了不需要检索的大字段(比如图片Base64、大文本);
解决方案:给索引设置过期策略(比如日志索引保留30天,自动删除)、避免存储不需要检索的大字段(大字段存储到文件服务器,ES只存储文件路径)。
5. 问题5:批量操作失败
现象:bulk批量插入/删除数据,部分数据失败;
原因:1. 数据格式错误(bulk命令格式严格,每行必须符合JSON规范);2. 部分数据字段类型与映射不匹配;
解决方案:检查bulk命令格式、批量操作前先校验数据格式和字段类型、批量操作后查看失败日志,针对性修正。
6. 问题6:中文检索分词不准确
现象:搜索中文关键词,无法匹配到相关结果(比如搜索"手机",无法匹配"智能手机");
原因:使用了默认分词器(默认分词器不支持中文,会将中文拆分为单个字);
解决方案:安装IK中文分词器,配置自定义词典(比如添加部门业务相关的专有名词,避免被拆分)。