Skip to main content

summa_core/query/
docset.rs

1//! DocSet trait and concrete implementations for document iteration.
2//!
3//! `DocSet` is the base abstraction for forward-only cursors over sorted document IDs.
4//! Posting lists, filter results, and scorers all implement this trait.
5//! `PredicatedScorer` wraps a driving Scorer with pushed-down filter predicates.
6
7use std::sync::Arc;
8
9use crate::DocId;
10use crate::structures::TERMINATED;
11
12/// Bounded membership scratch for score-free collection, independent of hit count.
13pub const DOC_WINDOW_WORDS: usize = 64;
14pub const DOC_WINDOW_SIZE: u32 = DOC_WINDOW_WORDS as u32 * 64;
15pub type DocWindow = [u64; DOC_WINDOW_WORDS];
16
17/// Compact score-free membership scratch for sparse candidate streams.
18pub const DOC_BATCH_SIZE: usize = 128;
19pub type DocBatch = [DocId; DOC_BATCH_SIZE];
20
21pub(super) fn fill_batch<D: DocSet + ?Sized>(cursor: &mut D, docs: &mut DocBatch) -> usize {
22    let mut count = 0;
23    let mut doc = cursor.doc();
24    while count < docs.len() && doc != TERMINATED {
25        docs[count] = doc;
26        count += 1;
27        doc = cursor.advance();
28    }
29    count
30}
31
32pub(super) fn retain_batch<D: DocSet + ?Sized>(
33    cursor: &mut D,
34    docs: &mut DocBatch,
35    len: usize,
36) -> usize {
37    assert!(len <= docs.len());
38    let mut kept = 0;
39    for i in 0..len {
40        let doc = docs[i];
41        if cursor.seek(doc) == doc {
42            docs[kept] = doc;
43            kept += 1;
44        }
45    }
46    kept
47}
48
49pub(super) fn fill_window<D: DocSet + ?Sized>(docs: &mut D, base: DocId, bits: &mut DocWindow) {
50    bits.fill(0);
51    let end = base.saturating_add(DOC_WINDOW_SIZE);
52    let mut doc = docs.seek(base);
53    while doc < end {
54        let offset = (doc - base) as usize;
55        bits[offset / 64] |= 1u64 << (offset % 64);
56        doc = docs.advance();
57    }
58}
59
60// ── DocSet trait ─────────────────────────────────────────────────────────
61
62macro_rules! define_docset_trait {
63    ($($send_bounds:tt)*) => {
64        /// Forward-only cursor over sorted document IDs.
65        ///
66        /// This is the base iteration abstraction. Posting lists, filter cursors,
67        /// and scorers all implement this trait.
68        pub trait DocSet: $($send_bounds)* {
69            /// Current document ID, or [`TERMINATED`] if exhausted.
70            fn doc(&self) -> DocId;
71
72            /// Advance to the next document. Returns the new doc ID or [`TERMINATED`].
73            fn advance(&mut self) -> DocId;
74
75            /// Seek to the first document >= `target`. Returns doc ID or [`TERMINATED`].
76            fn seek(&mut self, target: DocId) -> DocId {
77                let mut doc = self.doc();
78                while doc < target {
79                    doc = self.advance();
80                }
81                doc
82            }
83
84            /// Estimated number of remaining documents.
85            fn size_hint(&self) -> u32;
86
87            /// Whether compact membership batches amortize this cursor's work.
88            fn supports_doc_batches(&self) -> bool { false }
89
90            /// Consume up to 128 exact sorted matches starting at doc(), leaving
91            /// the cursor on the first unconsumed match. Zero means exhausted.
92            fn fill_doc_batch(&mut self, docs: &mut DocBatch) -> usize {
93                fill_batch(self, docs)
94            }
95
96            /// Retain matches from the sorted unique prefix docs[..len]. The
97            /// cursor remains at or beyond the last probe; earlier IDs stay consumed.
98            fn retain_doc_batch(&mut self, docs: &mut DocBatch, len: usize) -> usize {
99                retain_batch(self, docs, len)
100            }
101
102            /// Whether this cursor benefits from bounded score-free membership batches.
103            fn supports_doc_windows(&self) -> bool { false }
104
105            /// Consume exact matches in `[base, base + DOC_WINDOW_SIZE)`, replacing
106            /// `bits`, and leave the cursor on its first match at or after the end.
107            /// Calls are forward-only; previously consumed documents stay consumed.
108            fn fill_doc_window(&mut self, base: DocId, bits: &mut DocWindow) {
109                fill_window(self, base, bits);
110            }
111
112        }
113    };
114}
115
116#[cfg(not(target_arch = "wasm32"))]
117define_docset_trait!(Send + Sync);
118
119#[cfg(target_arch = "wasm32")]
120define_docset_trait!();
121
122/// Owned bitmap cursor for already materialized membership. It keeps complete
123/// unions compact and serves the same bounded windows as posting cursors.
124pub(super) struct BitsetDocSet {
125    bits: super::DocBitset,
126    current: DocId,
127    count: u32,
128}
129
130impl BitsetDocSet {
131    pub(super) fn new(bits: super::DocBitset) -> Self {
132        let current = bits.next_set_bit(0).unwrap_or(TERMINATED);
133        let count = bits.count();
134        Self {
135            bits,
136            current,
137            count,
138        }
139    }
140}
141
142impl DocSet for BitsetDocSet {
143    fn doc(&self) -> DocId {
144        self.current
145    }
146
147    fn advance(&mut self) -> DocId {
148        self.seek(self.current.saturating_add(1))
149    }
150
151    fn seek(&mut self, target: DocId) -> DocId {
152        if target > self.current {
153            self.current = self.bits.next_set_bit(target).unwrap_or(TERMINATED);
154        }
155        self.current
156    }
157
158    fn size_hint(&self) -> u32 {
159        self.count
160    }
161
162    fn supports_doc_windows(&self) -> bool {
163        true
164    }
165
166    fn fill_doc_window(&mut self, base: DocId, window: &mut DocWindow) {
167        window.fill(0);
168        let end = base.saturating_add(DOC_WINDOW_SIZE);
169        let start = base.max(self.current);
170        if start >= end {
171            return;
172        }
173        let first = base as usize / 64;
174        let shift = base % 64;
175        for (offset, word) in window.iter_mut().enumerate() {
176            let index = first + offset;
177            *word = self.bits.bits.get(index).copied().unwrap_or(0) >> shift;
178            if shift != 0 {
179                *word |= self.bits.bits.get(index + 1).copied().unwrap_or(0) << (64 - shift);
180            }
181        }
182        let consumed = (start - base) as usize;
183        window[..consumed / 64].fill(0);
184        window[consumed / 64] &= u64::MAX << (consumed % 64);
185        self.current = self.bits.next_set_bit(end).unwrap_or(TERMINATED);
186    }
187}
188
189// ── DocSet for Box<dyn DocSet> ───────────────────────────────────────────
190
191impl DocSet for Box<dyn DocSet + '_> {
192    fn supports_doc_batches(&self) -> bool {
193        (**self).supports_doc_batches()
194    }
195    fn fill_doc_batch(&mut self, docs: &mut DocBatch) -> usize {
196        (**self).fill_doc_batch(docs)
197    }
198    fn retain_doc_batch(&mut self, docs: &mut DocBatch, len: usize) -> usize {
199        (**self).retain_doc_batch(docs, len)
200    }
201    fn supports_doc_windows(&self) -> bool {
202        (**self).supports_doc_windows()
203    }
204    fn fill_doc_window(&mut self, base: DocId, bits: &mut DocWindow) {
205        (**self).fill_doc_window(base, bits);
206    }
207    #[inline]
208    fn doc(&self) -> DocId {
209        (**self).doc()
210    }
211    #[inline]
212    fn advance(&mut self) -> DocId {
213        (**self).advance()
214    }
215    #[inline]
216    fn seek(&mut self, target: DocId) -> DocId {
217        (**self).seek(target)
218    }
219    #[inline]
220    fn size_hint(&self) -> u32 {
221        (**self).size_hint()
222    }
223}
224
225// ── SortedVecDocSet ──────────────────────────────────────────────────────
226
227/// DocSet backed by a sorted `Vec<u32>`. Binary search for seek.
228pub struct SortedVecDocSet {
229    docs: Arc<Vec<u32>>,
230    pos: usize,
231}
232
233impl SortedVecDocSet {
234    pub fn new(docs: Arc<Vec<u32>>) -> Self {
235        Self { docs, pos: 0 }
236    }
237}
238
239impl DocSet for SortedVecDocSet {
240    #[inline]
241    fn doc(&self) -> DocId {
242        self.docs.get(self.pos).copied().unwrap_or(TERMINATED)
243    }
244
245    #[inline]
246    fn advance(&mut self) -> DocId {
247        if self.pos < self.docs.len() {
248            self.pos += 1;
249        }
250        self.doc()
251    }
252
253    fn seek(&mut self, target: DocId) -> DocId {
254        if self.pos >= self.docs.len() {
255            return TERMINATED;
256        }
257        let remaining = &self.docs[self.pos..];
258        match remaining.binary_search(&target) {
259            Ok(offset) => {
260                self.pos += offset;
261                self.docs[self.pos]
262            }
263            Err(offset) => {
264                self.pos += offset;
265                self.doc()
266            }
267        }
268    }
269
270    fn size_hint(&self) -> u32 {
271        self.docs.len().saturating_sub(self.pos) as u32
272    }
273
274    fn supports_doc_batches(&self) -> bool {
275        true
276    }
277
278    fn fill_doc_batch(&mut self, docs: &mut DocBatch) -> usize {
279        let count = (self.docs.len() - self.pos).min(docs.len());
280        docs[..count].copy_from_slice(&self.docs[self.pos..self.pos + count]);
281        self.pos += count;
282        count
283    }
284}
285
286// ── IntersectionDocSet ───────────────────────────────────────────────────
287
288/// DocSet that yields the intersection of two DocSets.
289pub struct IntersectionDocSet<A: DocSet, B: DocSet> {
290    a: A,
291    b: B,
292}
293
294impl<A: DocSet, B: DocSet> IntersectionDocSet<A, B> {
295    pub fn new(mut a: A, mut b: B) -> Self {
296        // Align both on the first common doc
297        let mut da = a.doc();
298        let mut db = b.doc();
299        loop {
300            if da == TERMINATED || db == TERMINATED {
301                break;
302            }
303            if da == db {
304                break;
305            }
306            if da < db {
307                da = a.seek(db);
308            } else {
309                db = b.seek(da);
310            }
311        }
312        Self { a, b }
313    }
314}
315
316impl<A: DocSet, B: DocSet> DocSet for IntersectionDocSet<A, B> {
317    fn doc(&self) -> DocId {
318        let da = self.a.doc();
319        if da == TERMINATED || self.b.doc() == TERMINATED {
320            TERMINATED
321        } else {
322            da
323        }
324    }
325
326    fn advance(&mut self) -> DocId {
327        let mut da = self.a.advance();
328        let mut db = self.b.doc();
329        loop {
330            if da == TERMINATED || db == TERMINATED {
331                return TERMINATED;
332            }
333            if da == db {
334                return da;
335            }
336            if da < db {
337                da = self.a.seek(db);
338            } else {
339                db = self.b.seek(da);
340            }
341        }
342    }
343
344    fn seek(&mut self, target: DocId) -> DocId {
345        let mut da = self.a.seek(target);
346        let mut db = self.b.seek(target);
347        loop {
348            if da == TERMINATED || db == TERMINATED {
349                return TERMINATED;
350            }
351            if da == db {
352                return da;
353            }
354            if da < db {
355                da = self.a.seek(db);
356            } else {
357                db = self.b.seek(da);
358            }
359        }
360    }
361
362    fn size_hint(&self) -> u32 {
363        self.a.size_hint().min(self.b.size_hint())
364    }
365}
366
367// ── AllDocSet ────────────────────────────────────────────────────────────
368
369/// DocSet that yields all documents 0..num_docs.
370pub struct AllDocSet {
371    current: u32,
372    num_docs: u32,
373}
374
375impl AllDocSet {
376    pub fn new(num_docs: u32) -> Self {
377        Self {
378            current: 0,
379            num_docs,
380        }
381    }
382}
383
384impl DocSet for AllDocSet {
385    #[inline]
386    fn doc(&self) -> DocId {
387        if self.current >= self.num_docs {
388            TERMINATED
389        } else {
390            self.current
391        }
392    }
393
394    #[inline]
395    fn advance(&mut self) -> DocId {
396        if self.current < self.num_docs {
397            self.current += 1;
398        }
399        self.doc()
400    }
401
402    #[inline]
403    fn seek(&mut self, target: DocId) -> DocId {
404        self.current = self.current.max(target);
405        self.doc()
406    }
407
408    fn size_hint(&self) -> u32 {
409        self.num_docs.saturating_sub(self.current)
410    }
411}
412
413// ── EmptyDocSet ──────────────────────────────────────────────────────────
414
415/// DocSet that is always empty.
416pub struct EmptyDocSet;
417
418impl DocSet for EmptyDocSet {
419    #[inline]
420    fn doc(&self) -> DocId {
421        TERMINATED
422    }
423    #[inline]
424    fn advance(&mut self) -> DocId {
425        TERMINATED
426    }
427    #[inline]
428    fn seek(&mut self, _target: DocId) -> DocId {
429        TERMINATED
430    }
431    fn size_hint(&self) -> u32 {
432        0
433    }
434}
435
436// ── PredicatedScorer ─────────────────────────────────────────────────────
437
438/// Wraps a driving Scorer with filter conditions pushed down.
439///
440/// Used by the query planner to flip iteration order: the SHOULD scorer
441/// drives and MUST/MUST_NOT clauses are checked per-doc via:
442/// - O(1) predicate closures (e.g., fast-field range checks)
443/// - seek()-based verifier scorers (e.g., TermQuery posting list lookups)
444///
445/// Verifier scorers contribute their actual per-doc score (e.g., BM25).
446pub struct PredicatedScorer<'a> {
447    /// Driving scorer (typically SHOULD clauses)
448    driver: Box<dyn super::Scorer + 'a>,
449    /// O(1) predicate checks (from filter queries like RangeQuery, or negated MUST_NOT)
450    predicates: Vec<super::DocPredicate<'a>>,
451    /// MUST scorers verified via seek() — preserves per-doc scoring
452    must_verifiers: Vec<Box<dyn super::Scorer + 'a>>,
453    /// MUST_NOT scorers — docs are excluded if these land on them
454    must_not_verifiers: Vec<Box<dyn super::Scorer + 'a>>,
455}
456
457impl<'a> PredicatedScorer<'a> {
458    pub fn new(
459        driver: Box<dyn super::Scorer + 'a>,
460        predicates: Vec<super::DocPredicate<'a>>,
461        must_verifiers: Vec<Box<dyn super::Scorer + 'a>>,
462        must_not_verifiers: Vec<Box<dyn super::Scorer + 'a>>,
463    ) -> Self {
464        let mut s = Self {
465            driver,
466            predicates,
467            must_verifiers,
468            must_not_verifiers,
469        };
470        // Position on first matching doc
471        s.skip_non_matching();
472        s
473    }
474
475    /// Check whether `doc` passes all filter conditions.
476    #[inline]
477    fn check_filters(&mut self, doc: DocId) -> bool {
478        // O(1) predicate checks first (cheapest)
479        if !self.predicates.iter().all(|p| p(doc)) {
480            return false;
481        }
482        // MUST verifiers: seek to doc, must land exactly on it
483        if !self.must_verifiers.iter_mut().all(|s| s.seek(doc) == doc) {
484            return false;
485        }
486        // MUST_NOT verifiers: seek to doc, must NOT land on it
487        self.must_not_verifiers
488            .iter_mut()
489            .all(|s| s.seek(doc) != doc)
490    }
491
492    /// Filter the driver's batch before moving verifiers to the next window.
493    /// MUST scores retain the same left-to-right sum as scalar `score()`.
494    fn filter_window(
495        &mut self,
496        base: DocId,
497        bits: &mut DocWindow,
498        mut scores: Option<&mut [crate::Score; DOC_WINDOW_SIZE as usize]>,
499    ) {
500        for (word_index, word) in bits.iter_mut().enumerate() {
501            let mut remaining = *word;
502            while remaining != 0 {
503                let bit = remaining.trailing_zeros() as usize;
504                let offset = word_index * 64 + bit;
505                if self.check_filters(base + offset as u32) {
506                    if let Some(scores) = scores.as_deref_mut() {
507                        for verifier in &self.must_verifiers {
508                            scores[offset] += verifier.score();
509                        }
510                    }
511                } else {
512                    *word &= !(1u64 << bit);
513                }
514                remaining &= remaining - 1;
515            }
516        }
517        self.skip_non_matching();
518    }
519
520    /// Advance driver past non-matching docs.
521    fn skip_non_matching(&mut self) -> DocId {
522        let mut doc = self.driver.doc();
523        while doc != TERMINATED && !self.check_filters(doc) {
524            doc = self.driver.advance();
525        }
526        doc
527    }
528}
529
530impl DocSet for PredicatedScorer<'_> {
531    fn supports_doc_windows(&self) -> bool {
532        self.driver.supports_filtered_windows() && self.driver.supports_doc_windows()
533    }
534
535    fn fill_doc_window(&mut self, base: DocId, bits: &mut DocWindow) {
536        self.driver.fill_doc_window(base, bits);
537        self.filter_window(base, bits, None);
538    }
539
540    fn doc(&self) -> DocId {
541        self.driver.doc()
542    }
543
544    fn advance(&mut self) -> DocId {
545        self.driver.advance();
546        self.skip_non_matching()
547    }
548
549    fn seek(&mut self, target: DocId) -> DocId {
550        self.driver.seek(target);
551        self.skip_non_matching()
552    }
553
554    fn size_hint(&self) -> u32 {
555        self.driver.size_hint()
556    }
557}
558
559impl super::Scorer for PredicatedScorer<'_> {
560    fn supports_filtered_windows(&self) -> bool {
561        self.driver.supports_filtered_windows()
562    }
563
564    fn supports_score_windows(&self) -> bool {
565        self.driver.supports_filtered_windows() && self.driver.supports_score_windows()
566    }
567
568    fn fill_score_window(
569        &mut self,
570        base: DocId,
571        scores: &mut [crate::Score; DOC_WINDOW_SIZE as usize],
572        bits: &mut DocWindow,
573    ) {
574        self.driver.fill_score_window(base, scores, bits);
575        self.filter_window(base, bits, Some(scores));
576    }
577
578    fn score(&self) -> crate::Score {
579        let mut total = self.driver.score();
580        for v in &self.must_verifiers {
581            total += v.score();
582        }
583        total
584    }
585
586    fn matched_positions(&self) -> Option<super::MatchedPositions> {
587        let mut all: super::MatchedPositions = Vec::new();
588        if let Some(p) = self.driver.matched_positions() {
589            all.extend(p);
590        }
591        for v in &self.must_verifiers {
592            if let Some(p) = v.matched_positions() {
593                all.extend(p);
594            }
595        }
596        if all.is_empty() { None } else { Some(all) }
597    }
598}
599
600// ── Tests ────────────────────────────────────────────────────────────────
601
602#[cfg(test)]
603mod tests {
604    use super::*;
605
606    #[test]
607    fn bitmap_windows_preserve_unaligned_bases_consumed_prefixes_and_tail_seeks() {
608        for stride in [1, 2, 63, 127] {
609            let expected: Vec<u32> = (0..12291).step_by(stride).collect();
610            let mut bits = super::super::DocBitset::new(12291);
611            for &doc in &expected {
612                bits.set(doc);
613            }
614            for base in [
615                0, 1, 63, 64, 4094, 4095, 4096, 8191, 10000, 12290, 12291, TERMINATED,
616            ] {
617                let mut cursor = BitsetDocSet::new(bits.clone());
618                cursor.seek(base.saturating_add(17));
619                let previous = cursor.doc();
620                let end = base.saturating_add(DOC_WINDOW_SIZE);
621                let mut actual = [u64::MAX; DOC_WINDOW_WORDS];
622                cursor.fill_doc_window(base, &mut actual);
623                let mut wanted = [0; DOC_WINDOW_WORDS];
624                for &doc in &expected {
625                    if doc >= previous.max(base) && doc < end {
626                        let relative = (doc - base) as usize;
627                        wanted[relative / 64] |= 1 << (relative % 64);
628                    }
629                }
630                assert_eq!(actual, wanted, "stride={stride}, base={base}");
631                assert_eq!(
632                    cursor.doc(),
633                    expected
634                        .iter()
635                        .copied()
636                        .find(|&doc| doc >= previous.max(end))
637                        .unwrap_or(TERMINATED)
638                );
639            }
640            let mut cursor = BitsetDocSet::new(bits);
641            let mut actual = Vec::new();
642            while cursor.doc() != TERMINATED {
643                let base = cursor.doc();
644                let mut words = [0; DOC_WINDOW_WORDS];
645                cursor.fill_doc_window(base, &mut words);
646                for (i, &word) in words.iter().enumerate() {
647                    let mut word = word;
648                    while word != 0 {
649                        actual.push(base + i as u32 * 64 + word.trailing_zeros());
650                        word &= word - 1;
651                    }
652                }
653            }
654            assert_eq!(actual, expected);
655            assert_eq!(cursor.advance(), TERMINATED);
656            assert_eq!(cursor.seek(0), TERMINATED);
657        }
658    }
659
660    struct WindowScorer(SortedVecDocSet, f32, bool);
661
662    impl DocSet for WindowScorer {
663        fn doc(&self) -> DocId {
664            self.0.doc()
665        }
666        fn advance(&mut self) -> DocId {
667            self.0.advance()
668        }
669        fn seek(&mut self, target: DocId) -> DocId {
670            self.0.seek(target)
671        }
672        fn size_hint(&self) -> u32 {
673            self.0.size_hint()
674        }
675        fn supports_doc_windows(&self) -> bool {
676            true
677        }
678    }
679    impl crate::query::Scorer for WindowScorer {
680        fn supports_filtered_windows(&self) -> bool {
681            self.2
682        }
683        fn score(&self) -> f32 {
684            self.1 + (self.doc() % 7) as f32 * 0.125
685        }
686        fn supports_score_windows(&self) -> bool {
687            true
688        }
689    }
690
691    #[test]
692    fn filtering_keeps_leaf_windows_scalar_and_composite_windows_batched() {
693        use crate::query::Scorer;
694        for beneficial in [false, true] {
695            let driver = WindowScorer(
696                SortedVecDocSet::new(Arc::new(vec![1, 2, 3])),
697                1.0,
698                beneficial,
699            );
700            assert!(driver.supports_doc_windows() && driver.supports_score_windows());
701            let wrapped = PredicatedScorer::new(Box::new(driver), vec![], vec![], vec![]);
702            let wrapped = PredicatedScorer::new(Box::new(wrapped), vec![], vec![], vec![]);
703            assert_eq!(wrapped.supports_doc_windows(), beneficial);
704            assert_eq!(wrapped.supports_score_windows(), beneficial);
705        }
706    }
707
708    #[test]
709    fn filtered_windows_preserve_scores_membership_and_forward_cursor() {
710        use crate::query::Scorer;
711        let docs = Arc::new(vec![
712            0,
713            1,
714            2,
715            63,
716            64,
717            65,
718            4095,
719            4096,
720            4097,
721            8192,
722            8193,
723            u32::MAX - 2,
724            u32::MAX - 1,
725        ]);
726        let make = |required| {
727            let scorer = |docs, score| {
728                Box::new(WindowScorer(SortedVecDocSet::new(docs), score, true)) as Box<dyn Scorer>
729            };
730            PredicatedScorer::new(
731                scorer(docs.clone(), 0.1),
732                vec![Box::new(|doc| doc != 64 && doc != 8192)],
733                if required {
734                    vec![
735                        scorer(
736                            Arc::new(docs.iter().copied().filter(|doc| *doc != 4096).collect()),
737                            0.3,
738                        ),
739                        scorer(docs.clone(), 0.7),
740                    ]
741                } else {
742                    vec![]
743                },
744                vec![scorer(Arc::new(vec![1, 65, 4097]), 0.0)],
745            )
746        };
747        for required in [false, true] {
748            let mut scalar = make(required);
749            let mut expected = Vec::new();
750            while scalar.doc() != TERMINATED {
751                expected.push((scalar.doc(), scalar.score().to_bits()));
752                scalar.advance();
753            }
754            for scored in [false, true] {
755                let mut batched = make(required);
756                assert!(batched.supports_doc_windows());
757                assert!(batched.supports_score_windows());
758                let mut scores = [f32::NAN; DOC_WINDOW_SIZE as usize];
759                let mut bits = [u64::MAX; DOC_WINDOW_WORDS];
760                let mut actual = Vec::new();
761                for base in [0, 0, 4096, 8192, 12288, u32::MAX - 4096, TERMINATED] {
762                    if scored {
763                        batched.fill_score_window(base, &mut scores, &mut bits);
764                    } else {
765                        batched.fill_doc_window(base, &mut bits);
766                    }
767                    for (word, &mask) in bits.iter().enumerate() {
768                        let mut mask = mask;
769                        while mask != 0 {
770                            let offset = word * 64 + mask.trailing_zeros() as usize;
771                            let doc = base + offset as u32;
772                            actual.push((doc, if scored { scores[offset].to_bits() } else { 0 }));
773                            mask &= mask - 1;
774                        }
775                    }
776                    let next = expected
777                        .iter()
778                        .find(|(doc, _)| *doc >= base.saturating_add(DOC_WINDOW_SIZE));
779                    assert_eq!(batched.doc(), next.map_or(TERMINATED, |(doc, _)| *doc));
780                    if let Some((_, score)) = next {
781                        assert_eq!(batched.score().to_bits(), *score);
782                    }
783                }
784                let expected: Vec<_> = expected
785                    .iter()
786                    .map(|&(doc, score)| (doc, if scored { score } else { 0 }))
787                    .collect();
788                assert_eq!(actual, expected);
789            }
790        }
791    }
792
793    #[test]
794    fn test_sorted_vec_docset_basic() {
795        let docs = Arc::new(vec![1, 3, 5, 7, 9]);
796        let mut ds = SortedVecDocSet::new(docs);
797
798        assert_eq!(ds.doc(), 1);
799        assert_eq!(ds.advance(), 3);
800        assert_eq!(ds.advance(), 5);
801        assert_eq!(ds.seek(7), 7);
802        assert_eq!(ds.advance(), 9);
803        assert_eq!(ds.advance(), TERMINATED);
804        assert_eq!(ds.doc(), TERMINATED);
805    }
806
807    #[test]
808    fn test_sorted_vec_docset_seek_past() {
809        let docs = Arc::new(vec![1, 5, 10, 20]);
810        let mut ds = SortedVecDocSet::new(docs);
811
812        assert_eq!(ds.seek(3), 5);
813        assert_eq!(ds.seek(15), 20);
814        assert_eq!(ds.seek(21), TERMINATED);
815    }
816
817    #[test]
818    fn test_sorted_vec_docset_empty() {
819        let docs = Arc::new(vec![]);
820        let ds = SortedVecDocSet::new(docs);
821        assert_eq!(ds.doc(), TERMINATED);
822    }
823
824    #[test]
825    fn test_all_docset() {
826        let mut ds = AllDocSet::new(3);
827        assert_eq!(ds.doc(), 0);
828        assert_eq!(ds.advance(), 1);
829        assert_eq!(ds.advance(), 2);
830        assert_eq!(ds.advance(), TERMINATED);
831    }
832
833    #[test]
834    fn test_all_docset_seek() {
835        let mut ds = AllDocSet::new(10);
836        assert_eq!(ds.seek(5), 5);
837        assert_eq!(ds.seek(9), 9);
838        assert_eq!(ds.seek(10), TERMINATED);
839    }
840
841    #[test]
842    fn test_empty_docset() {
843        let mut ds = EmptyDocSet;
844        assert_eq!(ds.doc(), TERMINATED);
845        assert_eq!(ds.advance(), TERMINATED);
846        assert_eq!(ds.seek(5), TERMINATED);
847        assert_eq!(ds.size_hint(), 0);
848    }
849
850    #[test]
851    fn test_intersection_docset() {
852        let a = SortedVecDocSet::new(Arc::new(vec![1, 3, 5, 7, 9]));
853        let b = SortedVecDocSet::new(Arc::new(vec![2, 3, 5, 8, 9, 10]));
854        let mut isect = IntersectionDocSet::new(a, b);
855
856        assert_eq!(isect.doc(), 3);
857        assert_eq!(isect.advance(), 5);
858        assert_eq!(isect.advance(), 9);
859        assert_eq!(isect.advance(), TERMINATED);
860    }
861
862    #[test]
863    fn test_intersection_docset_empty() {
864        let a = SortedVecDocSet::new(Arc::new(vec![1, 3, 5]));
865        let b = SortedVecDocSet::new(Arc::new(vec![2, 4, 6]));
866        let isect = IntersectionDocSet::new(a, b);
867        assert_eq!(isect.doc(), TERMINATED);
868    }
869
870    #[test]
871    fn test_intersection_docset_seek() {
872        let a = SortedVecDocSet::new(Arc::new(vec![1, 5, 10, 20, 30]));
873        let b = SortedVecDocSet::new(Arc::new(vec![5, 10, 15, 20, 25, 30]));
874        let mut isect = IntersectionDocSet::new(a, b);
875
876        assert_eq!(isect.doc(), 5);
877        assert_eq!(isect.seek(15), 20);
878        assert_eq!(isect.advance(), 30);
879        assert_eq!(isect.advance(), TERMINATED);
880    }
881
882    #[test]
883    fn test_size_hint() {
884        let docs = Arc::new(vec![1, 2, 3, 4, 5]);
885        let mut ds = SortedVecDocSet::new(docs);
886        assert_eq!(ds.size_hint(), 5);
887        ds.advance();
888        assert_eq!(ds.size_hint(), 4);
889        ds.seek(4);
890        assert_eq!(ds.size_hint(), 2); // pos=3, remaining: [4, 5]
891    }
892}