ES 查询性能调优 Checklist

22 May 2026 – wusfe · 4 min read

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

发现 skutext 类型,字段被分词。实际 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 一开真相大白。

下篇预告:分片大小的黄金法则与再平衡策略。