From 98ebbf922d5715941afa5afd1fc817a8c81ded97 Mon Sep 17 00:00:00 2001 From: PSeitz Date: Fri, 30 Jan 2026 17:06:41 +0100 Subject: [PATCH] faster exclude queries (#2825) * faster exclude queries Faster exclude queries with multiple terms. Changes `Exclude` to be able to exclude multiple DocSets, instead of putting the docsets into a union. Use `seek_danger` in `Exclude`. closes #2822 * replace unwrap with match --- src/query/boolean_query/boolean_weight.rs | 37 ++++++------ src/query/exclude.rs | 69 +++++++++++++++-------- src/query/mod.rs | 2 +- 3 files changed, 63 insertions(+), 45 deletions(-) diff --git a/src/query/boolean_query/boolean_weight.rs b/src/query/boolean_query/boolean_weight.rs index c46e9b0b1..062449b8a 100644 --- a/src/query/boolean_query/boolean_weight.rs +++ b/src/query/boolean_query/boolean_weight.rs @@ -291,18 +291,6 @@ impl BooleanWeight { } }; - let exclude_scorer_opt: Option> = if exclude_scorers.is_empty() { - None - } else { - let exclude_specialized_scorer: SpecializedScorer = - scorer_union(exclude_scorers, DoNothingCombiner::default, num_docs); - Some(into_box_scorer( - exclude_specialized_scorer, - DoNothingCombiner::default, - num_docs, - )) - }; - let include_scorer = match (should_scorers, must_scorers) { (ShouldScorersCombinationMethod::Ignored, must_scorers) => { // No SHOULD clauses (or they were absorbed into MUST). @@ -380,16 +368,23 @@ impl BooleanWeight { } } }; - if let Some(exclude_scorer) = exclude_scorer_opt { - let include_scorer_boxed = - into_box_scorer(include_scorer, &score_combiner_fn, num_docs); - Ok(SpecializedScorer::Other(Box::new(Exclude::new( - include_scorer_boxed, - exclude_scorer, - )))) - } else { - Ok(include_scorer) + if exclude_scorers.is_empty() { + return Ok(include_scorer); } + + let include_scorer_boxed = into_box_scorer(include_scorer, &score_combiner_fn, num_docs); + let scorer: Box = if exclude_scorers.len() == 1 { + let exclude_scorer = exclude_scorers.pop().unwrap(); + match exclude_scorer.downcast::() { + // Cast to TermScorer succeeded + Ok(exclude_scorer) => Box::new(Exclude::new(include_scorer_boxed, *exclude_scorer)), + // We get back the original Box + Err(exclude_scorer) => Box::new(Exclude::new(include_scorer_boxed, exclude_scorer)), + } + } else { + Box::new(Exclude::new(include_scorer_boxed, exclude_scorers)) + }; + Ok(SpecializedScorer::Other(scorer)) } } diff --git a/src/query/exclude.rs b/src/query/exclude.rs index 15e609c1e..a1f28f87a 100644 --- a/src/query/exclude.rs +++ b/src/query/exclude.rs @@ -1,48 +1,71 @@ -use crate::docset::{DocSet, TERMINATED}; +use crate::docset::{DocSet, SeekDangerResult, TERMINATED}; use crate::query::Scorer; use crate::{DocId, Score}; -#[inline] -fn is_within(docset: &mut TDocSetExclude, doc: DocId) -> bool { - docset.doc() <= doc && docset.seek(doc) == doc -} - -/// Filters a given `DocSet` by removing the docs from a given `DocSet`. +/// An exclusion set is a set of documents +/// that should be excluded from a given DocSet. /// -/// The excluding docset has no impact on scoring. -pub struct Exclude { - underlying_docset: TDocSet, - excluding_docset: TDocSetExclude, +/// It can be a single DocSet, or a Vec of DocSets. +pub trait ExclusionSet: Send { + /// Returns `true` if the given `doc` is in the exclusion set. + fn contains(&mut self, doc: DocId) -> bool; } -impl Exclude +impl ExclusionSet for TDocSet { + #[inline] + fn contains(&mut self, doc: DocId) -> bool { + self.seek_danger(doc) == SeekDangerResult::Found + } +} + +impl ExclusionSet for Vec { + #[inline] + fn contains(&mut self, doc: DocId) -> bool { + for docset in self.iter_mut() { + if docset.seek_danger(doc) == SeekDangerResult::Found { + return true; + } + } + false + } +} + +/// Filters a given `DocSet` by removing the docs from an exclusion set. +/// +/// The excluding docsets have no impact on scoring. +pub struct Exclude { + underlying_docset: TDocSet, + exclusion_set: TExclusionSet, +} + +impl Exclude where TDocSet: DocSet, - TDocSetExclude: DocSet, + TExclusionSet: ExclusionSet, { /// Creates a new `ExcludeScorer` pub fn new( mut underlying_docset: TDocSet, - mut excluding_docset: TDocSetExclude, - ) -> Exclude { + mut exclusion_set: TExclusionSet, + ) -> Exclude { while underlying_docset.doc() != TERMINATED { let target = underlying_docset.doc(); - if !is_within(&mut excluding_docset, target) { + if !exclusion_set.contains(target) { break; } underlying_docset.advance(); } Exclude { underlying_docset, - excluding_docset, + exclusion_set, } } } -impl DocSet for Exclude +impl DocSet for Exclude where TDocSet: DocSet, - TDocSetExclude: DocSet, + TExclusionSet: ExclusionSet, { fn advance(&mut self) -> DocId { loop { @@ -50,7 +73,7 @@ where if candidate == TERMINATED { return TERMINATED; } - if !is_within(&mut self.excluding_docset, candidate) { + if !self.exclusion_set.contains(candidate) { return candidate; } } @@ -61,7 +84,7 @@ where if candidate == TERMINATED { return TERMINATED; } - if !is_within(&mut self.excluding_docset, candidate) { + if !self.exclusion_set.contains(candidate) { return candidate; } self.advance() @@ -79,10 +102,10 @@ where } } -impl Scorer for Exclude +impl Scorer for Exclude where TScorer: Scorer, - TDocSetExclude: DocSet + 'static, + TExclusionSet: ExclusionSet + 'static, { #[inline] fn score(&mut self) -> Score { diff --git a/src/query/mod.rs b/src/query/mod.rs index 0bc865921..e33768950 100644 --- a/src/query/mod.rs +++ b/src/query/mod.rs @@ -43,7 +43,7 @@ pub use self::boost_query::{BoostQuery, BoostWeight}; pub use self::const_score_query::{ConstScoreQuery, ConstScorer}; pub use self::disjunction_max_query::DisjunctionMaxQuery; pub use self::empty_query::{EmptyQuery, EmptyScorer, EmptyWeight}; -pub use self::exclude::Exclude; +pub use self::exclude::{Exclude, ExclusionSet}; pub use self::exist_query::ExistsQuery; pub use self::explanation::Explanation; #[cfg(test)]