Rust in Everything
基于 tantivy 0.27.0

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 上的小读。

小结

  • quickwit feature 把词典从 FST 换成 sstable;依赖图里任何一个 crate 打开它,整个程序都换。
  • 两种格式互不认。meta.json 不记词典类型,打开索引、建 searcher 都成功,第一次查询某个字段才报 Unsupported dictionary type。
  • 用 cargo tree -e features -i tantivy 查谁开了 quickwit;读写同一个索引的程序必须一致,已写的索引只能重建。
  • 启动时对每个段、每个索引字段调一次 inverted_index,把错误提前。

💬 关联讨论

发起讨论
加载中...