Count 能数,TopDocs 报 Field "n" is not indexed.
主线位置:查询主线第三步,
Query::weight。一个字段可以只进快速字段(列存,见快速字段)而不进倒排索引。这样的字段上,同一个TermQuery在要不要打分的两种情况下走的是两条完全不同的路。
flowchart TB
tq["TermQuery n=3"] --> idx{"字段 INDEXED"}
idx -->|是| tw["TermWeight"]
idx -->|"否 只有 FAST"| sc{"要打分"}
sc -->|"否 Count"| rq["退成快速字段范围"]
sc -->|"是 TopDocs"| err["not indexed"]
实验在 exp-tantivy/pitb-lab,bin p24_fastonly(release),对照组同一份代码链接 0.26.1(pitb-026)。100 篇文档,body 是 TEXT,n 是只有 FAST 的 u64,值为 i % 7。M2 Max、macOS 27、rustc 1.99.0。
现象
TermQuery n=3 Count=Ok(14) TopDocs=Err("Schema error: 'Field \"n\" is not indexed.'")
parse n:3 parse Err: The field 'n' is not declared as indexed
parse n:[3 TO 3] Count=Ok(14) TopDocs=Ok(3)
parse old AND n:[3 TO 3] Count=Ok(7) TopDocs=Ok(3)
parse n:IN [3 4] Count=Err("Schema error: 'Field \"n\" is not indexed.'") TopDocs=Err("Schema error: 'Field \"n\" is not indexed.'")
- 同一个
TermQuery,交给Count数出 14 篇,交给按分数的TopDocs报错。在测试里只用Count验证过的查询,到了搜索接口上才失败。 QueryParser拒绝n:3,却接受n:[3 TO 3],两者意思一样。n:IN [3 4]连Count都不行。
原因
TermQuery::weight 开头有一条后路:
if !field_entry.is_indexed()
&& field_entry.is_fast()
&& is_type_valid_for_fastfield_range_query(self.term.typ())
&& !enable_scoring.is_scoring_enabled()
{
let range_query = RangeQuery::new(
Bound::Included(self.term.clone()),
Bound::Included(self.term.clone()),
);
return range_query.weight(enable_scoring);
}
Ok(Box::new(self.specialized_weight(enable_scoring)?))
字段没进倒排索引但是快速字段时,把「等于 3」改写成快速字段上的范围 [3, 3],扫列(范围查询的两种实现见短语与其他查询)。条件的最后一项是不打分:快速字段上没有词频、没有 BM25 统计,算不出分数。Count 不要分数,走后路;按分数的 TopDocs 要分数,落到 specialized_weight,报 Field "n" is not indexed.。这条后路是 #2693(7963b0b4a,首次发布于 0.26.0)加的,提交说明里第二条就是「only fallback without scores」。
解析器那边规则不同:词项走 compute_logical_ast_for_leaf,字段不是 INDEXED 就报 FieldNotIndexed;范围走 compute_boundary_term,快速字段也放行,生成的 RangeQuery 本来就是常数分,打分与否都能用。TermSetQuery 的 specialized_weight 没有后路,只认倒排索引。
修法
不改 schema 的话,告诉 tantivy「这一项不需要分数」:
RangeQuery [3, 3] Count=Ok(14) TopDocs=Ok(3)
ConstScoreQuery(TermQuery n=3) Count=Ok(14) TopDocs=Ok(3)
old AND TermQuery n=3 Count=Ok(7) TopDocs=Err("Schema error: 'Field \"n\" is not indexed.'")
- 直接用
RangeQuery(或解析器的n:[3 TO 3]),它在快速字段上本来就是常数分。 - 用
ConstScoreQuery::new(Box::new(term_query), 1.0)包起来,它会让里面的查询在不打分的模式下建Weight,于是走到后路。 - 和别的子句放进同一个
BooleanQuery不会自动变成过滤:最后一行,old AND n=3照样报错,要包一层ConstScoreQuery再放进去。
ConstScoreQuery 也救不了它0.26.1 的 ConstScoreQuery::weight 把调用方的 enable_scoring 原样传给里面的查询,外面要分数,里面也要,TermQuery 的后路走不到。同一段程序链接 0.26.1:
ConstScoreQuery(TermQuery n=3) Count=Ok(14) TopDocs=Err("Schema error: 'Field \"n\" is not indexed.'")
#3059(7b0e97a0b,「fix(query): disable child scoring in ConstScoreQuery」,首次发布于 0.27.0)把 weight 改成对子查询一律 enable_scoring.scoring_disabled(),文档改为「The wrapped query is evaluated with scoring disabled and only determines which documents match」,并加了一个测试 test_const_score_query_supports_term_query_on_fast_only_field,正是这个场景。CHANGELOG 0.27 记为「ConstScoreQuery disables scoring of the wrapped query」。
要真正的词项查询和 BM25 分数,只能给字段加上 INDEXED 重建索引(schema 改不了已有索引,见 The field 'body' does not have positions indexed)。
怎么检查
- 遍历 schema,列出
is_fast() && !is_indexed()的字段;对这些字段的等值查询,一律包ConstScoreQuery或改成范围。 - 测试里用按分数的
TopDocs跑一遍查询,别只用Count。 - 交给解析器的输入里有这类字段时,
field:value会报The field 'n' is not declared as indexed,可以在提示里告诉用户写成field:[value TO value]。
小结
- 只有
FAST的字段上,TermQuery不打分时退成快速字段范围查询,打分时报Field "n" is not indexed.;所以Count能数、TopDocs报错(#2693,0.26.0 起)。 QueryParser拒绝n:3、接受n:[3 TO 3];TermSetQuery(n:IN [...])没有后路。- 修法:用
RangeQuery,或者包ConstScoreQuery。后者在 0.27.0 才有效(#3059)。