ES 写入/查询原理深度解析
面试官:ES 写入文档后,为什么不能立刻被搜索到?
你:因为 ES 是近实时(NRT)搜索,写入的文档会先进入内存缓冲区,默认每隔 1 秒进行一次
refresh,将内存数据写入 Segment 并打开供搜索。在refresh之前写入的文档,无法被搜索到。面试官:那如果机器宕机,还没有 flush 到磁盘的数据会丢失吗?
链式追问一:写入流程
Section titled “链式追问一:写入流程”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 超 512MB | Segment fsync → 磁盘,清空 Translog | 写磁盘 |
fsync | flush 时调用 | 强制将 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 打开供搜索链式追问二:查询相关性
Section titled “链式追问二:查询相关性”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:query 和 filter 的区别?必考
Section titled “Q6:query 和 filter 的区别?”| 维度 | query | filter |
|---|---|---|
| 是否计算分数 | ✅ 计算相关性分数(慢) | ❌ 不计算分数(快) |
| 结果缓存 | ❌ 不缓存 | ✅ 缓存到 bitset(极快) |
| 适用场景 | 全文搜索、需要排序 | 精确匹配(状态、时间范围、数字) |
最佳实践:全文搜索用 query,过滤条件用 filter:
{ "query": { "bool": { "must": [ { "match": { "title": "Java 面试" } } // 全文搜索,计算分数 ], "filter": [ { "term": { "status": "published" } }, // 精确匹配,走缓存 { "range": { "date": { "gte": "2024-01-01" } } } ] } }}