如何优化 ElasticSearch 搜索性能?
结论先行:优化分四层——索引设计(字段类型、分片数、副本)、查询写法(filter 优先、避免前缀通配、控制返回字段)、写入策略(批量、refresh 间隔、translog)、运维(冷热分离、段合并、慢日志)。多数“ES 慢”不是集群不行,而是映射错了或查询写错了。
一、四层优化清单
| 层次 | 手段 | 收益 |
|---|
| 映射 | 精确值用 keyword,全文用 text,不需要检索的字段 index:false | 减小倒排、加快写入 |
| 映射 | doc_values:false 关闭不需要排序聚合的字段 | 省磁盘与内存 |
| 映射 | 只保留必要字段的 _source,或用 includes | 减少回表 IO |
| 分片 | 单分片 10~50GB,分片数 ≈ 数据量 / 30GB | 避免过多协调开销 |
| 查询 | filter 代替 must,不算分可缓存 | 常见 2~10 倍提升 |
| 查询 | 避免 wildcard 前缀 *abc、regexp、脚本查询 | 避免全词典扫描 |
| 查询 | 只取需要的字段 _source: ["id","title"] | 减少网络与反序列化 |
| 查询 | 用 search_after 替代深分页 | 内存恒定 |
| 写入 | 批量 bulk(每批 5~15MB),调大 refresh_interval | 提升吞吐数倍 |
| 运维 | 冷热分离、force merge 只读索引、慢日志 | 稳定长尾 |
二、索引设计示例
PUT /article
{
"settings": {
"number_of_shards": 3,
"number_of_replicas": 1,
"refresh_interval": "5s",
"translog": { "durability": "async", "sync_interval": "30s" }
},
"mappings": {
"dynamic": "strict",
"properties": {
"title": { "type": "text", "analyzer": "ik_max_word" },
"tags": { "type": "keyword" },
"content": { "type": "text", "index": true, "norms": false },
"view_cnt":{ "type": "integer", "doc_values": true },
"raw_html":{ "type": "keyword", "index": false, "doc_values": false }
}
}
}
refresh_interval 从 1s 调到 5s,写入吞吐通常能提升 2~5 倍;代价是数据可见延迟变长,导出类索引可以调到 30s 甚至 -1。
三、查询写法对比
// 慢:must 参与打分,且前缀通配无法走词典
{ "query": { "bool": {
"must": [
{ "match": { "title": "分布式" } },
{ "wildcard": { "tags": "*缓存*" } }
]
} } }
// 快:过滤条件放 filter,精确匹配用 term
{ "query": { "bool": {
"filter": [
{ "term": { "tags": "cache" } },
{ "range": { "publish_time": { "gte": "now-30d/d" } } }
],
"must": [ { "match": { "title": "分布式" } } ]
}, "_source": ["id", "title", "publish_time"] }
| 反模式 | 问题 | 替代 |
|---|
wildcard: "*x*" | 遍历全部 term | 分词后用 match,或用 ngram |
script 排序 | 每文档执行脚本 | 写入时算好冗余字段 |
terms 传上万个值 | 查询解析开销大 | 拆批或改用 bitmap 方案 |
from+size 翻 1000 页 | 内存随页码增长 | search_after |
聚合无 size 限制 | 返回海量桶 | terms.size 限定 + composite 分页 |
四、定位慢查询
// 1. 慢日志
PUT /article/_settings
{ "index.search.slowlog.threshold.query.warn": "1s",
"index.search.slowlog.threshold.fetch.warn": "500ms" }
// 2. profile 看时间花在哪
GET /article/_search
{ "profile": true, "query": { "match": { "title": "分布式" } } }
// 3. 看分片是否倾斜
GET /_cat/shards/article?v&h=index,shard,prirep,docs,store,node
| 现象 | 可能原因 |
|---|
| 只有个别分片慢 | 分片倾斜、热点文档 |
| fetch 阶段慢 | _source 太大、返回字段太多 |
| query 阶段慢 | 通配符、脚本、range 范围过大 |
| 写入慢且 CPU 高 | 段太多、merge 跟不上 |
五、容量与运维
分片规划:
单分片 10~50GB,日增 100GB 的索引按月建(article-2026.11)
副本数 ≥1 保证高可用,读多写少可加副本分摊查询
冷热分离:
热节点 SSD 存近 7 天,温节点 HDD 存历史,用 ILM 自动迁移
只读索引:
force merge 到 1 个段,减少段数量与内存开销
常见追问
- 追问:分片是不是越多越好?不是。每个分片都是一个 Lucene 实例,有固定开销(文件句柄、堆内存、集群状态),分片过多会让协调与恢复变慢。
- 追问:
refresh_interval 调大影响什么?只影响“写入后多久可被搜索到”,不影响持久化(持久化由 translog 保证)。
- 追问:副本能提升写入性能吗?不能,写入要同步到副本,副本越多写入越慢;副本提升的是读吞吐与可用性。
小结
- 先看映射与查询写法,再看分片与运维,最后才考虑加机器。
filter 代替 must、精确值用 keyword 是性价比最高的两条。
- 写入侧靠批量与
refresh_interval 换吞吐。
- 慢日志 + profile + 分片分布,是定位问题的三件套。