Skip to content

ES 写入/查询原理深度解析

面试官:ES 写入文档后,为什么不能立刻被搜索到?

:因为 ES 是近实时(NRT)搜索,写入的文档会先进入内存缓冲区,默认每隔 1 秒进行一次 refresh,将内存数据写入 Segment 并打开供搜索。在 refresh 之前写入的文档,无法被搜索到。

面试官:那如果机器宕机,还没有 flush 到磁盘的数据会丢失吗?


Q1:ES 文档写入的完整流程是什么?必考

Section titled “Q1:ES 文档写入的完整流程是什么?”
客户端写入请求
协调节点(Coordinating Node)
│ 路由算法确定主分片
主分片(Primary Shard)
├── 1. 写入 Translog(Write-Ahead Log)
│ └── 顺序写磁盘(fsync),保证宕机不丢数据
├── 2. 写入 In-Memory Buffer(内存缓冲区)
│ └── 文档暂存内存,尚不可搜索
├── 3. 每隔 1s:refresh
│ └── 内存缓冲区 → Segment(内存中的 Lucene 段)
│ └── Segment 打开后文档可被搜索(近实时!)
├── 4. 并行同步到副本分片
├── 5. 每隔 30min 或 Translog 过大:flush
│ └── Segment fsync 到磁盘 → 生成 .seg 文件
│ └── 清空 Translog
└── 返回写入成功给客户端(主分片 + 至少一个副本写成功)

Q2:refresh、flush、fsync 的区别?高频

Section titled “Q2:refresh、flush、fsync 的区别?”
操作触发时机作用磁盘 IO
refresh默认每 1s内存缓冲区 → 内存 Segment(可搜索)不写磁盘
flush默认每 30min 或 Translog 超 512MBSegment fsync → 磁盘,清空 Translog写磁盘
fsyncflush 时调用强制将 OS 页缓存刷入磁盘写磁盘

关键理解

  • refresh 之后文档可搜索,但 Segment 还在内存,宕机会丢
  • flush 之后数据持久化到磁盘,Translog 清空
  • Translog 是宕机恢复的保障(类似 MySQL 的 redo log)

Q3:Translog 如何保证数据不丢失?高频

Section titled “Q3:Translog 如何保证数据不丢失?”
写入流程中:
每个写操作 → 先同步写入 Translog(append-only 顺序写)
→ 再写内存
宕机恢复:
ES 重启时
└── 读取最近一次 flush 之后的 Translog
└── 重放(replay)其中的操作
└── 恢复内存中还未 flush 的数据

Translog 的持久化级别index.translog.durability):

  • request(默认):每次写操作都 fsync Translog → 不丢数据,性能略低
  • async:后台每 5s 批量 fsync → 可能丢 5s 数据,性能更高

Q4:什么是 Segment 合并(Merge)?为什么需要?

Section titled “Q4:什么是 Segment 合并(Merge)?为什么需要?”

背景:每次 refresh 都会生成一个新的 Segment,时间长了 Segment 数量越来越多:

  • 每次查询需要访问所有 Segment(查询变慢)
  • 小 Segment 利用率低(磁盘空间浪费)

Segment 合并(Merge):后台自动将多个小 Segment 合并为一个大 Segment

合并过程:
1. 选择需要合并的 Segment(按大小策略)
2. 将多个 Segment 的文档合并到新 Segment
3. 删除被标记为删除的文档(物理删除!)
4. 原 Segment 标记删除
5. 新 Segment 打开供搜索

Q5:ES 的相关性评分(BM25)是怎么计算的?高频

Section titled “Q5:ES 的相关性评分(BM25)是怎么计算的?”

ES 7.x+ 默认使用 BM25(Best Match 25) 算法计算文档与查询的相关性分数。

核心思想:相关性 = 词频(TF)+ 逆文档频率(IDF)

BM25 分数 ≈ IDF(t) × TF_normalized(t, d)
IDF(t) = log(1 + (N - n(t) + 0.5) / (n(t) + 0.5))
N = 文档总数
n(t) = 包含词项 t 的文档数
→ 词越罕见,IDF 越高(如"Java" < "量子纠缠")
TF_normalized = TF × (k1 + 1) / (TF + k1 × (1 - b + b × dl/avgdl))
TF = 词项在文档中出现的次数
dl = 文档长度
avgdl = 平均文档长度
b = 0.75(长文档惩罚系数)
k1 = 1.2(TF 饱和系数,防止词频无限增益)

直觉理解

  • 词项越罕见,分数越高(IDF)
  • 词项出现次数越多,分数越高,但有上限(TF 饱和)
  • 文档越短,相同词频的分数越高(dl/avgdl 归一化)

Q6:queryfilter 的区别?必考

Section titled “Q6:query 和 filter 的区别?”
维度queryfilter
是否计算分数✅ 计算相关性分数(慢)❌ 不计算分数(快)
结果缓存❌ 不缓存✅ 缓存到 bitset(极快)
适用场景全文搜索、需要排序精确匹配(状态、时间范围、数字)

最佳实践:全文搜索用 query,过滤条件用 filter

{
"query": {
"bool": {
"must": [
{ "match": { "title": "Java 面试" } } // 全文搜索,计算分数
],
"filter": [
{ "term": { "status": "published" } }, // 精确匹配,走缓存
{ "range": { "date": { "gte": "2024-01-01" } } }
]
}
}
}