Elasticsearch 搜索引擎性能调优实战

2026-05-06 21:47 Elasticsearch 搜索引擎性能调优实战已关闭评论

Elasticsearch 搜索引擎性能调优实战:10 个让查询提速 10 倍的硬核操作

先说结论:90% 的 ES 性能问题,根源不在配置,而在数据建模和查询写法上。别急着调 JVM 堆大小,先看看你的 mapping 和 query 是不是在“裸奔”。

我接手过一个日均 5 亿日志的搜索集群,从 QPS 200 优化到 3000+,全程没改一个硬件。下面这 10 个操作,每个都踩过坑,直接上干货。


1. 拒绝“万能”的 match,用 term 精准打击

踩坑现场:之前同事写搜索,所有字段一律用 match。明明是个精确匹配的订单号,非要分词再匹配,结果慢 3 倍。

正确姿势:精确值字段(ID、状态码、枚举值)用 term 查询,不走分析器。

// 错误:走分词器,多一次分析
GET orders/_search
{
  "query": {
    "match": { "status": "paid" }
  }
}

// 正确:直接查倒排索引
GET orders/_search
{
  "query": {
    "term": { "status": "paid" }
  }
}

注意term 查询要求字段类型是 keyword,不是 text。否则会报错或变慢。


2. 关闭不需要的 _source 存储

_source 是 ES 默认存储的原始文档 JSON。如果你只需要搜索后返回几个字段,关掉它,磁盘 I/O 直接减半。

操作:mapping 中设置 "_source": { "enabled": false }

PUT logs/_mapping
{
  "_source": {
    "enabled": false
  }
}

:关掉后就不能用 updatereindex 了。如果确定只读日志,放心关。


3. 用 filter 代替 must 做范围查询

filter 不计算相关性分数,走缓存。must 要算分,每次重新计算。对于时间范围、状态过滤这种场景,filter 快一个数量级。

// 慢:must 算分
GET orders/_search
{
  "query": {
    "bool": {
      "must": [
        { "range": { "create_time": { "gte": "now-1d" } } }
      ]
    }
  }
}

// 快:filter 缓存
GET orders/_search
{
  "query": {
    "bool": {
      "filter": [
        { "range": { "create_time": { "gte": "now-1d" } } }
      ]
    }
  }
}

实测:1 亿数据量下,filtermust 快 5-8 倍。


4. 索引分片数不是越多越好

常见误区:分片数 = 节点数 × 2。这是 5.x 时代的旧经验。

真相:一个分片是一个 Lucene 实例,每个分片有独立的线程池和内存。分片过多导致上下文切换爆炸。

经验公式:单分片大小控制在 20-40GB。比如 200GB 数据,分片数 = 200 / 30 ≈ 7 个。

# elasticsearch.yml
index.number_of_shards: 5   # 别超过节点数的 2 倍
index.number_of_replicas: 1 # 生产环境至少 1 副本

5. 禁用 normsdoc_values(如果不需要)

norms 用于算分时的归一化因子,doc_values 用于排序和聚合。如果你不需要排序、算分,关掉它们。

PUT my_index
{
  "mappings": {
    "properties": {
      "title": {
        "type": "text",
        "norms": false
      },
      "price": {
        "type": "float",
        "doc_values": false
      }
    }
  }
}

效果:磁盘占用减少 30%,写入速度提升 20%。


6. 用 search_after 代替 from + size 做深度分页

from + size 在深度分页时(比如第 1000 页),ES 要取出所有匹配文档再截取,内存直接炸。

正确做法:用 search_after 配合排序字段,每次只取下一页。

// 第一页
GET orders/_search
{
  "size": 10,
  "sort": [
    { "order_id": "asc" }
  ]
}

// 第二页:传入上一页最后一个文档的 sort 值
GET orders/_search
{
  "size": 10,
  "search_after": [1000042],
  "sort": [
    { "order_id": "asc" }
  ]
}

:排序字段必须唯一,否则可能丢数据。建议用 _id 或自增 ID。


7. 合并小段(Segment)减少查询开销

ES 默认每 1 秒自动 refresh 生成一个段。小段多了,查询时要扫描更多文件。

操作:手动强制合并,或者调整 refresh 间隔。

# 强制合并到 1 个段(只对只读索引做)
POST my_index/_forcemerge?max_num_segments=1

# 调整 refresh 间隔(写入密集型场景)
PUT my_index/_settings
{
  "index.refresh_interval": "30s"
}

注意:强制合并是 I/O 密集型操作,在低峰期执行。


8. 用 keyword 代替 text 做精确匹配

text 类型会分词,产生大量 token。如果你不需要全文搜索,直接用 keyword

// 错误:text 类型,搜索 "order-123" 会被拆成 "order" 和 "123"
PUT orders/_mapping
{
  "properties": {
    "order_no": { "type": "text" }
  }
}

// 正确:keyword 类型,存原值
PUT orders/_mapping
{
  "properties": {
    "order_no": { "type": "keyword" }
  }
}

实测:同样 1000 万条数据,keywordterm 查询比 textmatch 快 10 倍。


9. 开启 index_optionsdocs 减少索引开销

index_options 控制倒排索引中存储的信息。默认 positions 存了词频和位置信息。如果你不需要短语查询(match_phrase),设为 docs 即可。

PUT my_index
{
  "mappings": {
    "properties": {
      "content": {
        "type": "text",
        "index_options": "docs"
      }
    }
  }
}

效果:索引大小减少 40%,写入速度提升 30%。


10. 用 profile API 精准定位慢查询

别靠猜。ES 自带的 profile API 能告诉你每个查询阶段耗时多少。

GET orders/_search
{
  "profile": true,
  "query": {
    "match": { "title": "手机" }
  }
}

输出解读
- query 阶段:看 advancenext_doc 时间,如果过高说明段太多或查询太宽。
- fetch 阶段:看 _source 加载时间,如果过高考虑关 _source


延伸思考

以上 10 个操作,本质都在做一件事:减少数据量和计算量。但 ES 调优是个无底洞,还有几个方向值得你深挖:

  1. 分片路由:用 routing 把相关数据路由到同一个分片,避免跨分片查询。
  2. 冷热分离:热节点用 SSD,冷节点用 HDD,按时间自动迁移。
  3. 异步搜索:ES 7.16+ 支持 async_search,大查询不阻塞主线程。

最后送一句话:别让性能问题成为你的借口,先动手,再动嘴。 拿你的慢查询日志,对着这 10 条一条条改,你会发现 ES 其实很快。

你可能感兴趣的文章

来源:每日教程每日一例,深入学习实用技术教程,关注公众号TeachCourse
转载请注明出处: https://teachcourse.cn/4103.html ,谢谢支持!

资源分享

一寸照片处理工具操作手册 一寸照片处理工具操作手册
Windows 10设置默认操作系统常见问题总结 Windows 10设置默认操作系统常见
Android开发中比较难理解的排序算法:快速排序 Android开发中比较难理解的排序
夏天适合喝冰凉的水、饮料吗?为什么喝冰凉的水反而不解渴 夏天适合喝冰凉的水、饮料吗?为

评论已关闭!