ES 的定位是检索与分析引擎,不是数据库。用它做「搜索、模糊匹配、聚合分析」很爽,用它做「主数据存储 + 事务」很痛苦。
一、倒排索引:为什么它搜得快
二、Mapping 设计
Mapping 一旦写入就不能改字段类型,只能加字段或重建索引。所以设计阶段要慎重。
PUT /article
{
"mappings": {
"properties": {
"title": { "type": "text", "analyzer": "ik_max_word",
"fields": { "keyword": { "type": "keyword", "ignore_above": 256 } } },
"status": { "type": "integer" },
"tags": { "type": "keyword" },
"amount": { "type": "scaled_float", "scaling_factor": 100 },
"content": { "type": "text", "analyzer": "ik_smart" },
"createTime": { "type": "date", "format": "yyyy-MM-dd HH:mm:ss||epoch_millis" },
"location": { "type": "geo_point" }
}
},
"settings": { "number_of_shards": 3, "number_of_replicas": 1 }
}
关键决策:
| 字段 | 类型选择 | 理由 |
|---|---|---|
| 需要分词的文本 | text |
会被分词,用于全文检索 |
| 需要精确匹配/聚合/排序 | keyword |
不分词,整体作为一个 term |
| 两者都要 | text + keyword 子字段(如上 title) |
最常用的写法 |
| 金额 | scaled_float 或 long(存分) |
float 有精度问题 |
| 枚举/状态 | keyword 或 integer |
不要用 text |
| 大文本(只存不搜) | "index": false |
省索引空间 |
中文分词必须装 IK 插件(ik_max_word 细粒度用于索引,ik_smart 粗粒度用于查询)。
三、查询:term vs match
// term:精确匹配,不分词。用于 keyword / 数值 / 状态
{ "query": { "term": { "status": 1 } } }
// match:会先对查询词分词,再查。用于 text 字段
{ "query": { "match": { "title": "小程序开发" } } }
// bool 组合:filter 不打分可缓存,must 参与打分
{ "query": { "bool": {
"must": [ { "match": { "title": "小程序" } } ],
"filter": [ { "term": { "status": 1 } },
{ "range": { "createTime": { "gte": "2026-01-01" } } } ]
} } }
性能铁律:能用 filter 就不用 must。filter 不计算相关性分数,且结果会被缓存,快很多。
四、深分页问题
from + size 的分页在深翻时是灾难:from=10000 时,每个分片都要先取出 10010 条,协调节点汇总再排序,成本和内存都爆炸。ES 默认限制 from + size <= 10000(max_result_window)。
| 方案 | 适用 | 说明 |
|---|---|---|
from + size |
浅分页(前 1000 条) | 简单,深翻会 OOM |
search_after |
深翻、导出 | 用上一页的排序值做游标,推荐 |
scroll |
全量导出(已不推荐) | 占用资源,7.x 后改用 PIT |
| PIT + search_after | 全量导出 | 7.10+ 推荐组合 |
// search_after:排序字段必须唯一(加 _id 兜底)
{ "size": 20, "sort": [{ "createTime": "desc" }, { "_id": "asc" }],
"search_after": [1696000000000, "abc123"] }
五、写入调优
// bulk 批量写(单批 5–15MB 最佳,别一次几万条)
POST _bulk
{ "index": { "_index": "article", "_id": "1" } }
{ "title": "...", "status": 1 }
{ "index": { "_index": "article", "_id": "2" } }
{ "title": "...", "status": 1 }
导入大量数据时的三板斧:
PUT /article/_settings
{ "refresh_interval": "-1", // 关闭自动刷新
"number_of_replicas": 0 } // 先不要副本
// 导完再改回来
PUT /article/_settings
{ "refresh_interval": "1s", "number_of_replicas": 1 }
其它要点:
- 用 bulk,不要一条条 index —— 网络往返是主要开销。
- ID 自己指定且避免随机 UUID(用业务主键,ES 要判断 ID 是否存在)。
- translog 设为 async(
index.translog.durability: async)能提吞吐,但宕机可能丢几秒数据。 - 按时间建索引(
log-2026-09),过期直接删整个索引,比按条删除快得多。
六、集群与分片规划
# 健康状态(green / yellow / red)
GET _cluster/health
# 分片分布
GET _cat/shards?v
# 节点
GET _cat/nodes?v
# 索引大小
GET _cat/indices?v&s=store.size:desc
# 热点线程(排查 CPU 高)
GET _nodes/hot_threads
分片规划原则:
- 单个分片大小控制在 20–50GB(日志类可以到 100GB)。
- 分片数 = 数据总量 / 30GB,不要拍脑袋设 1000 个。
- 分片数创建后不能改(除非 reindex)。
- 分片太多会导致:元数据膨胀、查询要合并更多结果、集群恢复变慢。
七、生产实践
案例:搜索接口偶发超时
背景:某财税 SaaS 公司的知识库检索(AI 诊断助手的召回层)。
现象:搜索接口 P99 从 100ms 涨到 3s,偶发超时,监控显示 ES CPU 不高。
排查:
GET _nodes/hot_threads没发现明显热点。- 用
_profile分析慢查询,发现大量时间花在一个must里的通配符查询上。 - 看代码:有个查询用了
wildcard且以*开头(*关键字*)—— 前置通配符会扫描全部 term,非常慢。
处理:
- 把
*关键字*改成match_phrase+ IK 分词,或者用ngram分词器在建索引时就切好。 - 把不参与打分的条件从
must改成filter。 - 加
timeout参数(如?timeout=2s)避免个别慢查询拖垮线程池。 - 对热点查询加缓存(Redis,TTL 60s)。
案例:分片失控导致集群变黄
现象:集群状态长期 yellow,新索引创建后一直不健康。
排查:GET _cat/shards 发现分片数量已经 3000+,且每个索引都设了 5 分片 2 副本 —— 索引是按天建的日志,一年下来分片爆炸。
处理:
- 日志索引改成单分片(日志不需要并行度),副本 1。
- 老索引通过
_shrink收缩分片,或干脆reindex到新索引后删除。 - 加 ILM(索引生命周期管理):热节点 7 天 → 温节点 30 天 → 删除。
- 设
cluster.max_shards_per_node上限防止再次失控。
八、几条硬规则
- ES 不是主数据源,数据以数据库为准,ES 挂了能重建。
- Mapping 上线前定好,改类型只能 reindex。
- 禁用
from+size深翻,用search_after。 - 避免前置通配符和过多
should。 - 给索引配生命周期(ILM),否则日志类索引会吃满磁盘。
- 写入用 bulk,批量导入先关副本和 refresh。
- ES 的 JVM 堆不超过 31GB( compressed oops 的临界点),剩余内存留给 OS 页缓存。
一起聊聊