Skip to content

Reduce sorting in TopDocs #2646

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 38 additions & 2 deletions benches/agg_bench.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@ use rand_distr::Distribution;
use serde_json::json;
use tantivy::aggregation::agg_req::Aggregations;
use tantivy::aggregation::AggregationCollector;
use tantivy::collector::TopDocs;
use tantivy::fastfield::FastValue;
use tantivy::query::{AllQuery, TermQuery};
use tantivy::schema::{IndexRecordOption, Schema, TextFieldIndexing, FAST, STRING};
use tantivy::{doc, Index, Term};
use tantivy::{doc, Index, Order, Term};

#[global_allocator]
pub static GLOBAL: &PeakMemAlloc<std::alloc::System> = &INSTRUMENTED_SYSTEM;
Expand Down Expand Up @@ -73,6 +75,12 @@ fn bench_agg(mut group: InputGroup<Index>) {
register!(group, histogram_with_avg_sub_agg);
register!(group, avg_and_range_with_avg_sub_agg);

register!(group, top_docs_small_shallow);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you put that in a different file? collector_bench for instance?

register!(group, top_docs_small_deep);

register!(group, top_docs_large_shallow);
register!(group, top_docs_large_deep);

group.run();
}

Expand Down Expand Up @@ -359,6 +367,34 @@ fn avg_and_range_with_avg_sub_agg(index: &Index) {
execute_agg(index, agg_req);
}

fn execute_top_docs<F: FastValue>(
index: &Index,
fast_field: &str,
order: Order,
offset: usize,
limit: usize,
) {
let collector = TopDocs::with_limit(limit)
.and_offset(offset)
.order_by_fast_field::<F>(fast_field, order);

let reader = index.reader().unwrap();
let searcher = reader.searcher();
black_box(searcher.search(&AllQuery, &collector).unwrap());
}
fn top_docs_small_deep(index: &Index) {
execute_top_docs::<u64>(index, "score", Order::Asc, 10000, 10);
}
fn top_docs_small_shallow(index: &Index) {
execute_top_docs::<u64>(index, "score", Order::Asc, 0, 10);
}
fn top_docs_large_deep(index: &Index) {
execute_top_docs::<u64>(index, "score", Order::Asc, 10000, 1000);
}
fn top_docs_large_shallow(index: &Index) {
execute_top_docs::<u64>(index, "score", Order::Asc, 0, 1000);
}

#[derive(Clone, Copy, Hash, Default, Debug, PartialEq, Eq, PartialOrd, Ord)]
enum Cardinality {
/// All documents contain exactly one value.
Expand Down Expand Up @@ -402,7 +438,7 @@ fn get_test_index_bench(cardinality: Cardinality) -> tantivy::Result<Index> {
.collect::<Vec<_>>();
{
let mut rng = StdRng::from_seed([1u8; 32]);
let mut index_writer = index.writer_with_num_threads(1, 200_000_000)?;
let mut index_writer = index.writer_with_num_threads(8, 200_000_000)?;
// To make the different test cases comparable we just change one doc to force the
// cardinality
if cardinality == Cardinality::OptionalDense {
Expand Down
14 changes: 7 additions & 7 deletions src/collector/top_collector.rs
Original file line number Diff line number Diff line change
Expand Up @@ -114,9 +114,7 @@ where T: PartialOrd + Clone
}

Ok(top_collector
.into_sorted_vec()
.into_iter()
.skip(self.offset)
.into_sorted_after(self.offset)
.map(|cdoc| (cdoc.feature, cdoc.doc))
.collect())
}
Expand Down Expand Up @@ -169,7 +167,7 @@ impl<T: PartialOrd + Clone> TopSegmentCollector<T> {
pub fn harvest(self) -> Vec<(T, DocAddress)> {
let segment_ord = self.segment_ord;
self.topn_computer
.into_sorted_vec()
.into_vec()
.into_iter()
.map(|comparable_doc| {
(
Expand Down Expand Up @@ -206,10 +204,11 @@ mod tests {
top_collector.collect(5, 0.3);
assert_eq!(
top_collector.harvest(),
// Note: Individual segments are not sorted.
vec![
(0.8, DocAddress::new(0, 1)),
(0.2, DocAddress::new(0, 3)),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i am not fond of the assert now. The order is now very specific to the implementation. Can you assert on the set. For instance, by sorting the results (in the unit test.)

(0.3, DocAddress::new(0, 5)),
(0.2, DocAddress::new(0, 3))
]
);
}
Expand All @@ -224,11 +223,12 @@ mod tests {
top_collector.collect(9, -0.2);
assert_eq!(
top_collector.harvest(),
// Note: Individual segments are not sorted.
vec![
(0.9, DocAddress::new(0, 7)),
(0.8, DocAddress::new(0, 1)),
(0.2, DocAddress::new(0, 3)),
(0.3, DocAddress::new(0, 5)),
(0.2, DocAddress::new(0, 3))
(0.9, DocAddress::new(0, 7)),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same as above

]
);
}
Expand Down
18 changes: 18 additions & 0 deletions src/collector/top_score_collector.rs
Original file line number Diff line number Diff line change
Expand Up @@ -850,6 +850,24 @@ where
self.buffer
}

/// Returns the elements between `offset` and `top_n` in sorted order.
pub fn into_sorted_after(
mut self,
offset: usize,
) -> impl Iterator<Item = ComparableDoc<Score, D, R>> {
if self.buffer.len() > self.top_n {
self.truncate_top_n();
}

if offset >= self.buffer.len() {
return vec![].into_iter().skip(0);
}

let (_, _, remainder) = self.buffer.select_nth_unstable(offset);
remainder.sort_unstable();
self.buffer.into_iter().skip(offset)
Comment on lines +866 to +868
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not convinced this is actually beneficial, do you have any benchmarks demonstrating that doing these two effective sorts is more efficient than the one sort?

Copy link
Contributor Author

@stuhood stuhood Jun 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

select_nth is not a sort: it only pivots elements around the nth position (in linear time).

But no: I have not run benchmarks! Does CI run a benchmark suite?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, I would create a set of micro benchmarks between the two changes.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For small values of offset, it is probably harmful.
For high values of offset, it is probably helpful.

...

Can we have unit tests?

There are benchmarks but we do not run them in CI.

}

/// Returns the top n elements in stored order.
/// Useful if you do not need the elements in sorted order,
/// for example when merging the results of multiple segments.
Expand Down