Rust in Everything
基于 tantivy 0.27.0

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 } }
📝NOTE
冷知识:0.26.1 上,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(..)) 校验。

💬 关联讨论

发起讨论
加载中...