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, _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 );
214 }
215
216 if let Some(infos) = extract_all_sparse_infos(should) {
219 if let Some((raw, info)) =
220 build_sparse_bmp_results(&infos, reader, limit)
221 {
222 return Ok(combine_sparse_results(raw, info.combiner, info.field, limit));
223 }
224 if let Some((executor, info)) =
225 build_sparse_maxscore_executor(&infos, reader, limit, None)
226 {
227 let raw = executor.$execute_fn() $(. $aw)* ?;
228 return Ok(combine_sparse_results(raw, info.combiner, info.field, limit));
229 }
230 }
231
232 if let Some(grouping) = prepare_per_field_grouping(should, reader, limit, global_stats)
234 {
235 let mut scorers: Vec<Box<dyn Scorer + '_>> = Vec::new();
236 let shared_threshold = std::cell::Cell::new(0.0f32);
238 for (field, avg_field_len, infos) in &grouping.multi_term_groups {
239 let mut posting_lists = Vec::with_capacity(infos.len());
240 for info in infos {
241 if let Some(pl) = reader.$get_postings_fn(info.field, &info.term)
242 $(. $aw)* ?
243 {
244 let idf = compute_idf(
245 &pl, *field, &info.term, grouping.num_docs, global_stats,
246 );
247 posting_lists.push((pl, idf));
248 }
249 }
250 if !posting_lists.is_empty() {
251 scorers.push(finish_text_maxscore(
252 posting_lists,
253 *avg_field_len,
254 grouping.per_field_limit,
255 &shared_threshold,
256 )?);
257 }
258 }
259 for &idx in &grouping.fallback_indices {
260 scorers.push(should[idx].$scorer_fn(reader, limit) $(. $aw)* ?);
261 }
262 return Ok(build_should_scorer(scorers));
263 }
264 }
265
266 if !should.is_empty() && !must.is_empty() && limit < usize::MAX / 4 {
268 let should_is_sparse = extract_all_sparse_infos(should).is_some();
275
276 let mut predicates: Vec<super::DocPredicate<'_>> = Vec::new();
282 let mut must_verifiers: Vec<Box<dyn super::Scorer + '_>> = Vec::new();
283 for q in must {
284 if let Some(pred) = q.as_doc_predicate(reader) {
285 log::debug!("BooleanQuery planner 3a: MUST clause → predicate ({})", q);
286 predicates.push(pred);
287 } else if should_is_sparse {
288 if let Some(bitset) = q.as_doc_bitset(reader) {
289 log::debug!("BooleanQuery planner 3a: MUST clause → bitset predicate ({})", q);
290 predicates.push(Box::new(move |doc_id| bitset.contains(doc_id)));
291 } else {
292 log::debug!("BooleanQuery planner 3a: MUST clause → verifier scorer ({})", q);
293 must_verifiers.push(q.$scorer_fn(reader, limit) $(. $aw)* ?);
294 }
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 }
300 let mut must_not_verifiers: Vec<Box<dyn super::Scorer + '_>> = Vec::new();
302 for q in must_not {
303 if let Some(pred) = q.as_doc_predicate(reader) {
304 let negated: super::DocPredicate<'_> =
305 Box::new(move |doc_id| !pred(doc_id));
306 predicates.push(negated);
307 } else if should_is_sparse {
308 if let Some(bitset) = q.as_doc_bitset(reader) {
309 log::debug!("BooleanQuery planner 3a: MUST_NOT clause → bitset predicate ({})", q);
310 predicates.push(Box::new(move |doc_id| !bitset.contains(doc_id)));
311 } else {
312 must_not_verifiers.push(q.$scorer_fn(reader, limit) $(. $aw)* ?);
313 }
314 } else {
315 must_not_verifiers.push(q.$scorer_fn(reader, limit) $(. $aw)* ?);
316 }
317 }
318
319 if must_verifiers.is_empty()
321 && must_not_verifiers.is_empty()
322 && !predicates.is_empty()
323 {
324 if let Some(infos) = extract_all_sparse_infos(should) {
325 let bitset_result = build_combined_bitset(must, must_not, reader);
328 if let Some(ref bitset) = bitset_result {
329 let bitset_pred = |doc_id: crate::DocId| bitset.contains(doc_id);
330 if let Some((raw, info)) =
331 build_sparse_bmp_results_filtered(&infos, reader, limit, &bitset_pred)
332 {
333 log::debug!(
334 "BooleanQuery planner: bitset-aware sparse BMP, {} dims, {} matching docs",
335 infos.len(),
336 bitset.count()
337 );
338 return Ok(combine_sparse_results(raw, info.combiner, info.field, limit));
339 }
340 }
341
342 let combined = chain_predicates(predicates);
344 if let Some((raw, info)) =
345 build_sparse_bmp_results_filtered(&infos, reader, limit, &*combined)
346 {
347 log::debug!(
348 "BooleanQuery planner: predicate-aware sparse BMP, {} dims",
349 infos.len()
350 );
351 return Ok(combine_sparse_results(raw, info.combiner, info.field, limit));
352 }
353 if let Some((executor, info)) =
355 build_sparse_maxscore_executor(&infos, reader, limit, Some(combined))
356 {
357 log::debug!(
358 "BooleanQuery planner: predicate-aware sparse MaxScore, {} dims",
359 infos.len()
360 );
361 let raw = executor.$execute_fn() $(. $aw)* ?;
362 return Ok(combine_sparse_results(raw, info.combiner, info.field, limit));
363 }
364 predicates = Vec::new();
368 for q in must {
369 if let Some(pred) = q.as_doc_predicate(reader) {
370 predicates.push(pred);
371 } else if let Some(bitset) = q.as_doc_bitset(reader) {
372 predicates.push(Box::new(move |doc_id| bitset.contains(doc_id)));
373 }
374 }
375 for q in must_not {
376 if let Some(pred) = q.as_doc_predicate(reader) {
377 let negated: super::DocPredicate<'_> =
378 Box::new(move |doc_id| !pred(doc_id));
379 predicates.push(negated);
380 } else if let Some(bitset) = q.as_doc_bitset(reader) {
381 predicates.push(Box::new(move |doc_id| !bitset.contains(doc_id)));
382 }
383 }
384 }
385 }
386
387 let has_filters = !predicates.is_empty()
389 || !must_verifiers.is_empty()
390 || !must_not_verifiers.is_empty();
391 let should_limit = if has_filters { limit * 4 } else { limit };
392 let should_scorer = if should.len() == 1 {
393 should[0].$scorer_fn(reader, should_limit) $(. $aw)* ?
394 } else {
395 let sub = BooleanQuery {
396 must: Vec::new(),
397 should: should.to_vec(),
398 must_not: Vec::new(),
399 global_stats: global_stats.cloned(),
400 };
401 sub.$scorer_fn(reader, should_limit) $(. $aw)* ?
402 };
403
404 let use_predicated =
405 must_verifiers.is_empty() || should_scorer.size_hint() >= limit as u32;
406
407 if use_predicated {
408 log::debug!(
409 "BooleanQuery planner: PredicatedScorer {} preds + {} must_v + {} must_not_v, \
410 SHOULD size_hint={}, over_fetch={}",
411 predicates.len(), must_verifiers.len(), must_not_verifiers.len(),
412 should_scorer.size_hint(), should_limit
413 );
414 return Ok(Box::new(super::PredicatedScorer::new(
415 should_scorer, predicates, must_verifiers, must_not_verifiers,
416 )));
417 }
418
419 log::debug!(
421 "BooleanQuery planner: BooleanScorer fallback, size_hint={} < limit={}, \
422 {} must_v + {} must_not_v",
423 should_scorer.size_hint(), limit,
424 must_verifiers.len(), must_not_verifiers.len()
425 );
426 let mut scorer = BooleanScorer {
427 must: must_verifiers,
428 should: vec![should_scorer],
429 must_not: must_not_verifiers,
430 current_doc: 0,
431 };
432 scorer.current_doc = scorer.find_next_match();
433 return Ok(Box::new(scorer));
434 }
435
436 let mut must_scorers = Vec::with_capacity(must.len());
438 for q in must {
439 must_scorers.push(q.$scorer_fn(reader, limit) $(. $aw)* ?);
440 }
441 let mut should_scorers = Vec::with_capacity(should.len());
442 for q in should {
443 should_scorers.push(q.$scorer_fn(reader, limit) $(. $aw)* ?);
444 }
445 let mut must_not_scorers = Vec::with_capacity(must_not.len());
446 for q in must_not {
447 must_not_scorers.push(q.$scorer_fn(reader, limit) $(. $aw)* ?);
448 }
449 let mut scorer = BooleanScorer {
450 must: must_scorers,
451 should: should_scorers,
452 must_not: must_not_scorers,
453 current_doc: 0,
454 };
455 scorer.current_doc = scorer.find_next_match();
456 Ok(Box::new(scorer) as Box<dyn Scorer + '_>)
457 }};
458}
459
460impl Query for BooleanQuery {
461 fn scorer<'a>(&self, reader: &'a SegmentReader, limit: usize) -> ScorerFuture<'a> {
462 let must = self.must.clone();
463 let should = self.should.clone();
464 let must_not = self.must_not.clone();
465 let global_stats = self.global_stats.clone();
466 Box::pin(async move {
467 boolean_plan!(
468 must,
469 should,
470 must_not,
471 global_stats.as_ref(),
472 reader,
473 limit,
474 scorer,
475 get_postings,
476 execute,
477 await
478 )
479 })
480 }
481
482 #[cfg(feature = "sync")]
483 fn scorer_sync<'a>(
484 &self,
485 reader: &'a SegmentReader,
486 limit: usize,
487 ) -> crate::Result<Box<dyn Scorer + 'a>> {
488 boolean_plan!(
489 self.must,
490 self.should,
491 self.must_not,
492 self.global_stats.as_ref(),
493 reader,
494 limit,
495 scorer_sync,
496 get_postings_sync,
497 execute_sync
498 )
499 }
500
501 fn as_doc_bitset(&self, reader: &SegmentReader) -> Option<super::DocBitset> {
502 if self.must.is_empty() && self.should.is_empty() {
503 return None;
504 }
505
506 let num_docs = reader.num_docs();
507
508 let mut result: Option<super::DocBitset> = None;
510 for q in &self.must {
511 let bs = q.as_doc_bitset(reader)?;
512 match result {
513 None => result = Some(bs),
514 Some(ref mut acc) => acc.intersect_with(&bs),
515 }
516 }
517
518 if !self.should.is_empty() {
520 let mut should_union = super::DocBitset::new(num_docs);
521 for q in &self.should {
522 let bs = q.as_doc_bitset(reader)?;
523 should_union.union_with(&bs);
524 }
525 match result {
526 None => result = Some(should_union),
527 Some(ref mut acc) => {
528 if self.must.is_empty() {
531 *acc = should_union;
532 }
533 }
534 }
535 }
536
537 if let Some(ref mut acc) = result {
539 for q in &self.must_not {
540 {
541 let bs = q.as_doc_bitset(reader)?;
542 acc.subtract(&bs);
543 }
544 }
545 }
546
547 result
548 }
549
550 fn as_doc_predicate<'a>(&self, reader: &'a SegmentReader) -> Option<super::DocPredicate<'a>> {
551 if self.must.is_empty() && self.should.is_empty() {
553 return None;
554 }
555
556 let must_preds: Vec<_> = self
558 .must
559 .iter()
560 .map(|q| q.as_doc_predicate(reader))
561 .collect::<Option<Vec<_>>>()?;
562 let should_preds: Vec<_> = self
563 .should
564 .iter()
565 .map(|q| q.as_doc_predicate(reader))
566 .collect::<Option<Vec<_>>>()?;
567 let must_not_preds: Vec<_> = self
568 .must_not
569 .iter()
570 .map(|q| q.as_doc_predicate(reader))
571 .collect::<Option<Vec<_>>>()?;
572
573 let has_must = !must_preds.is_empty();
574
575 Some(Box::new(move |doc_id| {
576 if !must_preds.iter().all(|p| p(doc_id)) {
578 return false;
579 }
580 if !has_must && !should_preds.is_empty() && !should_preds.iter().any(|p| p(doc_id)) {
582 return false;
583 }
584 must_not_preds.iter().all(|p| !p(doc_id))
586 }))
587 }
588
589 fn count_estimate<'a>(&self, reader: &'a SegmentReader) -> CountFuture<'a> {
590 let must = self.must.clone();
591 let should = self.should.clone();
592
593 Box::pin(async move {
594 if !must.is_empty() {
595 let mut estimates = Vec::with_capacity(must.len());
596 for q in &must {
597 estimates.push(q.count_estimate(reader).await?);
598 }
599 estimates
600 .into_iter()
601 .min()
602 .ok_or_else(|| crate::Error::Corruption("Empty must clause".to_string()))
603 } else if !should.is_empty() {
604 let mut sum = 0u32;
605 for q in &should {
606 sum = sum.saturating_add(q.count_estimate(reader).await?);
607 }
608 Ok(sum)
609 } else {
610 Ok(0)
611 }
612 })
613 }
614}
615
616struct BooleanScorer<'a> {
617 must: Vec<Box<dyn Scorer + 'a>>,
618 should: Vec<Box<dyn Scorer + 'a>>,
619 must_not: Vec<Box<dyn Scorer + 'a>>,
620 current_doc: DocId,
621}
622
623impl BooleanScorer<'_> {
624 fn find_next_match(&mut self) -> DocId {
625 if self.must.is_empty() && self.should.is_empty() {
626 return TERMINATED;
627 }
628
629 loop {
630 let candidate = if !self.must.is_empty() {
631 let mut max_doc = self
632 .must
633 .iter()
634 .map(|s| s.doc())
635 .max()
636 .unwrap_or(TERMINATED);
637
638 if max_doc == TERMINATED {
639 return TERMINATED;
640 }
641
642 loop {
643 let mut all_match = true;
644 for scorer in &mut self.must {
645 let doc = scorer.seek(max_doc);
646 if doc == TERMINATED {
647 return TERMINATED;
648 }
649 if doc > max_doc {
650 max_doc = doc;
651 all_match = false;
652 break;
653 }
654 }
655 if all_match {
656 break;
657 }
658 }
659 max_doc
660 } else {
661 self.should
662 .iter()
663 .map(|s| s.doc())
664 .filter(|&d| d != TERMINATED)
665 .min()
666 .unwrap_or(TERMINATED)
667 };
668
669 if candidate == TERMINATED {
670 return TERMINATED;
671 }
672
673 let excluded = self.must_not.iter_mut().any(|scorer| {
674 let doc = scorer.seek(candidate);
675 doc == candidate
676 });
677
678 if !excluded {
679 for scorer in &mut self.should {
681 scorer.seek(candidate);
682 }
683 self.current_doc = candidate;
684 return candidate;
685 }
686
687 if !self.must.is_empty() {
689 for scorer in &mut self.must {
690 scorer.advance();
691 }
692 } else {
693 for scorer in &mut self.should {
695 if scorer.doc() <= candidate && scorer.doc() != TERMINATED {
696 scorer.seek(candidate + 1);
697 }
698 }
699 }
700 }
701 }
702}
703
704impl super::docset::DocSet for BooleanScorer<'_> {
705 fn doc(&self) -> DocId {
706 self.current_doc
707 }
708
709 fn advance(&mut self) -> DocId {
710 if !self.must.is_empty() {
711 for scorer in &mut self.must {
712 scorer.advance();
713 }
714 } else {
715 for scorer in &mut self.should {
716 if scorer.doc() == self.current_doc {
717 scorer.advance();
718 }
719 }
720 }
721
722 self.current_doc = self.find_next_match();
723 self.current_doc
724 }
725
726 fn seek(&mut self, target: DocId) -> DocId {
727 for scorer in &mut self.must {
728 scorer.seek(target);
729 }
730
731 for scorer in &mut self.should {
732 scorer.seek(target);
733 }
734
735 self.current_doc = self.find_next_match();
736 self.current_doc
737 }
738
739 fn size_hint(&self) -> u32 {
740 if !self.must.is_empty() {
741 self.must.iter().map(|s| s.size_hint()).min().unwrap_or(0)
742 } else {
743 self.should.iter().map(|s| s.size_hint()).sum()
744 }
745 }
746}
747
748impl Scorer for BooleanScorer<'_> {
749 fn score(&self) -> Score {
750 let mut total = 0.0;
751
752 for scorer in &self.must {
753 if scorer.doc() == self.current_doc {
754 total += scorer.score();
755 }
756 }
757
758 for scorer in &self.should {
759 if scorer.doc() == self.current_doc {
760 total += scorer.score();
761 }
762 }
763
764 total
765 }
766
767 fn matched_positions(&self) -> Option<super::MatchedPositions> {
768 let mut all_positions: super::MatchedPositions = Vec::new();
769
770 for scorer in &self.must {
771 if scorer.doc() == self.current_doc
772 && let Some(positions) = scorer.matched_positions()
773 {
774 all_positions.extend(positions);
775 }
776 }
777
778 for scorer in &self.should {
779 if scorer.doc() == self.current_doc
780 && let Some(positions) = scorer.matched_positions()
781 {
782 all_positions.extend(positions);
783 }
784 }
785
786 if all_positions.is_empty() {
787 None
788 } else {
789 Some(all_positions)
790 }
791 }
792}
793
794#[cfg(test)]
795mod tests {
796 use super::*;
797 use crate::dsl::Field;
798 use crate::query::{QueryDecomposition, TermQuery};
799
800 #[test]
801 fn test_maxscore_eligible_pure_or_same_field() {
802 let query = BooleanQuery::new()
804 .should(TermQuery::text(Field(0), "hello"))
805 .should(TermQuery::text(Field(0), "world"))
806 .should(TermQuery::text(Field(0), "foo"));
807
808 assert!(
810 query
811 .should
812 .iter()
813 .all(|q| matches!(q.decompose(), QueryDecomposition::TextTerm(_)))
814 );
815
816 let infos: Vec<_> = query
818 .should
819 .iter()
820 .filter_map(|q| match q.decompose() {
821 QueryDecomposition::TextTerm(info) => Some(info),
822 _ => None,
823 })
824 .collect();
825 assert_eq!(infos.len(), 3);
826 assert!(infos.iter().all(|i| i.field == Field(0)));
827 }
828
829 #[test]
830 fn test_maxscore_not_eligible_different_fields() {
831 let query = BooleanQuery::new()
833 .should(TermQuery::text(Field(0), "hello"))
834 .should(TermQuery::text(Field(1), "world")); let infos: Vec<_> = query
837 .should
838 .iter()
839 .filter_map(|q| match q.decompose() {
840 QueryDecomposition::TextTerm(info) => Some(info),
841 _ => None,
842 })
843 .collect();
844 assert_eq!(infos.len(), 2);
845 assert!(infos[0].field != infos[1].field);
847 }
848
849 #[test]
850 fn test_maxscore_not_eligible_with_must() {
851 let query = BooleanQuery::new()
853 .must(TermQuery::text(Field(0), "required"))
854 .should(TermQuery::text(Field(0), "hello"))
855 .should(TermQuery::text(Field(0), "world"));
856
857 assert!(!query.must.is_empty());
859 }
860
861 #[test]
862 fn test_maxscore_not_eligible_with_must_not() {
863 let query = BooleanQuery::new()
865 .should(TermQuery::text(Field(0), "hello"))
866 .should(TermQuery::text(Field(0), "world"))
867 .must_not(TermQuery::text(Field(0), "excluded"));
868
869 assert!(!query.must_not.is_empty());
871 }
872
873 #[test]
874 fn test_maxscore_not_eligible_single_term() {
875 let query = BooleanQuery::new().should(TermQuery::text(Field(0), "hello"));
877
878 assert_eq!(query.should.len(), 1);
880 }
881
882 #[test]
883 fn test_term_query_info_extraction() {
884 let term_query = TermQuery::text(Field(42), "test");
885 match term_query.decompose() {
886 QueryDecomposition::TextTerm(info) => {
887 assert_eq!(info.field, Field(42));
888 assert_eq!(info.term, b"test");
889 }
890 _ => panic!("Expected TextTerm decomposition"),
891 }
892 }
893
894 #[test]
895 fn test_boolean_query_no_term_info() {
896 let query = BooleanQuery::new().should(TermQuery::text(Field(0), "hello"));
898
899 assert!(matches!(query.decompose(), QueryDecomposition::Opaque));
900 }
901}