面试知识库
困难

ES写入链路与搜索质量#

一句话答案#

ES 写入链路核心是 refresh(内存→segment可搜索,默认1s)和 flush(translog→磁盘持久化),segment merge 是后台合并小段提升查询性能;搜索质量用 Precision/Recall/NDCG 衡量,优化手段是调分词器、调 BM25 参数、加 function_score 业务权重。

核心要点

一、写入链路详解

文档写入 → 写 translog(WAL,防丢失)
         → 写 Index Buffer(内存)

              ├─ refresh(默认1s)→ 生成新 segment(不可变)→ 可被搜索

              └─ flush(默认30min或translog达512MB)→ segment 持久化到磁盘
                                                      → 清空 translog
plaintext
阶段触发条件效果性能影响
写 translog每次写入保证数据不丢磁盘追加写,快
refresh默认1s / 手动数据可搜索生成新 segment,消耗内存
flush30min / translog 达 512MBsegment 持久化fsync 到磁盘,可能阻塞
merge后台自动合并小 segment 为大 segmentIO 密集,需限速

近实时搜索的本质: 1 秒 refresh 间隔 = 写入后最多 1 秒可搜索到。需要实时可见用 ?refresh=true(性能差)。

refresh ≠ 持久化(高频深挖):

refresh 做的是:Index Buffer → 生成新 segment → 写入 OS page cache(文件系统缓存)
            ↑ 这个 segment 此刻只在内存/page cache,可被搜索,但【没有 fsync 到磁盘】
            ↑ 此时机器断电,这个 segment 会丢——但数据不丢,因为 translog 还在

flush 做的是:触发 Lucene commit → 对 segment 执行 fsync 落盘 → 写 commit point → 清空 translog
            ↑ 这一步才是真正的持久化
plaintext
操作产物在哪可搜索已持久化
refreshin-memory segmentpage cache(未 fsync)
flushLucene commit磁盘(已 fsync)
  • refresh 让数据可搜,flush 才让数据持久——两者解耦,所以 ES 能做到 1 秒可搜索却不必每次都 fsync(fsync 慢)。
  • crash 恢复靠 translog replay:refresh 后、flush 前的 segment 虽未落盘,但每条写操作都已先追加进 translog(且 translog 默认每次请求 fsync,durability=request)。节点重启时,ES 从最后一次 commit point 开始重放 translog,把 commit 之后、还没 flush 的操作补回来,保证已确认的写不丢。
  • 所以记忆链路是:translog 保安全(落盘) → refresh 可搜索(仅 page cache) → flush 才持久化(fsync+清 translog)

二、Segment Merge 机制

写入不断生成小 segment:
  seg_1(10doc) + seg_2(10doc) + seg_3(10doc) ...
       ↓ merge
  seg_A(30doc)
       ↓ 继续 merge
  seg_B(100doc)

merge 过程:
1. 选择大小相近的 segments
2. 合并到新 segment(合并倒排索引+正排+删除标记的文档物理删除)
3. 删除旧 segments
4. 过程中不影响查询(搜索看旧+新,commit后切换)
plaintext

merge 相关参数:

参数默认值调优建议
index.merge.scheduler.max_thread_countCPU核数/2(max 1 for spinning disk)SSD 可适当增加
index.merge.policy.max_merged_segment5GB太大合并太慢,太小 segment 太多
indices.store.throttle.max_bytes_per_sec20MB/s(旧版)控制 merge IO 速率

三、Bulk 写入最佳实践

建议原因
批量大小 5-15MB太小网络开销大,太大内存压力
禁用 refresh_interval=-1(批量导入时)避免频繁生成 segment
减少副本数(批量导入时)减少写放大
使用 routing 控制分片减少跨分片查询
合理设置 translog 持久化async 模式提升吞吐

四、搜索质量评估指标

指标公式/含义适用
Precision@K前K个结果中相关文档的占比用户体验(前几个结果好不好)
Recall@K前K个结果中包含了多少相关文档召回完整性
MRR第一个相关文档的排名倒数的均值首条结果质量
NDCG@K加权排名质量(考虑位置折扣)综合排名质量
F1@K2×P×R/(P+R)P和R的平衡

五、搜索质量优化手段

层次手段示例
分词选择合适分词器中文用 ik_max_word(召回)或 ik_smart(精确)
索引多字段索引(keyword+text)name.keyword 精确匹配 + name 全文检索
查询multi_match + best_fields/cross_fields多字段搜索时的评分策略
评分function_score 加业务权重热度、时效性、用户偏好加权
排序BM25 调参 k1/bk1 控制词频饱和度,b 控制长度归一化
后处理rescore 重新打分前 100 粗排 + rescore 精排

BM25 参数调优:

BM25(q,d) = IDF × (tf × (k1+1)) / (tf + k1 × (1-b+b×dl/avgdl))

k1 (默认1.2): 控制词频饱和速度
  k1大 → 高频词权重更高(适合长文档)
  k1小 → 词频影响更快饱和(适合短文本)

b (默认0.75): 控制文档长度归一化
  b=1 → 完全归一化(短文档优势大)
  b=0 → 不归一化(长文档不受惩罚)
plaintext
面试回答(2分钟版)

ES 写入链路分三个阶段:写入时先追加 translog 做 WAL 防丢失,再写入内存的 Index Buffer。refresh 默认每秒一次把 Buffer 生成新的不可变 segment,这时文档才可被搜索,所以 ES 是近实时搜索。flush 把 segment 持久化到磁盘并清空 translog。后台 merge 任务不断合并小 segment 为大 segment,同时物理删除标记删除的文档,merge 是 IO 密集操作需要限速避免影响查询。批量导入时建议关闭 refresh 和减少副本数提升吞吐。搜索质量方面我用 Precision、Recall、MRR 和 NDCG 四个指标评估。优化手段分几层:分词层选合适的分词器比如中文用 ik_max_word 提升召回,查询层用 multi_match 配合 best_fields 做多字段搜索,评分层用 function_score 加入业务权重如热度和时效性,还可以用 rescore 对粗排 top-100 做精排。BM25 的 k1 和 b 参数也可以根据文档特点调优。

追问与易错

追问方向:

  • refresh 和 flush 的区别?(refresh 是内存→可搜索,flush 是磁盘持久化+清 translog)
  • “segment 不可变有什么好处?”→ 无锁并发读、易缓存、便于压缩;缺点是删除不能原地改只能标记
  • “为什么不立即 merge?”→ merge IO 密集影响查询性能,需要限速控制
  • “怎么评估分词器效果?”→ 对比不同分词器的 Recall@K 和 Precision@K

易错点:

  • ❌ “ES 实时搜索”——默认 1 秒 refresh 间隔,是近实时
  • ❌ “删除文档立即释放空间”——只是标记删除,merge 时才物理删除
  • ❌ “segment 越少越好”——太大的 segment 查询也慢,需要平衡
  • ✅ 写入链路口诀:translog 保安全、refresh 可搜索、flush 持久化、merge 优结构