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 持久化到磁盘
→ 清空 translogplaintext| 阶段 | 触发条件 | 效果 | 性能影响 |
|---|---|---|---|
| 写 translog | 每次写入 | 保证数据不丢 | 磁盘追加写,快 |
| refresh | 默认1s / 手动 | 数据可搜索 | 生成新 segment,消耗内存 |
| flush | 30min / translog 达 512MB | segment 持久化 | fsync 到磁盘,可能阻塞 |
| merge | 后台自动 | 合并小 segment 为大 segment | IO 密集,需限速 |
近实时搜索的本质: 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| 操作 | 产物 | 在哪 | 可搜索 | 已持久化 |
|---|---|---|---|---|
| refresh | in-memory segment | page cache(未 fsync) | 是 | 否 |
| flush | Lucene 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后切换)plaintextmerge 相关参数:
| 参数 | 默认值 | 调优建议 |
|---|---|---|
| index.merge.scheduler.max_thread_count | CPU核数/2(max 1 for spinning disk) | SSD 可适当增加 |
| index.merge.policy.max_merged_segment | 5GB | 太大合并太慢,太小 segment 太多 |
| indices.store.throttle.max_bytes_per_sec | 20MB/s(旧版) | 控制 merge IO 速率 |
三、Bulk 写入最佳实践
| 建议 | 原因 |
|---|---|
| 批量大小 5-15MB | 太小网络开销大,太大内存压力 |
| 禁用 refresh_interval=-1(批量导入时) | 避免频繁生成 segment |
| 减少副本数(批量导入时) | 减少写放大 |
| 使用 routing 控制分片 | 减少跨分片查询 |
| 合理设置 translog 持久化 | async 模式提升吞吐 |
四、搜索质量评估指标
| 指标 | 公式/含义 | 适用 |
|---|---|---|
| Precision@K | 前K个结果中相关文档的占比 | 用户体验(前几个结果好不好) |
| Recall@K | 前K个结果中包含了多少相关文档 | 召回完整性 |
| MRR | 第一个相关文档的排名倒数的均值 | 首条结果质量 |
| NDCG@K | 加权排名质量(考虑位置折扣) | 综合排名质量 |
| F1@K | 2×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/b | k1 控制词频饱和度,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 优结构