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, build_sparse_results, build_sparse_results_filtered, cap_terms,
12 chain_predicates, combine_sparse_results, compute_idf, extract_all_sparse_infos,
13 finish_chunked_text_maxscore, finish_text_maxscore, prepare_per_field_grouping,
14 prepare_text_maxscore, sparse_result_scorer, text_maxscore_allowed,
15};
16use super::{CountFuture, EmptyScorer, GlobalStats, Query, Scorer, ScorerFuture};
17
18#[derive(Clone)]
23pub struct BooleanQuery {
24 pub must: Vec<Arc<dyn Query>>,
25 pub should: Vec<Arc<dyn Query>>,
26 pub must_not: Vec<Arc<dyn Query>>,
27 global_stats: Option<Arc<GlobalStats>>,
29 proximity: Option<super::ProximityConfig>,
32 text_heap_factor: f32,
35 max_terms: usize,
38}
39
40fn shared_or_extract_sparse_infos<'a>(
41 plan: Option<&'a Arc<super::bmp::LspSegmentPlan>>,
42 should: &[Arc<dyn Query>],
43) -> Option<std::borrow::Cow<'a, [super::SparseTermQueryInfo]>> {
44 plan.map(|plan| std::borrow::Cow::Borrowed(plan.infos.as_ref()))
45 .or_else(|| extract_all_sparse_infos(should).map(std::borrow::Cow::Owned))
46}
47
48impl std::fmt::Debug for BooleanQuery {
49 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
50 f.debug_struct("BooleanQuery")
51 .field("must_count", &self.must.len())
52 .field("should_count", &self.should.len())
53 .field("must_not_count", &self.must_not.len())
54 .field("has_global_stats", &self.global_stats.is_some())
55 .field("proximity", &self.proximity)
56 .finish()
57 }
58}
59
60impl std::fmt::Display for BooleanQuery {
61 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
62 write!(f, "Boolean(")?;
63 let mut first = true;
64 for q in &self.must {
65 if !first {
66 write!(f, " ")?;
67 }
68 write!(f, "+{}", q)?;
69 first = false;
70 }
71 for q in &self.should {
72 if !first {
73 write!(f, " ")?;
74 }
75 write!(f, "{}", q)?;
76 first = false;
77 }
78 for q in &self.must_not {
79 if !first {
80 write!(f, " ")?;
81 }
82 write!(f, "-{}", q)?;
83 first = false;
84 }
85 if let Some(proximity) = &self.proximity {
86 write!(f, " ~proximity({}, {})", proximity.weight, proximity.window)?;
87 }
88 if self.text_heap_factor < 1.0 {
89 write!(f, " ~heap({})", self.text_heap_factor)?;
90 }
91 if self.max_terms > 0 {
92 write!(f, " ~max_terms({})", self.max_terms)?;
93 }
94 write!(f, ")")
95 }
96}
97
98impl Default for BooleanQuery {
99 fn default() -> Self {
100 Self {
101 must: Vec::new(),
102 should: Vec::new(),
103 must_not: Vec::new(),
104 global_stats: None,
105 proximity: None,
106 text_heap_factor: 1.0,
107 max_terms: 0,
108 }
109 }
110}
111
112impl BooleanQuery {
113 pub fn new() -> Self {
114 Self::default()
115 }
116
117 pub fn must(mut self, query: impl Query + 'static) -> Self {
118 self.must.push(Arc::new(query));
119 self
120 }
121
122 pub fn should(mut self, query: impl Query + 'static) -> Self {
123 self.should.push(Arc::new(query));
124 self
125 }
126
127 pub fn must_not(mut self, query: impl Query + 'static) -> Self {
128 self.must_not.push(Arc::new(query));
129 self
130 }
131
132 pub fn with_global_stats(mut self, stats: Arc<GlobalStats>) -> Self {
134 self.global_stats = Some(stats);
135 self
136 }
137
138 pub fn with_proximity(mut self, config: super::ProximityConfig) -> Self {
142 self.proximity = config.is_active().then_some(config);
143 self
144 }
145
146 pub fn with_text_heap_factor(mut self, heap_factor: f32) -> Self {
151 self.text_heap_factor = heap_factor;
152 self
153 }
154
155 pub fn with_max_terms(mut self, max_terms: usize) -> Self {
158 self.max_terms = max_terms;
159 self
160 }
161}
162
163fn flatten_should(should: &[Arc<dyn Query>]) -> std::borrow::Cow<'_, [Arc<dyn Query>]> {
170 if !should.iter().any(|query| query.should_children().is_some()) {
171 return std::borrow::Cow::Borrowed(should);
172 }
173
174 fn push_flat(out: &mut Vec<Arc<dyn Query>>, query: &Arc<dyn Query>) {
175 match query.should_children() {
176 Some(children) => children.iter().for_each(|child| push_flat(out, child)),
177 None => out.push(Arc::clone(query)),
178 }
179 }
180
181 let mut flat = Vec::with_capacity(should.len());
182 should.iter().for_each(|query| push_flat(&mut flat, query));
183 std::borrow::Cow::Owned(flat)
184}
185
186fn build_should_scorer<'a>(scorers: Vec<Box<dyn Scorer + 'a>>) -> Box<dyn Scorer + 'a> {
188 if scorers.is_empty() {
189 return Box::new(EmptyScorer);
190 }
191 if scorers.len() == 1 {
192 return scorers.into_iter().next().unwrap();
193 }
194 let mut scorer = BooleanScorer {
195 must: vec![],
196 should: scorers,
197 must_not: vec![],
198 current_doc: 0,
199 lead: 0,
200 doc_limit: 0,
201 };
202 scorer.initialize();
203 Box::new(scorer)
204}
205
206macro_rules! boolean_plan {
220 ($must:expr, $should:expr, $must_not:expr, $global_stats:expr, $proximity:expr, $text_tuning:expr,
221 $reader:expr, $limit:expr, $scorer_options:expr,
222 $scorer_fn:ident, $get_postings_fn:ident, $execute_fn:ident
223 $(, $aw:tt)*) => {{
224 let must: &[Arc<dyn Query>] = &$must;
225 let should_flat = flatten_should(&$should);
226 let should_all: &[Arc<dyn Query>] = &should_flat;
227 let must_not: &[Arc<dyn Query>] = &$must_not;
228 let global_stats: Option<&Arc<GlobalStats>> = $global_stats;
229 let reader: &SegmentReader = $reader;
230 let limit: usize = $limit;
231 let mut scorer_options: super::ScorerOptions = $scorer_options;
232 scorer_options.global_stats = global_stats.cloned();
235 if !$text_tuning.0.is_finite() || !(0.0..=1.0).contains(&$text_tuning.0) {
236 return Err(crate::Error::Query(
237 "Text heap_factor must be finite and between 0 and 1".into(),
238 ));
239 }
240 if scorer_options.stop_if_expired() {
241 return Ok(Box::new(EmptyScorer) as Box<dyn Scorer + '_>);
242 }
243
244 let should_capped: Vec<Arc<dyn Query>>;
247 let should: &[Arc<dyn Query>] = if should_all.len() > super::MAX_QUERY_TERMS {
248 let is_predicate: Vec<bool> = should_all
249 .iter()
250 .map(|q| q.is_filter() || q.as_doc_predicate(reader).is_some())
251 .collect();
252 let cursor_count = is_predicate.iter().filter(|&&p| !p).count();
253
254 if cursor_count > super::MAX_QUERY_TERMS {
255 let mut kept = Vec::with_capacity(should_all.len());
256 let mut cursor_kept = 0usize;
257 for (q, &is_pred) in should_all.iter().zip(is_predicate.iter()) {
258 if is_pred {
259 kept.push(q.clone());
260 } else if cursor_kept < super::MAX_QUERY_TERMS {
261 kept.push(q.clone());
262 cursor_kept += 1;
263 }
264 }
265 log::warn!(
266 "BooleanQuery: capping cursor SHOULD from {} to {} ({} fast-field predicates exempt); dropped clauses do not match or score",
267 cursor_count,
268 super::MAX_QUERY_TERMS,
269 kept.len() - cursor_kept,
270 );
271 should_capped = kept;
272 &should_capped
273 } else {
274 log::debug!(
275 "BooleanQuery: {} SHOULD clauses OK ({} need cursors, {} fast-field predicates)",
276 should_all.len(),
277 cursor_count,
278 should_all.len() - cursor_count,
279 );
280 should_all
281 }
282 } else {
283 should_all
284 };
285
286 if must_not.is_empty() {
288 if must.len() == 1 && should.is_empty() {
289 return must[0].$scorer_fn(reader, limit, scorer_options) $(. $aw)* ;
290 }
291 if should.len() == 1 && must.is_empty() && $text_tuning.0 == 1.0 {
292 return should[0].$scorer_fn(reader, limit, scorer_options) $(. $aw)* ;
293 }
294 }
295
296 if (scorer_options.complete_text_matches || scorer_options.physical_text_field.is_some()) && must.is_empty() && must_not.is_empty()
297 && !should.is_empty()
298 && let Some((infos, field, avg_field_len, num_docs)) = prepare_text_maxscore(should, reader, global_stats)
299 && text_maxscore_allowed(reader, field, scorer_options.collect_positions)
300 {
301 if $proximity.is_some() {
302 return Err(crate::Error::Query("proximity scoring inside a required text clause is not supported".into()));
303 }
304 let mut postings = Vec::with_capacity(infos.len());
305 for info in infos {
306 if let Some(pl) = reader.$get_postings_fn(info.field, &info.term) $(. $aw)* ? {
307 let idf = compute_idf(&pl, field, &info.term, num_docs, global_stats) * info.weight;
308 postings.push((pl, idf));
309 }
310 }
311 return super::term::complete_text_scorer(postings, avg_field_len, reader, field, &scorer_options);
312 }
313
314 let counted_limit = scorer_options.ranked_count_limit.filter(|_| should.is_empty()
320 && scorer_options.shared_threshold.as_ref().and_then(super::SharedThreshold::deadline).is_none());
321 let ranked_limit = counted_limit.unwrap_or(limit);
322 if !must.is_empty() && must_not.is_empty()
323 && must.len() + should.len() <= super::MAX_QUERY_TERMS
324 && ranked_limit < reader.num_docs() as usize
325 && (!scorer_options.complete_text_matches || counted_limit.is_some()) && !scorer_options.collect_positions
326 && $proximity.is_none() && $text_tuning.0 == 1.0 && $text_tuning.1 == 0
327 && let Some((required, field, _, _)) = prepare_text_maxscore(must, reader, global_stats)
328 && let Some(optional) = ranked_optional_terms(should, field, reader, global_stats)
329 && required.iter().chain(&optional).all(|info| info.weight == 1.0)
330 && (!reader.has_text_mapping(field)
331 || (scorer_options.physical_text_field == Some(field) && reader.alive_docs().is_none()))
332 {
333 let required_count = required.len();
334 let params = super::Bm25Params::for_field(reader.schema(), field);
335 let mut cursors = Vec::with_capacity(required_count + optional.len());
336 for (index, info) in required.into_iter().chain(optional).enumerate() {
337 let Some(postings) = reader.$get_postings_fn(field, &info.term) $(. $aw)* ? else {
338 if index < required_count {
339 log::debug!("BooleanQuery planner: required term absent → empty result");
340 return Ok(Box::new(EmptyScorer) as Box<dyn Scorer + '_>);
341 }
342 continue;
343 };
344 let (idf, avg_len) = super::term::compute_term_idf(
345 &postings, field, reader, global_stats, &info.term,
346 );
347 cursors.push(super::TermCursor::text_with_params(
348 postings, idf, avg_len,
349 reader.chunk_map(field).map(super::LengthSource::Chunks)
350 .or_else(|| reader.doc_lengths(field).map(super::LengthSource::Docs)), params,
351 ));
352 }
353 log::debug!(
354 "BooleanQuery planner: ranked text windows, {} required + {} optional terms",
355 required_count,
356 cursors.len() - required_count
357 );
358 let executor = ranked_text_executor(cursors, required_count, ranked_limit, reader, field, &scorer_options);
359 if counted_limit.is_some() {
360 let (mut results, count) = executor.execute_counted_conjunction()?;
361 super::text_mapping::physical_results(&mut results, reader, scorer_options.physical_text_field);
362 return Ok(Box::new(super::planner::TopKResultScorer::new(results).with_exact_count(count)) as Box<dyn Scorer + '_>);
363 }
364 let mut results = executor.$execute_fn() $(. $aw)* ?;
365 super::text_mapping::physical_results(&mut results, reader, scorer_options.physical_text_field);
366 return Ok(Box::new(super::planner::TopKResultScorer::new(results)) as Box<dyn Scorer + '_>);
367 }
368
369 if scorer_options.physical_text_field.is_none() {
371 if must.is_empty() && must_not.is_empty()
373 && (should.len() >= 2 || (should.len() == 1 && $text_tuning.0 < 1.0)) {
374 if !scorer_options.complete_text_matches
376 && let Some((mut infos, text_field, avg_field_len, num_docs)) =
377 prepare_text_maxscore(should, reader, global_stats)
378 && text_maxscore_allowed(reader, text_field, scorer_options.collect_positions)
379 {
380 let mut posting_lists = Vec::with_capacity(infos.len());
381 let mut term_bytes: Vec<Vec<u8>> = Vec::new();
382 for info in infos.drain(..) {
383 if let Some(pl) = reader.$get_postings_fn(info.field, &info.term)
384 $(. $aw)* ?
385 {
386 let idf = compute_idf(&pl, info.field, &info.term, num_docs, global_stats) * info.weight;
387 posting_lists.push((pl, idf));
388 term_bytes.push(info.term.clone());
389 }
390 }
391 cap_terms(&mut posting_lists, &mut term_bytes, $text_tuning.1);
392 if reader.has_text_mapping(text_field) {
394 return finish_chunked_text_maxscore(
395 posting_lists, avg_field_len, limit, reader, text_field, eligibility_predicate(&scorer_options),
396 $proximity.map(|config| (config, term_bytes)),
397 $text_tuning.0,
398 scorer_options.shared_threshold.as_ref(),
399 );
400 }
401 let shared_threshold = std::cell::Cell::new(scorer_options.initial_threshold);
407 return finish_text_maxscore(
408 posting_lists,
409 avg_field_len,
410 reader.doc_lengths(text_field),
411 limit,
412 &shared_threshold,
413 reader,
414 text_field,
415 eligibility_predicate(&scorer_options),
416 super::Bm25Params::for_field(reader.schema(), text_field),
417 $proximity.map(|config| (config, term_bytes)),
418 $text_tuning.0,
419 scorer_options.shared_threshold.as_ref(),
420 );
421 }
422
423 if let Some(infos) =
426 shared_or_extract_sparse_infos(scorer_options.lsp_plan.as_ref(), should)
427 {
428 if !scorer_options.complete_text_matches
429 && let Some((raw, info)) = build_sparse_results(&infos, reader, limit, &scorer_options)?
430 {
431 return Ok(sparse_result_scorer(raw, info.field));
432 }
433 if let Some((raw, info)) =
434 build_sparse_bmp_results(&infos, reader, limit, &scorer_options)?
435 {
436 return Ok(combine_sparse_results(raw, info.combiner, info.field, limit));
437 }
438 if let Some((executor, info)) =
439 build_sparse_maxscore_executor(&infos, reader, limit, None, &scorer_options)
440 {
441 let raw = executor.$execute_fn() $(. $aw)* ?;
442 return Ok(combine_sparse_results(raw, info.combiner, info.field, limit));
443 }
444 }
445
446 if !scorer_options.complete_text_matches
448 && let Some(grouping) = prepare_per_field_grouping(
449 should,
450 reader,
451 limit,
452 global_stats,
453 scorer_options.collect_positions,
454 ) {
455 let mut scorers: Vec<Box<dyn Scorer + '_>> = Vec::new();
456 let shared_threshold = std::cell::Cell::new(0.0f32);
458 for (field, avg_field_len, infos) in &grouping.multi_term_groups {
459 let corpus_size = reader.text_corpus_size(*field);
461 let mut posting_lists = Vec::with_capacity(infos.len());
462 let mut term_bytes: Vec<Vec<u8>> = Vec::new();
463 for info in infos {
464 if let Some(pl) = reader.$get_postings_fn(info.field, &info.term)
465 $(. $aw)* ?
466 {
467 let idf = compute_idf(
468 &pl, *field, &info.term, corpus_size, global_stats,
469 ) * info.weight;
470 posting_lists.push((pl, idf));
471 term_bytes.push(info.term.clone());
472 }
473 }
474 cap_terms(&mut posting_lists, &mut term_bytes, $text_tuning.1);
475 if reader.has_text_mapping(*field) {
476 scorers.push(finish_chunked_text_maxscore(
477 posting_lists,
478 *avg_field_len,
479 grouping.per_field_limit,
480 reader,
481 *field,
482 eligibility_predicate(&scorer_options),
483 $proximity.map(|config| (config, term_bytes)),
484 $text_tuning.0,
485 scorer_options.shared_threshold.as_ref(),
486 )?);
487 } else if !posting_lists.is_empty() {
488 scorers.push(finish_text_maxscore(
489 posting_lists,
490 *avg_field_len,
491 reader.doc_lengths(*field),
492 grouping.per_field_limit,
493 &shared_threshold,
494 reader,
495 *field,
496 eligibility_predicate(&scorer_options),
497 super::Bm25Params::for_field(reader.schema(), *field),
498 $proximity.map(|config| (config, term_bytes)),
499 $text_tuning.0,
500 scorer_options.shared_threshold.as_ref(),
501 )?);
502 }
503 }
504 for &idx in &grouping.fallback_indices {
507 scorers.push(should[idx].$scorer_fn(
508 reader,
509 limit,
510 scorer_options.for_required_clause(),
511 ) $(. $aw)* ?);
512 }
513 return Ok(build_should_scorer(scorers));
514 }
515 }
516
517 if (!scorer_options.complete_text_matches || extract_all_sparse_infos(should).is_some())
524 && !should.is_empty() && (!must.is_empty() || !must_not.is_empty()) {
525 let text_groups: Option<Vec<(crate::Field, Vec<super::TermQueryInfo>)>> = {
537 let mut groups: Vec<(crate::Field, Vec<super::TermQueryInfo>)> = Vec::new();
538 let mut all_text = true;
539 for q in should {
540 match q.decompose() {
541 super::QueryDecomposition::TextTerm(info)
542 if info.global_stats.is_none() && text_maxscore_allowed(
543 reader, info.field, scorer_options.collect_positions,
544 ) =>
545 {
546 match groups.iter_mut().find(|(f, _)| *f == info.field) {
547 Some((_, infos)) => infos.push(info),
548 None => groups.push((info.field, vec![info])),
549 }
550 }
551 _ => {
552 all_text = false;
553 break;
554 }
555 }
556 }
557 all_text.then_some(groups)
558 };
559 if must.iter().all(|query| {
560 query.is_filter()
561 || query.as_doc_predicate(reader).is_some()
562 || (!matches!(
563 query.decompose(),
564 super::QueryDecomposition::TextTerm(_)
565 ) && scorer_options.doc_bitset(query.as_ref(), reader).is_some())
566 })
567 && let Some(groups) = text_groups
568 && (groups.len() == 1
569 || ($proximity.is_none()
570 && groups
571 .iter()
572 .all(|(field, _)| !reader.has_text_mapping(*field))))
573 && let Some(bitset) = build_combined_bitset(must, must_not, reader, &scorer_options)
574 {
575 if scorer_options.stop_if_expired() {
576 return Ok(Box::new(EmptyScorer) as Box<dyn Scorer + '_>);
577 }
578 let bitset = std::sync::Arc::new(bitset);
579 let single_field = groups.len() == 1;
580
581 if !single_field {
588 let mut cursors = Vec::new();
589 for (field, infos) in groups {
590 let corpus_size = reader.text_corpus_size(field);
591 let avg_field_len = global_stats
592 .map(|stats| stats.avg_field_len(field))
593 .unwrap_or_else(|| reader.avg_field_len(field));
594 let params = super::Bm25Params::for_field(reader.schema(), field);
595 let mut posting_lists = Vec::with_capacity(infos.len());
596 let mut term_bytes = Vec::with_capacity(infos.len());
597 for info in &infos {
598 if let Some(postings) =
599 reader.$get_postings_fn(field, &info.term) $(. $aw)* ?
600 {
601 let idf = compute_idf(
602 &postings,
603 field,
604 &info.term,
605 corpus_size,
606 global_stats,
607 ) * info.weight;
608 posting_lists.push((postings, idf));
609 term_bytes.push(info.term.clone());
610 }
611 }
612 cap_terms(&mut posting_lists, &mut term_bytes, $text_tuning.1);
613 cursors.extend(posting_lists.into_iter().map(|(postings, idf)| {
614 super::TermCursor::text_with_params(
615 postings,
616 idf,
617 avg_field_len,
618 reader.doc_lengths(field).map(super::LengthSource::Docs),
619 params,
620 )
621 }));
622 }
623
624 let filter = bitset.clone();
625 let predicate: super::DocPredicate<'_> =
626 Box::new(move |doc_id| filter.contains(doc_id));
627 let mut executor = super::MaxScoreExecutor::new(
628 cursors,
629 limit,
630 $text_tuning.0,
631 )
632 .with_metric_labels(reader.schema().index_label(), "<multiple>")
633 .with_predicate(predicate)
634 .with_budget(scorer_options.shared_threshold.clone());
635 if $text_tuning.0 == 1.0 && scorer_options.initial_threshold > 0.0 {
636 executor.seed_threshold(scorer_options.initial_threshold);
637 }
638 let results = executor.execute_sync()?;
639 let found = results.len() as u32;
640 let should_scorer: Box<dyn Scorer + '_> =
641 Box::new(super::planner::TopKResultScorer::new(results));
642 if !must.is_empty() && (found as usize) < limit && bitset.count() > found {
643 return Ok(Box::new(super::planner::BitsetFillScorer::new(
644 should_scorer,
645 bitset,
646 )));
647 }
648 return Ok(should_scorer);
649 }
650
651 let group_limit = if single_field {
652 limit
653 } else {
654 super::max_candidate_limit(limit)
655 .min(reader.num_docs() as usize)
656 .max(1)
657 };
658 let shared_threshold = std::cell::Cell::new(if single_field {
662 scorer_options.initial_threshold
663 } else {
664 0.0
665 });
666 let mut scorers: Vec<Box<dyn Scorer + '_>> = Vec::new();
667 let mut found = 0u32;
668 let mut complete = true;
669 for (field, infos) in groups {
670 let corpus_size = reader.text_corpus_size(field);
671 let avg_field_len = global_stats
672 .map(|s| s.avg_field_len(field))
673 .unwrap_or_else(|| reader.avg_field_len(field));
674 let mut posting_lists = Vec::with_capacity(infos.len());
675 let mut term_bytes: Vec<Vec<u8>> = Vec::new();
676 for info in &infos {
677 if let Some(pl) = reader.$get_postings_fn(field, &info.term) $(. $aw)* ? {
678 let idf = compute_idf(&pl, field, &info.term, corpus_size, global_stats) * info.weight;
679 posting_lists.push((pl, idf));
680 term_bytes.push(info.term.clone());
681 }
682 }
683 cap_terms(&mut posting_lists, &mut term_bytes, $text_tuning.1);
684 let filter = bitset.clone();
685 let predicate: super::DocPredicate<'_> =
686 Box::new(move |doc_id| filter.contains(doc_id));
687 let scorer = if reader.has_text_mapping(field) {
688 finish_chunked_text_maxscore(
689 posting_lists, avg_field_len, group_limit, reader, field, Some(predicate),
690 $proximity.map(|config| (config, term_bytes)),
691 $text_tuning.0,
692 scorer_options.shared_threshold.as_ref(),
693 )?
694 } else {
695 finish_text_maxscore(
696 posting_lists,
697 avg_field_len,
698 reader.doc_lengths(field),
699 group_limit,
700 &shared_threshold,
701 reader,
702 field,
703 Some(predicate),
704 super::Bm25Params::for_field(reader.schema(), field),
705 $proximity.map(|config| (config, term_bytes)),
706 $text_tuning.0,
707 scorer_options.shared_threshold.as_ref(),
708 )?
709 };
710 let hits = scorer.size_hint();
711 found = found.saturating_add(hits);
712 if hits as usize >= group_limit {
713 complete = false;
714 }
715 scorers.push(scorer);
716 }
717 log::debug!(
718 "BooleanQuery planner: bitset-aware text MaxScore, {} field group(s), \
719 {} filtered docs, {} scored hits",
720 scorers.len(),
721 bitset.count(),
722 found
723 );
724 let should_scorer = build_should_scorer(scorers);
725 if !must.is_empty()
726 && complete
727 && (found as usize) < limit
728 && bitset.count() > found
729 {
730 return Ok(Box::new(super::planner::BitsetFillScorer::new(
731 should_scorer,
732 bitset,
733 )));
734 }
735 return Ok(should_scorer);
736 }
737
738 let should_is_sparse = scorer_options.lsp_plan.is_some()
745 || extract_all_sparse_infos(should).is_some();
746 let bitset_predicates_allowed = should_is_sparse && !scorer_options.collect_positions;
747
748 super::planner::push_down_text_predicates(
754 must, should, must_not, reader, &mut scorer_options,
755 )?;
756 if scorer_options.stop_if_expired()
757 || scorer_options.eligibility.as_ref()
758 .is_some_and(|bits| bits.next_set_bit(0).is_none()) {
759 return Ok(Box::new(EmptyScorer) as Box<dyn Scorer + '_>);
760 }
761 let mut predicates: Vec<super::DocPredicate<'_>> = Vec::new();
762 let mut must_verifiers: Vec<Box<dyn super::Scorer + '_>> = Vec::new();
763 for q in must {
764 if let Some(pred) = q.as_doc_predicate(reader) {
765 log::debug!("BooleanQuery planner 3a: MUST clause → predicate ({})", q);
766 predicates.push(pred);
767 } else if bitset_predicates_allowed {
768 if let Some(bitset) = scorer_options.doc_bitset(q.as_ref(), reader) {
769 log::debug!("BooleanQuery planner 3a: MUST clause → bitset predicate ({})", q);
770 predicates.push(Box::new(move |doc_id| bitset.contains(doc_id)));
771 } else {
772 log::debug!("BooleanQuery planner 3a: MUST clause → verifier scorer ({})", q);
773 must_verifiers.push(q.$scorer_fn(
774 reader, limit, scorer_options.for_required_clause()
775 ) $(. $aw)* ?);
776 }
777 } else {
778 log::debug!("BooleanQuery planner 3a: MUST clause → verifier scorer ({})", q);
779 must_verifiers.push(q.$scorer_fn(
780 reader, limit, scorer_options.for_required_clause()
781 ) $(. $aw)* ?);
782 }
783 }
784 let mut must_not_verifiers: Vec<Box<dyn super::Scorer + '_>> = Vec::new();
786 for q in must_not {
787 if let Some(pred) = q.as_doc_predicate(reader) {
788 let negated: super::DocPredicate<'_> =
789 Box::new(move |doc_id| !pred(doc_id));
790 predicates.push(negated);
791 } else if bitset_predicates_allowed {
792 if let Some(bitset) = scorer_options.doc_bitset(q.as_ref(), reader) {
793 log::debug!("BooleanQuery planner 3a: MUST_NOT clause → bitset predicate ({})", q);
794 predicates.push(Box::new(move |doc_id| !bitset.contains(doc_id)));
795 } else {
796 must_not_verifiers.push(q.$scorer_fn(
797 reader, limit, scorer_options.for_required_clause()
798 ) $(. $aw)* ?);
799 }
800 } else {
801 must_not_verifiers.push(q.$scorer_fn(
802 reader, limit, scorer_options.for_required_clause()
803 ) $(. $aw)* ?);
804 }
805 }
806
807 if scorer_options.stop_if_expired() {
809 return Ok(Box::new(EmptyScorer) as Box<dyn Scorer + '_>);
810 }
811 if must_verifiers.is_empty()
812 && must_not_verifiers.is_empty()
813 && !predicates.is_empty()
814 {
815 let sparse_infos =
816 shared_or_extract_sparse_infos(scorer_options.lsp_plan.as_ref(), should);
817 if let Some(infos) = sparse_infos {
818 let bitset_result = build_combined_bitset(must, must_not, reader, &scorer_options);
821 if scorer_options.stop_if_expired() {
822 return Ok(Box::new(EmptyScorer) as Box<dyn Scorer + '_>);
823 }
824 if let Some(ref bitset) = bitset_result {
825 let bitset_pred = |doc_id: crate::DocId| bitset.contains(doc_id);
826 if !scorer_options.complete_text_matches
827 && let Some((raw, info)) = build_sparse_results_filtered(
828 &infos, reader, limit, &bitset_pred, &scorer_options
829 )?
830 {
831 return Ok(sparse_result_scorer(raw, info.field));
832 }
833 if let Some((raw, info)) =
834 build_sparse_bmp_results_filtered(
835 &infos, reader, limit, &bitset_pred, &scorer_options
836 )?
837 {
838 log::debug!(
839 "BooleanQuery planner: bitset-aware sparse BMP, {} dims, {} matching docs",
840 infos.len(),
841 bitset.count()
842 );
843 return Ok(combine_sparse_results(raw, info.combiner, info.field, limit));
844 }
845 }
846
847 let combined = chain_predicates(predicates);
849 if !scorer_options.complete_text_matches
850 && let Some((raw, info)) = build_sparse_results_filtered(
851 &infos, reader, limit, &*combined, &scorer_options
852 )?
853 {
854 return Ok(sparse_result_scorer(raw, info.field));
855 }
856 if let Some((raw, info)) =
857 build_sparse_bmp_results_filtered(
858 &infos, reader, limit, &*combined, &scorer_options
859 )?
860 {
861 log::debug!(
862 "BooleanQuery planner: predicate-aware sparse BMP, {} dims",
863 infos.len()
864 );
865 return Ok(combine_sparse_results(raw, info.combiner, info.field, limit));
866 }
867 if let Some((executor, info)) =
869 build_sparse_maxscore_executor(&infos, reader, limit, Some(combined), &scorer_options)
870 {
871 log::debug!(
872 "BooleanQuery planner: predicate-aware sparse MaxScore, {} dims",
873 infos.len()
874 );
875 let raw = executor.$execute_fn() $(. $aw)* ?;
876 return Ok(combine_sparse_results(raw, info.combiner, info.field, limit));
877 }
878 predicates = Vec::new();
882 for q in must {
883 if let Some(pred) = q.as_doc_predicate(reader) {
884 predicates.push(pred);
885 } else if let Some(bitset) = scorer_options.doc_bitset(q.as_ref(), reader) {
886 predicates.push(Box::new(move |doc_id| bitset.contains(doc_id)));
887 }
888 }
889 for q in must_not {
890 if let Some(pred) = q.as_doc_predicate(reader) {
891 let negated: super::DocPredicate<'_> =
892 Box::new(move |doc_id| !pred(doc_id));
893 predicates.push(negated);
894 } else if let Some(bitset) = scorer_options.doc_bitset(q.as_ref(), reader) {
895 predicates.push(Box::new(move |doc_id| !bitset.contains(doc_id)));
896 }
897 }
898 }
899 }
900
901 let mut should_options = if must_verifiers.is_empty() && must_not_verifiers.is_empty() {
906 scorer_options.without_threshold()
907 } else {
908 scorer_options.for_required_clause()
909 };
910 if should_is_sparse {
911 should_options.lsp_plan = scorer_options.lsp_plan.clone();
917 }
918 let proximity_should = $proximity.is_some();
919 if proximity_should {
920 should_options.complete_text_matches = false;
923 }
924 let combined_should = should.len() == 1 || should_is_sparse || proximity_should;
925 let should_scorer: Option<Box<dyn Scorer + '_>> = if should.len() == 1 {
926 Some(should[0].$scorer_fn(reader, limit, should_options.clone()) $(. $aw)* ?)
927 } else if should_is_sparse || proximity_should {
928 let sub = BooleanQuery {
929 must: Vec::new(),
930 should: should.to_vec(),
931 must_not: Vec::new(),
932 global_stats: global_stats.cloned(),
933 proximity: $proximity,
934 text_heap_factor: $text_tuning.0,
935 max_terms: $text_tuning.1,
936 };
937 let sub_limit = if proximity_should {
943 should
944 .first()
945 .and_then(|query| match query.decompose() {
946 super::QueryDecomposition::TextTerm(info) => {
947 Some(reader.text_corpus_size(info.field) as usize)
948 }
949 _ => None,
950 })
951 .unwrap_or(reader.num_docs() as usize)
952 .max(limit)
953 } else {
954 super::max_candidate_limit(limit)
955 };
956 Some(sub.$scorer_fn(
957 reader,
958 sub_limit,
959 should_options.clone(),
960 ) $(. $aw)* ?)
961 } else {
962 None
963 };
964 let should_scorers: Vec<Box<dyn Scorer + '_>> = match should_scorer {
965 Some(scorer) => vec![scorer],
966 None => {
967 let mut scorers = Vec::with_capacity(should.len());
968 for query in should {
969 scorers.push(query.$scorer_fn(
970 reader,
971 limit,
972 should_options.clone(),
973 ) $(. $aw)* ?);
974 }
975 scorers
976 }
977 };
978
979 if must_verifiers.is_empty() {
980 let should_scorer = build_should_scorer(should_scorers);
981 log::debug!(
982 "BooleanQuery planner: PredicatedScorer {} preds + {} must_not_v, \
983 SHOULD size_hint={}, combined={}",
984 predicates.len(), must_not_verifiers.len(),
985 should_scorer.size_hint(), combined_should
986 );
987 return Ok(Box::new(super::PredicatedScorer::new(
988 should_scorer, predicates, Vec::new(), must_not_verifiers,
989 )));
990 }
991
992 log::debug!(
994 "BooleanQuery planner: required-clause BooleanScorer {} must + {} should, \
995 {} preds + {} must_not_v",
996 must_verifiers.len(), should_scorers.len(),
997 predicates.len(), must_not_verifiers.len()
998 );
999 let mut driver = BooleanScorer {
1000 must: must_verifiers,
1001 should: should_scorers,
1002 must_not: Vec::new(),
1003 current_doc: 0,
1004 lead: 0,
1005 doc_limit: reader.num_docs(),
1006 };
1007 driver.initialize();
1008 return Ok(Box::new(super::PredicatedScorer::new(
1009 Box::new(driver),
1010 predicates,
1011 Vec::new(),
1012 must_not_verifiers,
1013 )));
1014 }
1015
1016 }
1017
1018 if scorer_options.physical_text_field.is_none() {
1020 super::planner::push_down_text_predicates(
1021 must, should, must_not, reader, &mut scorer_options,
1022 )?;
1023 }
1024 if scorer_options.stop_if_expired()
1025 || scorer_options.eligibility.as_ref()
1026 .is_some_and(|bits| bits.next_set_bit(0).is_none()) {
1027 return Ok(Box::new(EmptyScorer) as Box<dyn Scorer + '_>);
1028 }
1029 let mut must_scorers = Vec::with_capacity(must.len());
1030 let child_options = if scorer_options.complete_text_matches
1033 || should.len() > 1
1034 || !should.is_empty() && !must.is_empty()
1035 || must.iter().filter(|query| query.as_doc_predicate(reader).is_none()).count() > 1
1036 || must_not.iter().any(|query| query.as_doc_predicate(reader).is_none())
1037 {
1038 scorer_options.for_required_clause()
1039 } else {
1040 scorer_options.without_threshold()
1041 };
1042 if must.is_empty() && should.is_empty() && !must_not.is_empty() {
1043 must_scorers.push(Box::new(super::AllDocSet::new(reader.num_docs()))
1044 as Box<dyn Scorer + '_>);
1045 }
1046 for q in must {
1047 must_scorers.push(q.$scorer_fn(
1048 reader, limit, child_options.clone()
1049 ) $(. $aw)* ?);
1050 }
1051 let mut should_scorers = Vec::with_capacity(should.len());
1052 for q in should {
1053 should_scorers.push(q.$scorer_fn(
1054 reader, limit, child_options.clone()
1055 ) $(. $aw)* ?);
1056 }
1057 let mut must_not_scorers = Vec::with_capacity(must_not.len());
1058 for q in must_not {
1059 must_not_scorers.push(q.$scorer_fn(
1060 reader, limit, scorer_options.for_required_clause()
1061 ) $(. $aw)* ?);
1062 }
1063 let mut scorer = BooleanScorer {
1064 must: must_scorers,
1065 should: should_scorers,
1066 must_not: must_not_scorers,
1067 current_doc: 0,
1068 lead: 0,
1069 doc_limit: reader.num_docs(),
1070 };
1071 scorer.initialize();
1072 Ok(Box::new(scorer) as Box<dyn Scorer + '_>)
1073 }};
1074}
1075
1076impl Query for BooleanQuery {
1077 fn physical_text_field(&self, reader: &SegmentReader, complete: bool) -> Option<crate::Field> {
1078 if self.proximity.is_some() || self.text_heap_factor != 1.0 || self.max_terms != 0 {
1079 return None;
1080 }
1081 if !complete
1083 && self.must.is_empty()
1084 && self.must_not.is_empty()
1085 && self
1086 .should
1087 .iter()
1088 .all(|q| matches!(q.decompose(), super::QueryDecomposition::TextTerm(_)))
1089 {
1090 return None;
1091 }
1092 let mut clauses = self.must.iter().chain(&self.should).chain(&self.must_not);
1093 let field = clauses.next()?.physical_text_field(reader, true)?;
1094 clauses
1095 .all(|q| q.physical_text_field(reader, true) == Some(field))
1096 .then_some(field)
1097 }
1098 fn candidate_query(&self) -> crate::Result<crate::query::CandidateQuery> {
1099 if !self.must.is_empty() || !self.must_not.is_empty() || self.proximity.is_some() {
1100 return Err(crate::Error::Query("L1 scoring branches support SHOULD composition; move required/excluded constraints into fusion.filters and use explicit phrase branches for proximity".into()));
1101 }
1102 if let super::QueryDecomposition::SparseTerms(infos) = self.decompose() {
1103 return super::CandidateQuery::from_decomposition(
1104 super::QueryDecomposition::SparseTerms(infos),
1105 );
1106 }
1107 super::CandidateQuery::sum(self.should.iter().map(|query| query.candidate_query()))
1108 }
1109
1110 fn scorer<'a>(&self, reader: &'a SegmentReader, limit: usize) -> ScorerFuture<'a> {
1111 self.scorer_with_options(reader, limit, super::ScorerOptions::with_positions())
1112 }
1113
1114 fn scorer_with_options<'a>(
1115 &self,
1116 reader: &'a SegmentReader,
1117 limit: usize,
1118 options: super::ScorerOptions,
1119 ) -> ScorerFuture<'a> {
1120 let must = self.must.clone();
1121 let should = self.should.clone();
1122 let must_not = self.must_not.clone();
1123 let global_stats = self
1124 .global_stats
1125 .clone()
1126 .or_else(|| options.global_stats.clone());
1127 let proximity = self.proximity;
1128 let text_tuning = (self.text_heap_factor, self.max_terms);
1129 Box::pin(async move {
1130 boolean_plan!(
1131 must,
1132 should,
1133 must_not,
1134 global_stats.as_ref(),
1135 proximity,
1136 text_tuning,
1137 reader,
1138 limit,
1139 options,
1140 scorer_with_options,
1141 get_postings,
1142 execute,
1143 await
1144 )
1145 })
1146 }
1147
1148 #[cfg(feature = "sync")]
1149 fn scorer_sync<'a>(
1150 &self,
1151 reader: &'a SegmentReader,
1152 limit: usize,
1153 ) -> crate::Result<Box<dyn Scorer + 'a>> {
1154 self.scorer_sync_with_options(reader, limit, super::ScorerOptions::with_positions())
1155 }
1156
1157 #[cfg(feature = "sync")]
1158 fn scorer_sync_with_options<'a>(
1159 &self,
1160 reader: &'a SegmentReader,
1161 limit: usize,
1162 options: super::ScorerOptions,
1163 ) -> crate::Result<Box<dyn Scorer + 'a>> {
1164 let global_stats = self
1165 .global_stats
1166 .clone()
1167 .or_else(|| options.global_stats.clone());
1168 boolean_plan!(
1169 self.must,
1170 self.should,
1171 self.must_not,
1172 global_stats.as_ref(),
1173 self.proximity,
1174 (self.text_heap_factor, self.max_terms),
1175 reader,
1176 limit,
1177 options,
1178 scorer_sync_with_options,
1179 get_postings_sync,
1180 execute_sync
1181 )
1182 }
1183
1184 fn text_terms(&self, out: &mut Vec<(crate::dsl::Field, Vec<u8>)>) {
1185 for clause in self.must.iter().chain(&self.should).chain(&self.must_not) {
1186 clause.text_terms(out);
1187 }
1188 }
1189
1190 fn decompose(&self) -> super::QueryDecomposition {
1191 if !self.must.is_empty() || !self.must_not.is_empty() {
1192 return super::QueryDecomposition::Opaque;
1193 }
1194 self.sparse_decomposition()
1195 }
1196
1197 fn count_equivalent_term(&self) -> Option<super::TermQueryInfo> {
1198 if self.must.len() == 1
1199 && self.must_not.is_empty()
1200 && self.proximity.is_none()
1201 && self.text_heap_factor == 1.0
1202 && self.max_terms == 0
1203 && self
1204 .should
1205 .iter()
1206 .all(|query| matches!(query.decompose(), super::QueryDecomposition::TextTerm(_)))
1207 {
1208 self.must[0].count_equivalent_term()
1209 } else {
1210 None
1211 }
1212 }
1213
1214 fn supports_ranked_conjunction_count(&self) -> bool {
1215 self.must.len() >= 2 && self.should.is_empty() && self.must_not.is_empty()
1216 && self.proximity.is_none() && self.text_heap_factor == 1.0 && self.max_terms == 0
1217 && self.must.iter().all(|query| matches!(query.decompose(),
1218 super::QueryDecomposition::TextTerm(info) if info.weight == 1.0 && info.global_stats.is_none()))
1219 }
1220
1221 fn ranked_count_equivalent_term(&self) -> Option<super::TermQueryInfo> {
1222 let count = self.count_equivalent_term()?;
1223 let super::QueryDecomposition::TextTerm(required) = self.must[0].decompose() else {
1224 return None;
1225 };
1226 (required.weight.is_finite()
1227 && required.weight > 0.0
1228 && self.should.iter().all(|query| {
1229 matches!(query.decompose(), super::QueryDecomposition::TextTerm(info)
1230 if info.field == required.field && info.weight.is_finite() && info.weight > 0.0)
1231 }))
1232 .then_some(count)
1233 }
1234
1235 fn sparse_decomposition(&self) -> super::QueryDecomposition {
1236 if self.should.is_empty() || self.must.iter().any(|query| !query.is_filter()) {
1242 return super::QueryDecomposition::Opaque;
1243 }
1244 extract_all_sparse_infos(&self.should)
1245 .map(super::QueryDecomposition::SparseTerms)
1246 .unwrap_or(super::QueryDecomposition::Opaque)
1247 }
1248
1249 fn should_children(&self) -> Option<&[Arc<dyn Query>]> {
1250 if self.must.is_empty()
1251 && self.must_not.is_empty()
1252 && !self.should.is_empty()
1253 && self.proximity.is_none()
1254 && self.global_stats.is_none()
1255 && self.text_heap_factor == 1.0
1256 && self.max_terms == 0
1257 {
1258 Some(&self.should)
1259 } else {
1260 None
1261 }
1262 }
1263
1264 fn as_doc_bitset(&self, reader: &SegmentReader) -> Option<super::DocBitset> {
1265 self.as_doc_bitset_with_options(reader, &super::ScorerOptions::default())
1266 }
1267
1268 fn as_doc_bitset_with_options(
1269 &self,
1270 reader: &SegmentReader,
1271 options: &super::ScorerOptions,
1272 ) -> Option<super::DocBitset> {
1273 if options.stop_if_expired() {
1274 return None;
1275 }
1276 if self.must.is_empty() && self.should.is_empty() && self.must_not.is_empty() {
1277 return None;
1278 }
1279
1280 let num_docs = reader.num_docs();
1281
1282 let mut result = (self.must.is_empty() && self.should.is_empty())
1284 .then(|| super::DocBitset::all(num_docs));
1285 for q in &self.must {
1286 let bs = options.doc_bitset(q.as_ref(), reader)?;
1287 match result {
1288 None => result = Some(bs),
1289 Some(ref mut acc) => acc.intersect_with(&bs),
1290 }
1291 }
1292
1293 if !self.should.is_empty() {
1295 let mut should_union = super::DocBitset::new(num_docs);
1296 for q in &self.should {
1297 let bs = options.doc_bitset(q.as_ref(), reader)?;
1298 should_union.union_with(&bs);
1299 }
1300 match result {
1301 None => result = Some(should_union),
1302 Some(ref mut acc) => {
1303 if self.must.is_empty() {
1306 *acc = should_union;
1307 }
1308 }
1309 }
1310 }
1311
1312 if let Some(ref mut acc) = result {
1314 for q in &self.must_not {
1315 {
1316 let bs = options.doc_bitset(q.as_ref(), reader)?;
1317 acc.subtract(&bs);
1318 }
1319 }
1320 }
1321
1322 if options.stop_if_expired() {
1323 None
1324 } else {
1325 result
1326 }
1327 }
1328
1329 fn as_doc_predicate<'a>(&self, reader: &'a SegmentReader) -> Option<super::DocPredicate<'a>> {
1330 if self.must.is_empty() && self.should.is_empty() && self.must_not.is_empty() {
1332 return None;
1333 }
1334
1335 let must_preds: Vec<_> = self
1337 .must
1338 .iter()
1339 .map(|q| q.as_doc_predicate(reader))
1340 .collect::<Option<Vec<_>>>()?;
1341 let should_preds: Vec<_> = self
1342 .should
1343 .iter()
1344 .map(|q| q.as_doc_predicate(reader))
1345 .collect::<Option<Vec<_>>>()?;
1346 let must_not_preds: Vec<_> = self
1347 .must_not
1348 .iter()
1349 .map(|q| q.as_doc_predicate(reader))
1350 .collect::<Option<Vec<_>>>()?;
1351
1352 let has_must = !must_preds.is_empty();
1353
1354 Some(Box::new(move |doc_id| {
1355 if !must_preds.iter().all(|p| p(doc_id)) {
1357 return false;
1358 }
1359 if !has_must && !should_preds.is_empty() && !should_preds.iter().any(|p| p(doc_id)) {
1361 return false;
1362 }
1363 must_not_preds.iter().all(|p| !p(doc_id))
1365 }))
1366 }
1367
1368 fn count_estimate<'a>(&self, reader: &'a SegmentReader) -> CountFuture<'a> {
1369 let must = self.must.clone();
1370 let should = self.should.clone();
1371 let has_exclusions = !self.must_not.is_empty();
1372
1373 Box::pin(async move {
1374 if !must.is_empty() {
1375 let mut estimates = Vec::with_capacity(must.len());
1376 for q in &must {
1377 estimates.push(q.count_estimate(reader).await?);
1378 }
1379 estimates
1380 .into_iter()
1381 .min()
1382 .ok_or_else(|| crate::Error::Corruption("Empty must clause".to_string()))
1383 } else if !should.is_empty() {
1384 let mut sum = 0u32;
1385 for q in &should {
1386 sum = sum.saturating_add(q.count_estimate(reader).await?);
1387 }
1388 Ok(sum)
1389 } else if has_exclusions {
1390 Ok(reader.num_docs())
1391 } else {
1392 Ok(0)
1393 }
1394 })
1395 }
1396}
1397
1398pub(super) struct BooleanScorer<'a> {
1399 must: Vec<Box<dyn Scorer + 'a>>,
1400 should: Vec<Box<dyn Scorer + 'a>>,
1401 must_not: Vec<Box<dyn Scorer + 'a>>,
1402 current_doc: DocId,
1403 lead: usize,
1405 doc_limit: u32,
1407}
1408
1409impl<'a> BooleanScorer<'a> {
1410 pub(super) fn disjunction(should: Vec<Box<dyn Scorer + 'a>>) -> Self {
1411 let mut scorer = Self {
1412 must: Vec::new(),
1413 should,
1414 must_not: Vec::new(),
1415 current_doc: 0,
1416 lead: 0,
1417 doc_limit: 0,
1418 };
1419 scorer.initialize();
1420 scorer
1421 }
1422
1423 fn initialize(&mut self) {
1424 self.lead = self
1425 .must
1426 .iter()
1427 .enumerate()
1428 .min_by_key(|(_, scorer)| match scorer.size_hint() {
1429 0 => u32::MAX,
1430 cost => cost,
1431 })
1432 .map_or(0, |(index, _)| index);
1433 self.current_doc = self.find_next_match();
1434 }
1435
1436 fn find_next_match(&mut self) -> DocId {
1437 if self.must.is_empty() && self.should.is_empty() {
1438 return TERMINATED;
1439 }
1440
1441 loop {
1442 let candidate = if !self.must.is_empty() {
1443 let mut candidate = self.must[self.lead].doc();
1444 'align: loop {
1445 if candidate == TERMINATED {
1446 return TERMINATED;
1447 }
1448 for index in 0..self.must.len() {
1449 if index == self.lead {
1450 continue;
1451 }
1452 let doc = self.must[index].seek_candidate(candidate);
1453 if doc > candidate {
1454 candidate = self.must[self.lead].seek_candidate(doc);
1458 continue 'align;
1459 }
1460 }
1461 break candidate;
1462 }
1463 } else {
1464 self.should
1465 .iter()
1466 .map(|s| s.doc())
1467 .filter(|&d| d != TERMINATED)
1468 .min()
1469 .unwrap_or(TERMINATED)
1470 };
1471
1472 if candidate == TERMINATED {
1473 return TERMINATED;
1474 }
1475
1476 if !self
1477 .must
1478 .iter_mut()
1479 .all(|scorer| scorer.confirm_candidate())
1480 {
1481 self.must[self.lead].advance_candidate();
1482 continue;
1483 }
1484
1485 let excluded = self.must_not.iter_mut().any(|scorer| {
1486 let doc = scorer.seek(candidate);
1487 doc == candidate
1488 });
1489
1490 if !excluded {
1491 for scorer in &mut self.should {
1493 scorer.seek(candidate);
1494 }
1495 self.current_doc = candidate;
1496 return candidate;
1497 }
1498
1499 if !self.must.is_empty() {
1501 self.must[self.lead].advance_candidate();
1502 } else {
1503 for scorer in &mut self.should {
1505 if scorer.doc() <= candidate && scorer.doc() != TERMINATED {
1506 scorer.seek(candidate + 1);
1507 }
1508 }
1509 }
1510 }
1511 }
1512}
1513
1514impl super::docset::DocSet for BooleanScorer<'_> {
1515 fn supports_doc_batches(&self) -> bool {
1516 self.must.len() > 1
1517 && self.should.is_empty()
1518 && self.must_not.is_empty()
1519 && self.must.iter().all(|child| child.supports_doc_batches())
1520 }
1521
1522 fn fill_doc_batch(&mut self, docs: &mut super::docset::DocBatch) -> usize {
1523 if !self.supports_doc_batches() {
1524 return super::docset::fill_batch(self, docs);
1525 }
1526 if self.current_doc == TERMINATED {
1527 return 0;
1528 }
1529 let mut count = self.must[self.lead].fill_doc_batch(docs);
1530 for (index, child) in self.must.iter_mut().enumerate() {
1531 if index != self.lead {
1532 count = child.retain_doc_batch(docs, count);
1533 if count == 0 {
1534 break;
1535 }
1536 }
1537 }
1538 self.current_doc = self.find_next_match();
1539 count
1540 }
1541
1542 fn supports_doc_windows(&self) -> bool {
1543 self.must_not.is_empty()
1544 && if self.must.is_empty() {
1545 self.should.iter().all(|child| child.supports_doc_windows())
1546 } else {
1547 self.should.is_empty()
1548 && u64::from(self.must[self.lead].size_hint())
1549 * u64::from(super::docset::DOC_WINDOW_SIZE)
1550 >= u64::from(self.doc_limit) * super::docset::DOC_WINDOW_WORDS as u64
1551 && self.must.iter().all(|child| child.supports_doc_windows())
1552 }
1553 }
1554
1555 fn fill_doc_window(&mut self, base: DocId, bits: &mut super::docset::DocWindow) {
1556 if !self.supports_doc_windows() {
1557 return super::docset::fill_window(self, base, bits);
1558 }
1559 bits.fill(0);
1560 if self.current_doc == TERMINATED {
1561 return;
1562 }
1563 let mut child_bits = [0; super::docset::DOC_WINDOW_WORDS];
1564 if self.must.is_empty() {
1565 for child in &mut self.should {
1566 child.fill_doc_window(base, &mut child_bits);
1567 for (out, child_word) in bits.iter_mut().zip(child_bits) {
1568 *out |= child_word;
1569 }
1570 }
1571 } else {
1572 self.must[self.lead].fill_doc_window(base, bits);
1573 for (index, child) in self.must.iter_mut().enumerate() {
1574 if index == self.lead {
1575 continue;
1576 }
1577 let candidates: u32 = bits.iter().map(|word| word.count_ones()).sum();
1578 if candidates == 0 {
1579 break;
1580 }
1581 if candidates > super::docset::DOC_WINDOW_WORDS as u32 {
1584 child.fill_doc_window(base, &mut child_bits);
1585 for (out, child_word) in bits.iter_mut().zip(child_bits) {
1586 *out &= child_word;
1587 }
1588 } else {
1589 for (word_index, word) in bits.iter_mut().enumerate() {
1590 let mut remaining = *word;
1591 while remaining != 0 {
1592 let bit = remaining.trailing_zeros();
1593 let doc = base + word_index as u32 * 64 + bit;
1594 if child.seek(doc) != doc {
1595 *word &= !(1u64 << bit);
1596 }
1597 remaining &= remaining - 1;
1598 }
1599 }
1600 }
1601 }
1602 }
1603 self.current_doc = self.find_next_match();
1604 }
1605
1606 fn doc(&self) -> DocId {
1607 self.current_doc
1608 }
1609
1610 fn advance(&mut self) -> DocId {
1611 if !self.must.is_empty() {
1612 self.must[self.lead].advance_candidate();
1613 } else {
1614 for scorer in &mut self.should {
1615 if scorer.doc() == self.current_doc {
1616 scorer.advance();
1617 }
1618 }
1619 }
1620
1621 self.current_doc = self.find_next_match();
1622 self.current_doc
1623 }
1624
1625 fn seek(&mut self, target: DocId) -> DocId {
1626 if self.must.is_empty() {
1627 for scorer in &mut self.should {
1628 scorer.seek(target);
1629 }
1630 } else {
1631 self.must[self.lead].seek_candidate(target);
1632 }
1633
1634 self.current_doc = self.find_next_match();
1635 self.current_doc
1636 }
1637
1638 fn size_hint(&self) -> u32 {
1639 if !self.must.is_empty() {
1640 self.must.iter().map(|s| s.size_hint()).min().unwrap_or(0)
1641 } else {
1642 self.should
1643 .iter()
1644 .fold(0u32, |total, s| total.saturating_add(s.size_hint()))
1645 }
1646 }
1647}
1648
1649impl Scorer for BooleanScorer<'_> {
1650 fn supports_score_batches(&self) -> bool {
1651 !self.must.is_empty()
1652 && (self.must.len() > 1 || !self.must_not.is_empty())
1653 && self.should.is_empty()
1654 && self.must[self.lead].size_hint() > super::docset::DOC_BATCH_SIZE as u32
1655 && self.must.iter().all(|child| child.supports_score_batches())
1656 }
1657
1658 fn fill_score_batch(
1659 &mut self,
1660 docs: &mut super::docset::DocBatch,
1661 scores: &mut super::ScoreBatch,
1662 ) -> usize {
1663 if !self.supports_score_batches() {
1664 return super::traits::fill_score_batch_scalar(self, docs, scores);
1665 }
1666 if self.current_doc == TERMINATED {
1667 return 0;
1668 }
1669 let mut lead_scores = [0.0; super::docset::DOC_BATCH_SIZE];
1670 let mut count = self.must[self.lead].fill_score_batch(docs, &mut lead_scores);
1671 let mut origins: [u8; super::docset::DOC_BATCH_SIZE] = std::array::from_fn(|i| i as u8);
1672 let mut values = [0.0; super::docset::DOC_BATCH_SIZE];
1673 let mut matches = [0; 2];
1674 scores[..count].fill(0.0);
1675 for (index, child) in self.must.iter_mut().enumerate() {
1676 if index == self.lead {
1677 for row in 0..count {
1678 scores[row] += lead_scores[origins[row] as usize];
1679 }
1680 continue;
1681 }
1682 child.score_batch_matches(docs, count, &mut values, &mut matches);
1683 let mut kept = 0;
1684 for row in 0..count {
1685 if matches[row / 64] & (1 << (row % 64)) != 0 {
1686 docs[kept] = docs[row];
1687 scores[kept] = scores[row] + values[row];
1688 origins[kept] = origins[row];
1689 kept += 1;
1690 }
1691 }
1692 count = kept;
1693 if count == 0 {
1694 break;
1695 }
1696 }
1697 if !self.must_not.is_empty() {
1698 let mut kept = 0;
1699 for row in 0..count {
1700 let doc = docs[row];
1701 if self.must_not.iter_mut().all(|child| child.seek(doc) != doc) {
1702 docs[kept] = doc;
1703 scores[kept] = scores[row];
1704 kept += 1;
1705 }
1706 }
1707 count = kept;
1708 }
1709 self.current_doc = self.find_next_match();
1710 count
1711 }
1712
1713 fn supports_filtered_windows(&self) -> bool {
1714 super::docset::DocSet::supports_doc_windows(self)
1715 && (self.must.len() > 1 || self.should.len() > 1)
1716 }
1717
1718 fn supports_score_windows(&self) -> bool {
1719 self.must.is_empty()
1720 && self.should.len() > 1
1721 && super::docset::DocSet::supports_doc_windows(self)
1722 }
1723
1724 fn fill_score_window(
1725 &mut self,
1726 base: DocId,
1727 scores: &mut [Score; super::docset::DOC_WINDOW_SIZE as usize],
1728 bits: &mut super::docset::DocWindow,
1729 ) {
1730 scores.fill(0.0);
1731 bits.fill(0);
1732 if !self.supports_score_windows() {
1733 self.accumulate_score_window(base, scores, bits);
1734 return;
1735 }
1736 if self.current_doc == TERMINATED {
1737 return;
1738 }
1739 for child in &mut self.should {
1742 child.accumulate_score_window(base, scores, bits);
1743 }
1744 self.current_doc = self.find_next_match();
1745 }
1746
1747 fn score(&self) -> Score {
1748 let mut total = 0.0;
1749
1750 for scorer in &self.must {
1751 if scorer.doc() == self.current_doc {
1752 total += scorer.score();
1753 }
1754 }
1755
1756 for scorer in &self.should {
1757 if scorer.doc() == self.current_doc {
1758 total += scorer.score();
1759 }
1760 }
1761
1762 total
1763 }
1764
1765 fn matched_positions(&self) -> Option<super::MatchedPositions> {
1766 let mut all_positions: super::MatchedPositions = Vec::new();
1767
1768 for scorer in &self.must {
1769 if scorer.doc() == self.current_doc
1770 && let Some(positions) = scorer.matched_positions()
1771 {
1772 all_positions.extend(positions);
1773 }
1774 }
1775
1776 for scorer in &self.should {
1777 if scorer.doc() == self.current_doc
1778 && let Some(positions) = scorer.matched_positions()
1779 {
1780 all_positions.extend(positions);
1781 }
1782 }
1783
1784 if all_positions.is_empty() {
1785 None
1786 } else {
1787 Some(merge_matched_positions(all_positions))
1788 }
1789 }
1790}
1791
1792pub(super) fn merge_matched_positions(
1800 positions: super::MatchedPositions,
1801) -> super::MatchedPositions {
1802 if positions.len() < 2 {
1803 return positions;
1804 }
1805 let mut merged: super::MatchedPositions = Vec::with_capacity(positions.len());
1806 for (field_id, scored) in positions {
1807 match merged
1808 .iter_mut()
1809 .find(|(existing, _)| *existing == field_id)
1810 {
1811 Some((_, existing)) => existing.extend(scored),
1812 None => merged.push((field_id, scored)),
1813 }
1814 }
1815 for (_, scored) in &mut merged {
1816 if scored.len() < 2 {
1817 continue;
1818 }
1819 scored.sort_by_key(|sp| sp.position);
1820 let mut write = 0usize;
1821 for read in 1..scored.len() {
1822 if scored[read].position == scored[write].position {
1823 scored[write].score += scored[read].score;
1824 } else {
1825 write += 1;
1826 scored[write] = scored[read];
1827 }
1828 }
1829 scored.truncate(write + 1);
1830 }
1831 merged
1832}
1833
1834fn ranked_optional_terms(
1837 should: &[Arc<dyn Query>],
1838 field: crate::Field,
1839 reader: &SegmentReader,
1840 global_stats: Option<&Arc<GlobalStats>>,
1841) -> Option<Vec<super::TermQueryInfo>> {
1842 if should.is_empty() {
1843 return Some(Vec::new());
1844 }
1845 let (optional, optional_field, _, _) = prepare_text_maxscore(should, reader, global_stats)?;
1846 (optional_field == field).then_some(optional)
1847}
1848
1849fn ranked_text_executor<'a>(
1853 cursors: Vec<super::TermCursor<'a>>,
1854 required_count: usize,
1855 limit: usize,
1856 reader: &'a SegmentReader,
1857 field: crate::Field,
1858 options: &super::ScorerOptions,
1859) -> super::MaxScoreExecutor<'a> {
1860 let all_required = required_count == cursors.len() && cursors.len() >= 2;
1861 let executor = super::MaxScoreExecutor::new(cursors, limit, 1.0);
1862 let mut executor = if all_required {
1863 executor.require_all_terms()
1864 } else {
1865 executor.require_prefix_terms(required_count)
1866 }
1867 .with_metric_labels(
1868 reader.schema().index_label(),
1869 reader.schema().get_field_name(field).unwrap_or("?"),
1870 )
1871 .with_budget(options.shared_threshold.clone());
1872 if options.physical_text_field == Some(field) {
1873 executor =
1874 executor.with_document_map(reader.chunk_map(field).expect("admitted document map"));
1875 }
1876 if let Some(predicate) = eligibility_predicate(options) {
1877 executor = executor.with_predicate(predicate);
1878 }
1879 if options.initial_threshold > 0.0 {
1880 executor.seed_threshold(options.initial_threshold);
1881 }
1882 executor
1883}
1884
1885fn eligibility_predicate(options: &super::ScorerOptions) -> Option<super::DocPredicate<'static>> {
1886 options.eligibility.as_ref().map(|filter| {
1887 let filter = filter.clone();
1888 Box::new(move |doc| filter.contains(doc)) as super::DocPredicate<'static>
1889 })
1890}
1891
1892#[cfg(test)]
1893mod tests {
1894 use super::*;
1895 use crate::dsl::Field;
1896 use crate::query::{DocSet, QueryDecomposition, TermQuery};
1897
1898 #[test]
1899 fn ranked_count_hints_require_an_exact_compatible_text_plan() {
1900 struct CountOnly(TermQuery);
1901 impl std::fmt::Display for CountOnly {
1902 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1903 self.0.fmt(f)
1904 }
1905 }
1906 impl Query for CountOnly {
1907 fn scorer<'a>(&self, reader: &'a SegmentReader, limit: usize) -> ScorerFuture<'a> {
1908 self.0.scorer(reader, limit)
1909 }
1910 fn count_estimate<'a>(&self, reader: &'a SegmentReader) -> CountFuture<'a> {
1911 self.0.count_estimate(reader)
1912 }
1913 fn count_equivalent_term(&self) -> Option<super::super::TermQueryInfo> {
1914 self.0.count_equivalent_term()
1915 }
1916 }
1917 let field = Field(0);
1918 let compatible = BooleanQuery::new()
1919 .must(TermQuery::text(field, "required"))
1920 .should(TermQuery::text(field, "optional"));
1921 assert!(compatible.ranked_count_equivalent_term().is_some());
1922 assert!(
1923 Arc::new(compatible.clone())
1924 .ranked_count_equivalent_term()
1925 .is_some()
1926 );
1927 for query in [
1928 compatible
1929 .clone()
1930 .must_not(TermQuery::text(field, "excluded")),
1931 compatible.clone().must(TermQuery::text(field, "second")),
1932 compatible.clone().with_text_heap_factor(0.5),
1933 compatible.clone().with_max_terms(1),
1934 compatible
1935 .clone()
1936 .should(TermQuery::text(Field(1), "other-field")),
1937 compatible.should(super::super::BoostQuery::new(
1938 TermQuery::text(field, "negative"),
1939 -1.0,
1940 )),
1941 BooleanQuery::new()
1942 .must(CountOnly(TermQuery::text(field, "opaque")))
1943 .should(TermQuery::text(field, "optional")),
1944 ] {
1945 assert!(query.ranked_count_equivalent_term().is_none());
1946 }
1947 let opaque = CountOnly(TermQuery::text(field, "opaque"));
1948 assert!(opaque.count_equivalent_term().is_some());
1949 assert!(opaque.ranked_count_equivalent_term().is_none());
1950 }
1951
1952 #[test]
1953 fn compact_score_batches_preserve_query_order_and_nested_scores() {
1954 use crate::query::docset::{DOC_BATCH_SIZE, DocSet, SortedVecDocSet};
1955 struct ExactScore {
1956 docs: SortedVecDocSet,
1957 value: f32,
1958 }
1959 impl DocSet for ExactScore {
1960 fn doc(&self) -> u32 {
1961 self.docs.doc()
1962 }
1963 fn advance(&mut self) -> u32 {
1964 self.docs.advance()
1965 }
1966 fn seek(&mut self, target: u32) -> u32 {
1967 self.docs.seek(target)
1968 }
1969 fn size_hint(&self) -> u32 {
1970 self.docs.size_hint()
1971 }
1972 }
1973 impl Scorer for ExactScore {
1974 fn score(&self) -> f32 {
1975 self.value
1976 }
1977 fn supports_score_batches(&self) -> bool {
1978 true
1979 }
1980 }
1981 fn leaf(step: u32, value: f32) -> Box<dyn Scorer> {
1982 Box::new(ExactScore {
1983 docs: SortedVecDocSet::new(Arc::new(
1984 (0..20000).filter(|d| d % step == 0).collect(),
1985 )),
1986 value,
1987 })
1988 }
1989 fn conjunction(children: Vec<Box<dyn Scorer>>) -> BooleanScorer<'static> {
1990 let mut result = BooleanScorer::disjunction(Vec::new());
1991 result.must = children;
1992 result.initialize();
1993 result
1994 }
1995 fn make(nested: bool, excluded: bool, single: bool) -> BooleanScorer<'static> {
1996 let middle: Box<dyn Scorer> = if nested {
1997 Box::new(conjunction(vec![leaf(7, 1.0e10), leaf(7, 1.0)]))
1998 } else {
1999 leaf(7, 1.0e10)
2000 };
2001 let mut scorer = if single {
2002 conjunction(vec![leaf(3, 1.0)])
2003 } else {
2004 conjunction(vec![leaf(3, -1.0e10), middle, leaf(11, 1.0)])
2005 };
2006 if excluded {
2007 scorer.must_not = vec![leaf(2, 500.0), leaf(13, 500.0)];
2008 scorer.initialize();
2009 }
2010 scorer
2011 }
2012 for (nested, excluded, single) in [
2013 (false, false, false),
2014 (true, false, false),
2015 (false, true, false),
2016 (true, true, false),
2017 (false, true, true),
2018 ] {
2019 let mut scalar = make(nested, excluded, single);
2020 let mut batched = make(nested, excluded, single);
2021 assert_eq!(batched.lead, if single { 0 } else { 2 });
2022 assert!(batched.supports_score_batches());
2023 let mut docs = [0; DOC_BATCH_SIZE];
2024 let mut scores = [0.0; DOC_BATCH_SIZE];
2025 assert_eq!(batched.seek(17), scalar.seek(17));
2026 while batched.doc() != TERMINATED {
2027 let count = batched.fill_score_batch(&mut docs, &mut scores);
2028 assert!(count > 0);
2029 for i in 0..count {
2030 assert_eq!(docs[i], scalar.doc());
2031 assert_eq!(scores[i].to_bits(), scalar.score().to_bits());
2032 assert_eq!(
2033 scores[i], 1.0,
2034 "lead-first or distributed sums change this value"
2035 );
2036 scalar.advance();
2037 }
2038 assert_eq!(batched.doc(), scalar.doc());
2039 assert_eq!(batched.advance(), scalar.advance());
2040 }
2041 assert_eq!(scalar.doc(), TERMINATED);
2042 assert_eq!(batched.fill_score_batch(&mut docs, &mut scores), 0);
2043 }
2044 }
2045
2046 #[test]
2047 fn compact_boolean_batches_preserve_scalar_resume_and_nested_membership() {
2048 use crate::query::docset::{DOC_BATCH_SIZE, DocSet, SortedVecDocSet};
2049 struct ExactDocs(SortedVecDocSet);
2050 impl DocSet for ExactDocs {
2051 fn doc(&self) -> u32 {
2052 self.0.doc()
2053 }
2054 fn advance(&mut self) -> u32 {
2055 self.0.advance()
2056 }
2057 fn seek(&mut self, target: u32) -> u32 {
2058 self.0.seek(target)
2059 }
2060 fn size_hint(&self) -> u32 {
2061 self.0.size_hint()
2062 }
2063 fn supports_doc_batches(&self) -> bool {
2064 true
2065 }
2066 }
2067 impl Scorer for ExactDocs {
2068 fn score(&self) -> f32 {
2069 self.doc() as f32
2070 }
2071 }
2072 fn leaf(step: u32) -> Box<dyn Scorer> {
2073 Box::new(ExactDocs(SortedVecDocSet::new(Arc::new(
2074 (0..5000).filter(|doc| doc % step == 0).collect(),
2075 ))))
2076 }
2077 fn make(nested: bool) -> BooleanScorer<'static> {
2078 let mut scorer = BooleanScorer::disjunction(Vec::new());
2079 scorer.must = vec![leaf(3), leaf(7)];
2080 if nested {
2081 scorer.must.push(Box::new(make(false)));
2082 }
2083 scorer.doc_limit = 1_000_000;
2084 scorer.initialize();
2085 scorer
2086 }
2087 for nested in [false, true] {
2088 let mut scalar = make(nested);
2089 let mut batched = make(nested);
2090 assert!(batched.supports_doc_batches());
2091 assert_eq!(batched.seek(17), scalar.seek(17));
2092 let mut docs = [0; DOC_BATCH_SIZE];
2093 while batched.doc() != TERMINATED {
2094 assert_eq!(batched.doc(), scalar.doc());
2095 assert_eq!(batched.score().to_bits(), scalar.score().to_bits());
2096 let count = batched.fill_doc_batch(&mut docs);
2097 assert!(count > 0);
2098 for &doc in &docs[..count] {
2099 assert_eq!(doc, scalar.doc());
2100 scalar.advance();
2101 }
2102 assert_eq!(batched.doc(), scalar.doc());
2103 assert_eq!(batched.advance(), scalar.advance());
2104 }
2105 assert_eq!(scalar.doc(), TERMINATED);
2106 assert_eq!(batched.fill_doc_batch(&mut docs), 0);
2107 }
2108 }
2109
2110 #[test]
2111 fn score_windows_preserve_nested_sums_seek_and_exhaustion() {
2112 struct Scores {
2113 hits: Vec<(DocId, Score)>,
2114 index: usize,
2115 }
2116 impl DocSet for Scores {
2117 fn doc(&self) -> DocId {
2118 self.hits.get(self.index).map_or(TERMINATED, |hit| hit.0)
2119 }
2120 fn advance(&mut self) -> DocId {
2121 self.index = (self.index + 1).min(self.hits.len());
2122 self.doc()
2123 }
2124 fn size_hint(&self) -> u32 {
2125 self.hits.len() as u32
2126 }
2127 fn supports_doc_windows(&self) -> bool {
2128 true
2129 }
2130 }
2131 impl Scorer for Scores {
2132 fn score(&self) -> Score {
2133 self.hits[self.index].1
2134 }
2135 }
2136 fn child(seed: u32) -> Box<dyn Scorer> {
2137 let hits = (0..16_400)
2138 .chain([u32::MAX - 4097, u32::MAX - 2])
2139 .filter(|doc| (doc % (seed + 2)) != 1)
2140 .map(|doc| {
2141 (
2142 doc,
2143 match seed {
2144 0 => 16_777_216.0,
2145 1 => 1.0,
2146 2 => -16_777_216.0,
2147 _ => -0.0,
2148 },
2149 )
2150 })
2151 .collect();
2152 Box::new(Scores { hits, index: 0 })
2153 }
2154 fn build(shape: u8) -> BooleanScorer<'static> {
2155 let children = if shape == 2 {
2156 let mut conjunction = BooleanScorer {
2157 must: vec![child(1), child(2)],
2158 should: Vec::new(),
2159 must_not: Vec::new(),
2160 current_doc: 0,
2161 lead: 0,
2162 doc_limit: 0,
2163 };
2164 conjunction.initialize();
2165 vec![child(0), Box::new(conjunction), child(3)]
2166 } else if shape == 1 {
2167 vec![
2168 child(0),
2169 Box::new(BooleanScorer::disjunction(vec![child(1), child(2)])),
2170 child(3),
2171 ]
2172 } else {
2173 (0..4).map(child).collect()
2174 };
2175 BooleanScorer::disjunction(children)
2176 }
2177 for shape in 0..3 {
2178 for start in [0, 17, 4095, 8193, u32::MAX - 4098, TERMINATED] {
2179 let mut scalar = build(shape);
2180 let mut batched = build(shape);
2181 scalar.seek(start);
2182 batched.seek(start);
2183 assert!(batched.supports_score_windows());
2184 let mut scores =
2185 Box::new([f32::NAN; super::super::docset::DOC_WINDOW_SIZE as usize]);
2186 let mut bits = [u64::MAX; super::super::docset::DOC_WINDOW_WORDS];
2187 while batched.doc() != TERMINATED {
2188 let base = batched.doc();
2189 let end = base.saturating_add(super::super::docset::DOC_WINDOW_SIZE);
2190 batched.fill_score_window(base, &mut scores, &mut bits);
2191 for (index, &word) in bits.iter().enumerate() {
2192 let mut remaining = word;
2193 while remaining != 0 {
2194 let slot = index * 64 + remaining.trailing_zeros() as usize;
2195 assert_eq!(base + slot as u32, scalar.doc());
2196 assert_eq!(
2197 scores[slot].to_bits(),
2198 scalar.score().to_bits(),
2199 "shape={shape} start={start} doc={}",
2200 scalar.doc()
2201 );
2202 scalar.advance();
2203 remaining &= remaining - 1;
2204 }
2205 }
2206 assert!(scalar.doc() >= end);
2207 assert_eq!(scalar.doc(), batched.doc());
2208 if scalar.doc() != TERMINATED {
2209 assert_eq!(scalar.score().to_bits(), batched.score().to_bits());
2210 }
2211 }
2212 batched.fill_score_window(TERMINATED, &mut scores, &mut bits);
2213 assert!(bits.iter().all(|word| *word == 0));
2214 assert_eq!(batched.advance(), TERMINATED);
2215 assert_eq!(batched.seek(0), TERMINATED);
2216 }
2217 }
2218 }
2219
2220 #[tokio::test]
2221 async fn document_windows_preserve_nested_boolean_membership_and_next_scores() {
2222 use crate::query::{Collector, CountCollector, ScorerOptions, collect_segment};
2223 use crate::segment::{SegmentBuilder, SegmentBuilderConfig, SegmentId};
2224 use crate::structures::PostingCodec;
2225 use std::sync::Arc;
2226 struct Ids(Vec<u32>);
2227 impl Collector for Ids {
2228 fn needs_scores(&self) -> bool {
2229 false
2230 }
2231 fn collect(
2232 &mut self,
2233 doc: u32,
2234 score: f32,
2235 positions: &[(u32, Vec<crate::query::ScoredPosition>)],
2236 ) {
2237 assert_eq!(score, 0.0);
2238 assert!(positions.is_empty());
2239 self.0.push(doc);
2240 }
2241 }
2242 for codec in [
2243 PostingCodec::Rounded,
2244 PostingCodec::Packed,
2245 PostingCodec::Pfor,
2246 PostingCodec::Simd4x,
2247 ] {
2248 let dir = crate::RamDirectory::new();
2249 let mut schema = crate::SchemaBuilder::default();
2250 let field = schema.add_text_field("text", true, false);
2251 schema.set_positions(field, crate::dsl::PositionMode::TokenPosition);
2252 let schema = Arc::new(schema.build());
2253 let config = SegmentBuilderConfig {
2254 posting_codec: codec,
2255 ..Default::default()
2256 };
2257 let mut builder = SegmentBuilder::new(schema.clone(), config).unwrap();
2258 for doc_id in 0..12_345 {
2259 let mut text = String::from("padding ");
2260 for (term, divisor) in [("alpha", 2), ("beta", 3), ("gamma", 11), ("rare", 997)] {
2261 if doc_id % divisor == 0 {
2262 text.push_str(&format!("{term} {term} "));
2263 }
2264 }
2265 let mut doc = crate::Document::new();
2266 doc.add_text(field, text);
2267 builder.add_document(doc).unwrap();
2268 }
2269 let id = SegmentId::new();
2270 builder.build(&dir, id, None).await.unwrap();
2271 let reader = SegmentReader::open(&dir, id, schema, 16).await.unwrap();
2272 let term = |name| TermQuery::text(field, name);
2273 for shape in 0..9 {
2274 let query = match shape {
2275 0 => BooleanQuery::new()
2276 .should(term("alpha"))
2277 .should(term("beta")),
2278 1 => BooleanQuery::new()
2279 .must(term("alpha"))
2280 .must(term("beta"))
2281 .should(term("gamma")),
2282 2 => BooleanQuery::new()
2283 .must(
2284 BooleanQuery::new()
2285 .should(term("alpha"))
2286 .should(term("beta")),
2287 )
2288 .must_not(term("gamma")),
2289 3 => BooleanQuery::new()
2290 .must(term("rare"))
2291 .must(
2292 BooleanQuery::new()
2293 .should(term("alpha"))
2294 .should(term("beta")),
2295 )
2296 .must_not(term("gamma")),
2297 4 => BooleanQuery::new().must(term("alpha")).should(term("beta")),
2298 5 => BooleanQuery::new().must(term("alpha")).must(term("beta")),
2299 6 => BooleanQuery::new().must(term("rare")).must(
2300 BooleanQuery::new()
2301 .should(term("alpha"))
2302 .should(term("beta")),
2303 ),
2304 7 => BooleanQuery::new()
2305 .must(term("alpha"))
2306 .must(term("missing")),
2307 _ => BooleanQuery::new()
2308 .must(BooleanQuery::new().must(term("alpha")).must(term("beta")))
2309 .must(term("gamma")),
2310 };
2311 let expected: Vec<u32> = (0..12_345)
2312 .filter(|doc| {
2313 let a = doc % 2 == 0;
2314 let b = doc % 3 == 0;
2315 let g = doc % 11 == 0;
2316 match shape {
2317 0 => a || b,
2318 1 => a && b,
2319 2 => (a || b) && !g,
2320 3 => doc % 997 == 0 && (a || b) && !g,
2321 4 => a,
2322 5 => a && b,
2323 6 => doc % 997 == 0 && (a || b),
2324 7 => false,
2325 _ => a && b && g,
2326 }
2327 })
2328 .collect();
2329 let options = ScorerOptions {
2330 complete_text_matches: true,
2331 collect_positions: true,
2332 ..Default::default()
2333 };
2334 let mut scalar = query
2335 .scorer_with_options(&reader, 10, options.clone())
2336 .await
2337 .unwrap();
2338 let mut batched = query
2339 .scorer_with_options(&reader, 10, options)
2340 .await
2341 .unwrap();
2342 assert_eq!(
2343 batched.supports_doc_windows(),
2344 matches!(shape, 0 | 5 | 8),
2345 "shape={shape}"
2346 );
2347 let mut actual = Vec::new();
2348 while batched.doc() != TERMINATED {
2349 let base = batched.doc();
2350 let mut bits = [u64::MAX; super::super::docset::DOC_WINDOW_WORDS];
2351 batched.fill_doc_window(base, &mut bits);
2352 for (index, word) in bits.into_iter().enumerate() {
2353 for bit in 0..64 {
2354 if word & (1 << bit) != 0 {
2355 actual.push(base + index as u32 * 64 + bit);
2356 }
2357 }
2358 }
2359 scalar.seek(base.saturating_add(super::super::docset::DOC_WINDOW_SIZE));
2360 assert_eq!(batched.doc(), scalar.doc());
2361 if scalar.doc() != TERMINATED {
2362 assert_eq!(batched.score().to_bits(), scalar.score().to_bits());
2363 let positions = |value: Option<crate::query::MatchedPositions>| {
2364 value.map(|fields| {
2365 fields
2366 .into_iter()
2367 .map(|(field, values)| {
2368 (
2369 field,
2370 values
2371 .into_iter()
2372 .map(|p| (p.position, p.score.to_bits()))
2373 .collect::<Vec<_>>(),
2374 )
2375 })
2376 .collect::<Vec<_>>()
2377 })
2378 };
2379 assert_eq!(
2380 positions(batched.matched_positions()),
2381 positions(scalar.matched_positions())
2382 );
2383 }
2384 }
2385 assert_eq!(actual, expected, "codec={codec:?} shape={shape}");
2386 let mut count = CountCollector::new();
2387 collect_segment(&reader, &query, &mut count).await.unwrap();
2388 assert_eq!(count.count() as usize, expected.len());
2389 let mut ids = Ids(Vec::new());
2390 collect_segment(&reader, &query, &mut ids).await.unwrap();
2391 assert_eq!(ids.0, expected);
2392 #[cfg(feature = "sync")]
2393 {
2394 let mut sync = query
2395 .scorer_sync_with_options(
2396 &reader,
2397 10,
2398 ScorerOptions {
2399 complete_text_matches: true,
2400 ..Default::default()
2401 },
2402 )
2403 .unwrap();
2404 let mut sync_ids = Vec::new();
2405 while sync.doc() != TERMINATED {
2406 let base = sync.doc();
2407 let mut bits = [0; super::super::docset::DOC_WINDOW_WORDS];
2408 sync.fill_doc_window(base, &mut bits);
2409 for (index, word) in bits.into_iter().enumerate() {
2410 for bit in 0..64 {
2411 if word & (1 << bit) != 0 {
2412 sync_ids.push(base + index as u32 * 64 + bit);
2413 }
2414 }
2415 }
2416 }
2417 assert_eq!(sync_ids, expected);
2418 }
2419 }
2420 }
2421 }
2422
2423 #[tokio::test]
2424 async fn conjunction_chooses_the_rare_term_over_a_common_phrase() {
2425 let dir = crate::RamDirectory::new();
2426 let mut schema = crate::SchemaBuilder::default();
2427 let field = schema.add_text_field("text", true, false);
2428 schema.set_positions(field, crate::dsl::PositionMode::TokenPosition);
2429 schema.set_default_fields(vec!["text".into()]);
2430 let config = crate::IndexConfig {
2431 num_indexing_threads: 1,
2432 num_threads: 1,
2433 ..Default::default()
2434 };
2435 let mut writer = crate::IndexWriter::create(dir.clone(), schema.build(), config.clone())
2436 .await
2437 .unwrap();
2438 for doc_id in 0..32 {
2439 let mut doc = crate::Document::new();
2440 doc.add_text(
2441 field,
2442 if doc_id == 24 {
2443 "alpha beta rare"
2444 } else {
2445 "alpha beta"
2446 },
2447 );
2448 writer.add_document(doc).unwrap();
2449 }
2450 writer.commit().await.unwrap();
2451 writer.shutdown().await.unwrap();
2452 let index = crate::Index::open(dir, config).await.unwrap();
2453 let reader = index.reader().await.unwrap();
2454 let searcher = reader.searcher().await.unwrap();
2455 let segment = &searcher.segment_readers()[0];
2456 let parser = searcher.query_parser();
2457 let phrase = parser.parse_strict("\"alpha beta\"").unwrap();
2458 let term = parser.parse_strict("rare").unwrap();
2459 let mut scorer = BooleanScorer {
2460 must: vec![
2461 phrase.scorer(segment, 100).await.unwrap(),
2462 term.scorer(segment, 100).await.unwrap(),
2463 ],
2464 should: Vec::new(),
2465 must_not: Vec::new(),
2466 current_doc: 0,
2467 lead: 0,
2468 doc_limit: 0,
2469 };
2470 scorer.initialize();
2471 assert_eq!(
2472 scorer.lead, 1,
2473 "the rarer term must drive candidate traversal"
2474 );
2475 assert_eq!(scorer.doc(), 24);
2476 assert!(scorer.score() > 0.0);
2477 assert_eq!(scorer.advance(), TERMINATED);
2478 }
2479
2480 struct ObservedDocSet {
2481 docs: Vec<u32>,
2482 offset: usize,
2483 cost: u32,
2484 advances: Arc<std::sync::atomic::AtomicUsize>,
2485 }
2486 impl super::super::DocSet for ObservedDocSet {
2487 fn doc(&self) -> u32 {
2488 self.docs.get(self.offset).copied().unwrap_or(TERMINATED)
2489 }
2490 fn advance(&mut self) -> u32 {
2491 self.advances
2492 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
2493 self.offset = (self.offset + 1).min(self.docs.len());
2494 self.doc()
2495 }
2496 fn seek(&mut self, target: u32) -> u32 {
2497 self.offset += self.docs[self.offset..].partition_point(|doc| *doc < target);
2498 self.doc()
2499 }
2500 fn size_hint(&self) -> u32 {
2501 self.cost
2502 }
2503 }
2504 impl Scorer for ObservedDocSet {
2505 fn score(&self) -> f32 {
2506 1.0
2507 }
2508 }
2509
2510 #[test]
2511 fn conjunction_skips_expensive_advances_without_changing_matches_or_scores() {
2512 let expensive = Arc::new(std::sync::atomic::AtomicUsize::new(0));
2513 let cheap = Arc::new(std::sync::atomic::AtomicUsize::new(0));
2514 let mut scorer = BooleanScorer {
2515 must: vec![
2516 Box::new(ObservedDocSet {
2517 docs: (0..201).collect(),
2518 offset: 0,
2519 cost: 201,
2520 advances: expensive.clone(),
2521 }),
2522 Box::new(ObservedDocSet {
2523 docs: vec![0, 100, 200],
2524 offset: 0,
2525 cost: 3,
2526 advances: cheap.clone(),
2527 }),
2528 ],
2529 should: Vec::new(),
2530 must_not: Vec::new(),
2531 current_doc: 0,
2532 lead: 0,
2533 doc_limit: 0,
2534 };
2535 scorer.initialize();
2536 assert_eq!(scorer.doc(), 0);
2537 assert_eq!(scorer.score(), 2.0);
2538 assert_eq!(scorer.advance(), 100);
2539 assert_eq!(scorer.score(), 2.0);
2540 assert_eq!(scorer.seek(150), 200);
2541 assert_eq!(scorer.score(), 2.0);
2542 assert_eq!(scorer.advance(), TERMINATED);
2543 assert_eq!(expensive.load(std::sync::atomic::Ordering::Relaxed), 0);
2544 assert_eq!(cheap.load(std::sync::atomic::Ordering::Relaxed), 2);
2545 }
2546
2547 struct ObservedTwoPhase {
2548 doc: DocId,
2549 confirmations: Arc<std::sync::atomic::AtomicUsize>,
2550 }
2551
2552 impl super::super::docset::DocSet for ObservedTwoPhase {
2553 fn doc(&self) -> DocId {
2554 self.doc
2555 }
2556 fn advance(&mut self) -> DocId {
2557 self.seek(self.doc.saturating_add(1))
2558 }
2559 fn seek(&mut self, target: DocId) -> DocId {
2560 self.seek_candidate(target);
2561 while self.doc != TERMINATED && !self.confirm_candidate() {
2562 self.advance_candidate();
2563 }
2564 self.doc
2565 }
2566 fn size_hint(&self) -> u32 {
2567 201
2568 }
2569 }
2570
2571 impl Scorer for ObservedTwoPhase {
2572 fn score(&self) -> Score {
2573 1.0
2574 }
2575 fn advance_candidate(&mut self) -> DocId {
2576 self.seek_candidate(self.doc.saturating_add(1))
2577 }
2578 fn seek_candidate(&mut self, target: DocId) -> DocId {
2579 self.doc = self.doc.max(target);
2580 if self.doc > 200 {
2581 self.doc = TERMINATED;
2582 }
2583 self.doc
2584 }
2585 fn confirm_candidate(&mut self) -> bool {
2586 self.confirmations
2587 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
2588 self.doc != TERMINATED && self.doc.is_multiple_of(50)
2589 }
2590 }
2591
2592 #[test]
2593 fn conjunction_confirms_only_aligned_candidates_and_rejects_false_positives() {
2594 let confirmations = Arc::new(std::sync::atomic::AtomicUsize::new(0));
2595 let mut scorer = BooleanScorer {
2596 must: vec![
2597 Box::new(ObservedTwoPhase {
2598 doc: 0,
2599 confirmations: confirmations.clone(),
2600 }),
2601 Box::new(ObservedDocSet {
2602 docs: vec![0, 20, 100, 200],
2603 offset: 0,
2604 cost: 4,
2605 advances: Arc::new(std::sync::atomic::AtomicUsize::new(0)),
2606 }),
2607 ],
2608 should: Vec::new(),
2609 must_not: vec![Box::new(ObservedDocSet {
2610 docs: vec![100],
2611 offset: 0,
2612 cost: 1,
2613 advances: Arc::new(std::sync::atomic::AtomicUsize::new(0)),
2614 })],
2615 current_doc: 0,
2616 lead: 0,
2617 doc_limit: 0,
2618 };
2619 scorer.initialize();
2620 assert_eq!(scorer.doc(), 0);
2621 assert_eq!(scorer.score(), 2.0);
2622 assert_eq!(scorer.advance(), 200);
2623 assert_eq!(scorer.score(), 2.0);
2624 assert_eq!(scorer.advance(), TERMINATED);
2625 assert_eq!(confirmations.load(std::sync::atomic::Ordering::Relaxed), 4);
2626 }
2627
2628 #[test]
2629 fn disjunction_cardinality_hints_saturate_instead_of_overflowing() {
2630 let scorer = BooleanScorer::disjunction(
2631 (0..2)
2632 .map(|_| {
2633 Box::new(ObservedDocSet {
2634 docs: vec![0],
2635 offset: 0,
2636 cost: u32::MAX,
2637 advances: Arc::new(std::sync::atomic::AtomicUsize::new(0)),
2638 }) as Box<dyn Scorer>
2639 })
2640 .collect(),
2641 );
2642 assert_eq!(scorer.size_hint(), u32::MAX);
2643 }
2644
2645 #[test]
2646 fn test_maxscore_eligible_pure_or_same_field() {
2647 let query = BooleanQuery::new()
2649 .should(TermQuery::text(Field(0), "hello"))
2650 .should(TermQuery::text(Field(0), "world"))
2651 .should(TermQuery::text(Field(0), "foo"));
2652
2653 assert!(
2655 query
2656 .should
2657 .iter()
2658 .all(|q| matches!(q.decompose(), QueryDecomposition::TextTerm(_)))
2659 );
2660
2661 let infos: Vec<_> = query
2663 .should
2664 .iter()
2665 .filter_map(|q| match q.decompose() {
2666 QueryDecomposition::TextTerm(info) => Some(info),
2667 _ => None,
2668 })
2669 .collect();
2670 assert_eq!(infos.len(), 3);
2671 assert!(infos.iter().all(|i| i.field == Field(0)));
2672 }
2673
2674 #[test]
2675 fn test_maxscore_not_eligible_different_fields() {
2676 let query = BooleanQuery::new()
2678 .should(TermQuery::text(Field(0), "hello"))
2679 .should(TermQuery::text(Field(1), "world")); let infos: Vec<_> = query
2682 .should
2683 .iter()
2684 .filter_map(|q| match q.decompose() {
2685 QueryDecomposition::TextTerm(info) => Some(info),
2686 _ => None,
2687 })
2688 .collect();
2689 assert_eq!(infos.len(), 2);
2690 assert!(infos[0].field != infos[1].field);
2692 }
2693
2694 #[test]
2695 fn test_term_query_info_extraction() {
2696 let term_query = TermQuery::text(Field(42), "test");
2697 match term_query.decompose() {
2698 QueryDecomposition::TextTerm(info) => {
2699 assert_eq!(info.field, Field(42));
2700 assert_eq!(info.term, b"test");
2701 }
2702 _ => panic!("Expected TextTerm decomposition"),
2703 }
2704 }
2705
2706 #[test]
2707 fn test_boolean_query_no_term_info() {
2708 let query = BooleanQuery::new().should(TermQuery::text(Field(0), "hello"));
2710
2711 assert!(matches!(query.decompose(), QueryDecomposition::Opaque));
2712 }
2713}
2714
2715#[cfg(test)]
2716#[path = "boolean/conjunction_window_tests.rs"]
2717mod conjunction_window_tests;