ElasticSearch 为什么快?
结论先行:ES 快在“用空间换时间 + 用不可变换缓存”。倒排索引让检索从“遍历文档”变成“查词表拿文档号”,FST 把词典压进内存,跳表与压缩编码让求交飞快;段(segment)不可变,因此可以整段放进操作系统文件缓存,还能生成 filter 缓存位图。再加上分片并行,单次查询被拆到多台机器同时算。
一、加速手段与落点
| 机制 | 加速哪一步 | 原理 |
|---|---|---|
| 倒排索引 | 找到候选文档 | term → posting list,避免全表扫描 |
| FST 词典 | 定位 term | 有限状态转换器,前缀共享,常驻内存 |
| 跳表 + FOR 压缩 | posting 求交 | 跳表跳跃对齐,差值编码省空间 |
| DocValues | 排序与聚合 | 列式存储,按文档号顺序取值 |
| 段不可变 | 缓存 | 段不变即可整段缓存,无需失效判断 |
| filter 缓存 | 重复过滤 | 结果缓存为 bitset,后续直接位运算 |
| 分片并行 | 大结果集 | query 分发到各分片,协调节点归并 |
| 近实时 refresh | 写入可见 | 1 秒生成新段,写入不阻塞检索 |
二、一次检索的路径
GET /article/_search {"query":{"match":{"title":"分布式锁"}}}
① 协调节点收到请求,按路由把 query 发到每个分片(主或副本)
② 分片内:分词 → 查 FST 定位 term → 取 posting list → 求交/打分
③ 每个分片返回 top-N(from+size)文档 ID 与分数
④ 协调节点归并排序,取全局 top-N
⑤ 回查 _source,组装响应
关键点:只有第 ③ 步返回的是“候选集”,第 ⑤ 步才取原始文档,所以深度分页会成为问题。
三、filter 为什么比 query 快
{
"query": {
"bool": {
"filter": [
{ "term": { "status": "published" } },
{ "range": { "publish_time": { "gte": "2026-01-01" } } }
],
"must": [
{ "match": { "title": "分布式锁" } }
]
}
}
}
| 子句 | 是否算分 | 是否可缓存 | 用途 |
|---|---|---|---|
filter | 否 | 是(bitset) | 状态、时间范围、标签 |
must | 是 | 否 | 需要相关性的全文检索 |
should | 是 | 否 | 提升特定词的权重 |
filter 不算分,只需判断“在与不在”,因此能缓存成位图;must 要跑 BM25,无法复用。
四、写入侧为什么也快
写请求 → 内存 buffer + translog
│ refresh(默认 1s)
▼
新 segment(不可变,先落文件缓存)
│ merge(后台,按策略合并小段)
▼
更大 segment(物理删除已标记删除的文档)
写入是“追加 + 后台合并”,没有 B+ 树的页分裂与随机写;代价是更新等于“标记删除 + 新增”,频繁更新会产生大量待合并段。
五、验证手段
GET /article/_search
{
"profile": true,
"query": { "match": { "title": "分布式锁" } }
}
profile 输出每个阶段的耗时,能看出时间花在词典查找、posting 求交还是打分上。
常见追问
- 追问:为什么段不可变很重要?不可变意味着读的时候不需要加锁、缓存不需要失效判断、可以安全地用 mmap;代价是删除与更新要延迟到 merge 才真正回收空间。
- 追问:FST 全量放内存吗?term 前缀索引(term index)常驻内存,完整的 term dictionary 与 posting list 在磁盘上按需读,靠文件缓存加速。
- 追问:分片越多越快吗?不是。分片过多会增加协调与归并开销,单分片建议 10~50GB,总量控制在合理范围。
小结
- 倒排索引解决“找得到”,FST 解决“找得快”,DocValues 解决“排得动”。
- 段不可变换来无锁读与整段缓存,代价是 merge 开销。
filter不算分可缓存,是性能优化的第一优先级。- 分片并行提升吞吐,但分片数量要克制。