Skip to main content

hermes_core/query/
boolean.rs

1//! Boolean query with MUST, SHOULD, and MUST_NOT clauses
2
3use std::sync::Arc;
4
5use crate::segment::SegmentReader;
6use crate::structures::TERMINATED;
7use crate::{DocId, Score};
8
9use super::planner::{
10    build_combined_bitset, build_sparse_bmp_results, build_sparse_bmp_results_filtered,
11    build_sparse_maxscore_executor, chain_predicates, combine_sparse_results, compute_idf,
12    extract_all_sparse_infos, finish_text_maxscore, prepare_per_field_grouping,
13    prepare_text_maxscore,
14};
15use super::{CountFuture, EmptyScorer, GlobalStats, Query, Scorer, ScorerFuture};
16
17/// Boolean query with MUST, SHOULD, and MUST_NOT clauses
18///
19/// When all clauses are SHOULD term queries on the same field, automatically
20/// uses MaxScore optimization for efficient top-k retrieval.
21#[derive(Default, Clone)]
22pub struct BooleanQuery {
23    pub must: Vec<Arc<dyn Query>>,
24    pub should: Vec<Arc<dyn Query>>,
25    pub must_not: Vec<Arc<dyn Query>>,
26    /// Optional global statistics for cross-segment IDF
27    global_stats: Option<Arc<GlobalStats>>,
28}
29
30impl std::fmt::Debug for BooleanQuery {
31    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
32        f.debug_struct("BooleanQuery")
33            .field("must_count", &self.must.len())
34            .field("should_count", &self.should.len())
35            .field("must_not_count", &self.must_not.len())
36            .field("has_global_stats", &self.global_stats.is_some())
37            .finish()
38    }
39}
40
41impl std::fmt::Display for BooleanQuery {
42    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
43        write!(f, "Boolean(")?;
44        let mut first = true;
45        for q in &self.must {
46            if !first {
47                write!(f, " ")?;
48            }
49            write!(f, "+{}", q)?;
50            first = false;
51        }
52        for q in &self.should {
53            if !first {
54                write!(f, " ")?;
55            }
56            write!(f, "{}", q)?;
57            first = false;
58        }
59        for q in &self.must_not {
60            if !first {
61                write!(f, " ")?;
62            }
63            write!(f, "-{}", q)?;
64            first = false;
65        }
66        write!(f, ")")
67    }
68}
69
70impl BooleanQuery {
71    pub fn new() -> Self {
72        Self::default()
73    }
74
75    pub fn must(mut self, query: impl Query + 'static) -> Self {
76        self.must.push(Arc::new(query));
77        self
78    }
79
80    pub fn should(mut self, query: impl Query + 'static) -> Self {
81        self.should.push(Arc::new(query));
82        self
83    }
84
85    pub fn must_not(mut self, query: impl Query + 'static) -> Self {
86        self.must_not.push(Arc::new(query));
87        self
88    }
89
90    /// Set global statistics for cross-segment IDF
91    pub fn with_global_stats(mut self, stats: Arc<GlobalStats>) -> Self {
92        self.global_stats = Some(stats);
93        self
94    }
95}
96
97/// Build a SHOULD-only scorer from a vec of optimized scorers.
98fn build_should_scorer<'a>(scorers: Vec<Box<dyn Scorer + 'a>>) -> Box<dyn Scorer + 'a> {
99    if scorers.is_empty() {
100        return Box::new(EmptyScorer);
101    }
102    if scorers.len() == 1 {
103        return scorers.into_iter().next().unwrap();
104    }
105    let mut scorer = BooleanScorer {
106        must: vec![],
107        should: scorers,
108        must_not: vec![],
109        current_doc: 0,
110    };
111    scorer.current_doc = scorer.find_next_match();
112    Box::new(scorer)
113}
114
115// ── Planner macro ────────────────────────────────────────────────────────
116//
117// Unified planner for both async and sync paths.  Parameterised on:
118//   $scorer_fn      – scorer | scorer_sync
119//   $get_postings_fn – get_postings | get_postings_sync
120//   $execute_fn     – execute | execute_sync
121//   $($aw)*         – .await  (present for async, absent for sync)
122//
123// Decision order:
124//   1. Single-clause unwrap
125//   2. Pure OR → text MaxScore | sparse MaxScore | per-field MaxScore
126//   3. Filter push-down → predicate-aware sparse MaxScore | PredicatedScorer
127//   4. Standard BooleanScorer fallback
128macro_rules! boolean_plan {
129    ($must:expr, $should:expr, $must_not:expr, $global_stats:expr,
130     $reader:expr, $limit:expr,
131     $scorer_fn:ident, $get_postings_fn:ident, $execute_fn:ident
132     $(, $aw:tt)*) => {{
133        let must: &[Arc<dyn Query>] = &$must;
134        let should_all: &[Arc<dyn Query>] = &$should;
135        let must_not: &[Arc<dyn Query>] = &$must_not;
136        let global_stats: Option<&Arc<GlobalStats>> = $global_stats;
137        let reader: &SegmentReader = $reader;
138        let limit: usize = $limit;
139
140        // Cap SHOULD clauses to MAX_QUERY_TERMS, but only count queries that need
141        // posting-list cursors. Fast-field predicates (O(1) per doc) are exempt.
142        let should_capped: Vec<Arc<dyn Query>>;
143        let should: &[Arc<dyn Query>] = if should_all.len() > super::MAX_QUERY_TERMS {
144            let is_predicate: Vec<bool> = should_all
145                .iter()
146                .map(|q| q.is_filter() || q.as_doc_predicate(reader).is_some())
147                .collect();
148            let cursor_count = is_predicate.iter().filter(|&&p| !p).count();
149
150            if cursor_count > super::MAX_QUERY_TERMS {
151                let mut kept = Vec::with_capacity(should_all.len());
152                let mut cursor_kept = 0usize;
153                for (q, &is_pred) in should_all.iter().zip(is_predicate.iter()) {
154                    if is_pred {
155                        kept.push(q.clone());
156                    } else if cursor_kept < super::MAX_QUERY_TERMS {
157                        kept.push(q.clone());
158                        cursor_kept += 1;
159                    }
160                }
161                log::debug!(
162                    "BooleanQuery: capping cursor SHOULD from {} to {} ({} fast-field predicates exempt)",
163                    cursor_count,
164                    super::MAX_QUERY_TERMS,
165                    kept.len() - cursor_kept,
166                );
167                should_capped = kept;
168                &should_capped
169            } else {
170                log::debug!(
171                    "BooleanQuery: {} SHOULD clauses OK ({} need cursors, {} fast-field predicates)",
172                    should_all.len(),
173                    cursor_count,
174                    should_all.len() - cursor_count,
175                );
176                should_all
177            }
178        } else {
179            should_all
180        };
181
182        // ── 1. Single-clause optimisation ────────────────────────────────
183        if must_not.is_empty() {
184            if must.len() == 1 && should.is_empty() {
185                return must[0].$scorer_fn(reader, limit) $(.  $aw)* ;
186            }
187            if should.len() == 1 && must.is_empty() {
188                return should[0].$scorer_fn(reader, limit) $(. $aw)* ;
189            }
190        }
191
192        // ── 2. Pure OR → MaxScore optimisations ──────────────────────────
193        if must.is_empty() && must_not.is_empty() && should.len() >= 2 {
194            // 2a. Text MaxScore (single-field, all term queries)
195            if let Some((mut infos, text_field, avg_field_len, num_docs)) =
196                prepare_text_maxscore(should, reader, global_stats)
197            {
198                let mut posting_lists = Vec::with_capacity(infos.len());
199                for info in infos.drain(..) {
200                    if let Some(pl) = reader.$get_postings_fn(info.field, &info.term)
201                        $(. $aw)* ?
202                    {
203                        let idf = compute_idf(&pl, info.field, &info.term, num_docs, global_stats);
204                        posting_lists.push((pl, idf));
205                    }
206                }
207                let shared_threshold = std::cell::Cell::new(0.0f32);
208                return finish_text_maxscore(
209                    posting_lists,
210                    avg_field_len,
211                    limit,
212                    &shared_threshold,
213                    reader.schema().index_label(),
214                    reader.schema().get_field_name(text_field).unwrap_or("?"),
215                );
216            }
217
218            // 2b. Sparse (single-field, all sparse term queries)
219            // Auto-detect: BMP executor if field has BMP index, else MaxScore
220            if let Some(infos) = extract_all_sparse_infos(should) {
221                if let Some((raw, info)) =
222                    build_sparse_bmp_results(&infos, reader, limit)
223                {
224                    return Ok(combine_sparse_results(raw, info.combiner, info.field, limit));
225                }
226                if let Some((executor, info)) =
227                    build_sparse_maxscore_executor(&infos, reader, limit, None)
228                {
229                    let raw = executor.$execute_fn() $(. $aw)* ?;
230                    return Ok(combine_sparse_results(raw, info.combiner, info.field, limit));
231                }
232            }
233
234            // 2c. Per-field text MaxScore (multi-field term grouping)
235            if let Some(grouping) = prepare_per_field_grouping(should, reader, limit, global_stats)
236            {
237                let mut scorers: Vec<Box<dyn Scorer + '_>> = Vec::new();
238                // Query-local cross-group threshold seeding (see finish_text_maxscore)
239                let shared_threshold = std::cell::Cell::new(0.0f32);
240                for (field, avg_field_len, infos) in &grouping.multi_term_groups {
241                    let mut posting_lists = Vec::with_capacity(infos.len());
242                    for info in infos {
243                        if let Some(pl) = reader.$get_postings_fn(info.field, &info.term)
244                            $(. $aw)* ?
245                        {
246                            let idf = compute_idf(
247                                &pl, *field, &info.term, grouping.num_docs, global_stats,
248                            );
249                            posting_lists.push((pl, idf));
250                        }
251                    }
252                    if !posting_lists.is_empty() {
253                        scorers.push(finish_text_maxscore(
254                            posting_lists,
255                            *avg_field_len,
256                            grouping.per_field_limit,
257                            &shared_threshold,
258                            reader.schema().index_label(),
259                            reader.schema().get_field_name(*field).unwrap_or("?"),
260                        )?);
261                    }
262                }
263                for &idx in &grouping.fallback_indices {
264                    scorers.push(should[idx].$scorer_fn(reader, limit) $(. $aw)* ?);
265                }
266                return Ok(build_should_scorer(scorers));
267            }
268        }
269
270        // ── 3. Filter push-down (MUST + SHOULD) ─────────────────────────
271        if !should.is_empty() && !must.is_empty() && limit < usize::MAX / 4 {
272            // Pre-check: is SHOULD all-sparse? This determines whether we can
273            // use bitset fallback for MUST clauses that lack fast-field predicates.
274            // For sparse SHOULD, the predicate is pushed into BMP/MaxScore traversal
275            // so all qualifying docs are found. For text SHOULD, we must NOT convert
276            // MUST to a predicate (PredicatedScorer would drop MUST-only docs that
277            // don't match SHOULD), so those go to verifier → BooleanScorer.
278            let should_is_sparse = extract_all_sparse_infos(should).is_some();
279
280            // 3a. Compile MUST → predicates (O(1)) vs verifier scorers (seek)
281            //
282            // Priority: as_doc_predicate (fast-field O(1)) > as_doc_bitset
283            // (posting-list materialization, O(1) lookup, sparse-SHOULD only)
284            // > verifier scorer (seek).
285            let mut predicates: Vec<super::DocPredicate<'_>> = Vec::new();
286            let mut must_verifiers: Vec<Box<dyn super::Scorer + '_>> = Vec::new();
287            for q in must {
288                if let Some(pred) = q.as_doc_predicate(reader) {
289                    log::debug!("BooleanQuery planner 3a: MUST clause → predicate ({})", q);
290                    predicates.push(pred);
291                } else if should_is_sparse {
292                    if let Some(bitset) = q.as_doc_bitset(reader) {
293                        log::debug!("BooleanQuery planner 3a: MUST clause → bitset predicate ({})", q);
294                        predicates.push(Box::new(move |doc_id| bitset.contains(doc_id)));
295                    } else {
296                        log::debug!("BooleanQuery planner 3a: MUST clause → verifier scorer ({})", q);
297                        must_verifiers.push(q.$scorer_fn(reader, limit) $(. $aw)* ?);
298                    }
299                } else {
300                    log::debug!("BooleanQuery planner 3a: MUST clause → verifier scorer ({})", q);
301                    must_verifiers.push(q.$scorer_fn(reader, limit) $(. $aw)* ?);
302                }
303            }
304            // Compile MUST_NOT → negated predicates vs verifier scorers
305            let mut must_not_verifiers: Vec<Box<dyn super::Scorer + '_>> = Vec::new();
306            for q in must_not {
307                if let Some(pred) = q.as_doc_predicate(reader) {
308                    let negated: super::DocPredicate<'_> =
309                        Box::new(move |doc_id| !pred(doc_id));
310                    predicates.push(negated);
311                } else if should_is_sparse {
312                    if let Some(bitset) = q.as_doc_bitset(reader) {
313                        log::debug!("BooleanQuery planner 3a: MUST_NOT clause → bitset predicate ({})", q);
314                        predicates.push(Box::new(move |doc_id| !bitset.contains(doc_id)));
315                    } else {
316                        must_not_verifiers.push(q.$scorer_fn(reader, limit) $(. $aw)* ?);
317                    }
318                } else {
319                    must_not_verifiers.push(q.$scorer_fn(reader, limit) $(. $aw)* ?);
320                }
321            }
322
323            // 3b. Fast path: pure predicates + sparse SHOULD → BMP or MaxScore w/ predicate
324            if must_verifiers.is_empty()
325                && must_not_verifiers.is_empty()
326                && !predicates.is_empty()
327            {
328                if let Some(infos) = extract_all_sparse_infos(should) {
329                    // Try BMP with bitset first: build compact bitset from MUST/MUST_NOT
330                    // posting lists (O(M) for term queries) for fast per-slot lookup.
331                    let bitset_result = build_combined_bitset(must, must_not, reader);
332                    if let Some(ref bitset) = bitset_result {
333                        let bitset_pred = |doc_id: crate::DocId| bitset.contains(doc_id);
334                        if let Some((raw, info)) =
335                            build_sparse_bmp_results_filtered(&infos, reader, limit, &bitset_pred)
336                        {
337                            log::debug!(
338                                "BooleanQuery planner: bitset-aware sparse BMP, {} dims, {} matching docs",
339                                infos.len(),
340                                bitset.count()
341                            );
342                            return Ok(combine_sparse_results(raw, info.combiner, info.field, limit));
343                        }
344                    }
345
346                    // Fallback: closure predicate (for queries that don't support bitsets)
347                    let combined = chain_predicates(predicates);
348                    if let Some((raw, info)) =
349                        build_sparse_bmp_results_filtered(&infos, reader, limit, &*combined)
350                    {
351                        log::debug!(
352                            "BooleanQuery planner: predicate-aware sparse BMP, {} dims",
353                            infos.len()
354                        );
355                        return Ok(combine_sparse_results(raw, info.combiner, info.field, limit));
356                    }
357                    // Try MaxScore with predicate
358                    if let Some((executor, info)) =
359                        build_sparse_maxscore_executor(&infos, reader, limit, Some(combined))
360                    {
361                        log::debug!(
362                            "BooleanQuery planner: predicate-aware sparse MaxScore, {} dims",
363                            infos.len()
364                        );
365                        let raw = executor.$execute_fn() $(. $aw)* ?;
366                        return Ok(combine_sparse_results(raw, info.combiner, info.field, limit));
367                    }
368                    // predicates consumed — cannot fall through; rebuild them
369                    // (this path only triggers if neither sparse index exists)
370                    // should_is_sparse is true here (we're inside extract_all_sparse_infos)
371                    predicates = Vec::new();
372                    for q in must {
373                        if let Some(pred) = q.as_doc_predicate(reader) {
374                            predicates.push(pred);
375                        } else if let Some(bitset) = q.as_doc_bitset(reader) {
376                            predicates.push(Box::new(move |doc_id| bitset.contains(doc_id)));
377                        }
378                    }
379                    for q in must_not {
380                        if let Some(pred) = q.as_doc_predicate(reader) {
381                            let negated: super::DocPredicate<'_> =
382                                Box::new(move |doc_id| !pred(doc_id));
383                            predicates.push(negated);
384                        } else if let Some(bitset) = q.as_doc_bitset(reader) {
385                            predicates.push(Box::new(move |doc_id| !bitset.contains(doc_id)));
386                        }
387                    }
388                }
389            }
390
391            // 3c. PredicatedScorer fallback (over-fetch 4x when any filter is present)
392            let has_filters = !predicates.is_empty()
393                || !must_verifiers.is_empty()
394                || !must_not_verifiers.is_empty();
395            let should_limit = if has_filters { limit * 4 } else { limit };
396            let should_scorer = if should.len() == 1 {
397                should[0].$scorer_fn(reader, should_limit) $(. $aw)* ?
398            } else {
399                let sub = BooleanQuery {
400                    must: Vec::new(),
401                    should: should.to_vec(),
402                    must_not: Vec::new(),
403                    global_stats: global_stats.cloned(),
404                };
405                sub.$scorer_fn(reader, should_limit) $(. $aw)* ?
406            };
407
408            let use_predicated =
409                must_verifiers.is_empty() || should_scorer.size_hint() >= limit as u32;
410
411            if use_predicated {
412                log::debug!(
413                    "BooleanQuery planner: PredicatedScorer {} preds + {} must_v + {} must_not_v, \
414                     SHOULD size_hint={}, over_fetch={}",
415                    predicates.len(), must_verifiers.len(), must_not_verifiers.len(),
416                    should_scorer.size_hint(), should_limit
417                );
418                return Ok(Box::new(super::PredicatedScorer::new(
419                    should_scorer, predicates, must_verifiers, must_not_verifiers,
420                )));
421            }
422
423            // size_hint < limit with verifiers → BooleanScorer
424            log::debug!(
425                "BooleanQuery planner: BooleanScorer fallback, size_hint={} < limit={}, \
426                 {} must_v + {} must_not_v",
427                should_scorer.size_hint(), limit,
428                must_verifiers.len(), must_not_verifiers.len()
429            );
430            let mut scorer = BooleanScorer {
431                must: must_verifiers,
432                should: vec![should_scorer],
433                must_not: must_not_verifiers,
434                current_doc: 0,
435            };
436            scorer.current_doc = scorer.find_next_match();
437            return Ok(Box::new(scorer));
438        }
439
440        // ── 4. Standard BooleanScorer fallback ───────────────────────────
441        let mut must_scorers = Vec::with_capacity(must.len());
442        for q in must {
443            must_scorers.push(q.$scorer_fn(reader, limit) $(. $aw)* ?);
444        }
445        let mut should_scorers = Vec::with_capacity(should.len());
446        for q in should {
447            should_scorers.push(q.$scorer_fn(reader, limit) $(. $aw)* ?);
448        }
449        let mut must_not_scorers = Vec::with_capacity(must_not.len());
450        for q in must_not {
451            must_not_scorers.push(q.$scorer_fn(reader, limit) $(. $aw)* ?);
452        }
453        let mut scorer = BooleanScorer {
454            must: must_scorers,
455            should: should_scorers,
456            must_not: must_not_scorers,
457            current_doc: 0,
458        };
459        scorer.current_doc = scorer.find_next_match();
460        Ok(Box::new(scorer) as Box<dyn Scorer + '_>)
461    }};
462}
463
464impl Query for BooleanQuery {
465    fn scorer<'a>(&self, reader: &'a SegmentReader, limit: usize) -> ScorerFuture<'a> {
466        let must = self.must.clone();
467        let should = self.should.clone();
468        let must_not = self.must_not.clone();
469        let global_stats = self.global_stats.clone();
470        Box::pin(async move {
471            boolean_plan!(
472                must,
473                should,
474                must_not,
475                global_stats.as_ref(),
476                reader,
477                limit,
478                scorer,
479                get_postings,
480                execute,
481                await
482            )
483        })
484    }
485
486    #[cfg(feature = "sync")]
487    fn scorer_sync<'a>(
488        &self,
489        reader: &'a SegmentReader,
490        limit: usize,
491    ) -> crate::Result<Box<dyn Scorer + 'a>> {
492        boolean_plan!(
493            self.must,
494            self.should,
495            self.must_not,
496            self.global_stats.as_ref(),
497            reader,
498            limit,
499            scorer_sync,
500            get_postings_sync,
501            execute_sync
502        )
503    }
504
505    fn as_doc_bitset(&self, reader: &SegmentReader) -> Option<super::DocBitset> {
506        if self.must.is_empty() && self.should.is_empty() {
507            return None;
508        }
509
510        let num_docs = reader.num_docs();
511
512        // MUST clauses: intersect bitsets (AND)
513        let mut result: Option<super::DocBitset> = None;
514        for q in &self.must {
515            let bs = q.as_doc_bitset(reader)?;
516            match result {
517                None => result = Some(bs),
518                Some(ref mut acc) => acc.intersect_with(&bs),
519            }
520        }
521
522        // SHOULD clauses: union bitsets (OR), then intersect with MUST result
523        if !self.should.is_empty() {
524            let mut should_union = super::DocBitset::new(num_docs);
525            for q in &self.should {
526                let bs = q.as_doc_bitset(reader)?;
527                should_union.union_with(&bs);
528            }
529            match result {
530                None => result = Some(should_union),
531                Some(ref mut acc) => {
532                    // When MUST clauses exist, SHOULD is optional (doesn't filter).
533                    // When no MUST clauses, at least one SHOULD must match.
534                    if self.must.is_empty() {
535                        *acc = should_union;
536                    }
537                }
538            }
539        }
540
541        // MUST_NOT clauses: subtract bitsets (ANDNOT)
542        if let Some(ref mut acc) = result {
543            for q in &self.must_not {
544                {
545                    let bs = q.as_doc_bitset(reader)?;
546                    acc.subtract(&bs);
547                }
548            }
549        }
550
551        result
552    }
553
554    fn as_doc_predicate<'a>(&self, reader: &'a SegmentReader) -> Option<super::DocPredicate<'a>> {
555        // Need at least some clauses
556        if self.must.is_empty() && self.should.is_empty() {
557            return None;
558        }
559
560        // Try converting all clauses to predicates; bail if any child can't
561        let must_preds: Vec<_> = self
562            .must
563            .iter()
564            .map(|q| q.as_doc_predicate(reader))
565            .collect::<Option<Vec<_>>>()?;
566        let should_preds: Vec<_> = self
567            .should
568            .iter()
569            .map(|q| q.as_doc_predicate(reader))
570            .collect::<Option<Vec<_>>>()?;
571        let must_not_preds: Vec<_> = self
572            .must_not
573            .iter()
574            .map(|q| q.as_doc_predicate(reader))
575            .collect::<Option<Vec<_>>>()?;
576
577        let has_must = !must_preds.is_empty();
578
579        Some(Box::new(move |doc_id| {
580            // All MUST predicates must pass
581            if !must_preds.iter().all(|p| p(doc_id)) {
582                return false;
583            }
584            // When there are no MUST clauses, at least one SHOULD must pass
585            if !has_must && !should_preds.is_empty() && !should_preds.iter().any(|p| p(doc_id)) {
586                return false;
587            }
588            // No MUST_NOT predicate should pass
589            must_not_preds.iter().all(|p| !p(doc_id))
590        }))
591    }
592
593    fn count_estimate<'a>(&self, reader: &'a SegmentReader) -> CountFuture<'a> {
594        let must = self.must.clone();
595        let should = self.should.clone();
596
597        Box::pin(async move {
598            if !must.is_empty() {
599                let mut estimates = Vec::with_capacity(must.len());
600                for q in &must {
601                    estimates.push(q.count_estimate(reader).await?);
602                }
603                estimates
604                    .into_iter()
605                    .min()
606                    .ok_or_else(|| crate::Error::Corruption("Empty must clause".to_string()))
607            } else if !should.is_empty() {
608                let mut sum = 0u32;
609                for q in &should {
610                    sum = sum.saturating_add(q.count_estimate(reader).await?);
611                }
612                Ok(sum)
613            } else {
614                Ok(0)
615            }
616        })
617    }
618}
619
620struct BooleanScorer<'a> {
621    must: Vec<Box<dyn Scorer + 'a>>,
622    should: Vec<Box<dyn Scorer + 'a>>,
623    must_not: Vec<Box<dyn Scorer + 'a>>,
624    current_doc: DocId,
625}
626
627impl BooleanScorer<'_> {
628    fn find_next_match(&mut self) -> DocId {
629        if self.must.is_empty() && self.should.is_empty() {
630            return TERMINATED;
631        }
632
633        loop {
634            let candidate = if !self.must.is_empty() {
635                let mut max_doc = self
636                    .must
637                    .iter()
638                    .map(|s| s.doc())
639                    .max()
640                    .unwrap_or(TERMINATED);
641
642                if max_doc == TERMINATED {
643                    return TERMINATED;
644                }
645
646                loop {
647                    let mut all_match = true;
648                    for scorer in &mut self.must {
649                        let doc = scorer.seek(max_doc);
650                        if doc == TERMINATED {
651                            return TERMINATED;
652                        }
653                        if doc > max_doc {
654                            max_doc = doc;
655                            all_match = false;
656                            break;
657                        }
658                    }
659                    if all_match {
660                        break;
661                    }
662                }
663                max_doc
664            } else {
665                self.should
666                    .iter()
667                    .map(|s| s.doc())
668                    .filter(|&d| d != TERMINATED)
669                    .min()
670                    .unwrap_or(TERMINATED)
671            };
672
673            if candidate == TERMINATED {
674                return TERMINATED;
675            }
676
677            let excluded = self.must_not.iter_mut().any(|scorer| {
678                let doc = scorer.seek(candidate);
679                doc == candidate
680            });
681
682            if !excluded {
683                // Seek SHOULD scorers to candidate so score() can see their contributions
684                for scorer in &mut self.should {
685                    scorer.seek(candidate);
686                }
687                self.current_doc = candidate;
688                return candidate;
689            }
690
691            // Advance past excluded candidate
692            if !self.must.is_empty() {
693                for scorer in &mut self.must {
694                    scorer.advance();
695                }
696            } else {
697                // For SHOULD-only: seek all scorers past the excluded candidate
698                for scorer in &mut self.should {
699                    if scorer.doc() <= candidate && scorer.doc() != TERMINATED {
700                        scorer.seek(candidate + 1);
701                    }
702                }
703            }
704        }
705    }
706}
707
708impl super::docset::DocSet for BooleanScorer<'_> {
709    fn doc(&self) -> DocId {
710        self.current_doc
711    }
712
713    fn advance(&mut self) -> DocId {
714        if !self.must.is_empty() {
715            for scorer in &mut self.must {
716                scorer.advance();
717            }
718        } else {
719            for scorer in &mut self.should {
720                if scorer.doc() == self.current_doc {
721                    scorer.advance();
722                }
723            }
724        }
725
726        self.current_doc = self.find_next_match();
727        self.current_doc
728    }
729
730    fn seek(&mut self, target: DocId) -> DocId {
731        for scorer in &mut self.must {
732            scorer.seek(target);
733        }
734
735        for scorer in &mut self.should {
736            scorer.seek(target);
737        }
738
739        self.current_doc = self.find_next_match();
740        self.current_doc
741    }
742
743    fn size_hint(&self) -> u32 {
744        if !self.must.is_empty() {
745            self.must.iter().map(|s| s.size_hint()).min().unwrap_or(0)
746        } else {
747            self.should.iter().map(|s| s.size_hint()).sum()
748        }
749    }
750}
751
752impl Scorer for BooleanScorer<'_> {
753    fn score(&self) -> Score {
754        let mut total = 0.0;
755
756        for scorer in &self.must {
757            if scorer.doc() == self.current_doc {
758                total += scorer.score();
759            }
760        }
761
762        for scorer in &self.should {
763            if scorer.doc() == self.current_doc {
764                total += scorer.score();
765            }
766        }
767
768        total
769    }
770
771    fn matched_positions(&self) -> Option<super::MatchedPositions> {
772        let mut all_positions: super::MatchedPositions = Vec::new();
773
774        for scorer in &self.must {
775            if scorer.doc() == self.current_doc
776                && let Some(positions) = scorer.matched_positions()
777            {
778                all_positions.extend(positions);
779            }
780        }
781
782        for scorer in &self.should {
783            if scorer.doc() == self.current_doc
784                && let Some(positions) = scorer.matched_positions()
785            {
786                all_positions.extend(positions);
787            }
788        }
789
790        if all_positions.is_empty() {
791            None
792        } else {
793            Some(all_positions)
794        }
795    }
796}
797
798#[cfg(test)]
799mod tests {
800    use super::*;
801    use crate::dsl::Field;
802    use crate::query::{QueryDecomposition, TermQuery};
803
804    #[test]
805    fn test_maxscore_eligible_pure_or_same_field() {
806        // Pure OR query with multiple terms in same field should be MaxScore-eligible
807        let query = BooleanQuery::new()
808            .should(TermQuery::text(Field(0), "hello"))
809            .should(TermQuery::text(Field(0), "world"))
810            .should(TermQuery::text(Field(0), "foo"));
811
812        // All clauses should return term info
813        assert!(
814            query
815                .should
816                .iter()
817                .all(|q| matches!(q.decompose(), QueryDecomposition::TextTerm(_)))
818        );
819
820        // All should be same field
821        let infos: Vec<_> = query
822            .should
823            .iter()
824            .filter_map(|q| match q.decompose() {
825                QueryDecomposition::TextTerm(info) => Some(info),
826                _ => None,
827            })
828            .collect();
829        assert_eq!(infos.len(), 3);
830        assert!(infos.iter().all(|i| i.field == Field(0)));
831    }
832
833    #[test]
834    fn test_maxscore_not_eligible_different_fields() {
835        // OR query with terms in different fields should NOT use MaxScore
836        let query = BooleanQuery::new()
837            .should(TermQuery::text(Field(0), "hello"))
838            .should(TermQuery::text(Field(1), "world")); // Different field!
839
840        let infos: Vec<_> = query
841            .should
842            .iter()
843            .filter_map(|q| match q.decompose() {
844                QueryDecomposition::TextTerm(info) => Some(info),
845                _ => None,
846            })
847            .collect();
848        assert_eq!(infos.len(), 2);
849        // Fields are different, MaxScore should not be used
850        assert!(infos[0].field != infos[1].field);
851    }
852
853    #[test]
854    fn test_maxscore_not_eligible_with_must() {
855        // Query with MUST clause should NOT use MaxScore optimization
856        let query = BooleanQuery::new()
857            .must(TermQuery::text(Field(0), "required"))
858            .should(TermQuery::text(Field(0), "hello"))
859            .should(TermQuery::text(Field(0), "world"));
860
861        // Has MUST clause, so MaxScore optimization should not kick in
862        assert!(!query.must.is_empty());
863    }
864
865    #[test]
866    fn test_maxscore_not_eligible_with_must_not() {
867        // Query with MUST_NOT clause should NOT use MaxScore optimization
868        let query = BooleanQuery::new()
869            .should(TermQuery::text(Field(0), "hello"))
870            .should(TermQuery::text(Field(0), "world"))
871            .must_not(TermQuery::text(Field(0), "excluded"));
872
873        // Has MUST_NOT clause, so MaxScore optimization should not kick in
874        assert!(!query.must_not.is_empty());
875    }
876
877    #[test]
878    fn test_maxscore_not_eligible_single_term() {
879        // Single SHOULD clause should NOT use MaxScore (no benefit)
880        let query = BooleanQuery::new().should(TermQuery::text(Field(0), "hello"));
881
882        // Only one term, MaxScore not beneficial
883        assert_eq!(query.should.len(), 1);
884    }
885
886    #[test]
887    fn test_term_query_info_extraction() {
888        let term_query = TermQuery::text(Field(42), "test");
889        match term_query.decompose() {
890            QueryDecomposition::TextTerm(info) => {
891                assert_eq!(info.field, Field(42));
892                assert_eq!(info.term, b"test");
893            }
894            _ => panic!("Expected TextTerm decomposition"),
895        }
896    }
897
898    #[test]
899    fn test_boolean_query_no_term_info() {
900        // BooleanQuery itself should not return term info
901        let query = BooleanQuery::new().should(TermQuery::text(Field(0), "hello"));
902
903        assert!(matches!(query.decompose(), QueryDecomposition::Opaque));
904    }
905}