Rust in Everything
基于 tantivy 0.27.0

the trait bound TopDocs: Collector is not satisfied

主线位置:查询主线第四、五步。Searcher::search(&query, &collector) 要求第二个参数实现 Collector;TopDocs 在 0.27.0 里只是两个数,order_by_score() 这类方法才把它变成收集器。收集器的整套接口见收集器。

flowchart TB
  td["TopDocs with_limit"] --> plain["只有 limit 和 offset"]
  plain -->|直接交给 search| ce["E0277"]
  plain --> ob["order_by_score 等"]
  ob --> col["TopBySortKeyCollector"]
  col --> ok["searcher.search"]
  zero["with_limit 0"] --> panic["panic"]
  range["for_doc_range 空区间"] --> empty["返回空 不 panic"]

实验在 exp-tantivy/pitb-lab,bin p21_limit,编译失败的那一份保存为 p21_ce.rs.txt(release);M2 Max、macOS 27、rustc 1.99.0,tantivy 0.27.0。

现象

照 0.25 及以前的写法:

let top: Vec<(Score, DocAddress)> = s.search(&AllQuery, &TopDocs::with_limit(10))?;
error[E0277]: the trait bound `TopDocs: Collector` is not satisfied
   --> src/bin/p21_ce.rs:11:59
    |
 11 |   let top: Vec<(Score, DocAddress)> = s.search(&AllQuery, &TopDocs::with_limit(10))?;
    |                                         ------            ^^^^^^^^^^^^^^^^^^^^^^^^ the trait `Collector` is not implemented for `TopDocs`
    |                                         |
    |                                         required by a bound introduced by this call
    |
    = help: the following other types implement trait `Collector`:
              (Left, Right)
              (One, Two, Three)
              (One, Two, Three, Four)
              AggregationCollector
              BytesFilterCollector<TCollector, TPredicate>
              DistributedAggregationCollector
              DocSetCollector
              FacetCollector
            and 6 others

编译器的提示列的是别的收集器,没有提到该调哪个方法。

改好以后,另一个坑在运行时:每页条数来自请求参数,有人传了 0:

size from request = 0

thread 'main' (6517956) panicked at ~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tantivy-0.27.0/src/collector/top_score_collector.rs:94:9:
assertion `left != right` failed: Limit must be greater than 0
  left: 0
 right: 0

原因

TopDocs 只有 limit、offset 两个字段,不实现 Collector。要先选排序键:order_by_score 返回 impl Collector<Fruit = Vec<(Score, DocAddress)>>,order_by_fast_field、order_by_string_fast_field、tweak_score、order_by 同理,内部都是同一个 TopBySortKeyCollector。

with_limit 第一行是 assert_ne!(limit, 0, "Limit must be greater than 0"),文档的「Panics」一节写明了。同一个类型的另一个构造函数 for_doc_range 却不检查,limit 用 saturating_sub 算出来,空区间就是 0,查询照常返回空列表。

📝NOTE
冷知识:PR 说明里写着「breaks public API」,CHANGELOG 没写

TopDocs 从 2018 年的 #437(「Collector refactoring and multithreaded search」)起一直自己实现 Collector,with_limit(10) 直接就能交给 search,默认按分数排序。#2726(63c66005d,「Lazy scorers」,首次发布于 0.26.0)把打分改造成 SortKeyComputer,删掉了 impl Collector for TopDocs,提交说明写的是「This PR breaks public API, but fixing code is straightforward.」同一个改动还把 order_by_u64_field 的结果从 Vec<(u64, DocAddress)> 换成了 Vec<(Option<u64>, DocAddress)>,缺值的文档以前拿到一个替代值(0.25.0 的 ScorerByField 用 first_or_default_col,降序时是 0),现在是 None。CHANGELOG 0.26 对应的那一条只写了「Add lazy scorers to stop score evaluation early when a doc won't reach the top-K threshold」,没有提到接口变了。

修法

按分数排序就加一个 .order_by_score():

let top = s.search(&q, &TopDocs::with_limit(3).order_by_score())?;
with_limit(3).order_by_score(): 3 hits

分页参数来自外部时,用 for_doc_range 表达「第 offset 条起取 size 条」,size 为 0、区间倒置都不会 panic:

let top = s.search(&q, &(TopDocs::for_doc_range(offset..offset + size).order_by_score(), Count))?;
for_doc_range(0..0): 0 hits, count=52
for_doc_range(5..5): 0 hits, count=52
for_doc_range(10..3): 0 hits, count=52

或者在调用前自己挡住 0:只要总数时只用 Count。上面把 TopDocs 和 Count 放在一个元组里只是为了同时打印,正式代码别这么写,原因见(TopDocs, Count) 一起收,剪枝没了。

怎么检查

  • 升级到 0.26 以后的编译错误里,凡是 TopDocs: Collector 的,补上 order_by_score();用过 order_by_u64_field 的,结果类型多了一层 Option。
  • 在代码里搜 TopDocs::with_limit(,参数不是字面常量的,确认上游保证了大于 0,或者换成 for_doc_range。
  • 搜索接口加一个 size=0 的测试。

小结

  • 0.26.0 起(#2726)TopDocs 不再是收集器,要先 order_by_score()、order_by_fast_field(...) 等选排序键;旧代码报 E0277,编译器提示里看不出该怎么改。
  • with_limit(0) panic「Limit must be greater than 0」;for_doc_range 不检查,空区间返回空结果,适合接外部的分页参数。

💬 关联讨论

发起讨论
加载中...