Rust in Everything
基于 tantivy 0.27.0

commit 返回了,却搜不到

主线位置:查询主线第一步,IndexReader 给出 Searcher 快照。快照什么时候换代、OnCommitWithDelay 背后的轮询线程、延迟分布的实测,见 IndexReader 与 Searcher;commit 本身做了什么见提交。

sequenceDiagram
  participant W as writer
  participant D as meta.json
  participant P as 轮询线程
  participant R as reader
  W->>D: commit 原子写
  W-->>W: commit 返回
  R->>R: searcher 仍是旧的
  P->>D: 每 500 ms 算 crc
  P->>R: reload
  R->>R: 新 searcher

实验 crate:exp-tantivy/pita-lab,src/bin/p12_visible.rs。MmapDirectory 临时目录,index.reader()(默认策略)。每次加一篇、commit(),然后每毫秒查一次 searcher().num_docs(),直到看见新文档;两次提交之间随机停 0 到 500 ms。M2 Max、macOS 27、rustc 1.99.0,release。

现象

commit #1 returned; searcher().num_docs() right after = 0
min 51 median 279 max 450
OnCommitWithDelay, ms until visible over 20 commits (random 0-500 ms pause between): [450, 95, 294, 51, 253, 279, 411, 265, 129, 112, 328, 206, 369, 363, 227, 413, 439, 268, 121, 360]
  • commit() 已经返回,meta.json 已经落盘,同一个进程里的 searcher() 仍然是 0 篇。
  • 20 次提交,等了 51 到 450 ms 才看得见(中位附近 279 ms,取的是排序后第 11 个)。
  • 典型的症状是「刚保存的文章搜不到,刷新一下又有了」,以及测试里「写一篇、立刻查」偶尔失败。

原因

Index::reader 用的是默认的 ReloadPolicy::OnCommitWithDelay。IndexReaderBuilder::try_into 在这种策略下向目录注册一个回调,回调里调 reload。MmapDirectory 发现提交的办法是一个轮询线程,每 POLLING_INTERVAL(500 ms)读一遍 meta.json 算 crc32,变了才触发回调。

commit 和 reader 之间没有任何通知:writer 不知道有哪些 reader,commit 返回时只保证数据已经持久化,不保证任何 reader 已经换代。所以从 commit 返回到可见,差的就是「离下一次轮询还有多久」,落在 0 到 500 ms 之间,再加 reload 本身的时间。

修法

需要「写完立刻能查」的地方,提交之后自己 reload。 reader 用 ReloadPolicy::Manual,免得后台线程和你重复 reload:

let reader: IndexReader = index
  .reader_builder()
  .reload_policy(ReloadPolicy::Manual)
  .try_into()?;
// 每次写入
writer.commit()?;
reader.reload()?;
Manual + reload() after commit, (num_docs before reload, after): [(20, 21), (21, 22), (22, 23), (23, 24), (24, 25)]

每一对的第一个数是 commit 之后、reload 之前的 num_docs,第二个是 reload 之后:Manual 下不调 reload 永远看不到新提交,调了立刻看到。IndexReader::reload 是同步的,返回时新的 Searcher 已经可用。

几点注意:

  • 已经拿在手里的 Searcher 是快照,reload 之后要重新 reader.searcher()。
  • 写入和查询在不同进程时,只能靠 OnCommitWithDelay 的轮询(或者你自己通知查询进程去 reload)。
  • 不要求读到自己刚写的内容时,默认策略就够了,不必改。

怎么检查

  • 搜代码里的 index.reader() 和 reader_builder():没有显式 reload_policy 的就是 OnCommitWithDelay。再看「提交之后马上查」的路径上有没有 reload()。
  • 测试里写完立刻断言命中数的,用 Manual 加 reload(),不要靠 sleep。上面实测最长 450 ms,sleep(100) 一类的写法会间歇失败。

小结

  • 默认的 OnCommitWithDelay 靠每 500 ms 轮询 meta.json 发现提交,commit 返回时 reader 不一定已换代;实测 51 到 450 ms 后才可见。
  • commit 和 reader 之间没有通知;同进程里要「写完即查」,就 ReloadPolicy::Manual 加提交后 reader.reload(),然后重新取 searcher()。

💬 关联讨论

发起讨论
加载中...