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
}
}
坑:关掉后就不能用 update 和 reindex 了。如果确定只读日志,放心关。
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 亿数据量下,filter 比 must 快 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. 禁用 norms 和 doc_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 万条数据,keyword 的 term 查询比 text 的 match 快 10 倍。
9. 开启 index_options 为 docs 减少索引开销
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 阶段:看 advance 和 next_doc 时间,如果过高说明段太多或查询太宽。
- fetch 阶段:看 _source 加载时间,如果过高考虑关 _source。
延伸思考
以上 10 个操作,本质都在做一件事:减少数据量和计算量。但 ES 调优是个无底洞,还有几个方向值得你深挖:
- 分片路由:用
routing把相关数据路由到同一个分片,避免跨分片查询。 - 冷热分离:热节点用 SSD,冷节点用 HDD,按时间自动迁移。
- 异步搜索:ES 7.16+ 支持
async_search,大查询不阻塞主线程。
最后送一句话:别让性能问题成为你的借口,先动手,再动嘴。 拿你的慢查询日志,对着这 10 条一条条改,你会发现 ES 其实很快。

评论已关闭!