delete_term 什么都没删,或者删多了
主线位置:写入主线的提交阶段,删除队列在段定稿和提交时落到各段,写出新的存活位图。删除队列、opstamp 的规则见提交,tantivy 为什么没有主键见设计取舍。
flowchart TB
dt["delete_term"] --> tq["包成 TermQuery"]
tq --> fld{"字段怎么分词"}
fld -->|TEXT| tok["存的是小写词元"]
fld -->|STRING| raw["存的是原值"]
tok --> a["原文 删 0 篇"]
tok --> b["一个词 删 N 篇"]
raw --> c["按 id 删 1 篇"]
实验 crate:exp-tantivy/pita-lab,src/bin/p06_delete.rs。schema:uid 是 STRING | STORED,title 是 TEXT | STORED。每种删法用一个新索引,先写三篇(a「The Old Man and the Sea, v3」、b「Frankenstein, v3」、c「Of Mice and Men, v2」)并提交,再 delete_term 一次、提交。M2 Max、macOS 27、rustc 1.99.0,release。
现象
delete_term(title = "The Old Man and the Sea, v3") -> opstamp 6; doc_freq before 0; num_docs 3 -> 3
delete_term(title = "v3" ) -> opstamp 6; doc_freq before 2; num_docs 3 -> 1
delete_term(title = "the" ) -> opstamp 6; doc_freq before 1; num_docs 3 -> 2
delete_term(uid = 7 (u64 term on a str field) ) -> opstamp 6; doc_freq before 0; num_docs 3 -> 3
delete_term(uid = "a" ) -> opstamp 6; doc_freq before 1; num_docs 3 -> 2
- 用
title的完整原文删:什么都没删。 - 用
v3删:想删一篇,删掉了两篇。 - 给字符串字段一个
u64的 term:什么都没删。 - 五次调用的返回值都是一个普通的 opstamp,没有一次报错,也不告诉你删了几篇。
- 只有按
STRING字段uid删,结果和预期一致。
原因
delete_term 的全部实现:
pub fn delete_term(&self, term: Term) -> Opstamp {
let query = TermQuery::new(term, IndexRecordOption::Basic);
// For backward compatibility, if Term is invalid for the index, do nothing but return an
// Opstamp
self.delete_query(Box::new(query))
.unwrap_or_else(|_| self.stamper.stamp())
}
它删的是「倒排表里含这个词的所有文档」,和查询里的 TermQuery 是同一回事,只是把搜到的文档标成删除。
TEXT字段写进倒排表的是分词、小写之后的词:the、old、man、and、sea、v3。整句原文"The Old Man and the Sea, v3"从来不是一个词,词典里没有它,命中 0 篇。Term::from_field_text不会帮你分词。v3是一个词,两篇都含它,两篇都删。the只出现在第一篇,删一篇,但那是巧合。- 给字符串字段一个
u64的 term,TermQuery照样建得起来,只是词典里没有这个字节串,0 篇。
真正出错的情况也看不到。src/bin/p06_swallow.rs 对一个只有 STORED、没有索引的字段 note 分别调 delete_query 和 delete_term:
delete_query(u64 term on str field): Ok(0)
delete_query(term on a STORED-only field): Err(SchemaError("Field \"note\" is not indexed."))
delete_term(term on a STORED-only field): 1
delete_query 返回了错误,delete_term 把同一个错误吞掉,换一个新 opstamp 返回。
返回值 opstamp 只表示「这个删除排在第几号」,和删了多少篇无关;真正去找文档要等段定稿、提交时才做(提交)。
delete_term 不返回 Result,是 0.19 留下的兼容0.19 之前,delete_term 直接把 term 放进删除队列:DeleteOperation { opstamp, term },这一步不可能失败,签名就是 -> Opstamp。PR #1535(fa3d786a2,「Add support for deleting all documents matching query」,首次发布在 0.19)把删除队列里的 term 换成一个 Weight,新增 delete_query;建 Weight 可能出错,delete_query 返回 crate::Result<Opstamp>。为了不改 delete_term 的签名,它改成调用 delete_query,出错时「do nothing but return an Opstamp」,就是上面那段注释。delete_query 至今带着 #[doc(hidden)]。
修法
按一个不分词的 id 字段删。 主键字段用 STRING(raw 分词器,整个值一个词)或数值字段,写入和删除都用它:
let uid = sb.add_text_field("uid", STRING | STORED);
// 更新 = 删旧的 + 加新的,同一次提交
writer.delete_term(Term::from_field_text(uid, "a"));
writer.add_document(doc!(uid => "a", title => "The Old Man and the Sea, v4"))?;
writer.commit()?;
上面最后一行就是这种删法:doc_freq 是 1,删掉 1 篇。同一次提交里先删后加,新文档的 opstamp 更大,不受这次删除影响(设计取舍里的 nopk 实验)。
如果 schema 里只有 TEXT 字段可用,没有可靠的按值删除办法:短语查询、布尔查询都只能表达「含有这些词」,不能表达「整个字段等于这个值」。加一个 STRING 的 id 字段,重建索引。
怎么检查
- 看字段: 用来删除的字段,
get_indexing_options().tokenizer()应该是raw(STRING就是),或者它是数值字段。 - 删之前数一下:
delete_term不告诉你删了几篇,自己在提交过的 searcher 上先问一次:
let doc_freq = searcher.doc_freq(&term)?;
上面每一行的 doc_freq before 就是这样来的。按 id 删时它应该恰好是 1(或者 0,表示这个 id 本来就不存在);大于 1 说明 id 不唯一或者字段被分词了。注意 doc_freq 只看已提交的段,还计入已删除但没被合并掉的文档(fieldnorm 与删除位图),当作检查够用,不当作精确计数。
小结
delete_term是「删掉含这个词的所有文档」。TEXT字段里存的是分词后的小写词元:用原文删 0 篇,用其中一个词删掉所有含它的文档。- 返回的 opstamp 不表示删了几篇;字段没有索引这类错误被吞掉,这是 0.19 为兼容保留的行为。
- 按
STRING或数值的 id 字段删;删之前用searcher.doc_freq(&term)核对数量。