grea* 什么都没查到
主线位置:查询主线第二、三步。
QueryParser只在引号后面认*,生成PhrasePrefixQuery;它在每个段的词典上取前缀范围,读满max_expansions个词就停。展开怎么做、分数怎么算见短语与其他查询。
flowchart TB a["grea*"] --> w["word 连星号一起读"] w --> t["分词器剥掉星号"] t --> tq["TermQuery grea 0 篇"] b["grea 加引号加星号"] --> one["只有一个词"] one --> e["报错 至少两个词"] c["old ma 加引号加星号"] --> pp["PhrasePrefixQuery"] pp --> cap["字典序前 50 个"] cap --> miss["没有 man 0 篇"]
实验在 exp-tantivy/pitb-lab,bin p18_prefix(release):wiki.json 1000 篇,body 是 TEXT,一个段。M2 Max、macOS 27、rustc 1.99.0,tantivy 0.27.0。
现象
grea* => 0 docs TermQuery(Term(field=2, type=Str, "grea"))
great => 35 docs TermQuery(Term(field=2, type=Str, "great"))
"grea"* => Err: The phrase '"grea"' does not produce at least two terms using the tokenizer '"default"'
"old ma"* => 0 docs PhrasePrefixQuery { field: Field(2), phrase_terms: [(0, Term(field=2, type=Str, "old"))], prefix: (1, Term(field=2, type=Str, "ma")), max_expansions: 50 }
"old man" => 1 docs PhraseQuery { field: Field(2), phrase_terms: [(0, Term(field=2, type=Str, "old")), (1, Term(field=2, type=Str, "man"))], slop: 0 }
- 按 Lucene 的习惯写
grea*,不报错,0 篇。 - 加上引号是解析器文档里写的前缀语法,单个词却报错。
- 两个词的
"old ma"*不报错,0 篇,可是"old man"明明有 1 篇。
原因
单个词没有前缀语法。 语法层的 word 不把 * 当特殊字符,grea* 整个是一个词;default 分词器按非字母数字切,* 被丢掉,剩下 grea,成了一个普通的 TermQuery。词典里没有 grea 这个词,所以 0 篇,而且没有任何提示。前缀标记只在 slop_or_prefix_val 里解析,它接在引号后面。
引号里只有一个词时拒绝。 generate_literals_for_str 在词数不超过 1 且带前缀标记时返回 PhrasePrefixRequiresAtLeastTwoTerms,也就是上面那句报错。所以通过解析器,任何写法都得不到单词前缀查询。
展开按字典序截断。 "old ma"* 生成的 PhrasePrefixQuery 带着默认的 DEFAULT_MAX_EXPANSIONS = 50。每个段从词典里 ma 开始往后读 50 个以 ma 开头的词,跟词频、跟它是否紧跟在 old 后面都无关。数一下这个段的词典:
terms starting with "grea": 4, "great" is #1
terms starting with "ma": 414, "man" is #151
man 排在第 151,前 50 个里没有它。解析器上没有设置 max_expansions 的方法。
修法
自己建查询,不经过解析器。单个词的前缀直接用 PhrasePrefixQuery,只放一个词;它会退化成词典上的范围查询,同样受 max_expansions 限制。或者用正则:
let one = PhrasePrefixQuery::new(vec![Term::from_field_text(body, "grea")]);
let rq = RegexQuery::from_pattern("grea.*", body)?;
PhrasePrefixQuery[grea] => 51 docs
RegexQuery grea.* => 51 docs
多个词的前缀用 set_max_expansions 放大上限:
PhrasePrefixQuery[old, ma] max_expansions=50 => 0 docs
PhrasePrefixQuery[old, ma] max_expansions=200 => 2 docs
PhrasePrefixQuery[old, ma] max_expansions=1000 => 4 docs
上限每放大一倍,每个段就多读一倍的倒排表。做「边输入边搜」时,更省的办法是前缀不到两三个字符就不发查询;或者干脆在写入时多建一个 n-gram 分词的字段,把前缀匹配变成普通的词项查询。
前缀词要和索引里的词同样处理过:default 分词器会小写,Grea 要先变成 grea 再交给 Term::from_field_text,RegexQuery 也一样(见 Regex queries are not allowed.)。
怎么检查
- 把解析结果打出来。用户输入里有
*,解析结果里却只有TermQuery,就是前缀被吞了。 - 用
inverted_index(field)?.terms().range().ge(prefix)数一下以某个前缀开头的词有多少,和max_expansions比一比;数量远超 50 的短前缀,结果基本是随机的一小部分。 - 搜索框的前缀补全,拿高频词(
man、the)的前两个字母测一次,看能不能补出来。
小结
QueryParser没有单词前缀:grea*被分词器剥掉星号成了grea,"grea"*报「至少两个词」。"old ma"*每个段只展开字典序最小的 50 个词;ma开头的词有 414 个,man排第 151,所以 0 篇。解析器不能改这个上限。- 修法:自己建
PhrasePrefixQuery(单词也行)或RegexQuery,按需set_max_expansions;输入要先按索引的方式小写。