中文字段用 default 分词:搜不到词,长句整句消失
主线位置:写入主线里
SegmentWriter把文本交给字段的分析器,查询主线里QueryParser用同一个分析器切查询串。default这条链的每一层、RemoveLongFilter为什么按字节算,见分词。本页讲中文字段直接用TEXT时,从搜索结果上看到的是什么。
flowchart TB
txt["连续汉字"] --> st["SimpleTokenizer"]
st --> one["整段一个词"]
one --> len{"40 字节以上"}
len -->|否| keep["整段进索引"]
len -->|是| drop["丢掉 不报错"]
keep --> q1["搜两字词 0 篇"]
drop --> q2["这句永远搜不到"]
实验 crate:exp-tantivy/pita-lab,src/bin/p04_zh.rs。同一个 schema 里两个字段:title_default 是 TEXT | STORED,title_jieba 用 tantivy-jieba 0.19 的 JiebaTokenizer::new()。M2 Max、macOS 27、rustc 1.99.0,release。
现象
写入两篇,标题是「老人与海」和「他是一个独自在湾流中一条小船上钓鱼的老人」,两个字段内容相同。查三个词:
title_default 老人 -> 0 hit(s)
title_default 老人与海 -> 1 hit(s)
title_default 钓鱼 -> 0 hit(s)
title_jieba 老人 -> 2 hit(s)
title_jieba 老人与海 -> 1 hit(s)
title_jieba 钓鱼 -> 1 hit(s)
default 字段里「老人」一篇也搜不到,「钓鱼」也是。只有和整段原文一字不差的「老人与海」能命中。写入、提交、查询都没有任何错误或警告。
把两个字段的词典列出来,原因一目了然:
title_default terms: ["老人与海"]
title_jieba terms: ["一个", "一条", "上", "中", "他", "在", "小船", "是", "湾流", "独自", "的", "老人", "老人与海", "钓鱼"]
title_default 的词典里只有一个词。第二篇的标题一个词都没留下。
原因
TEXT 的分词器名是 "default",即 SimpleTokenizer → RemoveLongFilter::limit(40) → LowerCaser(TokenizerManager::default)。两层各做了一件对英文合理、对中文致命的事:
SimpleTokenStream::advance按char::is_alphanumeric切:连续的字母数字是一个词,其他字符是分隔符。汉字是字母类,所以两个标点之间的整段中文是一个词。RemoveLongFilter只留len() < 40的词,len()是字节数。一个汉字 3 字节,13 个字以内保留,14 个字以上整段丢掉。
直接看分析器的输出:
default 12 bytes "老人与海" -> ["老人与海"]
default 33 bytes "一个老人独自在海上捕鱼" -> ["一个老人独自在海上捕鱼"]
default 60 bytes "他是一个独自在湾流中一条小船上钓鱼的老人" -> []
default 90 bytes "他是一个独自在湾流中一条小船上钓鱼的老人,至今已去了八十四天" -> ["至今已去了八十四天"]
default 34 bytes "Rust 写的全文检索库 tantivy" -> ["rust", "写的全文检索库", "tantivy"]
- 11 个字的短句是一个词:只有整句原文能搜到它。
- 20 个字的句子 60 字节,被丢掉,输出是空的。逗号把第四句切成两段,前段被丢,后段 9 个字留下。
- 中英混排时,英文按空格切开了,中文仍是整段。
查询侧用的是同一个分析器:「老人」切出来是词 老人,而索引里只有 老人与海,两者不等,所以 0 篇。这不是 bug,default 本来就是为空格分词的语言准备的。本站语料上整体有多少汉字因此消失(约 9%),见分词的统计。
修法
中文字段换一个中文分词器,写入和查询用同一个名字注册(tantivy-jieba 怎么接见分词):
let zh = TextFieldIndexing::default()
.set_tokenizer("jieba")
.set_index_option(IndexRecordOption::WithFreqsAndPositions);
let title_jieba = sb.add_text_field("title_jieba", TextOptions::default().set_indexing_options(zh));
// ...
index.tokenizers().register("jieba", JiebaTokenizer::new());
同一句话交给 jieba:
jieba "他是一个独自在湾流中一条小船上钓鱼的老人" -> ["他", "是", "一个", "独自", "在", "湾流", "中", "一条", "小船", "上", "钓鱼", "的", "老人"]
上面「现象」里 title_jieba 的三行就是换了之后的结果:「老人」命中两篇,「钓鱼」命中一篇。
已经用 default 写好的字段改不了分词器(schema 建好就不能改),只能新建一个用 jieba 的字段或新索引,把文档重新写一遍;原文没有 STORED 时,得从你自己的数据源重灌。
换了 jieba 以后还有两件事要知道:它把空白和标点也当词输出,position 是字符偏移而不是词序号,这些都影响 fieldnorm 和短语查询,见分词和在本站中。
怎么检查
- 看 schema: 每个可能有中文的
Str字段,get_indexing_options().tokenizer()不应该是default、en_stem、whitespace。TEXT这个常量就是default。 - 看分析器的输出:
index.tokenizer_for_field(field)?.token_stream(样例),拿一句 14 个字以上、不带标点的中文试一下,输出为空就是这个坑。 - 看词典: 对已有的索引,
segment_reader.inverted_index(field)?.terms().stream()?列出词,一眼能看出词是不是整句。上面的title_default terms就是这样打出来的。
小结
TEXT的default分析器按is_alphanumeric切词,汉字是字母类,两个标点之间的整段中文是一个词;搜其中的词查不到。RemoveLongFilter(40)按字节算,14 个汉字以上的整段直接丢掉,这段文字在索引里不存在,也不报错。- 中文字段注册 jieba 一类的分词器;已有字段只能重建。检查 schema 里的分词器名,或者把一个长句子喂给分析器看输出。