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()。