Elasticsearch 实战:从倒排索引到搜索调优
电商的商品搜索、日志平台的关键词检索、站内文档搜索,这些场景都有一个共同点:数据量大、查询条件多变、还要支持模糊匹配。直接用数据库的 LIKE 做全表扫描,数据量一上来就扛不住,Elasticsearch 就是为这类场景而生的。本文从倒排索引讲起,讲清楚 Mapping、分词、查询 DSL 与性能调优,帮你第一次就把 ES 用对。
一、倒排索引:ES 搜索快的根本原因
关系型数据库的索引是“正排”的:按主键或字段值找到数据行,适合精确查找。全文搜索恰恰相反:用户给一个词,要快速找出包含这个词的所有文档。倒排索引就是把这个映射关系倒过来:词典里每个词都挂着一个倒排列表(Posting List),记录包含该词的文档 ID。
举个例子,两篇文档写入后,索引内部大致是这样:
- “手机” → 文档 1、文档 3
- “充电” → 文档 1、文档 2
- “快充” → 文档 3
搜索“手机 快充”时,ES 不需要扫描全部文档,只要把“手机”和“快充”两条倒排列表取出来求交集,立刻就能定位到文档 3。倒排列表本身还会按文档 ID 排序并压缩存储,跳表(Skip List)让大列表的定位也很快。理解这一点,就理解了 ES 大多数设计的出发点。
二、分词与 Mapping:写对第一步
中文搜索的第一道坎是分词。ES 自带的 standard 分词器会把中文按单字切开,“无线耳机”会被切成“无 / 线 / 耳 / 机”,搜“无线”能命中,搜“耳机”也能命中,但也可能命中一堆不相关的单字组合。中文场景一般用 IK 分词器,ik_max_word 做最细粒度切分,适合搜索召回;ik_smart 做最粗粒度切分,适合节省索引体积。
Mapping 是索引的“表结构”,字段类型要在建索引时定好,核心区别就两个:
text类型:会被分词,适合全文搜索,比如商品标题、文章正文。keyword类型:不分词、整体作为一个词,适合精确匹配、排序和聚合,比如订单状态、品牌名。
一个常见的坑是把所有字符串字段都用默认的 text,需要按状态过滤、按品牌聚合时才发现查不动。正确做法是建索引时显式声明:需要搜索的字段用 text,需要过滤和聚合的字段用 keyword,两者都要的用多字段(multi-field)同时保留。
PUT /products
{
"mappings": {
"properties": {
"title": {
"type": "text",
"analyzer": "ik_max_word",
"fields": { "keyword": { "type": "keyword" } }
},
"brand": { "type": "keyword" },
"price": { "type": "double" }
}
}
}
三、写入链路:理解 refresh 与批量
文档写入 ES 不是立刻能搜到的。写入先进入内存缓冲区和事务日志(translog),只有经过一次 refresh(默认每 1 秒)后,文档才会进入新生成的段(Segment)并可被搜索。这就是 ES 的“近实时”:不是严格实时,而是有 1 秒左右的延迟。
写入性能主要看两个开关:批量写入用 Bulk API,一次提交几千条而不是一条一条发,吞吐能差出一个数量级;大批量导入时可以临时把 refresh_interval 调大(比如 30 秒)并关闭副本,导入完再恢复,能显著减少段合并的开销。段合并是后台自动做的,小段合并成大段,查询时需要扫描的段越少,搜索就越快。
四、查询 DSL:match、term 与 bool 的分工
ES 的查询分两类:全文查询会算相关性得分(_score),过滤查询只判断是否满足条件、不算分,过滤结果还会被缓存。写查询时先分清你要的是“ relevance 排序”还是“是 / 否筛选”。
match:对查询词先分词再匹配,适合搜索框输入。term:不分词精确匹配,只能用在keyword、数值、日期字段上。bool:把多个条件组合起来,must必须满足且算分,filter必须满足但不算分,should满足其一即可,must_not排除。
GET /products/_search
{
"query": {
"bool": {
"must": [
{ "match": { "title": "无线耳机" } }
],
"filter": [
{ "term": { "brand": "SoundCore" } },
{ "range": { "price": { "gte": 100, "lte": 500 } } }
]
}
},
"aggs": {
"by_brand": { "terms": { "field": "brand" } }
},
"sort": [{ "_score": "desc" }, { "price": "asc" }]
}
这个例子同时演示了聚合:aggs 对品牌做 terms 聚合,一次请求既拿到搜索结果又拿到各品牌的商品数量,列表页的筛选项就是这么来的。
五、性能调优:避开五个常见坑
1. 分片不是越多越好。 每个分片都是一个独立的 Lucene 索引,查询要汇总所有分片的结果。数据量 10GB 用 1 个主分片就够,盲目设 5 个分片只会增加协调开销。一般经验是单个分片控制在 20GB 到 50GB 之间。
2. 深分页不要用 from + size。 翻到第 10000 页时,ES 要在每个分片上取出前 10000 条再汇总排序,越往后越慢。替代方案是 search_after,用上一页最后一条的排序值作为游标往下翻,或者用 Scroll API 做全量导出。
3. 通配符查询不要以前缀开头。 wildcard 查询里 *耳机 这种前导通配符无法走词典,只能逐词扫描,数据量一大就拖垮集群。真有这类需求,考虑用 ngram 分词或改用专门的检索方案。
4. 避免在查询里做大聚合。 对高基数字段(比如用户 ID)做 terms 聚合会产生海量桶,内存和网络开销都大。先确认业务是不是真需要全量聚合,多数场景按 Top N 截断就够了。
5. 监控段数量和堆内存。 段太多会拖慢查询,堆内存超过 32GB 会失去指针压缩的优势。集群规模上来后,索引生命周期管理(ILM)按时间自动滚动新索引、删除旧索引,比人工维护可靠得多。
六、上线前核对清单
- 分词器是否按语言选对,中文是否用了 IK
- 字段类型是否区分 text 与 keyword,需要聚合的字段是否可聚合
- 分片数是否按数据量估算,而不是随手写了个默认值
- 写入是否用 Bulk 批量,导入期 refresh_interval 是否临时调大
- 查询是否把筛选条件放进 filter,深分页是否改用 search_after
- 监控是否覆盖集群健康、堆内存、段数量与慢查询日志
总结
Elasticsearch 的核心就三句话:倒排索引让全文搜索变快,分词与 Mapping 决定搜索准不准,查询 DSL 与分片策略决定跑得快不快。先把索引结构定对,再把查询写对,最后按清单逐项调优,第一次用 ES 就能避开绝大多数生产事故。
- 点赞
- 收藏
- 关注作者
评论(0)