ElasticSearch 为什么快?

#中间件#面试八股

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 不算分可缓存,是性能优化的第一优先级。
  • 分片并行提升吞吐,但分片数量要克制。
笔记加载中…