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