Skip to main content

summa_core/query/
traits.rs

1//! Query and Scorer traits with async support
2//!
3//! Provides the core abstractions for search queries and document scoring.
4
5use std::future::Future;
6use std::pin::Pin;
7
8use crate::segment::SegmentReader;
9use crate::{DocId, Result, Score};
10
11/// Future type for scorer creation
12#[cfg(not(target_arch = "wasm32"))]
13pub type ScorerFuture<'a> = Pin<Box<dyn Future<Output = Result<Box<dyn Scorer + 'a>>> + Send + 'a>>;
14#[cfg(target_arch = "wasm32")]
15pub type ScorerFuture<'a> = Pin<Box<dyn Future<Output = Result<Box<dyn Scorer + 'a>>> + 'a>>;
16
17/// Exact scores corresponding to one compact posting batch.
18pub type ScoreBatch = [Score; super::docset::DOC_BATCH_SIZE];
19pub type ScoreBatchMask = [u64; super::docset::DOC_BATCH_SIZE / 64];
20
21pub(super) fn fill_score_batch_scalar<S: Scorer + ?Sized>(
22    scorer: &mut S,
23    docs: &mut super::docset::DocBatch,
24    scores: &mut ScoreBatch,
25) -> usize {
26    let mut count = 0;
27    let mut doc = scorer.doc();
28    while count < docs.len() && doc != crate::structures::TERMINATED {
29        docs[count] = doc;
30        scores[count] = scorer.score();
31        count += 1;
32        doc = scorer.advance();
33    }
34    count
35}
36
37pub(super) fn score_batch_matches_scalar<S: Scorer + ?Sized>(
38    scorer: &mut S,
39    docs: &super::docset::DocBatch,
40    len: usize,
41    scores: &mut ScoreBatch,
42    matches: &mut ScoreBatchMask,
43) {
44    assert!(len <= docs.len());
45    matches.fill(0);
46    for i in 0..len {
47        if scorer.seek(docs[i]) == docs[i] {
48            scores[i] = scorer.score();
49            matches[i / 64] |= 1 << (i % 64);
50        }
51    }
52}
53
54/// Options that affect scorer construction rather than scoring semantics.
55///
56/// Position postings can be much larger than the top-k result itself. Keeping
57/// this explicit lets ID/score-only collectors avoid loading them while query
58/// types that need positions for matching (for example phrases) remain free to
59/// load their own internal data.
60#[derive(Debug, Clone, Default)]
61pub struct ScorerOptions {
62    /// Internal collection scope: children traverse this field's physical IDs.
63    /// Only an opted-in query tree may receive this; collection owns translation.
64    pub(crate) physical_text_field: Option<crate::Field>,
65    /// Required text clauses must expose membership before any top-k cutoff.
66    pub(crate) complete_text_matches: bool,
67    /// A top-level collector may accept bounded ranked hits plus exact count.
68    /// Nested clauses must still expose complete membership.
69    pub(crate) ranked_count_limit: Option<usize>,
70    /// Membership-only collection can skip optional scoring setup. Scoring
71    /// remains valid if a nested consumer requests it (canonical fallback).
72    pub(crate) skip_scoring_setup: bool,
73    /// Eligibility pushed into candidate collectors; never a scoring feature.
74    pub(crate) eligibility: Option<std::sync::Arc<DocBitset>>,
75    pub collect_positions: bool,
76    /// Initial top-k score floor to seed into MaxScore/BMP pruning. Used to
77    /// carry the running k-th score across the segments of one query so later
78    /// segments prune from a nonzero threshold (see `SharedThreshold`). 0.0 =
79    /// no seed. Only honored on exact, final-score executor paths.
80    pub initial_threshold: f32,
81    /// Live form of `initial_threshold`. Exact final-score executors may read
82    /// it during traversal so concurrently searched segments benefit as soon
83    /// as another segment establishes a stronger global floor.
84    pub shared_threshold: Option<super::scoring::SharedThreshold>,
85    /// Query-global LSP/0 selection projected onto this segment.
86    pub(crate) lsp_plan: Option<std::sync::Arc<super::bmp::LspSegmentPlan>>,
87    /// Query-global text statistics (document frequencies, corpus sizes,
88    /// average lengths aggregated over every segment of the searcher, or
89    /// supplied by a broker across shards). Text scorers use them for IDF
90    /// and length normalisation so a term scores the same in every segment;
91    /// a query's own `with_global_stats` takes precedence.
92    pub global_stats: Option<std::sync::Arc<super::GlobalStats>>,
93}
94
95impl ScorerOptions {
96    pub const fn with_positions() -> Self {
97        Self {
98            physical_text_field: None,
99            complete_text_matches: false,
100            ranked_count_limit: None,
101            skip_scoring_setup: false,
102            eligibility: None,
103            collect_positions: true,
104            initial_threshold: 0.0,
105            shared_threshold: None,
106            lsp_plan: None,
107            global_stats: None,
108        }
109    }
110
111    /// Preserve collection behavior while preventing a nested/component
112    /// scorer from applying a floor expressed in the outer query's score
113    /// space.
114    pub fn without_threshold(&self) -> Self {
115        Self {
116            physical_text_field: self.physical_text_field,
117            complete_text_matches: self.complete_text_matches,
118            ranked_count_limit: None,
119            skip_scoring_setup: self.skip_scoring_setup,
120            eligibility: self.eligibility.clone(),
121            collect_positions: self.collect_positions,
122            initial_threshold: 0.0,
123            shared_threshold: self
124                .shared_threshold
125                .as_ref()
126                .filter(|shared| shared.deadline().is_some())
127                .map(super::SharedThreshold::budget_only),
128            lsp_plan: None,
129            global_stats: self.global_stats.clone(),
130        }
131    }
132
133    pub(crate) fn for_required_clause(&self) -> Self {
134        Self {
135            complete_text_matches: true,
136            ..self.without_threshold()
137        }
138    }
139
140    pub(crate) fn stop_if_expired(&self) -> bool {
141        self.shared_threshold
142            .as_ref()
143            .is_some_and(super::SharedThreshold::stop_if_expired)
144    }
145
146    /// Materialization uses the same budget as scoring. Implementations must
147    /// never return a partial bitset (especially for MUST_NOT).
148    pub(crate) fn doc_bitset(
149        &self,
150        query: &dyn Query,
151        reader: &SegmentReader,
152    ) -> Option<DocBitset> {
153        query.as_doc_bitset_with_options(reader, self)
154    }
155}
156
157/// Future type for count estimation
158#[cfg(not(target_arch = "wasm32"))]
159pub type CountFuture<'a> = Pin<Box<dyn Future<Output = Result<u32>> + Send + 'a>>;
160#[cfg(target_arch = "wasm32")]
161pub type CountFuture<'a> = Pin<Box<dyn Future<Output = Result<u32>> + 'a>>;
162
163/// Per-document predicate closure type (platform-aware Send+Sync bounds)
164#[cfg(not(target_arch = "wasm32"))]
165pub type DocPredicate<'a> = Box<dyn Fn(DocId) -> bool + Send + Sync + 'a>;
166#[cfg(target_arch = "wasm32")]
167pub type DocPredicate<'a> = Box<dyn Fn(DocId) -> bool + 'a>;
168
169/// Compact bitset indexed by doc_id. O(1) lookup, ~2.25 MB for 18M docs.
170///
171/// Built from posting lists or predicate scans. Used by BMP filtered queries
172/// to avoid repeated fast-field decoding during per-slot predicate evaluation.
173/// Lookup cost depends on residency and the caller's dispatch, not just this type.
174#[derive(Debug, Clone)]
175pub struct DocBitset {
176    pub(crate) bits: Vec<u64>,
177}
178
179impl DocBitset {
180    /// Clear one document from this set.
181    #[cfg(any(feature = "native", feature = "wasm"))]
182    pub(crate) fn clear(&mut self, doc_id: u32) {
183        if let Some(word) = self.bits.get_mut(doc_id as usize / 64) {
184            *word &= !(1u64 << (doc_id % 64));
185        }
186    }
187    /// Create an empty bitset for `num_docs` documents.
188    pub fn new(num_docs: u32) -> Self {
189        let num_words = (num_docs as usize).div_ceil(64);
190        Self {
191            bits: vec![0u64; num_words],
192        }
193    }
194
195    /// The segment's document universe, with padding bits left clear.
196    pub(crate) fn all(num_docs: u32) -> Self {
197        let mut result = Self::new(num_docs);
198        result.bits.fill(u64::MAX);
199        if !num_docs.is_multiple_of(64)
200            && let Some(last) = result.bits.last_mut()
201        {
202            *last = (1u64 << (num_docs % 64)) - 1;
203        }
204        result
205    }
206
207    /// Set bit for `doc_id`.
208    #[inline]
209    pub fn set(&mut self, doc_id: u32) {
210        let word = doc_id as usize / 64;
211        let bit = doc_id as usize % 64;
212        if word < self.bits.len() {
213            self.bits[word] |= 1u64 << bit;
214        }
215    }
216
217    /// Add matches from consecutive values without a bitset read/modify/write
218    /// per hit. The caller supplies a range inside the document universe.
219    pub(super) fn insert_matching_values(
220        &mut self,
221        start: u32,
222        values: &[u64],
223        predicate: impl Fn(u64) -> bool,
224    ) {
225        for (chunk_index, chunk) in values.chunks(64).enumerate() {
226            let mut matches = [0u8; 64];
227            for (matched, &value) in matches.iter_mut().zip(chunk) {
228                *matched = u8::from(predicate(value));
229            }
230            let mask = matches
231                .iter()
232                .enumerate()
233                .fold(0u64, |mask, (bit, &matched)| {
234                    mask | (u64::from(matched) << bit)
235                });
236            let doc = start as usize + chunk_index * 64;
237            let word = doc / 64;
238            let shift = doc % 64;
239            self.bits[word] |= mask << shift;
240            // Short copied blocks can share words. OR preserves their earlier
241            // matches, and zero-filled comparison tails preserve padding.
242            if shift + chunk.len() > 64 {
243                self.bits[word + 1] |= mask >> (64 - shift);
244            }
245        }
246    }
247
248    /// First set bit at or after `from`, if any.
249    pub fn next_set_bit(&self, from: DocId) -> Option<DocId> {
250        let mut word = from as usize / 64;
251        if word >= self.bits.len() {
252            return None;
253        }
254        let mut bits = self.bits[word] & (u64::MAX << (from % 64));
255        loop {
256            if bits != 0 {
257                return Some((word * 64 + bits.trailing_zeros() as usize) as DocId);
258            }
259            word += 1;
260            if word >= self.bits.len() {
261                return None;
262            }
263            bits = self.bits[word];
264        }
265    }
266
267    /// Test if `doc_id` is in the bitset.
268    #[inline(always)]
269    pub fn contains(&self, doc_id: u32) -> bool {
270        let word = doc_id as usize / 64;
271        let bit = doc_id as usize % 64;
272        word < self.bits.len() && self.bits[word] & (1u64 << bit) != 0
273    }
274
275    /// Number of set bits (matching docs).
276    pub fn count(&self) -> u32 {
277        self.bits.iter().map(|w| w.count_ones()).sum()
278    }
279
280    /// Build bitset from a predicate by scanning all docs. O(N).
281    pub fn from_predicate(num_docs: u32, pred: &dyn Fn(DocId) -> bool) -> Self {
282        let mut bs = Self::new(num_docs);
283        for doc_id in 0..num_docs {
284            if pred(doc_id) {
285                bs.set(doc_id);
286            }
287        }
288        bs
289    }
290
291    /// In-place OR (union): `self |= other`.
292    pub fn union_with(&mut self, other: &DocBitset) {
293        for (a, b) in self.bits.iter_mut().zip(other.bits.iter()) {
294            *a |= *b;
295        }
296    }
297
298    /// In-place AND (intersection): `self &= other`.
299    pub fn intersect_with(&mut self, other: &DocBitset) {
300        for (a, b) in self.bits.iter_mut().zip(other.bits.iter()) {
301            *a &= *b;
302        }
303        // Zero out any words beyond `other`'s length
304        for a in self.bits.iter_mut().skip(other.bits.len()) {
305            *a = 0;
306        }
307    }
308
309    /// In-place ANDNOT (subtract): `self &= !other`.
310    pub fn subtract(&mut self, other: &DocBitset) {
311        for (a, b) in self.bits.iter_mut().zip(other.bits.iter()) {
312            *a &= !*b;
313        }
314    }
315
316    /// Keep only the set docs for which `pred` returns true. O(count) probes —
317    /// the planner uses this to refine a small accumulator against a wide
318    /// clause instead of materializing that clause's full bitset.
319    pub fn retain(&mut self, pred: &dyn Fn(DocId) -> bool) {
320        for (w, word) in self.bits.iter_mut().enumerate() {
321            let mut bits = *word;
322            while bits != 0 {
323                let b = bits.trailing_zeros();
324                let doc = (w * 64) as u32 + b;
325                if !pred(doc) {
326                    *word &= !(1u64 << b);
327                }
328                bits &= bits - 1;
329            }
330        }
331    }
332}
333
334/// Info for MaxScore-optimizable term queries
335#[derive(Debug, Clone)]
336pub struct TermQueryInfo {
337    /// Field being searched
338    pub field: crate::dsl::Field,
339    /// Term bytes (lowercase)
340    pub term: Vec<u8>,
341    /// Query-side weight of the term (a boost, or the query term frequency
342    /// of a de-duplicated match); scales the term's idf, hence its scores
343    /// and bounds alike. 1.0 = plain.
344    pub weight: f32,
345    /// Query-owned statistics override the parent's IDF and average length.
346    /// Grouping that cannot preserve them must retain the original scorer.
347    pub global_stats: Option<std::sync::Arc<super::GlobalStats>>,
348}
349
350/// Info for MaxScore-optimizable sparse term queries
351#[derive(Debug, Clone, Copy)]
352pub struct SparseTermQueryInfo {
353    /// Sparse vector field
354    pub field: crate::dsl::Field,
355    /// Dimension ID in the sparse vector
356    pub dim_id: u32,
357    /// Query weight for this dimension
358    pub weight: f32,
359    /// Whether this term participates in candidate generation. BMP/LSP uses
360    /// the pruned subset for maximum-grid traversal, then scores candidates
361    /// with every term retained in this decomposition.
362    pub candidate: bool,
363    /// MaxScore heap factor (1.0 = exact, lower = approximate)
364    pub heap_factor: f32,
365    /// Multi-value combiner for ordinal deduplication
366    pub combiner: super::MultiValueCombiner,
367    /// Multiplier on executor limit to compensate for ordinal deduplication
368    /// (1.0 = exact, 2.0 = fetch 2x then combine down)
369    pub over_fetch_factor: f32,
370    /// LSP/0 γ. None is depth-derived; Some(0) is exhaustive.
371    pub lsp_gamma: Option<usize>,
372    pub seismic_cut: usize,
373    pub seismic_factor: f32,
374    pub exhaustive: bool,
375}
376
377/// Decomposition of a query for MaxScore optimization.
378///
379/// The planner inspects this to decide whether to use text MaxScore,
380/// sparse MaxScore, or standard BooleanScorer execution.
381#[derive(Debug, Clone)]
382pub enum QueryDecomposition {
383    /// Single text term — eligible for text MaxScore grouping
384    TextTerm(TermQueryInfo),
385    /// One or more sparse dimensions — eligible for sparse MaxScore
386    SparseTerms(Vec<SparseTermQueryInfo>),
387    /// Not decomposable — falls back to standard execution
388    Opaque,
389}
390
391/// Matched positions for a field (field_id, list of scored positions)
392/// Each position includes its individual score contribution
393pub type MatchedPositions = Vec<(u32, Vec<super::ScoredPosition>)>;
394
395macro_rules! define_query_traits {
396    ($($send_bounds:tt)*) => {
397        /// A search query (async)
398        ///
399        /// Note: `scorer` takes `&self` (not `&'a self`) so that scorers don't borrow the query.
400        /// This enables query composition - queries can create sub-queries locally and get their scorers.
401        /// Implementations must clone/capture any data they need during scorer creation.
402        pub trait Query: std::fmt::Display + $($send_bounds)* {
403            /// Create a scorer for this query against a single segment (async)
404            ///
405            /// The `limit` parameter specifies the maximum number of results to return.
406            /// This is passed from the top-level search limit.
407            ///
408            /// Note: The scorer borrows only the reader, not the query. Implementations
409            /// should capture any needed query data (field, terms, etc.) during creation.
410            fn scorer<'a>(
411                &self,
412                reader: &'a SegmentReader,
413                limit: usize,
414            ) -> ScorerFuture<'a>;
415
416            /// Create a scorer with collector-specific construction options.
417            /// Query implementations that can avoid optional position data
418            /// should override this; the default preserves existing behavior.
419            fn scorer_with_options<'a>(
420                &self,
421                reader: &'a SegmentReader,
422                limit: usize,
423                options: ScorerOptions,
424            ) -> ScorerFuture<'a> {
425                let _ = options;
426                self.scorer(reader, limit)
427            }
428
429            /// Estimated number of matching documents in a segment (async)
430            fn count_estimate<'a>(&self, reader: &'a SegmentReader) -> CountFuture<'a>;
431
432            /// Create a scorer synchronously (mmap/RAM only).
433            ///
434            /// Available when the `sync` feature is enabled.
435            /// Default implementation returns an error.
436            #[cfg(feature = "sync")]
437            fn scorer_sync<'a>(
438                &self,
439                reader: &'a SegmentReader,
440                limit: usize,
441            ) -> Result<Box<dyn Scorer + 'a>> {
442                let _ = (reader, limit);
443                Err(crate::error::Error::Query(
444                    "sync scorer not supported for this query type".into(),
445                ))
446            }
447
448            /// Synchronous counterpart to [`Query::scorer_with_options`].
449            #[cfg(feature = "sync")]
450            fn scorer_sync_with_options<'a>(
451                &self,
452                reader: &'a SegmentReader,
453                limit: usize,
454                options: ScorerOptions,
455            ) -> Result<Box<dyn Scorer + 'a>> {
456                let _ = options;
457                self.scorer_sync(reader, limit)
458            }
459
460            /// Decompose this query for MaxScore optimization.
461            ///
462            /// Returns `TextTerm` for simple term queries, `SparseTerms` for
463            /// sparse vector queries (single or multi-dim), or `Opaque` if
464            /// the query cannot be decomposed.
465            fn decompose(&self) -> QueryDecomposition {
466                QueryDecomposition::Opaque
467            }
468
469            /// Opt into one field-local physical address space for collection.
470            /// Every child must honor the internal scorer scope. Unknown/custom
471            /// queries retain logical IDs. Ranked term/union executors may keep
472            /// their existing mapping by opting in only for complete streams.
473            fn physical_text_field(&self, _reader: &SegmentReader, _complete: bool) -> Option<crate::Field> {
474                None
475            }
476
477            /// Sparse terms for query-global BMP superblock planning only.
478            /// Unlike scoring decomposition, this never replaces a query's
479            /// scorer. A filter wrapper may expose its inner sparse query here
480            /// while remaining opaque to Boolean scoring optimizations.
481            fn sparse_decomposition(&self) -> QueryDecomposition {
482                self.decompose()
483            }
484
485            /// Exact scoring plan for a named L1 branch. Unsupported queries
486            /// reject explicitly rather than returning truncated retrieval scores.
487            fn candidate_query(&self) -> Result<super::CandidateQuery> {
488                super::CandidateQuery::from_decomposition(self.decompose())
489            }
490
491            /// Append every `(field, term)` this query scores with BM25 to
492            /// `out`. The searcher aggregates their document frequencies
493            /// across segments before scoring (see `ScorerOptions::global_stats`).
494            fn text_terms(&self, out: &mut Vec<(crate::dsl::Field, Vec<u8>)>) {
495                let _ = out;
496            }
497
498            /// True if this query is a pure filter (always scores 1.0, no positions).
499            /// Used by the planner to convert non-selective MUST filters into predicates.
500            fn is_filter(&self) -> bool {
501                false
502            }
503
504            /// For filter queries: return a cheap per-doc predicate against a segment.
505            /// The predicate does O(1) work per doc (e.g., fast-field lookup).
506            fn as_doc_predicate<'a>(
507                &self,
508                _reader: &'a SegmentReader,
509            ) -> Option<DocPredicate<'a>> {
510                None
511            }
512
513            /// Build a compact bitset of matching doc_ids for this query.
514            ///
515            /// Preferred over `as_doc_predicate` for BMP filtered queries because
516            /// bitset lookup is ~2ns vs ~30-40ns for a fast-field closure.
517            /// Default returns None; TermQuery overrides this to build from its
518            /// posting list in O(M) time.
519            fn as_doc_bitset(
520                &self,
521                _reader: &SegmentReader,
522            ) -> Option<DocBitset> {
523                None
524            }
525
526            /// Budget-aware materialization. `None` means unsupported or
527            /// cancelled; cancellation must flag the shared budget, and a
528            /// partial bitset must never escape as a complete filter.
529            fn as_doc_bitset_with_options(&self, reader: &SegmentReader, options: &ScorerOptions) -> Option<DocBitset> {
530                if options.stop_if_expired() { return None; }
531                let bitset = self.as_doc_bitset(reader);
532                if options.stop_if_expired() { None } else { bitset }
533            }
534
535            /// Cheap estimate of how many docs this filter clause matches in
536            /// the segment. Used by the boolean planner to order MUST/MUST_NOT
537            /// evaluation: the narrowest clause is materialized first and wider
538            /// clauses refine it with per-doc probes instead of being fully
539            /// materialized. `None` = unknown (treated as matching everything).
540            fn bitset_cardinality_estimate(&self, _reader: &SegmentReader) -> Option<u64> {
541                None
542            }
543
544            /// A term with identical membership for count-only collection.
545            /// This does not change scoring decomposition. The collector must
546            /// still account for deleted rows, chunks and missing metadata.
547            fn count_equivalent_term(&self) -> Option<super::TermQueryInfo> {
548                match self.decompose() {
549                    QueryDecomposition::TextTerm(info) => Some(info),
550                    _ => None,
551                }
552            }
553
554            /// A term-equivalent cardinality alongside an exact score-only
555            /// text rank plan. Unlike the count-only hint, opaque/custom plans
556            /// do not opt in automatically. The collector still validates the
557            /// indexed document space, deletions, mappings and cardinality gate.
558            fn ranked_count_equivalent_term(&self) -> Option<super::TermQueryInfo> {
559                match self.decompose() {
560                    QueryDecomposition::TextTerm(info) => Some(info),
561                    _ => None,
562                }
563            }
564
565            /// Opt into top-level bounded conjunction results with an exact
566            /// count. Wrappers must not inherit this automatically: they may
567            /// consume or hide a child's cardinality. Defaults to streaming.
568            fn supports_ranked_conjunction_count(&self) -> bool { false }
569
570            /// For a query that is a pure disjunction of sub-queries (a Boolean
571            /// query with only SHOULD clauses and no boost), the clauses.
572            ///
573            /// The boolean planner flattens these into the enclosing SHOULD
574            /// list: `OR(OR(a, b), c)` scores exactly like `OR(a, b, c)`, and
575            /// the flat form is eligible for MaxScore and filter push-down
576            /// where the nested form would be an opaque, top-k-truncated
577            /// sub-scorer.
578            fn should_children(&self) -> Option<&[std::sync::Arc<dyn Query>]> {
579                None
580            }
581        }
582
583        /// Scored document stream: a DocSet that also provides scores.
584        pub trait Scorer: super::docset::DocSet + $($send_bounds)* {
585            /// Score for current document
586            fn score(&self) -> Score;
587
588            /// Opt into final-score bounds before candidate confirmation.
589            /// Only a top-level ranked collector may use this to omit matches;
590            /// complete collectors and enclosing scorers retain exact traversal.
591            fn supports_candidate_score_bounds(&self) -> bool { false }
592
593            /// Conservative upper bound on `score()` if the current candidate
594            /// confirms. Must include floating-point rounding and use this
595            /// scorer's final score space. The default never excludes a score.
596            fn candidate_score_upper_bound(&self) -> Score { Score::INFINITY }
597
598            /// Conservative final-score bound through an inclusive physical doc ID.
599            /// The interval starts at the current candidate. Only a top-level ranked
600            /// collector may skip it; exact/nested traversal stays unchanged.
601            /// Returning None opts out for the remaining traversal. A skipped
602            /// interval is left through seek_candidate, including deadline checks.
603            fn candidate_block_upper_bound(&mut self) -> Option<(DocId, Score)> { None }
604
605            /// Advance while optionally omitting candidates whose final-score bound
606            /// cannot compete with `minimum`. Only top-level ranked collection
607            /// may call this. `allow_equal = false` certifies that every remaining
608            /// stable ID loses an equal-score tie against the full local heap.
609            fn advance_competitive_candidate(&mut self, _minimum: Score, _allow_equal: bool) -> DocId {
610                self.advance_candidate()
611            }
612
613            /// Optionally prove a lower bound on the kth best score using
614            /// distinct real matches. Restore the current candidate unless
615            /// cancelled. Emit no sampled hits; normal traversal still owns them.
616            /// Only top-level ranked collection may use this hint. Equality
617            /// remains competitive until its own heap resolves stable-ID ties.
618            fn seed_ranked_score(&mut self, _limit: usize) -> Option<Score> { None }
619
620
621            /// Whether this scorer's batches remain useful when every match
622            /// needs a predicate check. Composite scorers can amortize child
623            /// traversal; leaf bitmap production alone may cost more than a
624            /// scalar pass once filtering visits every set bit again.
625            fn supports_filtered_windows(&self) -> bool { false }
626
627            /// Whether compact exact-score batches amortize this scorer's work.
628            fn supports_score_batches(&self) -> bool { false }
629
630            /// Consume a sorted exact prefix, leaving the first unconsumed match.
631            /// Scores correspond to docs[..returned_len]; zero means exhausted.
632            fn fill_score_batch(&mut self, docs: &mut super::docset::DocBatch, scores: &mut ScoreBatch) -> usize {
633                fill_score_batch_scalar(self, docs, scores)
634            }
635
636            /// Probe sorted unique docs[..len] without changing their order.
637            /// Set membership bits index the input and its exact final scores.
638            /// The cursor remains at or beyond the last probe.
639            fn score_batch_matches(&mut self, docs: &super::docset::DocBatch, len: usize,
640                scores: &mut ScoreBatch, matches: &mut ScoreBatchMask) {
641                score_batch_matches_scalar(self, docs, len, scores, matches)
642            }
643
644            /// Whether score-only collection benefits from bounded score windows.
645            /// Positions still require ordinary per-document collection.
646            fn supports_score_windows(&self) -> bool { false }
647
648            /// Add each exact match's final score in a forward-only document window.
649            /// Existing values and membership bits are retained. A nested scorer
650            /// contributes its complete score once, preserving its summation order.
651            /// The cursor ends at the first match after the interval. Previously
652            /// consumed matches stay consumed, just as with `fill_doc_window`.
653            fn accumulate_score_window(
654                &mut self,
655                base: DocId,
656                scores: &mut [Score; super::docset::DOC_WINDOW_SIZE as usize],
657                bits: &mut super::docset::DocWindow,
658            ) {
659                let end = base.saturating_add(super::docset::DOC_WINDOW_SIZE);
660                let mut doc = self.seek(base);
661                while doc < end {
662                    let offset = (doc - base) as usize;
663                    scores[offset] += self.score();
664                    bits[offset / 64] |= 1u64 << (offset % 64);
665                    doc = self.advance();
666                }
667            }
668
669            /// Replace a bounded window of exact scores and membership. Only
670            /// advertised through `supports_score_windows` when it is beneficial.
671            fn fill_score_window(
672                &mut self,
673                base: DocId,
674                scores: &mut [Score; super::docset::DOC_WINDOW_SIZE as usize],
675                bits: &mut super::docset::DocWindow,
676            ) {
677                scores.fill(0.0);
678                bits.fill(0);
679                let end = base.saturating_add(super::docset::DOC_WINDOW_SIZE);
680                let mut doc = self.seek(base);
681                while doc < end {
682                    let offset = (doc - base) as usize;
683                    scores[offset] = self.score();
684                    bits[offset / 64] |= 1u64 << (offset % 64);
685                    doc = self.advance();
686                }
687            }
688
689            /// Move to the next candidate for a two-phase conjunction. Candidates
690            /// may be false positives: the caller must call `confirm_candidate`
691            /// before consuming scores or positions. Ordinary DocSet traversal
692            /// remains exact, including after candidate traversal. The default
693            /// simply advances the exact stream.
694            fn advance_candidate(&mut self) -> DocId {
695                self.advance()
696            }
697
698            /// Seek a candidate at or beyond `target`, without skipping any
699            /// possible exact match. See `advance_candidate` for the protocol.
700            fn seek_candidate(&mut self, target: DocId) -> DocId {
701                self.seek(target)
702            }
703
704            /// Exactly verify the current candidate without advancing it.
705            /// Repeated calls must agree unless cancellation ends the stream.
706            /// Only a true result permits consuming its score/positions.
707            fn confirm_candidate(&mut self) -> bool {
708                self.doc() != crate::structures::TERMINATED
709            }
710
711            /// Get matched positions for the current document (if available)
712            /// Returns (field_id, positions) pairs where positions are encoded as per PositionMode
713            fn matched_positions(&self) -> Option<MatchedPositions> {
714                None
715            }
716
717            /// Exact cardinality supplied only for an explicit top-level ranked
718            /// count request. Ordinary streams and ranked scorers return None.
719            fn exact_ranked_count(&self) -> Option<u64> { None }
720
721            /// Standalone fast path for scorers that wrap an already ranked
722            /// top-k list (text and vector executors). When this query is the top-level
723            /// query of a segment search, the caller may take the ranked list
724            /// directly instead of walking the DocSet and re-collecting it:
725            /// the result must be exactly what a `TopKCollector` of size
726            /// `limit` would produce (score desc, doc id asc, `total_seen`).
727            ///
728            /// Only valid before the first `advance`/`seek`. Default: `None`
729            /// (the scorer must be driven).
730            fn precomputed_top_k(
731                &mut self,
732                limit: usize,
733                collect_positions: bool,
734            ) -> Option<(Vec<super::SearchResult>, u32)> {
735                let _ = (limit, collect_positions);
736                None
737            }
738        }
739    };
740}
741
742#[cfg(not(target_arch = "wasm32"))]
743define_query_traits!(Send + Sync);
744
745#[cfg(target_arch = "wasm32")]
746define_query_traits!();
747
748impl Query for Box<dyn Query> {
749    fn as_doc_bitset_with_options(
750        &self,
751        reader: &SegmentReader,
752        options: &ScorerOptions,
753    ) -> Option<DocBitset> {
754        (**self).as_doc_bitset_with_options(reader, options)
755    }
756    fn scorer<'a>(&self, reader: &'a SegmentReader, limit: usize) -> ScorerFuture<'a> {
757        (**self).scorer(reader, limit)
758    }
759
760    fn count_estimate<'a>(&self, reader: &'a SegmentReader) -> CountFuture<'a> {
761        (**self).count_estimate(reader)
762    }
763
764    fn scorer_with_options<'a>(
765        &self,
766        reader: &'a SegmentReader,
767        limit: usize,
768        options: ScorerOptions,
769    ) -> ScorerFuture<'a> {
770        (**self).scorer_with_options(reader, limit, options)
771    }
772
773    fn candidate_query(&self) -> Result<super::CandidateQuery> {
774        (**self).candidate_query()
775    }
776
777    fn text_terms(&self, out: &mut Vec<(crate::dsl::Field, Vec<u8>)>) {
778        (**self).text_terms(out)
779    }
780
781    fn physical_text_field(&self, reader: &SegmentReader, complete: bool) -> Option<crate::Field> {
782        (**self).physical_text_field(reader, complete)
783    }
784
785    fn decompose(&self) -> QueryDecomposition {
786        (**self).decompose()
787    }
788
789    fn sparse_decomposition(&self) -> QueryDecomposition {
790        (**self).sparse_decomposition()
791    }
792
793    fn is_filter(&self) -> bool {
794        (**self).is_filter()
795    }
796
797    fn as_doc_predicate<'a>(&self, reader: &'a SegmentReader) -> Option<DocPredicate<'a>> {
798        (**self).as_doc_predicate(reader)
799    }
800
801    fn as_doc_bitset(&self, reader: &SegmentReader) -> Option<DocBitset> {
802        (**self).as_doc_bitset(reader)
803    }
804
805    fn should_children(&self) -> Option<&[std::sync::Arc<dyn Query>]> {
806        (**self).should_children()
807    }
808
809    fn count_equivalent_term(&self) -> Option<super::TermQueryInfo> {
810        (**self).count_equivalent_term()
811    }
812
813    fn ranked_count_equivalent_term(&self) -> Option<super::TermQueryInfo> {
814        (**self).ranked_count_equivalent_term()
815    }
816
817    fn supports_ranked_conjunction_count(&self) -> bool {
818        (**self).supports_ranked_conjunction_count()
819    }
820
821    fn bitset_cardinality_estimate(&self, reader: &SegmentReader) -> Option<u64> {
822        (**self).bitset_cardinality_estimate(reader)
823    }
824
825    #[cfg(feature = "sync")]
826    fn scorer_sync<'a>(
827        &self,
828        reader: &'a SegmentReader,
829        limit: usize,
830    ) -> Result<Box<dyn Scorer + 'a>> {
831        (**self).scorer_sync(reader, limit)
832    }
833
834    #[cfg(feature = "sync")]
835    fn scorer_sync_with_options<'a>(
836        &self,
837        reader: &'a SegmentReader,
838        limit: usize,
839        options: ScorerOptions,
840    ) -> Result<Box<dyn Scorer + 'a>> {
841        (**self).scorer_sync_with_options(reader, limit, options)
842    }
843}
844
845/// Empty scorer for terms that don't exist
846pub struct EmptyScorer;
847
848// A document universe is a neutral required cursor for exclusion-only Boolean
849// queries. It adds neither a relevance score nor matched positions.
850impl Scorer for super::AllDocSet {
851    fn score(&self) -> Score {
852        0.0
853    }
854}
855
856impl super::docset::DocSet for EmptyScorer {
857    fn doc(&self) -> DocId {
858        crate::structures::TERMINATED
859    }
860
861    fn advance(&mut self) -> DocId {
862        crate::structures::TERMINATED
863    }
864
865    fn seek(&mut self, _target: DocId) -> DocId {
866        crate::structures::TERMINATED
867    }
868
869    fn size_hint(&self) -> u32 {
870        0
871    }
872}
873
874impl Scorer for EmptyScorer {
875    fn score(&self) -> Score {
876        0.0
877    }
878}
879
880#[cfg(test)]
881mod bitset_tests {
882    use super::DocBitset;
883
884    #[test]
885    fn batched_matches_preserve_neighbours_and_padding_at_every_bit_offset() {
886        for start in 0..64 {
887            for len in 0..=257 {
888                let end = start + len;
889                let mut bits = DocBitset::new(end + 1);
890                if start != 0 {
891                    bits.set(start - 1);
892                }
893                bits.set(end);
894                let values: Vec<_> = (0..len).map(|i| u64::from(i % 3)).collect();
895                bits.insert_matching_values(start, &values, |v| v == 1);
896                for doc in 0..=end {
897                    let expected = (start != 0 && doc == start - 1)
898                        || doc == end
899                        || (doc >= start && doc < end && (doc - start) % 3 == 1);
900                    assert_eq!(bits.contains(doc), expected, "start={start}, len={len}");
901                }
902                assert_eq!(bits.count(), u32::from(start != 0) + 1 + (len + 1) / 3);
903            }
904        }
905    }
906}