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,查询照常返回空列表。
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不检查,空区间返回空结果,适合接外部的分页参数。