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, 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#[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 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 pub fn with_global_stats(mut self, stats: Arc<GlobalStats>) -> Self {
92 self.global_stats = Some(stats);
93 self
94 }
95}
96
97fn 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
115macro_rules! boolean_plan {
129 ($must:expr, $should:expr, $must_not:expr, $global_stats:expr,
130 $reader:expr, $limit:expr,
131 $scorer_fn:ident, $get_postings_fn:ident, $execute_fn:ident
132 $(, $aw:tt)*) => {{
133 let must: &[Arc<dyn Query>] = &$must;
134 let should_all: &[Arc<dyn Query>] = &$should;
135 let must_not: &[Arc<dyn Query>] = &$must_not;
136 let global_stats: Option<&Arc<GlobalStats>> = $global_stats;
137 let reader: &SegmentReader = $reader;
138 let limit: usize = $limit;
139
140 let should_capped: Vec<Arc<dyn Query>>;
143 let should: &[Arc<dyn Query>] = if should_all.len() > super::MAX_QUERY_TERMS {
144 let is_predicate: Vec<bool> = should_all
145 .iter()
146 .map(|q| q.is_filter() || q.as_doc_predicate(reader).is_some())
147 .collect();
148 let cursor_count = is_predicate.iter().filter(|&&p| !p).count();
149
150 if cursor_count > super::MAX_QUERY_TERMS {
151 let mut kept = Vec::with_capacity(should_all.len());
152 let mut cursor_kept = 0usize;
153 for (q, &is_pred) in should_all.iter().zip(is_predicate.iter()) {
154 if is_pred {
155 kept.push(q.clone());
156 } else if cursor_kept < super::MAX_QUERY_TERMS {
157 kept.push(q.clone());
158 cursor_kept += 1;
159 }
160 }
161 log::debug!(
162 "BooleanQuery: capping cursor SHOULD from {} to {} ({} fast-field predicates exempt)",
163 cursor_count,
164 super::MAX_QUERY_TERMS,
165 kept.len() - cursor_kept,
166 );
167 should_capped = kept;
168 &should_capped
169 } else {
170 log::debug!(
171 "BooleanQuery: {} SHOULD clauses OK ({} need cursors, {} fast-field predicates)",
172 should_all.len(),
173 cursor_count,
174 should_all.len() - cursor_count,
175 );
176 should_all
177 }
178 } else {
179 should_all
180 };
181
182 if must_not.is_empty() {
184 if must.len() == 1 && should.is_empty() {
185 return must[0].$scorer_fn(reader, limit) $(. $aw)* ;
186 }
187 if should.len() == 1 && must.is_empty() {
188 return should[0].$scorer_fn(reader, limit) $(. $aw)* ;
189 }
190 }
191
192 if must.is_empty() && must_not.is_empty() && should.len() >= 2 {
194 if let Some((mut infos, text_field, avg_field_len, num_docs)) =
196 prepare_text_maxscore(should, reader, global_stats)
197 {
198 let mut posting_lists = Vec::with_capacity(infos.len());
199 for info in infos.drain(..) {
200 if let Some(pl) = reader.$get_postings_fn(info.field, &info.term)
201 $(. $aw)* ?
202 {
203 let idf = compute_idf(&pl, info.field, &info.term, num_docs, global_stats);
204 posting_lists.push((pl, idf));
205 }
206 }
207 let shared_threshold = std::cell::Cell::new(0.0f32);
208 return finish_text_maxscore(
209 posting_lists,
210 avg_field_len,
211 limit,
212 &shared_threshold,
213 reader.schema().index_label(),
214 reader.schema().get_field_name(text_field).unwrap_or("?"),
215 );
216 }
217
218 if let Some(infos) = extract_all_sparse_infos(should) {
221 if let Some((raw, info)) =
222 build_sparse_bmp_results(&infos, reader, limit)
223 {
224 return Ok(combine_sparse_results(raw, info.combiner, info.field, limit));
225 }
226 if let Some((executor, info)) =
227 build_sparse_maxscore_executor(&infos, reader, limit, None)
228 {
229 let raw = executor.$execute_fn() $(. $aw)* ?;
230 return Ok(combine_sparse_results(raw, info.combiner, info.field, limit));
231 }
232 }
233
234 if let Some(grouping) = prepare_per_field_grouping(should, reader, limit, global_stats)
236 {
237 let mut scorers: Vec<Box<dyn Scorer + '_>> = Vec::new();
238 let shared_threshold = std::cell::Cell::new(0.0f32);
240 for (field, avg_field_len, infos) in &grouping.multi_term_groups {
241 let mut posting_lists = Vec::with_capacity(infos.len());
242 for info in infos {
243 if let Some(pl) = reader.$get_postings_fn(info.field, &info.term)
244 $(. $aw)* ?
245 {
246 let idf = compute_idf(
247 &pl, *field, &info.term, grouping.num_docs, global_stats,
248 );
249 posting_lists.push((pl, idf));
250 }
251 }
252 if !posting_lists.is_empty() {
253 scorers.push(finish_text_maxscore(
254 posting_lists,
255 *avg_field_len,
256 grouping.per_field_limit,
257 &shared_threshold,
258 reader.schema().index_label(),
259 reader.schema().get_field_name(*field).unwrap_or("?"),
260 )?);
261 }
262 }
263 for &idx in &grouping.fallback_indices {
264 scorers.push(should[idx].$scorer_fn(reader, limit) $(. $aw)* ?);
265 }
266 return Ok(build_should_scorer(scorers));
267 }
268 }
269
270 if !should.is_empty() && !must.is_empty() && limit < usize::MAX / 4 {
272 let should_is_sparse = extract_all_sparse_infos(should).is_some();
279
280 let mut predicates: Vec<super::DocPredicate<'_>> = Vec::new();
286 let mut must_verifiers: Vec<Box<dyn super::Scorer + '_>> = Vec::new();
287 for q in must {
288 if let Some(pred) = q.as_doc_predicate(reader) {
289 log::debug!("BooleanQuery planner 3a: MUST clause → predicate ({})", q);
290 predicates.push(pred);
291 } else if should_is_sparse {
292 if let Some(bitset) = q.as_doc_bitset(reader) {
293 log::debug!("BooleanQuery planner 3a: MUST clause → bitset predicate ({})", q);
294 predicates.push(Box::new(move |doc_id| bitset.contains(doc_id)));
295 } else {
296 log::debug!("BooleanQuery planner 3a: MUST clause → verifier scorer ({})", q);
297 must_verifiers.push(q.$scorer_fn(reader, limit) $(. $aw)* ?);
298 }
299 } else {
300 log::debug!("BooleanQuery planner 3a: MUST clause → verifier scorer ({})", q);
301 must_verifiers.push(q.$scorer_fn(reader, limit) $(. $aw)* ?);
302 }
303 }
304 let mut must_not_verifiers: Vec<Box<dyn super::Scorer + '_>> = Vec::new();
306 for q in must_not {
307 if let Some(pred) = q.as_doc_predicate(reader) {
308 let negated: super::DocPredicate<'_> =
309 Box::new(move |doc_id| !pred(doc_id));
310 predicates.push(negated);
311 } else if should_is_sparse {
312 if let Some(bitset) = q.as_doc_bitset(reader) {
313 log::debug!("BooleanQuery planner 3a: MUST_NOT clause → bitset predicate ({})", q);
314 predicates.push(Box::new(move |doc_id| !bitset.contains(doc_id)));
315 } else {
316 must_not_verifiers.push(q.$scorer_fn(reader, limit) $(. $aw)* ?);
317 }
318 } else {
319 must_not_verifiers.push(q.$scorer_fn(reader, limit) $(. $aw)* ?);
320 }
321 }
322
323 if must_verifiers.is_empty()
325 && must_not_verifiers.is_empty()
326 && !predicates.is_empty()
327 {
328 if let Some(infos) = extract_all_sparse_infos(should) {
329 let bitset_result = build_combined_bitset(must, must_not, reader);
332 if let Some(ref bitset) = bitset_result {
333 let bitset_pred = |doc_id: crate::DocId| bitset.contains(doc_id);
334 if let Some((raw, info)) =
335 build_sparse_bmp_results_filtered(&infos, reader, limit, &bitset_pred)
336 {
337 log::debug!(
338 "BooleanQuery planner: bitset-aware sparse BMP, {} dims, {} matching docs",
339 infos.len(),
340 bitset.count()
341 );
342 return Ok(combine_sparse_results(raw, info.combiner, info.field, limit));
343 }
344 }
345
346 let combined = chain_predicates(predicates);
348 if let Some((raw, info)) =
349 build_sparse_bmp_results_filtered(&infos, reader, limit, &*combined)
350 {
351 log::debug!(
352 "BooleanQuery planner: predicate-aware sparse BMP, {} dims",
353 infos.len()
354 );
355 return Ok(combine_sparse_results(raw, info.combiner, info.field, limit));
356 }
357 if let Some((executor, info)) =
359 build_sparse_maxscore_executor(&infos, reader, limit, Some(combined))
360 {
361 log::debug!(
362 "BooleanQuery planner: predicate-aware sparse MaxScore, {} dims",
363 infos.len()
364 );
365 let raw = executor.$execute_fn() $(. $aw)* ?;
366 return Ok(combine_sparse_results(raw, info.combiner, info.field, limit));
367 }
368 predicates = Vec::new();
372 for q in must {
373 if let Some(pred) = q.as_doc_predicate(reader) {
374 predicates.push(pred);
375 } else if let Some(bitset) = q.as_doc_bitset(reader) {
376 predicates.push(Box::new(move |doc_id| bitset.contains(doc_id)));
377 }
378 }
379 for q in must_not {
380 if let Some(pred) = q.as_doc_predicate(reader) {
381 let negated: super::DocPredicate<'_> =
382 Box::new(move |doc_id| !pred(doc_id));
383 predicates.push(negated);
384 } else if let Some(bitset) = q.as_doc_bitset(reader) {
385 predicates.push(Box::new(move |doc_id| !bitset.contains(doc_id)));
386 }
387 }
388 }
389 }
390
391 let has_filters = !predicates.is_empty()
393 || !must_verifiers.is_empty()
394 || !must_not_verifiers.is_empty();
395 let should_limit = if has_filters { limit * 4 } else { limit };
396 let should_scorer = if should.len() == 1 {
397 should[0].$scorer_fn(reader, should_limit) $(. $aw)* ?
398 } else {
399 let sub = BooleanQuery {
400 must: Vec::new(),
401 should: should.to_vec(),
402 must_not: Vec::new(),
403 global_stats: global_stats.cloned(),
404 };
405 sub.$scorer_fn(reader, should_limit) $(. $aw)* ?
406 };
407
408 let use_predicated =
409 must_verifiers.is_empty() || should_scorer.size_hint() >= limit as u32;
410
411 if use_predicated {
412 log::debug!(
413 "BooleanQuery planner: PredicatedScorer {} preds + {} must_v + {} must_not_v, \
414 SHOULD size_hint={}, over_fetch={}",
415 predicates.len(), must_verifiers.len(), must_not_verifiers.len(),
416 should_scorer.size_hint(), should_limit
417 );
418 return Ok(Box::new(super::PredicatedScorer::new(
419 should_scorer, predicates, must_verifiers, must_not_verifiers,
420 )));
421 }
422
423 log::debug!(
425 "BooleanQuery planner: BooleanScorer fallback, size_hint={} < limit={}, \
426 {} must_v + {} must_not_v",
427 should_scorer.size_hint(), limit,
428 must_verifiers.len(), must_not_verifiers.len()
429 );
430 let mut scorer = BooleanScorer {
431 must: must_verifiers,
432 should: vec![should_scorer],
433 must_not: must_not_verifiers,
434 current_doc: 0,
435 };
436 scorer.current_doc = scorer.find_next_match();
437 return Ok(Box::new(scorer));
438 }
439
440 let mut must_scorers = Vec::with_capacity(must.len());
442 for q in must {
443 must_scorers.push(q.$scorer_fn(reader, limit) $(. $aw)* ?);
444 }
445 let mut should_scorers = Vec::with_capacity(should.len());
446 for q in should {
447 should_scorers.push(q.$scorer_fn(reader, limit) $(. $aw)* ?);
448 }
449 let mut must_not_scorers = Vec::with_capacity(must_not.len());
450 for q in must_not {
451 must_not_scorers.push(q.$scorer_fn(reader, limit) $(. $aw)* ?);
452 }
453 let mut scorer = BooleanScorer {
454 must: must_scorers,
455 should: should_scorers,
456 must_not: must_not_scorers,
457 current_doc: 0,
458 };
459 scorer.current_doc = scorer.find_next_match();
460 Ok(Box::new(scorer) as Box<dyn Scorer + '_>)
461 }};
462}
463
464impl Query for BooleanQuery {
465 fn scorer<'a>(&self, reader: &'a SegmentReader, limit: usize) -> ScorerFuture<'a> {
466 let must = self.must.clone();
467 let should = self.should.clone();
468 let must_not = self.must_not.clone();
469 let global_stats = self.global_stats.clone();
470 Box::pin(async move {
471 boolean_plan!(
472 must,
473 should,
474 must_not,
475 global_stats.as_ref(),
476 reader,
477 limit,
478 scorer,
479 get_postings,
480 execute,
481 await
482 )
483 })
484 }
485
486 #[cfg(feature = "sync")]
487 fn scorer_sync<'a>(
488 &self,
489 reader: &'a SegmentReader,
490 limit: usize,
491 ) -> crate::Result<Box<dyn Scorer + 'a>> {
492 boolean_plan!(
493 self.must,
494 self.should,
495 self.must_not,
496 self.global_stats.as_ref(),
497 reader,
498 limit,
499 scorer_sync,
500 get_postings_sync,
501 execute_sync
502 )
503 }
504
505 fn as_doc_bitset(&self, reader: &SegmentReader) -> Option<super::DocBitset> {
506 if self.must.is_empty() && self.should.is_empty() {
507 return None;
508 }
509
510 let num_docs = reader.num_docs();
511
512 let mut result: Option<super::DocBitset> = None;
514 for q in &self.must {
515 let bs = q.as_doc_bitset(reader)?;
516 match result {
517 None => result = Some(bs),
518 Some(ref mut acc) => acc.intersect_with(&bs),
519 }
520 }
521
522 if !self.should.is_empty() {
524 let mut should_union = super::DocBitset::new(num_docs);
525 for q in &self.should {
526 let bs = q.as_doc_bitset(reader)?;
527 should_union.union_with(&bs);
528 }
529 match result {
530 None => result = Some(should_union),
531 Some(ref mut acc) => {
532 if self.must.is_empty() {
535 *acc = should_union;
536 }
537 }
538 }
539 }
540
541 if let Some(ref mut acc) = result {
543 for q in &self.must_not {
544 {
545 let bs = q.as_doc_bitset(reader)?;
546 acc.subtract(&bs);
547 }
548 }
549 }
550
551 result
552 }
553
554 fn as_doc_predicate<'a>(&self, reader: &'a SegmentReader) -> Option<super::DocPredicate<'a>> {
555 if self.must.is_empty() && self.should.is_empty() {
557 return None;
558 }
559
560 let must_preds: Vec<_> = self
562 .must
563 .iter()
564 .map(|q| q.as_doc_predicate(reader))
565 .collect::<Option<Vec<_>>>()?;
566 let should_preds: Vec<_> = self
567 .should
568 .iter()
569 .map(|q| q.as_doc_predicate(reader))
570 .collect::<Option<Vec<_>>>()?;
571 let must_not_preds: Vec<_> = self
572 .must_not
573 .iter()
574 .map(|q| q.as_doc_predicate(reader))
575 .collect::<Option<Vec<_>>>()?;
576
577 let has_must = !must_preds.is_empty();
578
579 Some(Box::new(move |doc_id| {
580 if !must_preds.iter().all(|p| p(doc_id)) {
582 return false;
583 }
584 if !has_must && !should_preds.is_empty() && !should_preds.iter().any(|p| p(doc_id)) {
586 return false;
587 }
588 must_not_preds.iter().all(|p| !p(doc_id))
590 }))
591 }
592
593 fn count_estimate<'a>(&self, reader: &'a SegmentReader) -> CountFuture<'a> {
594 let must = self.must.clone();
595 let should = self.should.clone();
596
597 Box::pin(async move {
598 if !must.is_empty() {
599 let mut estimates = Vec::with_capacity(must.len());
600 for q in &must {
601 estimates.push(q.count_estimate(reader).await?);
602 }
603 estimates
604 .into_iter()
605 .min()
606 .ok_or_else(|| crate::Error::Corruption("Empty must clause".to_string()))
607 } else if !should.is_empty() {
608 let mut sum = 0u32;
609 for q in &should {
610 sum = sum.saturating_add(q.count_estimate(reader).await?);
611 }
612 Ok(sum)
613 } else {
614 Ok(0)
615 }
616 })
617 }
618}
619
620struct BooleanScorer<'a> {
621 must: Vec<Box<dyn Scorer + 'a>>,
622 should: Vec<Box<dyn Scorer + 'a>>,
623 must_not: Vec<Box<dyn Scorer + 'a>>,
624 current_doc: DocId,
625}
626
627impl BooleanScorer<'_> {
628 fn find_next_match(&mut self) -> DocId {
629 if self.must.is_empty() && self.should.is_empty() {
630 return TERMINATED;
631 }
632
633 loop {
634 let candidate = if !self.must.is_empty() {
635 let mut max_doc = self
636 .must
637 .iter()
638 .map(|s| s.doc())
639 .max()
640 .unwrap_or(TERMINATED);
641
642 if max_doc == TERMINATED {
643 return TERMINATED;
644 }
645
646 loop {
647 let mut all_match = true;
648 for scorer in &mut self.must {
649 let doc = scorer.seek(max_doc);
650 if doc == TERMINATED {
651 return TERMINATED;
652 }
653 if doc > max_doc {
654 max_doc = doc;
655 all_match = false;
656 break;
657 }
658 }
659 if all_match {
660 break;
661 }
662 }
663 max_doc
664 } else {
665 self.should
666 .iter()
667 .map(|s| s.doc())
668 .filter(|&d| d != TERMINATED)
669 .min()
670 .unwrap_or(TERMINATED)
671 };
672
673 if candidate == TERMINATED {
674 return TERMINATED;
675 }
676
677 let excluded = self.must_not.iter_mut().any(|scorer| {
678 let doc = scorer.seek(candidate);
679 doc == candidate
680 });
681
682 if !excluded {
683 for scorer in &mut self.should {
685 scorer.seek(candidate);
686 }
687 self.current_doc = candidate;
688 return candidate;
689 }
690
691 if !self.must.is_empty() {
693 for scorer in &mut self.must {
694 scorer.advance();
695 }
696 } else {
697 for scorer in &mut self.should {
699 if scorer.doc() <= candidate && scorer.doc() != TERMINATED {
700 scorer.seek(candidate + 1);
701 }
702 }
703 }
704 }
705 }
706}
707
708impl super::docset::DocSet for BooleanScorer<'_> {
709 fn doc(&self) -> DocId {
710 self.current_doc
711 }
712
713 fn advance(&mut self) -> DocId {
714 if !self.must.is_empty() {
715 for scorer in &mut self.must {
716 scorer.advance();
717 }
718 } else {
719 for scorer in &mut self.should {
720 if scorer.doc() == self.current_doc {
721 scorer.advance();
722 }
723 }
724 }
725
726 self.current_doc = self.find_next_match();
727 self.current_doc
728 }
729
730 fn seek(&mut self, target: DocId) -> DocId {
731 for scorer in &mut self.must {
732 scorer.seek(target);
733 }
734
735 for scorer in &mut self.should {
736 scorer.seek(target);
737 }
738
739 self.current_doc = self.find_next_match();
740 self.current_doc
741 }
742
743 fn size_hint(&self) -> u32 {
744 if !self.must.is_empty() {
745 self.must.iter().map(|s| s.size_hint()).min().unwrap_or(0)
746 } else {
747 self.should.iter().map(|s| s.size_hint()).sum()
748 }
749 }
750}
751
752impl Scorer for BooleanScorer<'_> {
753 fn score(&self) -> Score {
754 let mut total = 0.0;
755
756 for scorer in &self.must {
757 if scorer.doc() == self.current_doc {
758 total += scorer.score();
759 }
760 }
761
762 for scorer in &self.should {
763 if scorer.doc() == self.current_doc {
764 total += scorer.score();
765 }
766 }
767
768 total
769 }
770
771 fn matched_positions(&self) -> Option<super::MatchedPositions> {
772 let mut all_positions: super::MatchedPositions = Vec::new();
773
774 for scorer in &self.must {
775 if scorer.doc() == self.current_doc
776 && let Some(positions) = scorer.matched_positions()
777 {
778 all_positions.extend(positions);
779 }
780 }
781
782 for scorer in &self.should {
783 if scorer.doc() == self.current_doc
784 && let Some(positions) = scorer.matched_positions()
785 {
786 all_positions.extend(positions);
787 }
788 }
789
790 if all_positions.is_empty() {
791 None
792 } else {
793 Some(all_positions)
794 }
795 }
796}
797
798#[cfg(test)]
799mod tests {
800 use super::*;
801 use crate::dsl::Field;
802 use crate::query::{QueryDecomposition, TermQuery};
803
804 #[test]
805 fn test_maxscore_eligible_pure_or_same_field() {
806 let query = BooleanQuery::new()
808 .should(TermQuery::text(Field(0), "hello"))
809 .should(TermQuery::text(Field(0), "world"))
810 .should(TermQuery::text(Field(0), "foo"));
811
812 assert!(
814 query
815 .should
816 .iter()
817 .all(|q| matches!(q.decompose(), QueryDecomposition::TextTerm(_)))
818 );
819
820 let infos: Vec<_> = query
822 .should
823 .iter()
824 .filter_map(|q| match q.decompose() {
825 QueryDecomposition::TextTerm(info) => Some(info),
826 _ => None,
827 })
828 .collect();
829 assert_eq!(infos.len(), 3);
830 assert!(infos.iter().all(|i| i.field == Field(0)));
831 }
832
833 #[test]
834 fn test_maxscore_not_eligible_different_fields() {
835 let query = BooleanQuery::new()
837 .should(TermQuery::text(Field(0), "hello"))
838 .should(TermQuery::text(Field(1), "world")); let infos: Vec<_> = query
841 .should
842 .iter()
843 .filter_map(|q| match q.decompose() {
844 QueryDecomposition::TextTerm(info) => Some(info),
845 _ => None,
846 })
847 .collect();
848 assert_eq!(infos.len(), 2);
849 assert!(infos[0].field != infos[1].field);
851 }
852
853 #[test]
854 fn test_maxscore_not_eligible_with_must() {
855 let query = BooleanQuery::new()
857 .must(TermQuery::text(Field(0), "required"))
858 .should(TermQuery::text(Field(0), "hello"))
859 .should(TermQuery::text(Field(0), "world"));
860
861 assert!(!query.must.is_empty());
863 }
864
865 #[test]
866 fn test_maxscore_not_eligible_with_must_not() {
867 let query = BooleanQuery::new()
869 .should(TermQuery::text(Field(0), "hello"))
870 .should(TermQuery::text(Field(0), "world"))
871 .must_not(TermQuery::text(Field(0), "excluded"));
872
873 assert!(!query.must_not.is_empty());
875 }
876
877 #[test]
878 fn test_maxscore_not_eligible_single_term() {
879 let query = BooleanQuery::new().should(TermQuery::text(Field(0), "hello"));
881
882 assert_eq!(query.should.len(), 1);
884 }
885
886 #[test]
887 fn test_term_query_info_extraction() {
888 let term_query = TermQuery::text(Field(42), "test");
889 match term_query.decompose() {
890 QueryDecomposition::TextTerm(info) => {
891 assert_eq!(info.field, Field(42));
892 assert_eq!(info.term, b"test");
893 }
894 _ => panic!("Expected TextTerm decomposition"),
895 }
896 }
897
898 #[test]
899 fn test_boolean_query_no_term_info() {
900 let query = BooleanQuery::new().should(TermQuery::text(Field(0), "hello"));
902
903 assert!(matches!(query.decompose(), QueryDecomposition::Opaque));
904 }
905}