Field body is not a fast field.
主线位置:查询主线第二、三步。
QueryParser在第二步把field:*换成ExistsQuery,不看 schema;第三步建Weight时ExistsQuery才去查字段是不是快速字段。它只读列的索引部分,见短语与其他查询和快速字段。
flowchart TB
in["body:*"] --> p["parse_query 成功"]
p --> eq["ExistsQuery"]
eq --> w["search 时建 Weight"]
w --> f{"字段是 FAST"}
f -->|是| ok["读列索引"]
f -->|否| err["SchemaError 整条失败"]
r["body:[* TO *]"] --> drop["解析时丢掉"]
drop --> zero["0 篇 无错误"]
实验在 exp-tantivy/pitb-lab,bin p25_exists、p25_range(release),对照组链接 0.26.1。四篇文档:title 都有;body(TEXT)和 body2(TEXT | FAST)一篇有正文、一篇是空字符串、两篇没有;tag 是 STRING | FAST。M2 Max、macOS 27、rustc 1.99.0。
现象
body:* parse=Ok Count=Err("Schema error: 'Field body is not a fast field.'") TopDocs=Err("Schema error: 'Field body is not a fast field.'")
body:[* TO *] parse=Ok Count=Ok(0) TopDocs=Ok(0)
body2:* parse=Ok Count=Ok(2) TopDocs=Ok(2)
tag:* parse=Ok Count=Ok(2) TopDocs=Ok(2)
title:a OR body:* parse=Ok Count=Err("Schema error: 'Field body is not a fast field.'") TopDocs=Err("Schema error: 'Field body is not a fast field.'")
lenient errors=[] Count=Err("Schema error: 'Field body is not a fast field.'")
- 解析成功,错误推迟到
search;只要查询里有一个body:*,title:a OR body:*整条都失败。 parse_query_lenient的错误列表是空的,它管不到这一步。- 想换成 Lucene 的
body:[* TO *],不报错,0 篇。 body2:*数出 2 篇:有正文的那篇,和正文是空字符串的那篇。
原因
convert_literal_to_query 把 Exists 叶子直接换成 ExistsQuery::new(field, true),不检查字段类型。ExistsQuery 的构造函数 new 的文档写着「This constructor never fails, but executing the search with this query will return an error if the specified field doesn't exists or is not a fast field」,检查在 weight 里:
let field_type = schema.get_field_entry(field).field_type();
if !field_type.is_fast() {
return Err(TantivyError::SchemaError(format!(
"Field {} is not a fast field.",
self.field_name
)));
}
Searcher::search 先建整棵查询的 Weight,任何一个叶子出错就整体返回错误,所以 OR 也救不了。
[* TO *] 是解析器特意丢掉的。范围的两头都无界时,这段代码直接返回 None,注释是「this range is useless, either because a user requested [* TO *], or because we failed to parse something. Either way, there is no point emitting it」。叶子没了,查询变成 EmptyQuery:
body:[* TO *] 0 docs EmptyQuery
body:[a TO *] 2 docs RangeQuery { bounds: BoundsRange { lower_bound: Included(Term(field=0, type=Str, "a")), upper_bound: Unbounded } }
field:* 在解析时就报错语法层早就认识 field:*,但 0.26.1 的 QueryParser 遇到 Exists 叶子一律返回 UnsupportedQuery,错误文字说的却是范围查询。同一段程序链接 0.26.1:
body:* parse=Err(Unsupported query: Range query need to target a specific field.)
tag:* parse=Err(Unsupported query: Range query need to target a specific field.)
lenient errors=[UnsupportedQuery("Range query need to target a specific field.")] Count=Ok(1)
报错文字和输入对不上,但至少在解析时就失败,容错解析也会把它挑出来。0.27.0 的 CHANGELOG 记为「Support exists queries (field:*) in the query parser」(#2798,其中的提交 f981e6e1f,「feat: Add support for Exists leaf in query parser」),加了 LogicalLiteral::Exists。快速字段上终于能用了,非快速字段上的错误则从解析阶段挪到了执行阶段。
修法
给字段加 FAST,重建索引。 文本字段写 TEXT | FAST 或 STRING | FAST。注意快速字段记的是值本身:空字符串也是一个值,上面 body2:* 把正文为 "" 的那篇也算成「存在」;倒排索引里空字符串一个词元都没有。
不改 schema、确实要在倒排索引上判断「至少有一个词」,自己建范围查询。 下界是空字符串、上界无界,就是这个字段的所有词:
let all_terms = RangeQuery::new(
Bound::Included(Term::from_field_text(body, "")),
Bound::Unbounded,
);
RangeQuery body ["", *) Count=Ok(1)
它要把字段里每个词的倒排表读进位图,词越多越慢(短语与其他查询里有范围查询的耗时);只适合偶尔用。
在执行前校验。 用户输入交给解析器以后,先不打分地建一次 Weight,能把这类「解析通过、执行失败」的错误提前挑出来,给用户一个提示,而不是让整个搜索请求失败:
let query = qp.parse_query(input)?;
query.weight(EnableScoring::disabled_from_schema(&schema))?;
check title:a OR body:* => Err("Schema error: 'Field body is not a fast field.'")
check title:a OR body2:* => Ok(())
怎么检查
- 遍历 schema,列出不是快速字段的字段名;用户输入里出现
这些字段:*就提示或改写。 - 搜索接口的错误日志里搜
is not a fast field。 - 从 0.26 升到 0.27 的,原来在解析阶段处理
UnsupportedQuery的分支不会再收到field:*,错误改从search返回。
小结
- 0.27.0 的
QueryParser把field:*换成ExistsQuery不看 schema;字段不是快速字段时,search报Field body is not a fast field.,整条查询失败,容错解析也不报。 [* TO *]被解析器丢掉,0 篇无错误。- 修法:给字段加
FAST(空字符串也算存在);不改 schema 就用下界为空字符串的RangeQuery;执行前用query.weight(EnableScoring::disabled_from_schema(..))校验。