1use 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, cap_terms, chain_predicates, combine_sparse_results,
12 compute_idf, extract_all_sparse_infos, finish_chunked_text_maxscore, finish_text_maxscore,
13 prepare_per_field_grouping, prepare_text_maxscore, text_maxscore_allowed,
14};
15use super::{CountFuture, EmptyScorer, GlobalStats, Query, Scorer, ScorerFuture};
16
17#[derive(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 global_stats: Option<Arc<GlobalStats>>,
28 proximity: Option<super::ProximityConfig>,
31 text_heap_factor: f32,
34 max_terms: usize,
37}
38
39fn shared_or_extract_sparse_infos<'a>(
40 plan: Option<&'a Arc<super::bmp::LspSegmentPlan>>,
41 should: &[Arc<dyn Query>],
42) -> Option<std::borrow::Cow<'a, [super::SparseTermQueryInfo]>> {
43 plan.map(|plan| std::borrow::Cow::Borrowed(plan.infos.as_ref()))
44 .or_else(|| extract_all_sparse_infos(should).map(std::borrow::Cow::Owned))
45}
46
47impl std::fmt::Debug for BooleanQuery {
48 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
49 f.debug_struct("BooleanQuery")
50 .field("must_count", &self.must.len())
51 .field("should_count", &self.should.len())
52 .field("must_not_count", &self.must_not.len())
53 .field("has_global_stats", &self.global_stats.is_some())
54 .field("proximity", &self.proximity)
55 .finish()
56 }
57}
58
59impl std::fmt::Display for BooleanQuery {
60 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
61 write!(f, "Boolean(")?;
62 let mut first = true;
63 for q in &self.must {
64 if !first {
65 write!(f, " ")?;
66 }
67 write!(f, "+{}", q)?;
68 first = false;
69 }
70 for q in &self.should {
71 if !first {
72 write!(f, " ")?;
73 }
74 write!(f, "{}", q)?;
75 first = false;
76 }
77 for q in &self.must_not {
78 if !first {
79 write!(f, " ")?;
80 }
81 write!(f, "-{}", q)?;
82 first = false;
83 }
84 if let Some(proximity) = &self.proximity {
85 write!(f, " ~proximity({}, {})", proximity.weight, proximity.window)?;
86 }
87 if self.text_heap_factor < 1.0 {
88 write!(f, " ~heap({})", self.text_heap_factor)?;
89 }
90 if self.max_terms > 0 {
91 write!(f, " ~max_terms({})", self.max_terms)?;
92 }
93 write!(f, ")")
94 }
95}
96
97impl Default for BooleanQuery {
98 fn default() -> Self {
99 Self {
100 must: Vec::new(),
101 should: Vec::new(),
102 must_not: Vec::new(),
103 global_stats: None,
104 proximity: None,
105 text_heap_factor: 1.0,
106 max_terms: 0,
107 }
108 }
109}
110
111impl BooleanQuery {
112 pub fn new() -> Self {
113 Self::default()
114 }
115
116 pub fn must(mut self, query: impl Query + 'static) -> Self {
117 self.must.push(Arc::new(query));
118 self
119 }
120
121 pub fn should(mut self, query: impl Query + 'static) -> Self {
122 self.should.push(Arc::new(query));
123 self
124 }
125
126 pub fn must_not(mut self, query: impl Query + 'static) -> Self {
127 self.must_not.push(Arc::new(query));
128 self
129 }
130
131 pub fn with_global_stats(mut self, stats: Arc<GlobalStats>) -> Self {
133 self.global_stats = Some(stats);
134 self
135 }
136
137 pub fn with_proximity(mut self, config: super::ProximityConfig) -> Self {
141 self.proximity = config.is_active().then_some(config);
142 self
143 }
144
145 pub fn with_text_heap_factor(mut self, heap_factor: f32) -> Self {
150 self.text_heap_factor = heap_factor;
151 self
152 }
153
154 pub fn with_max_terms(mut self, max_terms: usize) -> Self {
157 self.max_terms = max_terms;
158 self
159 }
160}
161
162fn flatten_should(should: &[Arc<dyn Query>]) -> std::borrow::Cow<'_, [Arc<dyn Query>]> {
169 if !should.iter().any(|query| query.should_children().is_some()) {
170 return std::borrow::Cow::Borrowed(should);
171 }
172
173 fn push_flat(out: &mut Vec<Arc<dyn Query>>, query: &Arc<dyn Query>) {
174 match query.should_children() {
175 Some(children) => children.iter().for_each(|child| push_flat(out, child)),
176 None => out.push(Arc::clone(query)),
177 }
178 }
179
180 let mut flat = Vec::with_capacity(should.len());
181 should.iter().for_each(|query| push_flat(&mut flat, query));
182 std::borrow::Cow::Owned(flat)
183}
184
185fn build_should_scorer<'a>(scorers: Vec<Box<dyn Scorer + 'a>>) -> Box<dyn Scorer + 'a> {
187 if scorers.is_empty() {
188 return Box::new(EmptyScorer);
189 }
190 if scorers.len() == 1 {
191 return scorers.into_iter().next().unwrap();
192 }
193 let mut scorer = BooleanScorer {
194 must: vec![],
195 should: scorers,
196 must_not: vec![],
197 current_doc: 0,
198 };
199 scorer.current_doc = scorer.find_next_match();
200 Box::new(scorer)
201}
202
203macro_rules! boolean_plan {
217 ($must:expr, $should:expr, $must_not:expr, $global_stats:expr, $proximity:expr, $text_tuning:expr,
218 $reader:expr, $limit:expr, $scorer_options:expr,
219 $scorer_fn:ident, $get_postings_fn:ident, $execute_fn:ident
220 $(, $aw:tt)*) => {{
221 let must: &[Arc<dyn Query>] = &$must;
222 let should_flat = flatten_should(&$should);
223 let should_all: &[Arc<dyn Query>] = &should_flat;
224 let must_not: &[Arc<dyn Query>] = &$must_not;
225 let global_stats: Option<&Arc<GlobalStats>> = $global_stats;
226 let reader: &SegmentReader = $reader;
227 let limit: usize = $limit;
228 let scorer_options: super::ScorerOptions = $scorer_options;
229 if !$text_tuning.0.is_finite() || !(0.0..=1.0).contains(&$text_tuning.0) {
230 return Err(crate::Error::Query(
231 "Text heap_factor must be finite and between 0 and 1".into(),
232 ));
233 }
234 if scorer_options.stop_if_expired() {
235 return Ok(Box::new(EmptyScorer) as Box<dyn Scorer + '_>);
236 }
237
238 let should_capped: Vec<Arc<dyn Query>>;
241 let should: &[Arc<dyn Query>] = if should_all.len() > super::MAX_QUERY_TERMS {
242 let is_predicate: Vec<bool> = should_all
243 .iter()
244 .map(|q| q.is_filter() || q.as_doc_predicate(reader).is_some())
245 .collect();
246 let cursor_count = is_predicate.iter().filter(|&&p| !p).count();
247
248 if cursor_count > super::MAX_QUERY_TERMS {
249 let mut kept = Vec::with_capacity(should_all.len());
250 let mut cursor_kept = 0usize;
251 for (q, &is_pred) in should_all.iter().zip(is_predicate.iter()) {
252 if is_pred {
253 kept.push(q.clone());
254 } else if cursor_kept < super::MAX_QUERY_TERMS {
255 kept.push(q.clone());
256 cursor_kept += 1;
257 }
258 }
259 log::debug!(
260 "BooleanQuery: capping cursor SHOULD from {} to {} ({} fast-field predicates exempt)",
261 cursor_count,
262 super::MAX_QUERY_TERMS,
263 kept.len() - cursor_kept,
264 );
265 should_capped = kept;
266 &should_capped
267 } else {
268 log::debug!(
269 "BooleanQuery: {} SHOULD clauses OK ({} need cursors, {} fast-field predicates)",
270 should_all.len(),
271 cursor_count,
272 should_all.len() - cursor_count,
273 );
274 should_all
275 }
276 } else {
277 should_all
278 };
279
280 if must_not.is_empty() {
282 if must.len() == 1 && should.is_empty() {
283 return must[0].$scorer_fn(reader, limit, scorer_options) $(. $aw)* ;
284 }
285 if should.len() == 1 && must.is_empty() && $text_tuning.0 == 1.0 {
286 return should[0].$scorer_fn(reader, limit, scorer_options) $(. $aw)* ;
287 }
288 }
289
290 if must.is_empty() && must_not.is_empty()
292 && (should.len() >= 2 || (should.len() == 1 && $text_tuning.0 < 1.0)) {
293 if let Some((mut infos, text_field, avg_field_len, num_docs)) =
295 prepare_text_maxscore(should, reader, global_stats)
296 && text_maxscore_allowed(reader, text_field, scorer_options.collect_positions)
297 {
298 let mut posting_lists = Vec::with_capacity(infos.len());
299 let mut term_bytes: Vec<Vec<u8>> = Vec::new();
300 for info in infos.drain(..) {
301 if let Some(pl) = reader.$get_postings_fn(info.field, &info.term)
302 $(. $aw)* ?
303 {
304 let idf = compute_idf(&pl, info.field, &info.term, num_docs, global_stats) * info.weight;
305 posting_lists.push((pl, idf));
306 term_bytes.push(info.term.clone());
307 }
308 }
309 cap_terms(&mut posting_lists, &mut term_bytes, $text_tuning.1);
310 if reader.is_chunked_field(text_field) {
312 return finish_chunked_text_maxscore(
313 posting_lists, limit, reader, text_field, None,
314 $proximity.map(|config| (config, term_bytes)),
315 $text_tuning.0,
316 scorer_options.shared_threshold.as_ref(),
317 );
318 }
319 let shared_threshold = std::cell::Cell::new(scorer_options.initial_threshold);
325 return finish_text_maxscore(
326 posting_lists,
327 avg_field_len,
328 reader.doc_lengths(text_field),
329 limit,
330 &shared_threshold,
331 reader,
332 text_field,
333 None,
334 super::Bm25Params::for_field(reader.schema(), text_field),
335 $proximity.map(|config| (config, term_bytes)),
336 $text_tuning.0,
337 scorer_options.shared_threshold.as_ref(),
338 );
339 }
340
341 if let Some(infos) =
344 shared_or_extract_sparse_infos(scorer_options.lsp_plan.as_ref(), should)
345 {
346 if let Some((raw, info)) =
347 build_sparse_bmp_results(&infos, reader, limit, &scorer_options)?
348 {
349 return Ok(combine_sparse_results(raw, info.combiner, info.field, limit));
350 }
351 if let Some((executor, info)) =
352 build_sparse_maxscore_executor(&infos, reader, limit, None)
353 {
354 let raw = executor.$execute_fn() $(. $aw)* ?;
355 return Ok(combine_sparse_results(raw, info.combiner, info.field, limit));
356 }
357 }
358
359 if let Some(grouping) = prepare_per_field_grouping(
361 should,
362 reader,
363 limit,
364 global_stats,
365 scorer_options.collect_positions,
366 ) {
367 let mut scorers: Vec<Box<dyn Scorer + '_>> = Vec::new();
368 let shared_threshold = std::cell::Cell::new(0.0f32);
370 for (field, avg_field_len, infos) in &grouping.multi_term_groups {
371 let corpus_size = reader.text_corpus_size(*field);
373 let mut posting_lists = Vec::with_capacity(infos.len());
374 let mut term_bytes: Vec<Vec<u8>> = Vec::new();
375 for info in infos {
376 if let Some(pl) = reader.$get_postings_fn(info.field, &info.term)
377 $(. $aw)* ?
378 {
379 let idf = compute_idf(
380 &pl, *field, &info.term, corpus_size, global_stats,
381 ) * info.weight;
382 posting_lists.push((pl, idf));
383 term_bytes.push(info.term.clone());
384 }
385 }
386 cap_terms(&mut posting_lists, &mut term_bytes, $text_tuning.1);
387 if reader.is_chunked_field(*field) {
388 scorers.push(finish_chunked_text_maxscore(
389 posting_lists,
390 grouping.per_field_limit,
391 reader,
392 *field,
393 None,
394 $proximity.map(|config| (config, term_bytes)),
395 $text_tuning.0,
396 scorer_options.shared_threshold.as_ref(),
397 )?);
398 } else if !posting_lists.is_empty() {
399 scorers.push(finish_text_maxscore(
400 posting_lists,
401 *avg_field_len,
402 reader.doc_lengths(*field),
403 grouping.per_field_limit,
404 &shared_threshold,
405 reader,
406 *field,
407 None,
408 super::Bm25Params::for_field(reader.schema(), *field),
409 $proximity.map(|config| (config, term_bytes)),
410 $text_tuning.0,
411 scorer_options.shared_threshold.as_ref(),
412 )?);
413 }
414 }
415 for &idx in &grouping.fallback_indices {
416 scorers.push(should[idx].$scorer_fn(
417 reader,
418 limit,
419 scorer_options.without_threshold(),
420 ) $(. $aw)* ?);
421 }
422 return Ok(build_should_scorer(scorers));
423 }
424 }
425
426 if !should.is_empty() && (!must.is_empty() || !must_not.is_empty()) {
433 let text_groups: Option<Vec<(crate::Field, Vec<super::TermQueryInfo>)>> = {
445 let mut groups: Vec<(crate::Field, Vec<super::TermQueryInfo>)> = Vec::new();
446 let mut all_text = true;
447 for q in should {
448 match q.decompose() {
449 super::QueryDecomposition::TextTerm(info)
450 if text_maxscore_allowed(
451 reader, info.field, scorer_options.collect_positions,
452 ) =>
453 {
454 match groups.iter_mut().find(|(f, _)| *f == info.field) {
455 Some((_, infos)) => infos.push(info),
456 None => groups.push((info.field, vec![info])),
457 }
458 }
459 _ => {
460 all_text = false;
461 break;
462 }
463 }
464 }
465 all_text.then_some(groups)
466 };
467 if must.iter().all(|query| {
468 query.is_filter()
469 || query.as_doc_predicate(reader).is_some()
470 || (!matches!(
471 query.decompose(),
472 super::QueryDecomposition::TextTerm(_)
473 ) && scorer_options.doc_bitset(query.as_ref(), reader).is_some())
474 })
475 && let Some(groups) = text_groups
476 && (groups.len() == 1
477 || ($proximity.is_none()
478 && groups
479 .iter()
480 .all(|(field, _)| !reader.is_chunked_field(*field))))
481 && let Some(bitset) = build_combined_bitset(must, must_not, reader, &scorer_options)
482 {
483 if scorer_options.stop_if_expired() {
484 return Ok(Box::new(EmptyScorer) as Box<dyn Scorer + '_>);
485 }
486 let bitset = std::sync::Arc::new(bitset);
487 let single_field = groups.len() == 1;
488
489 if !single_field {
496 let mut cursors = Vec::new();
497 for (field, infos) in groups {
498 let corpus_size = reader.text_corpus_size(field);
499 let avg_field_len = global_stats
500 .map(|stats| stats.avg_field_len(field))
501 .unwrap_or_else(|| reader.avg_field_len(field));
502 let params = super::Bm25Params::for_field(reader.schema(), field);
503 let mut posting_lists = Vec::with_capacity(infos.len());
504 let mut term_bytes = Vec::with_capacity(infos.len());
505 for info in &infos {
506 if let Some(postings) =
507 reader.$get_postings_fn(field, &info.term) $(. $aw)* ?
508 {
509 let idf = compute_idf(
510 &postings,
511 field,
512 &info.term,
513 corpus_size,
514 global_stats,
515 ) * info.weight;
516 posting_lists.push((postings, idf));
517 term_bytes.push(info.term.clone());
518 }
519 }
520 cap_terms(&mut posting_lists, &mut term_bytes, $text_tuning.1);
521 cursors.extend(posting_lists.into_iter().map(|(postings, idf)| {
522 super::TermCursor::text_with_params(
523 postings,
524 idf,
525 avg_field_len,
526 reader.doc_lengths(field).map(super::LengthSource::Docs),
527 params,
528 )
529 }));
530 }
531
532 let filter = bitset.clone();
533 let predicate: super::DocPredicate<'_> =
534 Box::new(move |doc_id| filter.contains(doc_id));
535 let mut executor = super::MaxScoreExecutor::new(
536 cursors,
537 limit,
538 $text_tuning.0,
539 )
540 .with_metric_labels(reader.schema().index_label(), "<multiple>")
541 .with_predicate(predicate)
542 .with_budget(scorer_options.shared_threshold.clone());
543 if $text_tuning.0 == 1.0 && scorer_options.initial_threshold > 0.0 {
544 executor.seed_threshold(scorer_options.initial_threshold);
545 }
546 let results = executor.execute_sync()?;
547 let found = results.len() as u32;
548 let should_scorer: Box<dyn Scorer + '_> =
549 Box::new(super::planner::TopKResultScorer::new(results));
550 if !must.is_empty() && (found as usize) < limit && bitset.count() > found {
551 return Ok(Box::new(super::planner::BitsetFillScorer::new(
552 should_scorer,
553 bitset,
554 )));
555 }
556 return Ok(should_scorer);
557 }
558
559 let group_limit = if single_field {
560 limit
561 } else {
562 super::max_candidate_limit(limit)
563 .min(reader.num_docs() as usize)
564 .max(1)
565 };
566 let shared_threshold = std::cell::Cell::new(if single_field {
570 scorer_options.initial_threshold
571 } else {
572 0.0
573 });
574 let mut scorers: Vec<Box<dyn Scorer + '_>> = Vec::new();
575 let mut found = 0u32;
576 let mut complete = true;
577 for (field, infos) in groups {
578 let corpus_size = reader.text_corpus_size(field);
579 let avg_field_len = global_stats
580 .map(|s| s.avg_field_len(field))
581 .unwrap_or_else(|| reader.avg_field_len(field));
582 let mut posting_lists = Vec::with_capacity(infos.len());
583 let mut term_bytes: Vec<Vec<u8>> = Vec::new();
584 for info in &infos {
585 if let Some(pl) = reader.$get_postings_fn(field, &info.term) $(. $aw)* ? {
586 let idf = compute_idf(&pl, field, &info.term, corpus_size, global_stats) * info.weight;
587 posting_lists.push((pl, idf));
588 term_bytes.push(info.term.clone());
589 }
590 }
591 cap_terms(&mut posting_lists, &mut term_bytes, $text_tuning.1);
592 let filter = bitset.clone();
593 let predicate: super::DocPredicate<'_> =
594 Box::new(move |doc_id| filter.contains(doc_id));
595 let scorer = if reader.is_chunked_field(field) {
596 finish_chunked_text_maxscore(
597 posting_lists, group_limit, reader, field, Some(predicate),
598 $proximity.map(|config| (config, term_bytes)),
599 $text_tuning.0,
600 scorer_options.shared_threshold.as_ref(),
601 )?
602 } else {
603 finish_text_maxscore(
604 posting_lists,
605 avg_field_len,
606 reader.doc_lengths(field),
607 group_limit,
608 &shared_threshold,
609 reader,
610 field,
611 Some(predicate),
612 super::Bm25Params::for_field(reader.schema(), field),
613 $proximity.map(|config| (config, term_bytes)),
614 $text_tuning.0,
615 scorer_options.shared_threshold.as_ref(),
616 )?
617 };
618 let hits = scorer.size_hint();
619 found = found.saturating_add(hits);
620 if hits as usize >= group_limit {
621 complete = false;
622 }
623 scorers.push(scorer);
624 }
625 log::debug!(
626 "BooleanQuery planner: bitset-aware text MaxScore, {} field group(s), \
627 {} filtered docs, {} scored hits",
628 scorers.len(),
629 bitset.count(),
630 found
631 );
632 let should_scorer = build_should_scorer(scorers);
633 if !must.is_empty()
634 && complete
635 && (found as usize) < limit
636 && bitset.count() > found
637 {
638 return Ok(Box::new(super::planner::BitsetFillScorer::new(
639 should_scorer,
640 bitset,
641 )));
642 }
643 return Ok(should_scorer);
644 }
645
646 let should_is_sparse = scorer_options.lsp_plan.is_some()
653 || extract_all_sparse_infos(should).is_some();
654 let bitset_predicates_allowed = should_is_sparse && !scorer_options.collect_positions;
655
656 let mut predicates: Vec<super::DocPredicate<'_>> = Vec::new();
662 let mut must_verifiers: Vec<Box<dyn super::Scorer + '_>> = Vec::new();
663 for q in must {
664 if let Some(pred) = q.as_doc_predicate(reader) {
665 log::debug!("BooleanQuery planner 3a: MUST clause → predicate ({})", q);
666 predicates.push(pred);
667 } else if bitset_predicates_allowed {
668 if let Some(bitset) = scorer_options.doc_bitset(q.as_ref(), reader) {
669 log::debug!("BooleanQuery planner 3a: MUST clause → bitset predicate ({})", q);
670 predicates.push(Box::new(move |doc_id| bitset.contains(doc_id)));
671 } else {
672 log::debug!("BooleanQuery planner 3a: MUST clause → verifier scorer ({})", q);
673 must_verifiers.push(q.$scorer_fn(
674 reader, limit, scorer_options.without_threshold()
675 ) $(. $aw)* ?);
676 }
677 } else {
678 log::debug!("BooleanQuery planner 3a: MUST clause → verifier scorer ({})", q);
679 must_verifiers.push(q.$scorer_fn(
680 reader, limit, scorer_options.without_threshold()
681 ) $(. $aw)* ?);
682 }
683 }
684 let mut must_not_verifiers: Vec<Box<dyn super::Scorer + '_>> = Vec::new();
686 for q in must_not {
687 if let Some(pred) = q.as_doc_predicate(reader) {
688 let negated: super::DocPredicate<'_> =
689 Box::new(move |doc_id| !pred(doc_id));
690 predicates.push(negated);
691 } else if bitset_predicates_allowed {
692 if let Some(bitset) = scorer_options.doc_bitset(q.as_ref(), reader) {
693 log::debug!("BooleanQuery planner 3a: MUST_NOT clause → bitset predicate ({})", q);
694 predicates.push(Box::new(move |doc_id| !bitset.contains(doc_id)));
695 } else {
696 must_not_verifiers.push(q.$scorer_fn(
697 reader, limit, scorer_options.without_threshold()
698 ) $(. $aw)* ?);
699 }
700 } else {
701 must_not_verifiers.push(q.$scorer_fn(
702 reader, limit, scorer_options.without_threshold()
703 ) $(. $aw)* ?);
704 }
705 }
706
707 if scorer_options.stop_if_expired() {
709 return Ok(Box::new(EmptyScorer) as Box<dyn Scorer + '_>);
710 }
711 if must_verifiers.is_empty()
712 && must_not_verifiers.is_empty()
713 && !predicates.is_empty()
714 {
715 let sparse_infos =
716 shared_or_extract_sparse_infos(scorer_options.lsp_plan.as_ref(), should);
717 if let Some(infos) = sparse_infos {
718 let bitset_result = build_combined_bitset(must, must_not, reader, &scorer_options);
721 if scorer_options.stop_if_expired() {
722 return Ok(Box::new(EmptyScorer) as Box<dyn Scorer + '_>);
723 }
724 if let Some(ref bitset) = bitset_result {
725 let bitset_pred = |doc_id: crate::DocId| bitset.contains(doc_id);
726 if let Some((raw, info)) =
727 build_sparse_bmp_results_filtered(
728 &infos, reader, limit, &bitset_pred, &scorer_options
729 )?
730 {
731 log::debug!(
732 "BooleanQuery planner: bitset-aware sparse BMP, {} dims, {} matching docs",
733 infos.len(),
734 bitset.count()
735 );
736 return Ok(combine_sparse_results(raw, info.combiner, info.field, limit));
737 }
738 }
739
740 let combined = chain_predicates(predicates);
742 if let Some((raw, info)) =
743 build_sparse_bmp_results_filtered(
744 &infos, reader, limit, &*combined, &scorer_options
745 )?
746 {
747 log::debug!(
748 "BooleanQuery planner: predicate-aware sparse BMP, {} dims",
749 infos.len()
750 );
751 return Ok(combine_sparse_results(raw, info.combiner, info.field, limit));
752 }
753 if let Some((executor, info)) =
755 build_sparse_maxscore_executor(&infos, reader, limit, Some(combined))
756 {
757 log::debug!(
758 "BooleanQuery planner: predicate-aware sparse MaxScore, {} dims",
759 infos.len()
760 );
761 let raw = executor.$execute_fn() $(. $aw)* ?;
762 return Ok(combine_sparse_results(raw, info.combiner, info.field, limit));
763 }
764 predicates = Vec::new();
768 for q in must {
769 if let Some(pred) = q.as_doc_predicate(reader) {
770 predicates.push(pred);
771 } else if let Some(bitset) = scorer_options.doc_bitset(q.as_ref(), reader) {
772 predicates.push(Box::new(move |doc_id| bitset.contains(doc_id)));
773 }
774 }
775 for q in must_not {
776 if let Some(pred) = q.as_doc_predicate(reader) {
777 let negated: super::DocPredicate<'_> =
778 Box::new(move |doc_id| !pred(doc_id));
779 predicates.push(negated);
780 } else if let Some(bitset) = scorer_options.doc_bitset(q.as_ref(), reader) {
781 predicates.push(Box::new(move |doc_id| !bitset.contains(doc_id)));
782 }
783 }
784 }
785 }
786
787 let mut should_options = scorer_options.without_threshold();
792 if should_is_sparse {
793 should_options.lsp_plan = scorer_options.lsp_plan.clone();
799 }
800 let proximity_should = $proximity.is_some();
801 let combined_should = should.len() == 1 || should_is_sparse || proximity_should;
802 let should_scorer: Option<Box<dyn Scorer + '_>> = if should.len() == 1 {
803 Some(should[0].$scorer_fn(reader, limit, should_options.clone()) $(. $aw)* ?)
804 } else if should_is_sparse || proximity_should {
805 let sub = BooleanQuery {
806 must: Vec::new(),
807 should: should.to_vec(),
808 must_not: Vec::new(),
809 global_stats: global_stats.cloned(),
810 proximity: $proximity,
811 text_heap_factor: $text_tuning.0,
812 max_terms: $text_tuning.1,
813 };
814 let sub_limit = if proximity_should {
820 should
821 .first()
822 .and_then(|query| match query.decompose() {
823 super::QueryDecomposition::TextTerm(info) => {
824 Some(reader.text_corpus_size(info.field) as usize)
825 }
826 _ => None,
827 })
828 .unwrap_or(reader.num_docs() as usize)
829 .max(limit)
830 } else {
831 super::max_candidate_limit(limit)
832 };
833 Some(sub.$scorer_fn(
834 reader,
835 sub_limit,
836 should_options.clone(),
837 ) $(. $aw)* ?)
838 } else {
839 None
840 };
841 let should_scorers: Vec<Box<dyn Scorer + '_>> = match should_scorer {
842 Some(scorer) => vec![scorer],
843 None => {
844 let mut scorers = Vec::with_capacity(should.len());
845 for query in should {
846 scorers.push(query.$scorer_fn(
847 reader,
848 limit,
849 should_options.clone(),
850 ) $(. $aw)* ?);
851 }
852 scorers
853 }
854 };
855
856 if must_verifiers.is_empty() {
857 let should_scorer = build_should_scorer(should_scorers);
858 log::debug!(
859 "BooleanQuery planner: PredicatedScorer {} preds + {} must_not_v, \
860 SHOULD size_hint={}, combined={}",
861 predicates.len(), must_not_verifiers.len(),
862 should_scorer.size_hint(), combined_should
863 );
864 return Ok(Box::new(super::PredicatedScorer::new(
865 should_scorer, predicates, Vec::new(), must_not_verifiers,
866 )));
867 }
868
869 log::debug!(
871 "BooleanQuery planner: required-clause BooleanScorer {} must + {} should, \
872 {} preds + {} must_not_v",
873 must_verifiers.len(), should_scorers.len(),
874 predicates.len(), must_not_verifiers.len()
875 );
876 let mut driver = BooleanScorer {
877 must: must_verifiers,
878 should: should_scorers,
879 must_not: Vec::new(),
880 current_doc: 0,
881 };
882 driver.current_doc = driver.find_next_match();
883 return Ok(Box::new(super::PredicatedScorer::new(
884 Box::new(driver),
885 predicates,
886 Vec::new(),
887 must_not_verifiers,
888 )));
889 }
890
891 let mut must_scorers = Vec::with_capacity(must.len());
893 for q in must {
894 must_scorers.push(q.$scorer_fn(
895 reader, limit, scorer_options.without_threshold()
896 ) $(. $aw)* ?);
897 }
898 let mut should_scorers = Vec::with_capacity(should.len());
899 for q in should {
900 should_scorers.push(q.$scorer_fn(
901 reader, limit, scorer_options.without_threshold()
902 ) $(. $aw)* ?);
903 }
904 let mut must_not_scorers = Vec::with_capacity(must_not.len());
905 for q in must_not {
906 must_not_scorers.push(q.$scorer_fn(
907 reader, limit, scorer_options.without_threshold()
908 ) $(. $aw)* ?);
909 }
910 let mut scorer = BooleanScorer {
911 must: must_scorers,
912 should: should_scorers,
913 must_not: must_not_scorers,
914 current_doc: 0,
915 };
916 scorer.current_doc = scorer.find_next_match();
917 Ok(Box::new(scorer) as Box<dyn Scorer + '_>)
918 }};
919}
920
921impl Query for BooleanQuery {
922 fn scorer<'a>(&self, reader: &'a SegmentReader, limit: usize) -> ScorerFuture<'a> {
923 self.scorer_with_options(reader, limit, super::ScorerOptions::with_positions())
924 }
925
926 fn scorer_with_options<'a>(
927 &self,
928 reader: &'a SegmentReader,
929 limit: usize,
930 options: super::ScorerOptions,
931 ) -> ScorerFuture<'a> {
932 let must = self.must.clone();
933 let should = self.should.clone();
934 let must_not = self.must_not.clone();
935 let global_stats = self
936 .global_stats
937 .clone()
938 .or_else(|| options.global_stats.clone());
939 let proximity = self.proximity;
940 let text_tuning = (self.text_heap_factor, self.max_terms);
941 Box::pin(async move {
942 boolean_plan!(
943 must,
944 should,
945 must_not,
946 global_stats.as_ref(),
947 proximity,
948 text_tuning,
949 reader,
950 limit,
951 options,
952 scorer_with_options,
953 get_postings,
954 execute,
955 await
956 )
957 })
958 }
959
960 #[cfg(feature = "sync")]
961 fn scorer_sync<'a>(
962 &self,
963 reader: &'a SegmentReader,
964 limit: usize,
965 ) -> crate::Result<Box<dyn Scorer + 'a>> {
966 self.scorer_sync_with_options(reader, limit, super::ScorerOptions::with_positions())
967 }
968
969 #[cfg(feature = "sync")]
970 fn scorer_sync_with_options<'a>(
971 &self,
972 reader: &'a SegmentReader,
973 limit: usize,
974 options: super::ScorerOptions,
975 ) -> crate::Result<Box<dyn Scorer + 'a>> {
976 let global_stats = self
977 .global_stats
978 .clone()
979 .or_else(|| options.global_stats.clone());
980 boolean_plan!(
981 self.must,
982 self.should,
983 self.must_not,
984 global_stats.as_ref(),
985 self.proximity,
986 (self.text_heap_factor, self.max_terms),
987 reader,
988 limit,
989 options,
990 scorer_sync_with_options,
991 get_postings_sync,
992 execute_sync
993 )
994 }
995
996 fn text_terms(&self, out: &mut Vec<(crate::dsl::Field, Vec<u8>)>) {
997 for clause in self.must.iter().chain(&self.should).chain(&self.must_not) {
998 clause.text_terms(out);
999 }
1000 }
1001
1002 fn decompose(&self) -> super::QueryDecomposition {
1003 if self.should.is_empty() || self.must.iter().any(|query| !query.is_filter()) {
1009 return super::QueryDecomposition::Opaque;
1010 }
1011 extract_all_sparse_infos(&self.should)
1012 .map(super::QueryDecomposition::SparseTerms)
1013 .unwrap_or(super::QueryDecomposition::Opaque)
1014 }
1015
1016 fn should_children(&self) -> Option<&[Arc<dyn Query>]> {
1017 if self.must.is_empty()
1018 && self.must_not.is_empty()
1019 && !self.should.is_empty()
1020 && self.proximity.is_none()
1021 && self.text_heap_factor == 1.0
1022 && self.max_terms == 0
1023 {
1024 Some(&self.should)
1025 } else {
1026 None
1027 }
1028 }
1029
1030 fn as_doc_bitset(&self, reader: &SegmentReader) -> Option<super::DocBitset> {
1031 self.as_doc_bitset_with_options(reader, &super::ScorerOptions::default())
1032 }
1033
1034 fn as_doc_bitset_with_options(
1035 &self,
1036 reader: &SegmentReader,
1037 options: &super::ScorerOptions,
1038 ) -> Option<super::DocBitset> {
1039 if options.stop_if_expired() {
1040 return None;
1041 }
1042 if self.must.is_empty() && self.should.is_empty() {
1043 return None;
1044 }
1045
1046 let num_docs = reader.num_docs();
1047
1048 let mut result: Option<super::DocBitset> = None;
1050 for q in &self.must {
1051 let bs = options.doc_bitset(q.as_ref(), reader)?;
1052 match result {
1053 None => result = Some(bs),
1054 Some(ref mut acc) => acc.intersect_with(&bs),
1055 }
1056 }
1057
1058 if !self.should.is_empty() {
1060 let mut should_union = super::DocBitset::new(num_docs);
1061 for q in &self.should {
1062 let bs = options.doc_bitset(q.as_ref(), reader)?;
1063 should_union.union_with(&bs);
1064 }
1065 match result {
1066 None => result = Some(should_union),
1067 Some(ref mut acc) => {
1068 if self.must.is_empty() {
1071 *acc = should_union;
1072 }
1073 }
1074 }
1075 }
1076
1077 if let Some(ref mut acc) = result {
1079 for q in &self.must_not {
1080 {
1081 let bs = options.doc_bitset(q.as_ref(), reader)?;
1082 acc.subtract(&bs);
1083 }
1084 }
1085 }
1086
1087 if options.stop_if_expired() {
1088 None
1089 } else {
1090 result
1091 }
1092 }
1093
1094 fn as_doc_predicate<'a>(&self, reader: &'a SegmentReader) -> Option<super::DocPredicate<'a>> {
1095 if self.must.is_empty() && self.should.is_empty() {
1097 return None;
1098 }
1099
1100 let must_preds: Vec<_> = self
1102 .must
1103 .iter()
1104 .map(|q| q.as_doc_predicate(reader))
1105 .collect::<Option<Vec<_>>>()?;
1106 let should_preds: Vec<_> = self
1107 .should
1108 .iter()
1109 .map(|q| q.as_doc_predicate(reader))
1110 .collect::<Option<Vec<_>>>()?;
1111 let must_not_preds: Vec<_> = self
1112 .must_not
1113 .iter()
1114 .map(|q| q.as_doc_predicate(reader))
1115 .collect::<Option<Vec<_>>>()?;
1116
1117 let has_must = !must_preds.is_empty();
1118
1119 Some(Box::new(move |doc_id| {
1120 if !must_preds.iter().all(|p| p(doc_id)) {
1122 return false;
1123 }
1124 if !has_must && !should_preds.is_empty() && !should_preds.iter().any(|p| p(doc_id)) {
1126 return false;
1127 }
1128 must_not_preds.iter().all(|p| !p(doc_id))
1130 }))
1131 }
1132
1133 fn count_estimate<'a>(&self, reader: &'a SegmentReader) -> CountFuture<'a> {
1134 let must = self.must.clone();
1135 let should = self.should.clone();
1136
1137 Box::pin(async move {
1138 if !must.is_empty() {
1139 let mut estimates = Vec::with_capacity(must.len());
1140 for q in &must {
1141 estimates.push(q.count_estimate(reader).await?);
1142 }
1143 estimates
1144 .into_iter()
1145 .min()
1146 .ok_or_else(|| crate::Error::Corruption("Empty must clause".to_string()))
1147 } else if !should.is_empty() {
1148 let mut sum = 0u32;
1149 for q in &should {
1150 sum = sum.saturating_add(q.count_estimate(reader).await?);
1151 }
1152 Ok(sum)
1153 } else {
1154 Ok(0)
1155 }
1156 })
1157 }
1158}
1159
1160struct BooleanScorer<'a> {
1161 must: Vec<Box<dyn Scorer + 'a>>,
1162 should: Vec<Box<dyn Scorer + 'a>>,
1163 must_not: Vec<Box<dyn Scorer + 'a>>,
1164 current_doc: DocId,
1165}
1166
1167impl BooleanScorer<'_> {
1168 fn find_next_match(&mut self) -> DocId {
1169 if self.must.is_empty() && self.should.is_empty() {
1170 return TERMINATED;
1171 }
1172
1173 loop {
1174 let candidate = if !self.must.is_empty() {
1175 let mut max_doc = self
1176 .must
1177 .iter()
1178 .map(|s| s.doc())
1179 .max()
1180 .unwrap_or(TERMINATED);
1181
1182 if max_doc == TERMINATED {
1183 return TERMINATED;
1184 }
1185
1186 loop {
1187 let mut all_match = true;
1188 for scorer in &mut self.must {
1189 let doc = scorer.seek(max_doc);
1190 if doc == TERMINATED {
1191 return TERMINATED;
1192 }
1193 if doc > max_doc {
1194 max_doc = doc;
1195 all_match = false;
1196 break;
1197 }
1198 }
1199 if all_match {
1200 break;
1201 }
1202 }
1203 max_doc
1204 } else {
1205 self.should
1206 .iter()
1207 .map(|s| s.doc())
1208 .filter(|&d| d != TERMINATED)
1209 .min()
1210 .unwrap_or(TERMINATED)
1211 };
1212
1213 if candidate == TERMINATED {
1214 return TERMINATED;
1215 }
1216
1217 let excluded = self.must_not.iter_mut().any(|scorer| {
1218 let doc = scorer.seek(candidate);
1219 doc == candidate
1220 });
1221
1222 if !excluded {
1223 for scorer in &mut self.should {
1225 scorer.seek(candidate);
1226 }
1227 self.current_doc = candidate;
1228 return candidate;
1229 }
1230
1231 if !self.must.is_empty() {
1233 for scorer in &mut self.must {
1234 scorer.advance();
1235 }
1236 } else {
1237 for scorer in &mut self.should {
1239 if scorer.doc() <= candidate && scorer.doc() != TERMINATED {
1240 scorer.seek(candidate + 1);
1241 }
1242 }
1243 }
1244 }
1245 }
1246}
1247
1248impl super::docset::DocSet for BooleanScorer<'_> {
1249 fn doc(&self) -> DocId {
1250 self.current_doc
1251 }
1252
1253 fn advance(&mut self) -> DocId {
1254 if !self.must.is_empty() {
1255 for scorer in &mut self.must {
1256 scorer.advance();
1257 }
1258 } else {
1259 for scorer in &mut self.should {
1260 if scorer.doc() == self.current_doc {
1261 scorer.advance();
1262 }
1263 }
1264 }
1265
1266 self.current_doc = self.find_next_match();
1267 self.current_doc
1268 }
1269
1270 fn seek(&mut self, target: DocId) -> DocId {
1271 for scorer in &mut self.must {
1272 scorer.seek(target);
1273 }
1274
1275 for scorer in &mut self.should {
1276 scorer.seek(target);
1277 }
1278
1279 self.current_doc = self.find_next_match();
1280 self.current_doc
1281 }
1282
1283 fn size_hint(&self) -> u32 {
1284 if !self.must.is_empty() {
1285 self.must.iter().map(|s| s.size_hint()).min().unwrap_or(0)
1286 } else {
1287 self.should.iter().map(|s| s.size_hint()).sum()
1288 }
1289 }
1290}
1291
1292impl Scorer for BooleanScorer<'_> {
1293 fn score(&self) -> Score {
1294 let mut total = 0.0;
1295
1296 for scorer in &self.must {
1297 if scorer.doc() == self.current_doc {
1298 total += scorer.score();
1299 }
1300 }
1301
1302 for scorer in &self.should {
1303 if scorer.doc() == self.current_doc {
1304 total += scorer.score();
1305 }
1306 }
1307
1308 total
1309 }
1310
1311 fn matched_positions(&self) -> Option<super::MatchedPositions> {
1312 let mut all_positions: super::MatchedPositions = Vec::new();
1313
1314 for scorer in &self.must {
1315 if scorer.doc() == self.current_doc
1316 && let Some(positions) = scorer.matched_positions()
1317 {
1318 all_positions.extend(positions);
1319 }
1320 }
1321
1322 for scorer in &self.should {
1323 if scorer.doc() == self.current_doc
1324 && let Some(positions) = scorer.matched_positions()
1325 {
1326 all_positions.extend(positions);
1327 }
1328 }
1329
1330 if all_positions.is_empty() {
1331 None
1332 } else {
1333 Some(merge_matched_positions(all_positions))
1334 }
1335 }
1336}
1337
1338pub(super) fn merge_matched_positions(
1346 positions: super::MatchedPositions,
1347) -> super::MatchedPositions {
1348 if positions.len() < 2 {
1349 return positions;
1350 }
1351 let mut merged: super::MatchedPositions = Vec::with_capacity(positions.len());
1352 for (field_id, scored) in positions {
1353 match merged
1354 .iter_mut()
1355 .find(|(existing, _)| *existing == field_id)
1356 {
1357 Some((_, existing)) => existing.extend(scored),
1358 None => merged.push((field_id, scored)),
1359 }
1360 }
1361 for (_, scored) in &mut merged {
1362 if scored.len() < 2 {
1363 continue;
1364 }
1365 scored.sort_by_key(|sp| sp.position);
1366 let mut write = 0usize;
1367 for read in 1..scored.len() {
1368 if scored[read].position == scored[write].position {
1369 scored[write].score += scored[read].score;
1370 } else {
1371 write += 1;
1372 scored[write] = scored[read];
1373 }
1374 }
1375 scored.truncate(write + 1);
1376 }
1377 merged
1378}
1379
1380#[cfg(test)]
1381mod tests {
1382 use super::*;
1383 use crate::dsl::Field;
1384 use crate::query::{QueryDecomposition, TermQuery};
1385
1386 #[test]
1387 fn test_maxscore_eligible_pure_or_same_field() {
1388 let query = BooleanQuery::new()
1390 .should(TermQuery::text(Field(0), "hello"))
1391 .should(TermQuery::text(Field(0), "world"))
1392 .should(TermQuery::text(Field(0), "foo"));
1393
1394 assert!(
1396 query
1397 .should
1398 .iter()
1399 .all(|q| matches!(q.decompose(), QueryDecomposition::TextTerm(_)))
1400 );
1401
1402 let infos: Vec<_> = query
1404 .should
1405 .iter()
1406 .filter_map(|q| match q.decompose() {
1407 QueryDecomposition::TextTerm(info) => Some(info),
1408 _ => None,
1409 })
1410 .collect();
1411 assert_eq!(infos.len(), 3);
1412 assert!(infos.iter().all(|i| i.field == Field(0)));
1413 }
1414
1415 #[test]
1416 fn test_maxscore_not_eligible_different_fields() {
1417 let query = BooleanQuery::new()
1419 .should(TermQuery::text(Field(0), "hello"))
1420 .should(TermQuery::text(Field(1), "world")); let infos: Vec<_> = query
1423 .should
1424 .iter()
1425 .filter_map(|q| match q.decompose() {
1426 QueryDecomposition::TextTerm(info) => Some(info),
1427 _ => None,
1428 })
1429 .collect();
1430 assert_eq!(infos.len(), 2);
1431 assert!(infos[0].field != infos[1].field);
1433 }
1434
1435 #[test]
1436 fn test_maxscore_not_eligible_with_must() {
1437 let query = BooleanQuery::new()
1439 .must(TermQuery::text(Field(0), "required"))
1440 .should(TermQuery::text(Field(0), "hello"))
1441 .should(TermQuery::text(Field(0), "world"));
1442
1443 assert!(!query.must.is_empty());
1445 }
1446
1447 #[test]
1448 fn test_maxscore_not_eligible_with_must_not() {
1449 let query = BooleanQuery::new()
1451 .should(TermQuery::text(Field(0), "hello"))
1452 .should(TermQuery::text(Field(0), "world"))
1453 .must_not(TermQuery::text(Field(0), "excluded"));
1454
1455 assert!(!query.must_not.is_empty());
1457 }
1458
1459 #[test]
1460 fn test_maxscore_not_eligible_single_term() {
1461 let query = BooleanQuery::new().should(TermQuery::text(Field(0), "hello"));
1463
1464 assert_eq!(query.should.len(), 1);
1466 }
1467
1468 #[test]
1469 fn test_term_query_info_extraction() {
1470 let term_query = TermQuery::text(Field(42), "test");
1471 match term_query.decompose() {
1472 QueryDecomposition::TextTerm(info) => {
1473 assert_eq!(info.field, Field(42));
1474 assert_eq!(info.term, b"test");
1475 }
1476 _ => panic!("Expected TextTerm decomposition"),
1477 }
1478 }
1479
1480 #[test]
1481 fn test_boolean_query_no_term_info() {
1482 let query = BooleanQuery::new().should(TermQuery::text(Field(0), "hello"));
1484
1485 assert!(matches!(query.decompose(), QueryDecomposition::Opaque));
1486 }
1487}