"搜索怎么又慢了?"
这是每一个后端工程师在大促期间最不想听到的来自产品经理的"灵魂拷问"。
Elasticsearch(ES)虽然是搜索界的扛把子,但它也是出了名的"资源吞噬者"。如果不懂底层原理,随意建索引、乱写 DSL,你的 ES 集群很快就会变成一个内存黑洞 和CPU 绞肉机。
上一篇我们讲了《搜索原理与 DSL 查询实战》,解决了"怎么查"的问题。今天,我们要解决更致命的问题:怎么查得快?
结合我在生产环境踩过的无数坑,总结出这 5 个立竿见影的实战技巧,照着做,至少能让你的搜索性能提升一个数量级。
技巧一:彻底告别 from + size,拥抱 search_after
场景 :你在做一个无限滚动的商品列表,用户滑到了第 100 页(from=1000, size=10)。
后果:ES 性能断崖式下跌,甚至直接 OOM。
原理 :
传统的 from + size 是"分散-收集"模式。协调节点需要从每个分片获取 from + size 条数据,然后在内存中进行全局排序,最后截取出你要的那 size 条。
比如 from=10000,协调节点需要从每个分片拉取 10010 条数据,网络开销巨大,且排序是 CPU 密集型操作。
实战方案 :
使用 search_after 代替分页。它利用上一页的最后一条数据的排序值作为"游标",告诉 ES:"从这个位置之后开始找"。
代码对比:
❌ 慢查询(深度分页):
json
GET /products/_search
{
"from": 10000,
"size": 10,
"query": { "match_all": {} },
"sort": [
{ "create_time": "desc" },
{ "_id": "asc" }
]
}
✅ 快查询(游标分页):
json
GET /products/_search
{
"size": 10,
"query": { "match_all": {} },
"search_after": [ "2023-10-27T10:00:00Z", "doc_id_12345" ],
"sort": [
{ "create_time": "desc" },
{ "_id": "asc" }
]
}
注意:search_after 的参数必须与 sort 顺序一致,且必须包含一个唯一值(如 _id)防止漏数据。
效果:查询耗时从秒级降低到毫秒级,且不随页码加深而变慢。
技巧二:Query 是杀手,Filter 是救星
场景 :查询"标题包含手机"且"状态为在售"的商品。
错误写法 :把所有条件都放在 must 里。
原理:
- Query Context(如
match,bool must) :需要计算相关性评分(_score),涉及 TF-IDF 算法,消耗 CPU。 - Filter Context(如
term,bool filter):只回答"是/否",不计算分数。结果会被缓存(Bitset),后续同样的过滤直接命中缓存,速度极快。
实战方案 :
黄金法则:能用 Filter 的,绝不用 Query!
❌ 低效写法:
json
"bool": {
"must": [
{ "match": { "title": "手机" } },
{ "term": { "status": "on_sale" } }
]
}
✅ 高效写法:
json
"bool": {
"must": [
{ "match": { "title": "手机" } }
],
"filter": [
{ "term": { "status": "on_sale" } }
]
}
先用 filter 砍掉 90% 的无关数据,再对剩下的 10% 做全文匹配评分,性能提升不是一点点。
技巧三:字段映射(Mapping)瘦身计划
场景 :索引越来越大,查询越来越慢,磁盘告急。
原因 :ES 默认很"大方"。一个 text 字段,它会自动生成多个子字段(keyword, text, _source 等),还会存储 norms(用于评分)和 _all。
实战方案:
- 禁用不需要的特性 :如果不需要对某字段做全文搜索评分(如 ID、状态码、标签),直接设为
keyword或match_only_text。 - 关闭
_source:对于日志类场景,如果原始 JSON 不需要存储(只需搜索),在 Mapping 中关闭_source,可节省 30%-40% 存储空间。 - 使用
match_only_text(ES 7.10+):如果你只需要分词搜索,不需要词频统计(评分),用这个类型代替text,性能更好,体积更小。
示例:
json
"mappings": {
"properties": {
"log_message": {
"type": "match_only_text"
},
"host_name": {
"type": "keyword"
}
}
}
技巧四:索引生命周期管理(ILM)------ 热温冷架构
场景:半年前的订单数据还在占用宝贵的 SSD 资源,导致新数据写入变慢。
实战方案 :
实施 ILM(Index Lifecycle Management) 策略,让数据"流动"起来:
- Hot(热)阶段:最近 7 天的数据。使用 SSD 硬盘,副本数设为 1,刷新间隔短(如 1s),保证写入和查询最快。
- Warm(温)阶段:7-30 天的数据。使用普通 HDD,副本数降为 0(或只读),合并 Segment,减少资源占用。
- Cold(冷)阶段 :30 天以上的数据。甚至可以冻结索引(Freeze Index),将其从内存中卸载,查询时再加载。查询虽然慢一点,但几乎不耗资源。
- Delete:超过保留期直接删除。
效果:用最低的成本支撑海量数据,不再为"历史数据拖慢实时业务"而头疼。
技巧五:拒绝"小索引"迷信,合理设置分片
误区 :"分片越多,并行度越高,查询越快?" ------ 大错特错!
原理 :
每个分片都是一个独立的 Lucene 实例,占用固定的内存和文件句柄。分片过多会导致:
- 集群状态频繁更新:主节点忙于维护分片状态,导致集群不稳定。
- 查询分散开销:一个查询需要 scatter-gather 到几十个分片,网络延迟叠加。
实战建议:
- 控制分片大小 :单个分片大小建议在 20GB - 50GB 之间。太小浪费资源,太大难迁移。
- 估算公式 :
总分片数 = (预估总数据量 * 1.2) / 50GB。 - 副本策略:热数据保留 1 个副本(高可用),温/冷数据设为 0 副本(省空间)。
一句话总结 :与其增加分片数量,不如增加单分片的硬件配置(CPU/IOPS)。
结语:优化是一场修行
Elasticsearch 的性能优化没有银弹,它是对数据结构、硬件资源、业务场景的综合权衡。
- 用
search_after解决深度分页; - 用
filter榨干缓存红利; - 用 Mapping 瘦身减少 IO;
- 用 ILM 分层存储降本增效;
- 用合理的分片策略避免资源碎片化。
把这 5 点做到位,你的 ES 集群至少能从"卡顿"变成"丝滑"。