unsupported variant zstd / unknown compressor id 4
主线位置:磁盘主线的
.store,以及写入主线最后一步写下的meta.json。文档存储的块、压缩器、合并时什么情况下重写块,见文档存储;那一页用手改meta.json演示了两种报错。本页走一条不用手改文件的路:用公开 API 换回 lz4,就会走到 panic。
flowchart TB z["zstd 构建写索引"] --> d1["默认构建 open"] d1 --> e1["unsupported variant"] z --> sw["settings_mut 改 lz4"] sw --> mix["meta lz4 旧段 zstd"] mix --> d2["默认构建 reader"] d2 --> p["panic id 4"] mix --> mg["zstd 构建合并"] mg --> ok["全是 lz4"]
实验 crate:exp-tantivy/pita-zstd(features = ["zstd-compression"])负责写、换设置、合并;exp-tantivy/pita-lab 的 p01_read(默认 feature)负责打开、建 reader、查 old man。M2 Max、macOS 27、rustc 1.99.0,release。
现象
第一步。 pita-zstd 用 IndexSettings { docstore_compression: Compressor::Zstd(..), .. } 建索引,提交两次,得到两个段。默认构建打开(Content 后面是 meta.json 全文,这里截掉了):
created, 2 segments
open_in_dir: Err(DataCorruption(Data corruption (in file `"meta.json"`): Meta file cannot be deserialized. Error("unsupported variant `zstd`, please enable Tantivy's `zstd-compression` feature", line: 3, column: 34). Content: "{\n \"index_settings\": {\n \"docstore_compression\": \"zstd\",\n ...
错误类型是 DataCorruption,但文件没坏,是这个构建不认 "zstd"。
第二步。 很自然的反应是「那就不用 zstd 了」。Index 有一个公开的 settings_mut。pita-zstd switch:打开索引,index.settings_mut().docstore_compression = Compressor::Lz4,开 writer、加一篇、提交。meta.json 里已经是 lz4,默认构建现在能打开了,却在建 reader 时 panic:
switched to lz4, 3 segments
3: "docstore_compression": "lz4",
open_in_dir: Ok
thread 'main' (6507238) panicked at ~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tantivy-0.27.0/src/store/decompressors.rs:40:18:
unknown compressor id 4
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
exit=101
panic 不是 Err,? 接不住,整个进程退出(退出码 101)。
原因
两处检查,一处返回错误,一处 panic。
meta.json 这一处。 Compressor 的 Deserialize 按 feature 决定认不认这个名字,不认就给一个带提示的 serde 错误:
#[cfg(not(feature = "zstd-compression"))]
_ if buf.starts_with("zstd") => {
return Err(serde::de::Error::custom(
"unsupported variant `zstd`, please enable Tantivy's `zstd-compression` \
feature",
))
}
段这一处。 meta.json 里的设置只管「以后新写的块用什么压」。每个 .store 自己的 footer 里记着写它时用的压缩器编号(lz4 是 1,zstd 是 4)。settings_mut 改的是前者,旧段的 footer 不会变;提交时 SegmentUpdater::save_metas 把 index.settings() 原样写进新的 meta.json,于是 meta.json 说 lz4、三个段里两个是 zstd。
index.reader() 建第一个 Searcher 时,要给每个段开一个 StoreReader。backtrace 里的 tantivy 帧依次是 Index::reader → IndexReaderBuilder::try_into → InnerIndexReader::create_searcher → SearcherInner::new → StoreReader::open → DocStoreFooter::extract_footer,最后到 Decompressor::from_id:
pub(crate) fn from_id(id: u8) -> Decompressor {
match id {
0 => Decompressor::None,
#[cfg(feature = "lz4-compression")]
1 => Decompressor::Lz4,
#[cfg(feature = "zstd-compression")]
4 => Decompressor::Zstd,
_ => panic!("unknown compressor id {id:?}"),
}
}
StoreReader::open 本身返回 io::Result,这里本可以返回错误。meta.json 那一处在 0.21 已经改成了带提示的错误(见下面的冷知识),footer 这一处没有跟上,这是上游的问题。0.27.0 之后 main 上没有改 src/store/。
meta.json 能打开PR #2121(7e6c4a185,「Include only built-in compression algorithms as enum variants」,首次发布在 0.21)之前,Compressor 的每个变体都无条件存在,"zstd" 总能反序列化,带 zstd 设置的索引在哪种构建下都打得开。这个 PR 给每个变体挂上 cfg(feature = ...),提交说明写明了代价:「The cost is that indexes using other compression algorithms cannot even be loaded (even though they are not fully accessible in any case).」同一个 PR 的第二条是「Provide more instructive error messages for unsupported, but not unknown compression variants」,也就是今天这句「please enable Tantivy's zstd-compression feature」。它只改了 Compressor 的反序列化;读 footer 用的 Decompressor::from_id 对未知编号仍是 panic!。
修法
最直接:读这个索引的构建也开 zstd-compression。 两种报错都消失,lz4 和 zstd 的段可以混着读。
真要换掉 zstd:用带 zstd 的构建,把设置改成 lz4 之后合并全部旧段。 合并文档存储时,旧段的压缩器和当前设置不同就逐篇解压重写(StorePlugin::merge,条件见文档存储),所以合并完的段是 lz4。接着上面的现场,pita-zstd merge(writer.merge(&所有段 id),再 wait_merging_threads()),然后再用默认构建打开:
settings: Lz4
merged 3 segments into Some(Seg("0d4e1a9b"))
open_in_dir: Ok
searcher: Ok, 1 segment(s), 3 doc(s)
search `old man`: Ok(1)
exit=0
顺序不能反:先合并,确认所有段都换过来,再让不带 zstd 的程序碰这个索引。默认的 LogMergePolicy 不会自己去合并这些段,它只看段大小,不看压缩器。
怎么检查
- 看
meta.json:docstore_compression是zstd或zstd(compression_level=…)时,读它的程序都要开zstd-compression。用cargo tree -e features -i tantivy看每个二进制实际开了哪些 feature。 - 看段,而不是只看
meta.json: 改过docstore_compression的索引,在旧段全部合并掉之前是混合的,meta.json上看不出来。每个.store的压缩器编号在文件末尾的 doc store footer 里,tantivy 没有公开读它的接口;最省事的判断是记住「改设置之后有没有做过一次全量合并」。 - 别指望
?兜底: 这条路径是 panic。换构建(去掉某个 feature、升级依赖)上线之前,用新构建对生产索引的一份拷贝跑一次index.reader()。
小结
zstd-compression不在默认 feature 里。zstd 写的索引,默认构建在open_in_dir就报unsupported variant `zstd`,类型却是DataCorruption。settings_mut换回 lz4 只影响新块;旧段 footer 里的编号 4 不变,默认构建建 reader 时在Decompressor::from_id里 panic,这是上游没把它改成错误。- 要么所有程序都开
zstd-compression,要么用带 zstd 的构建改设置、合并全部旧段,确认之后再交给默认构建。