Unsupported dictionary type
主线位置:磁盘主线的第三步,从
.term里打开一个字段的词典。两种词典的格式、末尾那 4 字节的类型号见词典,quickwit这个 feature 打开什么见怎么读这一卷。本页讲这个 feature 是怎么在你没写它的情况下被打开的,以及上线前怎么发现。
flowchart TB dep["某个依赖开 quickwit"] --> uni["feature 统一"] uni --> wr["写出 sstable 词典"] wr --> rd["默认构建的程序"] rd --> op["open 成功"] op --> se["searcher 成功"] se --> q["第一次查询"] q --> err["Unsupported dictionary type"]
实验 crate:exp-tantivy/pita-qw(应用自己只写 tantivy = "=0.27.0",另依赖一个本地 crate qwdep,它的 Cargo.toml 里是 tantivy = { version = "=0.27.0", features = ["quickwit"] })和 exp-tantivy/pita-lab(默认 feature)。M2 Max、macOS 27、rustc 1.99.0,release。
现象
pita-qw 建索引、写两篇文档、提交,自己查 old man 正常。另一个程序(默认 feature 的 pita-lab,比如运维脚本、离线任务,或者去掉那个依赖之后的同一个应用)打开同一个目录:
written by pita-qw: 1 hits for `old man`
open_in_dir: Ok
searcher: Ok, 1 segment(s), 2 doc(s)
search `old man`: Err(IoError(Custom { kind: Other, error: "Unsupported dictionary type, compiled tantivy with Fst, but got SSTable" }))
Index::open_in_dir、index.reader()、searcher()都成功,文档数也对。健康检查如果只看「索引能打开」,会是绿的。- 错误出在第一次查询,类型是
IoError,读起来像磁盘坏了。 - 反过来,开了
quickwit的程序读默认构建写的索引,报的是compiled tantivy with SSTable, but got Fst(见词典的实验)。
原因
pita-qw 的 Cargo.toml 里没有 quickwit,但它在依赖图里。cargo tree -e features -i tantivy(节选):
├── tantivy feature "futures-channel"
│ └── tantivy feature "quickwit"
│ └── qwdep v0.1.0 (~/exp-tantivy/pita-qw/qwdep) (*)
...
├── tantivy feature "sstable"
│ └── tantivy feature "quickwit" (*)
cargo 对同一个 crate 只编译一份,所有依赖方要的 feature 取并集。qwdep 要了 quickwit,整个程序里的 tantivy 就都是 quickwit 版。词典类型是编译期定死的,termdict/mod.rs 里两份 CURRENT_TYPE 各挂一个 cfg:写词典时把它追加在末尾,TermDictionary::open 读回来比较:
pub fn open(file: FileSlice) -> io::Result<Self> {
let (main_slice, dict_type) = file.split_from_end(4);
let mut dict_type = dict_type.read_bytes()?;
let dict_type = u32::deserialize(&mut dict_type)?;
// ...
if dict_type != CURRENT_TYPE {
return Err(io::Error::other(format!(
"Unsupported dictionary type, compiled tantivy with {CURRENT_TYPE:?}, but got \
{dict_type:?}",
)));
}
为什么拖到查询才报:meta.json 里没有任何字段记录词典类型,索引格式版本两边都是 7;SegmentReader 打开时不碰词典,SegmentReader::inverted_index 第一次被某个查询要到某个字段时,才去打开这个字段的词典。所以一个只查 title 的程序,body 的词典是什么格式它永远不知道。
这是有意的设计:sstable 词典是给 quickwit 那种「索引放在对象存储上、按块读」的场景准备的,两种格式不互通。但 feature 名字叫 quickwit,看不出它改了磁盘格式,加一个依赖就可能悄悄换掉。0.27.0 之后 main 上没有改 termdict/mod.rs。
修法
让读写同一个索引的所有程序用同一种词典。 找到是谁打开的 quickwit,然后二选一:
- 去掉那个依赖,或者换掉它要求
quickwit的那个 feature; - 读这个索引的每一个程序都开
quickwit。
已经用错的格式写好的索引不能原地转换,.term 文件得重写:用写它的那种构建把文档读出来(字段要 STORED),交给另一种构建重建索引。
怎么检查
构建时看 feature。 在每个会打开索引的二进制所在的包里跑:
$ cargo tree -e features -i tantivy | grep -A2 'feature "quickwit"$'
│ └── tantivy feature "quickwit"
│ └── qwdep v0.1.0 (~/exp-tantivy/pita-qw/qwdep) (*)
├── tantivy feature "futures-util"
第二行就是元凶。pita-lab 里同样的命令 grep -c quickwit 是 0。注意要在二进制的包(或者 workspace 根)里跑:feature 统一按一次构建算,库自己的 cargo tree 看不到下游加了什么。
启动时把词典打开一遍。 把错误从第一个用户查询提前到启动:
fn check(dir: &str) -> tantivy::Result<()> {
let index = Index::open_in_dir(dir)?;
let searcher = index.reader()?.searcher();
let schema = index.schema();
for seg in searcher.segment_readers() {
for (field, entry) in schema.fields() {
if entry.is_indexed() {
seg.inverted_index(field)?;
}
}
}
Ok(())
}
qw 是 pita-qw 写的索引,default 是默认构建写的:
check qw: An IO error occurred: 'Unsupported dictionary type, compiled tantivy with Fst, but got SSTable'
check default: ok
inverted_index 会把打开的结果缓存在 SegmentReader 里,这一遍不是白做的。段多、字段多时它要读每个 .term 的末尾,都是 mmap 上的小读。
小结
quickwitfeature 把词典从 FST 换成 sstable;依赖图里任何一个 crate 打开它,整个程序都换。- 两种格式互不认。
meta.json不记词典类型,打开索引、建 searcher 都成功,第一次查询某个字段才报Unsupported dictionary type。 - 用
cargo tree -e features -i tantivy查谁开了quickwit;读写同一个索引的程序必须一致,已写的索引只能重建。 - 启动时对每个段、每个索引字段调一次
inverted_index,把错误提前。