Appearance
Elasticsearch 核心原理
前面我们知道 ES 是一个分布式搜索引擎。这篇再往里走一步,把 ES 的核心原理串起来:倒排索引怎么工作?写入为什么准实时?分片为什么能扩展?搜索结果为什么有相关性分数?
ES 的底层是谁?
ES 底层依赖 Lucene。
Lucene 是一个单机全文检索库,负责倒排索引、分词、打分、segment 管理等底层能力。ES 在 Lucene 之上做了分布式封装:
- 用 index 管理一组业务文档。
- 用 shard 把一个 index 拆到多台机器。
- 用 replica 做高可用和读扩展。
- 用 cluster state 管理节点、索引、分片元数据。
- 用 HTTP API 和 Query DSL 对外提供易用的搜索能力。
所以可以简单理解:Lucene 负责“单分片怎么搜”,ES 负责“多个分片怎么组成集群并对外服务”。
正排索引和倒排索引
数据库里常见的是正排思路:通过文档 ID 找文档内容。
text
doc1 -> 我喜欢学习 Elasticsearch
doc2 -> Elasticsearch 适合全文搜索
doc3 -> MySQL 适合事务存储全文检索更需要倒排思路:通过词找到包含它的文档。
text
Elasticsearch -> doc1, doc2
全文搜索 -> doc2
MySQL -> doc3搜索“Elasticsearch”时,只要查词典,就能快速拿到包含该词的文档列表,不需要逐篇扫描正文。
倒排索引通常包含两类信息:
- term dictionary:词典,记录有哪些词。
- posting list:倒排列表,记录某个词出现在哪些文档里,以及词频、位置等信息。
位置数据可以支持 phrase 查询,词频可以用于相关性打分。
分词是倒排索引的入口
写入 text 字段时,ES 会先经过 analyzer:
text
原始文本 -> character filter -> tokenizer -> token filter -> terms例如“ES 写入流程”可能被切成:
text
es, 写入, 流程这些 term 会进入倒排索引。查询 match 时,查询词也会走 analyzer。写入分词和查询分词是否匹配,直接决定能不能搜到。
keyword 字段则不走全文分词,整个值作为一个 term,更适合精确匹配、排序和聚合。
Segment 为什么不可变?
Lucene 把索引数据写成 segment。segment 一旦生成就不可变。
不可变带来的好处是:
- 查询时不用担心文件被并发修改。
- 文件系统缓存更稳定。
- 新旧 segment 可以并存,refresh 后直接打开新 segment。
- 后台 merge 可以慢慢整理旧 segment。
代价是:更新和删除不能原地修改。
删除会写删除标记,查询返回前过滤掉被删除文档。更新则是“标记旧文档删除 + 写入新文档”。后台 segment merge 时,才会真正清理这些被删除的文档。
refresh、flush、merge 的区别
这三个词很容易混:
- refresh:让内存 buffer 里的数据生成新 segment,并打开给搜索使用。默认约 1 秒一次,所以 ES 是准实时。
- flush:把内存和文件系统缓存里的 segment 更完整地提交到磁盘,并清理旧 translog。
- merge:把多个小 segment 合并成大 segment,同时清理删除标记。
可以这样记:
text
refresh 解决“能不能搜到”
flush 解决“故障恢复成本”
merge 解决“segment 太多和删除垃圾”Translog 的作用
写入文档时,ES 不会每次都把 Lucene segment 完整提交到磁盘,因为那样太慢。
它会同时写 translog:
text
写入请求 -> memory buffer + translog如果节点异常重启,ES 可以用已经提交的 segment 加上 translog 重放,恢复最近的写入。
所以 translog 可以理解为 ES 写入链路里的 WAL。它不是用来搜索的,而是用来恢复的。
分片原理
一个 ES index 可以有多个 primary shard。每个 primary shard 本质上都是一个 Lucene 索引。
写入文档时,ES 根据 _routing 决定文档落在哪个主分片:
text
shard = hash(_routing) % primary_shard_count默认 _routing 是 _id。
这也是为什么主分片数创建后不能随便改:如果主分片数变了,路由公式结果就变了,文档应该落到哪个分片也会变。
如果一开始分片数设计不合理,常见做法是新建索引并 reindex。
副本原理
每个主分片可以有多个 replica shard。
副本有两个作用:
- 高可用:主分片所在节点挂了,副本可以提升为新的主分片。
- 读扩展:搜索和按 ID 读取可以落到主分片或副本分片。
副本不是越多越好。副本越多,写入时需要复制的目标越多,磁盘占用也越高。常见配置是 1 个副本,然后根据读流量和可用性要求调整。
协调节点做什么?
请求打到任意节点,这个节点就会作为 coordinating node。
按 ID 读取时,它负责路由到目标分片。
搜索时,它负责:
- 把 query 发到相关分片。
- 收集每个分片的 top results。
- 做全局排序和分页。
- 再到目标分片 fetch 文档内容。
- 组装结果返回客户端。
协调节点压力很容易被低估。深分页、大 size、大聚合都会让协调节点消耗大量内存和 CPU。
打分原理:从 TF-IDF 到 BM25
ES 5 之前默认相似度算法是 TF-IDF,后来默认使用 BM25。
TF-IDF 的核心思想是:
- TF:词在当前文档出现越多,文档越相关。
- IDF:词在全局越稀有,区分度越高。
但 TF-IDF 有个问题:词频特别高时,分数可能被拉得过高。BM25 对词频做了饱和处理,词频增加到一定程度后,分数增长会变慢。
可以直观理解为:
text
某个词出现 1 次到 3 次,相关性明显提升;
出现 30 次到 40 次,相关性不应该继续线性暴涨。BM25 还会考虑文档长度。相同词频下,短文档里的词通常更重要;长文档因为词多,命中某个词不一定代表特别相关。
为什么 filter 更快?
filter context 不计算 _score,只判断是否匹配。
比如状态、租户、时间范围这些条件,本质上是硬过滤,不需要相关性分数,放 filter 更合适:
json
{
"bool": {
"must": [
{ "match": { "title": "Elasticsearch" } }
],
"filter": [
{ "term": { "status": "published" } }
]
}
}filter 更容易被缓存,也不会干扰全文检索的 _score。
集群状态和 Master 节点
ES 集群会选出 master-eligible 节点中的一个作为 master。它负责管理集群元数据,例如:
- 节点加入和离开。
- 索引创建和删除。
- Mapping 更新。
- 分片分配。
- 集群状态发布。
Master 不负责承载所有查询和写入。生产集群里通常会设置专用 master 节点,避免数据节点高负载影响集群管理。
小结
ES 的核心原理可以串成一条线:
- 文档写入时,根据 Mapping 和 Analyzer 生成倒排索引。
- Lucene 用不可变 segment 存索引,refresh 后文档可搜索。
- translog 负责故障恢复,flush 负责提交,merge 负责整理 segment。
- ES 用 shard 把 Lucene 索引分布到多台机器,用 replica 做高可用。
- 搜索请求由协调节点分发到多个分片,再合并排序取回文档。
- 相关性默认基于 BM25,业务可以用 filter、boost、function_score 调整结果。
把这些原理想通,ES 的很多现象就有了解释:为什么刚写入搜不到、为什么更新成本高、为什么深分页慢、为什么 Mapping 和分词器这么重要。