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