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