ES 查询性能调优 Checklist
ES 查询性能调优 Checklist
Elasticsearch 查询性能调优不是玄学,而是一个有章可循的排查过程。本文从 Profile API 出发,带你逐层定位慢查询根因,最后给出一份可执行的 Checklist。
Profile API:查询性能的"火焰图"
Profile API 是排查慢查询的第一利器。只需在查询中加 "profile": true,ES 就会返回每个分片上各查询阶段的耗时分布。
POST /order_index/_search
{
"profile": true,
"query": {
"bool": {
"must": [
{ "match": { "product_name": "手机" } },
{ "range": { "price": { "gte": 1000, "lte": 5000 } } },
{ "script": { "script": "doc['price'].value * 0.9 > params.discount", "params": { "discount": 500 } } }
]
}
}
}
返回结果中重点关注以下几个字段:
| 字段 | 含义 | 排查方向 |
|---|---|---|
query.type |
查询类型 | 检查是否误用了 script、fuzzy 等高开销查询 |
query.time_in_nanos |
该查询耗时(纳秒) | 找到耗时最高的查询子句 |
collector.name |
收集器名称 | SimpleTopDocsCollector 表示正常;出现 MultiCollector 需关注 |
collector.time_in_nanos |
收集阶段耗时 | 该阶段做评分排序,慢则考虑减少结果集大小 |
解读技巧:通常 next_doc(遍历文档)和 score(算分)占大头。如果 next_doc 特别高,说明需要遍历的文档多,走 filter 上下文消除算分会好很多。
真实慢查询排查案例
某电商平台监控告警:/product/_search P99 延迟从 50ms 飙升到 800ms。
排查过程:
第一步:启用 Profile API 定位耗时阶段。
POST /product/_search
{
"profile": true,
"query": { "bool": { "must": [ { "wildcard": { "sku": { "value": "PHONE-2024*" } } }, { "range": { "create_time": { "gte": "2024-01-01" } } } ] } },
"sort": [ { "sales": "desc" } ],
"size": 20
}
第二步:Profile 输出显示 wildcard 子句耗时 650ms,且 next_doc 极高。wildcard 前缀通配会遍历所有 term,无法利用倒排索引。
第三步:检查 sku 的 mapping。
GET /product/_mapping/field/sku
发现 sku 是 text 类型,字段被分词。实际 sku 值类似 PHONE-2024-PRO-MAX-BLACK,分词后 wildcard 匹配效率极低。
第四步:改动最小、效果最好的优化方案——将 wildcard 改为 prefix 查询(prefix 可利用倒排索引的 term dictionary 二分查找,效率远高于 wildcard)。同时将 must 中的 range 改为 filter。
POST /product/_search
{
"query": {
"bool": {
"filter": [
{ "prefix": { "sku.keyword": "PHONE-2024" } },
{ "range": { "create_time": { "gte": "2024-01-01" } } }
]
}
},
"sort": [ { "sales": "desc" } ],
"size": 20
}
优化后 P99 降至 8ms,性能提升 100 倍。
查询性能调优 Checklist
1. filter 替代 must
| 场景 | 用法 | 原因 |
|---|---|---|
| 精确匹配(term) | filter |
无需评分,走缓存 |
| 范围过滤(range) | filter |
同上 |
| 全文搜索(match) | must |
需要相关性评分 |
| 组合条件 | filter + must |
过滤条件放 filter,搜索条件放 must |
核心原则:不需要评分的条件一律放 filter。filter 结果会被 Query Cache 缓存(参见第 22 篇缓存体系),重复查询几乎零开销。
2. 避免 script 查询与排序
script 是性能杀手,因为它需要逐文档执行脚本引擎计算。
// 不推荐:script 算折扣价排序
{ "sort": { "_script": { "type": "number", "script": { "source": "doc['price'].value * 0.9" } } } }
// 推荐:写入时将折扣价算好存为独立字段 discount_price
// 排序直接用
{ "sort": { "discount_price": "desc" } }
写入时计算(空间换时间) 是 ES 性能优化的黄金法则。
3. preference 参数控制路由
preference 参数可以控制查询命中哪些分片副本,减少请求扇出:
GET /index/_search?preference=_local
_local:优先本地分片,减少网络开销_primary:只查主分片(数据未完全同步时的场景)_shards:0,1:指定分片(调试用)- 自定义字符串:相同字符串请求路由到相同分片,可利用节点级缓存
4. 避免 wildcard 前缀通配搜索
// 危险:前缀通配
{ "wildcard": { "name": "手机*" } }
// 优化 1:改用 prefix 查询
{ "prefix": { "name.keyword": "手机" } }
// 优化 2:写入时用 edge_ngram tokenizer 分词,查询时用 match
*keyword 后缀通配稍好(可利用倒排索引),但能不用尽量不用。
5. 查询字段尽量少
// 不推荐:拉全量 _source
GET /index/_search
{ "query": ..., "_source": true }
// 推荐:只返回需要的字段
GET /index/_search
{ "query": ..., "_source": ["id", "name", "price"], "size": 20 }
_source 过滤不仅能减少网络传输,还能减少反序列化开销。对于只需要展示列表的场景,字段数控制在 10 个以内即可。
6. search_as_you_type 替代 match 做自动补全
search_as_you_type 是 ES 专门为"边输入边搜索"场景设计的数据类型,写入时自动生成 2-gram、3-gram 前缀子字段:
PUT /suggest_index
{
"mappings": {
"properties": {
"title": {
"type": "search_as_you_type"
}
}
}
}
POST /suggest_index/_doc
{ "title": "Elasticsearch 性能调优指南" }
GET /suggest_index/_search
{
"query": {
"multi_match": {
"query": "Elastic",
"type": "bool_prefix",
"fields": ["title", "title._2gram", "title._3gram"]
}
}
}
相比用 match + prefix 组合,search_as_you_type 的前缀子字段在索引时预生成,查询时直接走倒排索引 term 匹配,性能更优、召回更准。
总结口诀
filter 代替 must 走缓存,
script 能省则省换 ingest,
wildcard 前缀是大忌,
字段精简、副本本地读,
Profile 一开真相大白。
下篇预告:分片大小的黄金法则与再平衡策略。