1use std::cmp::Ordering;
4use std::collections::BinaryHeap;
5
6use crate::segment::SegmentReader;
7use crate::structures::TERMINATED;
8use crate::{DocId, Result, Score};
9
10use super::Query;
11
12#[derive(Debug, Clone, PartialEq, Eq, Hash)]
16pub struct DocAddress {
17 segment_id_raw: u128,
19 pub doc_id: DocId,
21}
22
23impl DocAddress {
24 pub fn new(segment_id: u128, doc_id: DocId) -> Self {
25 Self {
26 segment_id_raw: segment_id,
27 doc_id,
28 }
29 }
30
31 pub fn segment_id(&self) -> String {
33 format!("{:032x}", self.segment_id_raw)
34 }
35
36 pub fn segment_id_u128(&self) -> Option<u128> {
38 Some(self.segment_id_raw)
39 }
40}
41
42impl serde::Serialize for DocAddress {
43 fn serialize<S: serde::Serializer>(
44 &self,
45 serializer: S,
46 ) -> std::result::Result<S::Ok, S::Error> {
47 use serde::ser::SerializeStruct;
48 let mut s = serializer.serialize_struct("DocAddress", 2)?;
49 s.serialize_field("segment_id", &format!("{:032x}", self.segment_id_raw))?;
50 s.serialize_field("doc_id", &self.doc_id)?;
51 s.end()
52 }
53}
54
55impl<'de> serde::Deserialize<'de> for DocAddress {
56 fn deserialize<D: serde::Deserializer<'de>>(
57 deserializer: D,
58 ) -> std::result::Result<Self, D::Error> {
59 #[derive(serde::Deserialize)]
60 struct Helper {
61 segment_id: String,
62 doc_id: DocId,
63 }
64 let h = Helper::deserialize(deserializer)?;
65 let raw = u128::from_str_radix(&h.segment_id, 16).map_err(serde::de::Error::custom)?;
66 Ok(DocAddress {
67 segment_id_raw: raw,
68 doc_id: h.doc_id,
69 })
70 }
71}
72
73#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize)]
77pub struct ScoredPosition {
78 pub position: u32,
80 pub score: f32,
82}
83
84impl ScoredPosition {
85 pub fn new(position: u32, score: f32) -> Self {
86 Self { position, score }
87 }
88}
89
90#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
92pub struct SearchResult {
93 pub doc_id: DocId,
94 pub score: Score,
95 #[serde(default, skip_serializing_if = "is_zero_u128")]
97 pub segment_id: u128,
98 #[serde(default, skip_serializing_if = "Vec::is_empty")]
101 pub positions: Vec<(u32, Vec<ScoredPosition>)>,
102}
103
104fn is_zero_u128(v: &u128) -> bool {
105 *v == 0
106}
107
108pub(crate) fn compare_search_results_desc(a: &SearchResult, b: &SearchResult) -> Ordering {
110 b.score
111 .total_cmp(&a.score)
112 .then_with(|| a.segment_id.cmp(&b.segment_id))
113 .then_with(|| a.doc_id.cmp(&b.doc_id))
114}
115
116#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
118pub struct MatchedField {
119 pub field_id: u32,
121 pub ordinals: Vec<u32>,
124}
125
126impl SearchResult {
127 pub fn extract_ordinals(&self) -> Vec<MatchedField> {
131 self.positions
132 .iter()
133 .map(|(field_id, scored_positions)| {
134 let mut ordinals = Vec::with_capacity(scored_positions.len());
139 ordinals.extend(scored_positions.iter().map(|sp| {
140 if sp.position > 0xFFFFF {
143 sp.position >> 20
144 } else {
145 sp.position
146 }
147 }));
148 ordinals.sort_unstable();
149 ordinals.dedup();
150 MatchedField {
151 field_id: *field_id,
152 ordinals,
153 }
154 })
155 .collect()
156 }
157
158 pub fn field_positions(&self, field_id: u32) -> Option<&[ScoredPosition]> {
160 self.positions
161 .iter()
162 .find(|(fid, _)| *fid == field_id)
163 .map(|(_, positions)| positions.as_slice())
164 }
165}
166
167#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
169pub struct SearchHit {
170 pub address: DocAddress,
172 pub score: Score,
173 #[serde(default, skip_serializing_if = "Vec::is_empty")]
175 pub matched_fields: Vec<MatchedField>,
176}
177
178#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
180pub struct SearchResponse {
181 pub hits: Vec<SearchHit>,
182 pub total_hits: u32,
183}
184
185impl PartialEq for SearchResult {
186 fn eq(&self, other: &Self) -> bool {
187 self.score.to_bits() == other.score.to_bits()
188 && self.segment_id == other.segment_id
189 && self.doc_id == other.doc_id
190 }
191}
192
193impl Eq for SearchResult {}
194
195impl PartialOrd for SearchResult {
196 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
197 Some(self.cmp(other))
198 }
199}
200
201impl Ord for SearchResult {
202 fn cmp(&self, other: &Self) -> Ordering {
203 other
204 .score
205 .total_cmp(&self.score)
206 .then_with(|| self.segment_id.cmp(&other.segment_id))
207 .then_with(|| self.doc_id.cmp(&other.doc_id))
208 }
209}
210
211pub trait Collector {
216 fn collect(&mut self, doc_id: DocId, score: Score, positions: &[(u32, Vec<ScoredPosition>)]);
219
220 fn collect_count(&mut self, _count: u64) -> bool {
224 false
225 }
226
227 fn ranked_count_limit(&self) -> Option<usize> {
231 None
232 }
233
234 fn collect_omitted_count(&mut self, _count: u64) {
237 unreachable!("collector did not opt into separate exact counting");
238 }
239
240 fn supports_score_blocks(&self) -> bool {
244 false
245 }
246
247 fn collect_score_block(&mut self, base: DocId, scores: &[Score; 64], mut bits: u64) {
251 while bits != 0 {
252 let offset = bits.trailing_zeros() as usize;
253 self.collect(base + offset as u32, scores[offset], &[]);
254 bits &= bits - 1;
255 }
256 }
257
258 fn would_collect(&self, _doc_id: DocId, _score: Score) -> bool {
263 true
264 }
265
266 fn collect_owned(&mut self, doc_id: DocId, score: Score, positions: super::MatchedPositions) {
269 self.collect(doc_id, score, &positions);
270 }
271
272 fn needs_scores(&self) -> bool {
276 true
277 }
278
279 fn needs_positions(&self) -> bool {
281 false
282 }
283}
284
285#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
292struct ScoreOnlyResult(u64);
293
294impl ScoreOnlyResult {
295 #[inline]
296 fn new(doc_id: DocId, score: Score) -> Self {
297 let bits = score.to_bits();
298 let rank = bits ^ (((bits as i32 >> 31) as u32) | 0x8000_0000);
301 Self((u64::from(!rank) << 32) | u64::from(doc_id))
302 }
303
304 #[inline]
305 fn doc_id(self) -> DocId {
306 self.0 as u32
307 }
308
309 #[inline]
310 fn score(self) -> Score {
311 let rank = !(self.0 >> 32) as u32;
312 let mask = (rank >> 31).wrapping_sub(1) | 0x8000_0000;
313 f32::from_bits(rank ^ mask)
314 }
315}
316
317#[derive(Debug, Clone)]
320struct PositionedResult {
321 doc_id: DocId,
322 score: Score,
323 positions: super::MatchedPositions,
324}
325
326impl PartialEq for PositionedResult {
327 fn eq(&self, other: &Self) -> bool {
328 self.score.to_bits() == other.score.to_bits() && self.doc_id == other.doc_id
329 }
330}
331
332impl Eq for PositionedResult {}
333
334impl PartialOrd for PositionedResult {
335 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
336 Some(self.cmp(other))
337 }
338}
339
340impl Ord for PositionedResult {
341 fn cmp(&self, other: &Self) -> Ordering {
342 other
343 .score
344 .total_cmp(&self.score)
345 .then_with(|| self.doc_id.cmp(&other.doc_id))
346 }
347}
348
349enum TopKHeap {
350 Scores(BinaryHeap<ScoreOnlyResult>),
351 Positions(BinaryHeap<PositionedResult>),
352}
353
354#[inline(always)]
355fn ranks_ahead(doc_id: DocId, score: Score, worst_doc_id: DocId, worst_score: Score) -> bool {
356 let order = score.total_cmp(&worst_score);
357 order.is_gt() || (order.is_eq() && doc_id < worst_doc_id)
358}
359
360pub struct TopKCollector {
362 heap: TopKHeap,
363 k: usize,
364 total_seen: u32,
366}
367
368const MAX_INITIAL_TOP_K_CAPACITY: usize = 8 * 1024;
373
374impl TopKCollector {
375 pub fn new(k: usize) -> Self {
376 Self {
377 heap: TopKHeap::Scores(BinaryHeap::with_capacity(k.min(MAX_INITIAL_TOP_K_CAPACITY))),
378 k,
379 total_seen: 0,
380 }
381 }
382
383 pub fn with_positions(k: usize) -> Self {
385 Self {
386 heap: TopKHeap::Positions(BinaryHeap::with_capacity(k.min(MAX_INITIAL_TOP_K_CAPACITY))),
387 k,
388 total_seen: 0,
389 }
390 }
391
392 pub fn total_seen(&self) -> u32 {
394 self.total_seen
395 }
396
397 pub fn is_full(&self) -> bool {
399 match &self.heap {
400 TopKHeap::Scores(heap) => heap.len() >= self.k,
401 TopKHeap::Positions(heap) => heap.len() >= self.k,
402 }
403 }
404
405 fn competitive_score(&self) -> Score {
406 if !self.is_full() {
407 return Score::NEG_INFINITY;
408 }
409 match &self.heap {
410 TopKHeap::Scores(heap) => heap.peek().map_or(Score::NEG_INFINITY, |hit| hit.score()),
411 TopKHeap::Positions(heap) => heap.peek().map_or(Score::NEG_INFINITY, |hit| hit.score),
412 }
413 }
414
415 pub fn into_sorted_results(self) -> Vec<SearchResult> {
416 match self.heap {
417 TopKHeap::Scores(heap) => {
418 let mut compact = heap.into_vec();
419 compact.sort_unstable();
420 compact
421 .into_iter()
422 .map(|result| SearchResult {
423 doc_id: result.doc_id(),
424 score: result.score(),
425 segment_id: 0,
426 positions: Vec::new(),
427 })
428 .collect()
429 }
430 TopKHeap::Positions(heap) => {
431 let mut positioned = heap.into_vec();
432 positioned.sort_unstable_by(|a, b| {
433 b.score
434 .total_cmp(&a.score)
435 .then_with(|| a.doc_id.cmp(&b.doc_id))
436 });
437 positioned
438 .into_iter()
439 .map(|result| SearchResult {
440 doc_id: result.doc_id,
441 score: result.score,
442 segment_id: 0,
443 positions: result.positions,
444 })
445 .collect()
446 }
447 }
448 }
449
450 pub fn into_results_with_count(self) -> (Vec<SearchResult>, u32) {
452 let total = self.total_seen;
453 (self.into_sorted_results(), total)
454 }
455}
456
457impl Collector for TopKCollector {
458 fn ranked_count_limit(&self) -> Option<usize> {
459 (!self.needs_positions()).then_some(self.k)
460 }
461 fn collect_omitted_count(&mut self, count: u64) {
462 self.total_seen = self
463 .total_seen
464 .saturating_add(count.min(u64::from(u32::MAX)) as u32);
465 }
466
467 fn supports_score_blocks(&self) -> bool {
468 matches!(self.heap, TopKHeap::Scores(_))
469 }
470
471 fn collect_score_block(&mut self, base: DocId, scores: &[Score; 64], mut bits: u64) {
472 let TopKHeap::Scores(heap) = &mut self.heap else {
473 while bits != 0 {
474 let offset = bits.trailing_zeros() as usize;
475 self.collect(base + offset as u32, scores[offset], &[]);
476 bits &= bits - 1;
477 }
478 return;
479 };
480 self.total_seen = self.total_seen.saturating_add(bits.count_ones());
481 if self.k == 0 || bits == 0 {
482 return;
483 }
484 while heap.len() < self.k && bits != 0 {
485 let offset = bits.trailing_zeros() as usize;
486 heap.push(ScoreOnlyResult::new(base + offset as u32, scores[offset]));
487 bits &= bits - 1;
488 }
489 if bits == 0 {
490 return;
491 }
492 let mut worst = *heap.peek().expect("full top-k heap");
493 let mut eligible = 0u64;
497 for (i, &score) in scores.iter().enumerate() {
498 eligible |= (if score < worst.score() { 0 } else { 1 }) << i;
499 }
500 bits &= eligible;
501 while bits != 0 {
502 let offset = bits.trailing_zeros() as usize;
503 let doc_id = base + offset as u32;
504 let score = scores[offset];
505 let result = ScoreOnlyResult::new(doc_id, score);
506 if result < worst {
507 *heap.peek_mut().expect("full top-k heap") = result;
508 worst = *heap.peek().expect("full top-k heap");
509 }
510 bits &= bits - 1;
511 }
512 }
513
514 #[inline]
515 fn collect(&mut self, doc_id: DocId, score: Score, positions: &[(u32, Vec<ScoredPosition>)]) {
516 self.total_seen = self.total_seen.saturating_add(1);
517 if self.k == 0 {
518 return;
519 }
520
521 match &mut self.heap {
522 TopKHeap::Scores(heap) => {
523 let result = ScoreOnlyResult::new(doc_id, score);
524 if heap.len() < self.k {
525 heap.push(result);
526 } else if heap.peek().is_some_and(|worst| result < *worst) {
527 *heap.peek_mut().expect("full top-k heap") = result;
528 }
529 }
530 TopKHeap::Positions(heap) => {
531 if heap.len() >= self.k
532 && !heap
533 .peek()
534 .is_some_and(|worst| ranks_ahead(doc_id, score, worst.doc_id, worst.score))
535 {
536 return;
537 }
538 let result = PositionedResult {
539 doc_id,
540 score,
541 positions: positions.to_vec(),
544 };
545 if heap.len() < self.k {
546 heap.push(result);
547 } else {
548 *heap.peek_mut().expect("full top-k heap") = result;
549 }
550 }
551 }
552 }
553
554 #[inline]
555 fn would_collect(&self, doc_id: DocId, score: Score) -> bool {
556 if self.k == 0 {
557 return false;
558 }
559 match &self.heap {
560 TopKHeap::Scores(heap) => {
561 heap.len() < self.k
562 || heap
563 .peek()
564 .is_some_and(|min| ScoreOnlyResult::new(doc_id, score) < *min)
565 }
566 TopKHeap::Positions(heap) => {
567 heap.len() < self.k
568 || heap
569 .peek()
570 .is_some_and(|min| ranks_ahead(doc_id, score, min.doc_id, min.score))
571 }
572 }
573 }
574
575 #[inline]
576 fn collect_owned(&mut self, doc_id: DocId, score: Score, positions: super::MatchedPositions) {
577 self.total_seen = self.total_seen.saturating_add(1);
578 if self.k == 0 {
579 return;
580 }
581
582 match &mut self.heap {
583 TopKHeap::Scores(heap) => {
584 let result = ScoreOnlyResult::new(doc_id, score);
585 if heap.len() < self.k {
586 heap.push(result);
587 } else if heap.peek().is_some_and(|worst| result < *worst) {
588 *heap.peek_mut().expect("full top-k heap") = result;
589 }
590 }
591 TopKHeap::Positions(heap) => {
592 if heap.len() >= self.k
593 && !heap
594 .peek()
595 .is_some_and(|worst| ranks_ahead(doc_id, score, worst.doc_id, worst.score))
596 {
597 return;
598 }
599 let result = PositionedResult {
600 doc_id,
601 score,
602 positions,
603 };
604 if heap.len() < self.k {
605 heap.push(result);
606 } else {
607 *heap.peek_mut().expect("full top-k heap") = result;
608 }
609 }
610 }
611 }
612
613 #[inline]
614 fn needs_positions(&self) -> bool {
615 matches!(&self.heap, TopKHeap::Positions(_))
616 }
617}
618
619#[derive(Default)]
621pub struct CountCollector {
622 count: u64,
623}
624
625impl CountCollector {
626 pub fn new() -> Self {
627 Self { count: 0 }
628 }
629
630 pub fn count(&self) -> u64 {
632 self.count
633 }
634}
635
636impl Collector for CountCollector {
637 fn ranked_count_limit(&self) -> Option<usize> {
638 Some(0)
639 }
640 fn collect_omitted_count(&mut self, count: u64) {
641 self.count += count;
642 }
643
644 fn supports_score_blocks(&self) -> bool {
645 true
646 }
647
648 fn collect_score_block(&mut self, _base: DocId, _scores: &[Score; 64], bits: u64) {
649 self.count += u64::from(bits.count_ones());
650 }
651
652 fn collect_count(&mut self, count: u64) -> bool {
653 self.count += count;
654 true
655 }
656
657 fn needs_scores(&self) -> bool {
658 false
659 }
660
661 #[inline]
662 fn collect(
663 &mut self,
664 _doc_id: DocId,
665 _score: Score,
666 _positions: &[(u32, Vec<ScoredPosition>)],
667 ) {
668 self.count += 1;
669 }
670}
671
672pub async fn search_segment_with_count(
674 reader: &SegmentReader,
675 query: &dyn Query,
676 limit: usize,
677) -> Result<(Vec<SearchResult>, u32)> {
678 let segment_limit = limit.min(reader.num_docs() as usize);
679 let mut collector = TopKCollector::new(segment_limit);
680 collect_segment_with_limit(reader, query, &mut collector, segment_limit).await?;
681 Ok(collector.into_results_with_count())
682}
683
684pub async fn search_segment_with_positions_and_count(
686 reader: &SegmentReader,
687 query: &dyn Query,
688 limit: usize,
689) -> Result<(Vec<SearchResult>, u32)> {
690 let segment_limit = limit.min(reader.num_docs() as usize);
691 let mut collector = TopKCollector::with_positions(segment_limit);
692 collect_segment_with_limit(reader, query, &mut collector, segment_limit).await?;
693 Ok(collector.into_results_with_count())
694}
695
696fn positions_for_next_collector(
701 positions: &mut Option<super::MatchedPositions>,
702 remaining_consumers: &mut usize,
703) -> super::MatchedPositions {
704 assert!(
705 *remaining_consumers > 0,
706 "position consumer count underflow"
707 );
708 *remaining_consumers -= 1;
709 if *remaining_consumers == 0 {
710 positions
711 .take()
712 .expect("owned positions must remain for the final collector")
713 } else {
714 positions
715 .as_ref()
716 .cloned()
717 .expect("owned positions must remain while collectors are pending")
718 }
719}
720
721impl<A: Collector, B: Collector> Collector for (&mut A, &mut B) {
723 fn ranked_count_limit(&self) -> Option<usize> {
724 Some(
725 self.0
726 .ranked_count_limit()?
727 .max(self.1.ranked_count_limit()?),
728 )
729 }
730 fn collect_omitted_count(&mut self, count: u64) {
731 self.0.collect_omitted_count(count);
732 self.1.collect_omitted_count(count);
733 }
734
735 fn supports_score_blocks(&self) -> bool {
736 self.0.supports_score_blocks() && self.1.supports_score_blocks()
737 }
738 fn collect_score_block(&mut self, base: DocId, scores: &[Score; 64], bits: u64) {
739 self.0.collect_score_block(base, scores, bits);
740 self.1.collect_score_block(base, scores, bits);
741 }
742 fn needs_scores(&self) -> bool {
743 self.0.needs_scores() || self.1.needs_scores()
744 }
745 fn collect(&mut self, doc_id: DocId, score: Score, positions: &[(u32, Vec<ScoredPosition>)]) {
746 self.0.collect(doc_id, score, positions);
747 self.1.collect(doc_id, score, positions);
748 }
749 fn needs_positions(&self) -> bool {
750 self.0.needs_positions() || self.1.needs_positions()
751 }
752 fn would_collect(&self, doc_id: DocId, score: Score) -> bool {
753 (self.0.needs_positions() && self.0.would_collect(doc_id, score))
754 || (self.1.needs_positions() && self.1.would_collect(doc_id, score))
755 }
756 fn collect_owned(&mut self, doc_id: DocId, score: Score, positions: super::MatchedPositions) {
757 let wants = [
758 self.0.needs_positions() && self.0.would_collect(doc_id, score),
759 self.1.needs_positions() && self.1.would_collect(doc_id, score),
760 ];
761 let mut remaining = wants.iter().filter(|&&want| want).count();
762 let mut positions = Some(positions);
763
764 if wants[0] {
765 self.0.collect_owned(
766 doc_id,
767 score,
768 positions_for_next_collector(&mut positions, &mut remaining),
769 );
770 } else {
771 self.0.collect(doc_id, score, &[]);
772 }
773 if wants[1] {
774 self.1.collect_owned(
775 doc_id,
776 score,
777 positions_for_next_collector(&mut positions, &mut remaining),
778 );
779 } else {
780 self.1.collect(doc_id, score, &[]);
781 }
782 }
783}
784
785impl<A: Collector, B: Collector, C: Collector> Collector for (&mut A, &mut B, &mut C) {
787 fn ranked_count_limit(&self) -> Option<usize> {
788 Some(
789 self.0
790 .ranked_count_limit()?
791 .max(self.1.ranked_count_limit()?)
792 .max(self.2.ranked_count_limit()?),
793 )
794 }
795 fn collect_omitted_count(&mut self, count: u64) {
796 self.0.collect_omitted_count(count);
797 self.1.collect_omitted_count(count);
798 self.2.collect_omitted_count(count);
799 }
800
801 fn supports_score_blocks(&self) -> bool {
802 self.0.supports_score_blocks()
803 && self.1.supports_score_blocks()
804 && self.2.supports_score_blocks()
805 }
806 fn collect_score_block(&mut self, base: DocId, scores: &[Score; 64], bits: u64) {
807 self.0.collect_score_block(base, scores, bits);
808 self.1.collect_score_block(base, scores, bits);
809 self.2.collect_score_block(base, scores, bits);
810 }
811 fn needs_scores(&self) -> bool {
812 self.0.needs_scores() || self.1.needs_scores() || self.2.needs_scores()
813 }
814 fn collect(&mut self, doc_id: DocId, score: Score, positions: &[(u32, Vec<ScoredPosition>)]) {
815 self.0.collect(doc_id, score, positions);
816 self.1.collect(doc_id, score, positions);
817 self.2.collect(doc_id, score, positions);
818 }
819 fn needs_positions(&self) -> bool {
820 self.0.needs_positions() || self.1.needs_positions() || self.2.needs_positions()
821 }
822 fn would_collect(&self, doc_id: DocId, score: Score) -> bool {
823 (self.0.needs_positions() && self.0.would_collect(doc_id, score))
824 || (self.1.needs_positions() && self.1.would_collect(doc_id, score))
825 || (self.2.needs_positions() && self.2.would_collect(doc_id, score))
826 }
827 fn collect_owned(&mut self, doc_id: DocId, score: Score, positions: super::MatchedPositions) {
828 let wants = [
829 self.0.needs_positions() && self.0.would_collect(doc_id, score),
830 self.1.needs_positions() && self.1.would_collect(doc_id, score),
831 self.2.needs_positions() && self.2.would_collect(doc_id, score),
832 ];
833 let mut remaining = wants.iter().filter(|&&want| want).count();
834 let mut positions = Some(positions);
835
836 if wants[0] {
837 self.0.collect_owned(
838 doc_id,
839 score,
840 positions_for_next_collector(&mut positions, &mut remaining),
841 );
842 } else {
843 self.0.collect(doc_id, score, &[]);
844 }
845 if wants[1] {
846 self.1.collect_owned(
847 doc_id,
848 score,
849 positions_for_next_collector(&mut positions, &mut remaining),
850 );
851 } else {
852 self.1.collect(doc_id, score, &[]);
853 }
854 if wants[2] {
855 self.2.collect_owned(
856 doc_id,
857 score,
858 positions_for_next_collector(&mut positions, &mut remaining),
859 );
860 } else {
861 self.2.collect(doc_id, score, &[]);
862 }
863 }
864}
865
866fn streams_exhaustive_text(query: &dyn Query) -> bool {
867 matches!(query.decompose(), super::QueryDecomposition::TextTerm(_))
868 || query.should_children().is_some_and(|children| {
869 children
870 .iter()
871 .all(|child| streams_exhaustive_text(child.as_ref()))
872 })
873}
874
875async fn two_term_union_count(
878 reader: &SegmentReader,
879 query: &dyn Query,
880 minimum_count: u64,
881) -> Result<Option<u64>> {
882 if reader.alive_docs().is_some() {
883 return Ok(None);
884 }
885 let Some([left, right]) = query.should_children() else {
886 return Ok(None);
887 };
888 let (Some(a), Some(b)) = (left.count_equivalent_term(), right.count_equivalent_term()) else {
889 return Ok(None);
890 };
891 for info in [&a, &b] {
892 if reader.is_chunked_field(info.field)
893 || !reader
894 .schema()
895 .get_field_entry(info.field)
896 .is_some_and(|field| field.indexed)
897 {
898 return Ok(None);
899 }
900 }
901 let mapped = reader.has_text_mapping(a.field) || reader.has_text_mapping(b.field);
902 if mapped && a.field != b.field {
903 return Ok(None);
904 }
905 let a = reader.text_doc_freq(a.field, &a.term).await?;
906 let b = reader.text_doc_freq(b.field, &b.term).await?;
907 if a == 0 || b == 0 || u64::from(a.max(b)) < minimum_count {
909 return Ok(None);
910 }
911 let mut intersection = super::BooleanQuery::new();
912 intersection.must = vec![left.clone(), right.clone()];
913 let mut options = super::ScorerOptions {
914 physical_text_field: None,
915 complete_text_matches: true,
916 skip_scoring_setup: true,
917 collect_positions: false,
918 ..Default::default()
919 };
920 if mapped && super::text_mapping::prepare(reader, &intersection, &mut options, true).is_none() {
921 return Ok(None);
922 }
923 let mut scorer = intersection
924 .scorer_with_options(reader, usize::MAX / 2, options)
925 .await?;
926 let mut overlap = CountCollector::new();
927 drive_scorer(scorer.as_mut(), &mut overlap);
928 reader.check_posting_integrity()?;
929 let overlap = overlap.count();
930 if overlap > u64::from(a.min(b)) {
931 return Err(crate::Error::Corruption(
932 "term overlap exceeds dictionary cardinality".into(),
933 ));
934 }
935 Ok(Some(u64::from(a) + u64::from(b) - overlap))
936}
937
938async fn ranked_exact_count(
940 reader: &SegmentReader,
941 query: &dyn Query,
942 limit: usize,
943) -> Result<Option<u64>> {
944 if reader.alive_docs().is_some() {
945 return Ok(None);
946 }
947 let eligible = |info: super::TermQueryInfo| {
948 (info.weight.is_finite()
949 && info.weight > 0.0
950 && reader
951 .schema()
952 .get_field_entry(info.field)
953 .is_some_and(|entry| entry.indexed)
954 && (!reader.has_text_mapping(info.field)
955 || reader
956 .chunk_map(info.field)
957 .is_some_and(|map| map.is_document_map()))
958 && !reader.is_chunked_field(info.field))
959 .then_some(info)
960 };
961 if let Some(info) = query.ranked_count_equivalent_term()
962 && let Some(info) = eligible(info)
963 {
964 let count = reader.text_doc_freq(info.field, &info.term).await?;
965 return Ok(
966 (u64::from(count) >= (limit.max(128) as u64).saturating_mul(16))
967 .then_some(u64::from(count)),
968 );
969 }
970 if let Some([left, right]) = query.should_children()
971 && let (super::QueryDecomposition::TextTerm(a), super::QueryDecomposition::TextTerm(b)) =
972 (left.decompose(), right.decompose())
973 && let (Some(a), Some(b)) = (eligible(a), eligible(b))
974 && a.field == b.field
975 {
976 return two_term_union_count(reader, query, (limit.max(128) as u64).saturating_mul(128))
977 .await;
978 }
979 Ok(None)
980}
981
982pub async fn collect_segment<C: Collector>(
1001 reader: &SegmentReader,
1002 query: &dyn Query,
1003 collector: &mut C,
1004) -> Result<()> {
1005 reader.check_posting_integrity()?;
1006 if !collector.needs_scores()
1009 && !collector.needs_positions()
1010 && reader.alive_docs().is_none()
1011 && let Some(info) = query.count_equivalent_term()
1012 && !reader.is_chunked_field(info.field)
1013 && reader
1014 .schema()
1015 .get_field_entry(info.field)
1016 .is_some_and(|entry| entry.indexed)
1017 {
1018 let count = reader.text_doc_freq(info.field, &info.term).await?;
1019 if count > 0 && collector.collect_count(u64::from(count)) {
1022 return reader.check_posting_integrity();
1023 }
1024 }
1025 if !collector.needs_scores()
1026 && !collector.needs_positions()
1027 && query
1028 .should_children()
1029 .is_some_and(|children| children.len() == 2)
1030 && collector.collect_count(0)
1031 && let Some(count) = two_term_union_count(reader, query, 0).await?
1032 && collector.collect_count(count)
1033 {
1034 return reader.check_posting_integrity();
1035 }
1036 if !collector.needs_positions()
1037 && let Some(limit) = collector.ranked_count_limit()
1038 && limit > 0
1039 && limit < reader.num_docs() as usize
1040 && let Some(count) = ranked_exact_count(reader, query, limit).await?
1041 && count > limit as u64
1042 {
1043 let mut visited = CountCollector::new();
1044 collect_segment_with_limit(reader, query, &mut (&mut *collector, &mut visited), limit)
1045 .await?;
1046 let omitted = count.checked_sub(visited.count()).ok_or_else(|| {
1047 crate::Error::Corruption("ranked matches exceed exact cardinality".into())
1048 })?;
1049 collector.collect_omitted_count(omitted);
1050 return reader.check_posting_integrity();
1051 }
1052 let ranked_count_limit = collector.ranked_count_limit().filter(|&k| {
1053 query.supports_ranked_conjunction_count()
1054 && k > 0
1055 && k < reader.num_docs() as usize
1056 && !collector.needs_positions()
1057 && reader.alive_docs().is_none()
1058 });
1059 let mut options = super::ScorerOptions {
1060 physical_text_field: None,
1061 complete_text_matches: streams_exhaustive_text(query),
1065 ranked_count_limit,
1066 skip_scoring_setup: !collector.needs_scores() && !collector.needs_positions(),
1067 eligibility: reader.alive_docs(),
1068 collect_positions: collector.needs_positions(),
1069 ..Default::default()
1070 };
1071 let map = super::text_mapping::prepare(reader, query, &mut options, true);
1074 let scorer = query
1075 .scorer_with_options(reader, usize::MAX / 2, options)
1076 .await?;
1077 let exact_count = ranked_count_limit.and_then(|_| scorer.exact_ranked_count());
1078 let mut scorer = super::text_mapping::filtered(scorer, reader.alive_docs(), map);
1079 if let Some(count) = exact_count {
1080 let mut visited = CountCollector::new();
1081 drive_collected(
1082 scorer.as_mut(),
1083 &mut (&mut *collector, &mut visited),
1084 map,
1085 None,
1086 );
1087 let omitted = count.checked_sub(visited.count()).ok_or_else(|| {
1088 crate::Error::Corruption("ranked matches exceed exact cardinality".into())
1089 })?;
1090 collector.collect_omitted_count(omitted);
1091 } else {
1092 drive_collected(scorer.as_mut(), collector, map, None);
1093 }
1094 reader.check_posting_integrity()
1095}
1096
1097pub async fn collect_segment_with_limit<C: Collector>(
1106 reader: &SegmentReader,
1107 query: &dyn Query,
1108 collector: &mut C,
1109 limit: usize,
1110) -> Result<()> {
1111 collect_segment_with_limit_seeded(reader, query, collector, limit, 0.0).await
1112}
1113
1114pub async fn collect_segment_with_limit_seeded<C: Collector>(
1119 reader: &SegmentReader,
1120 query: &dyn Query,
1121 collector: &mut C,
1122 limit: usize,
1123 initial_threshold: f32,
1124) -> Result<()> {
1125 reader.check_posting_integrity()?;
1126 let mut options = super::ScorerOptions {
1127 physical_text_field: None,
1128 complete_text_matches: false,
1129 ranked_count_limit: None,
1130 skip_scoring_setup: !collector.needs_scores() && !collector.needs_positions(),
1131 eligibility: reader.alive_docs(),
1132 collect_positions: collector.needs_positions(),
1133 initial_threshold,
1134 shared_threshold: None,
1135 lsp_plan: None,
1136 global_stats: None,
1137 };
1138 let map = super::text_mapping::prepare(reader, query, &mut options, false);
1139 let scorer = query.scorer_with_options(reader, limit, options).await?;
1140 let mut scorer = super::text_mapping::filtered(scorer, reader.alive_docs(), map);
1141 drive_collected(scorer.as_mut(), collector, map, None);
1142 reader.check_posting_integrity()
1143}
1144
1145fn drive_scorer<C: Collector>(scorer: &mut dyn super::Scorer, collector: &mut C) {
1147 drive_scorer_budgeted(scorer, collector, None);
1148}
1149
1150struct MappedCollector<'a, C> {
1153 inner: &'a mut C,
1154 map: &'a crate::segment::chunk_map::ChunkMap,
1155}
1156
1157impl<C: Collector> Collector for MappedCollector<'_, C> {
1158 fn collect(&mut self, doc: DocId, score: Score, positions: &[(u32, Vec<ScoredPosition>)]) {
1159 self.inner.collect(self.map.doc_id(doc), score, positions);
1160 }
1161 fn collect_owned(&mut self, doc: DocId, score: Score, positions: super::MatchedPositions) {
1162 self.inner
1163 .collect_owned(self.map.doc_id(doc), score, positions);
1164 }
1165 fn would_collect(&self, doc: DocId, score: Score) -> bool {
1166 self.inner.would_collect(self.map.doc_id(doc), score)
1167 }
1168 fn collect_count(&mut self, count: u64) -> bool {
1169 self.inner.collect_count(count)
1170 }
1171 fn needs_scores(&self) -> bool {
1172 self.inner.needs_scores()
1173 }
1174 fn needs_positions(&self) -> bool {
1175 self.inner.needs_positions()
1176 }
1177}
1178
1179fn drive_collected<C: Collector>(
1180 scorer: &mut dyn super::Scorer,
1181 collector: &mut C,
1182 map: Option<&crate::segment::chunk_map::ChunkMap>,
1183 budget: Option<&super::SharedThreshold>,
1184) {
1185 if let Some(map) = map {
1186 drive_scorer_budgeted(
1187 scorer,
1188 &mut MappedCollector {
1189 inner: collector,
1190 map,
1191 },
1192 budget,
1193 );
1194 } else {
1195 drive_scorer_budgeted(scorer, collector, budget);
1196 }
1197}
1198
1199fn drive_scorer_budgeted<C: Collector>(
1200 scorer: &mut dyn super::Scorer,
1201 collector: &mut C,
1202 budget: Option<&super::SharedThreshold>,
1203) {
1204 let needs_positions = collector.needs_positions();
1205 let needs_scores = collector.needs_scores() || needs_positions;
1206 let mut doc = scorer.doc();
1207 if needs_scores && !needs_positions && scorer.supports_score_windows() {
1208 let mut scores = Box::new([0.0; super::docset::DOC_WINDOW_SIZE as usize]);
1209 let mut bits = [0; super::docset::DOC_WINDOW_WORDS];
1210 let collect_blocks = budget.and_then(super::SharedThreshold::deadline).is_none()
1211 && collector.supports_score_blocks();
1212 while doc != TERMINATED {
1213 if budget.is_some_and(super::SharedThreshold::stop_if_expired) {
1214 break;
1215 }
1216 scorer.fill_score_window(doc, &mut scores, &mut bits);
1217 if budget.is_some_and(super::SharedThreshold::stop_if_expired) {
1218 break;
1219 }
1220 if collect_blocks {
1221 for (index, (scores, &word)) in
1222 scores.as_chunks::<64>().0.iter().zip(&bits).enumerate()
1223 {
1224 if word != 0 {
1225 collector.collect_score_block(doc + index as u32 * 64, scores, word);
1226 }
1227 }
1228 doc = scorer.doc();
1229 continue;
1230 }
1231 for (index, &word) in bits.iter().enumerate() {
1232 let mut remaining = word;
1233 while remaining != 0 {
1234 if budget.is_some_and(super::SharedThreshold::stop_if_expired) {
1235 return;
1236 }
1237 let offset = index * 64 + remaining.trailing_zeros() as usize;
1238 collector.collect(doc + offset as u32, scores[offset], &[]);
1239 remaining &= remaining - 1;
1240 }
1241 }
1242 doc = scorer.doc();
1243 }
1244 return;
1245 }
1246 if needs_scores && !needs_positions && scorer.supports_score_batches() {
1247 let mut docs = [0; super::docset::DOC_BATCH_SIZE];
1248 let mut scores = [0.0; super::docset::DOC_BATCH_SIZE];
1249 while doc != TERMINATED {
1250 if budget.is_some_and(super::SharedThreshold::stop_if_expired) {
1251 break;
1252 }
1253 let count = scorer.fill_score_batch(&mut docs, &mut scores);
1254 if budget.is_some_and(super::SharedThreshold::stop_if_expired) {
1255 break;
1256 }
1257 for i in 0..count {
1258 if budget.is_some_and(super::SharedThreshold::stop_if_expired) {
1259 return;
1260 }
1261 collector.collect(docs[i], scores[i], &[]);
1262 }
1263 doc = scorer.doc();
1264 }
1265 return;
1266 }
1267 if !needs_scores && scorer.supports_doc_windows() {
1268 let mut bits = [0; super::docset::DOC_WINDOW_WORDS];
1269 while doc != TERMINATED {
1270 if budget.is_some_and(super::SharedThreshold::stop_if_expired) {
1271 break;
1272 }
1273 scorer.fill_doc_window(doc, &mut bits);
1274 if budget.is_some_and(super::SharedThreshold::stop_if_expired) {
1275 break;
1276 }
1277 let count = bits.iter().map(|word| u64::from(word.count_ones())).sum();
1278 if !collector.collect_count(count) {
1279 for (index, &word) in bits.iter().enumerate() {
1280 let mut remaining = word;
1281 while remaining != 0 {
1282 if budget.is_some_and(super::SharedThreshold::stop_if_expired) {
1283 return;
1284 }
1285 let offset = index as u32 * 64 + remaining.trailing_zeros();
1286 collector.collect(doc + offset, 0.0, &[]);
1287 remaining &= remaining - 1;
1288 }
1289 }
1290 }
1291 doc = scorer.doc();
1292 }
1293 return;
1294 }
1295 if !needs_scores && !needs_positions && scorer.supports_doc_batches() {
1296 let mut docs = [0; super::docset::DOC_BATCH_SIZE];
1297 while doc != TERMINATED {
1298 if budget.is_some_and(super::SharedThreshold::stop_if_expired) {
1299 break;
1300 }
1301 let count = scorer.fill_doc_batch(&mut docs);
1302 if budget.is_some_and(super::SharedThreshold::stop_if_expired) {
1303 break;
1304 }
1305 if !collector.collect_count(count as u64) {
1306 for &doc in &docs[..count] {
1307 if budget.is_some_and(super::SharedThreshold::stop_if_expired) {
1308 return;
1309 }
1310 collector.collect(doc, 0.0, &[]);
1311 }
1312 }
1313 doc = scorer.doc();
1314 }
1315 return;
1316 }
1317 while doc != TERMINATED {
1318 if budget.is_some_and(super::SharedThreshold::stop_if_expired) {
1321 break;
1322 }
1323 let score = if needs_scores { scorer.score() } else { 0.0 };
1324 if budget.is_some_and(super::SharedThreshold::stop_if_expired) {
1325 break;
1326 }
1327 if needs_positions && collector.would_collect(doc, score) {
1328 let positions = scorer.matched_positions().unwrap_or_default();
1329 if budget.is_some_and(super::SharedThreshold::stop_if_expired) {
1330 break;
1331 }
1332 collector.collect_owned(doc, score, positions);
1333 } else {
1334 collector.collect(doc, score, &[]);
1335 }
1336 doc = scorer.advance();
1337 }
1338}
1339
1340#[cfg(feature = "sync")]
1344pub fn search_segment_with_count_sync(
1345 reader: &SegmentReader,
1346 query: &dyn Query,
1347 limit: usize,
1348) -> Result<(Vec<SearchResult>, u32)> {
1349 let segment_limit = limit.min(reader.num_docs() as usize);
1350 let mut collector = TopKCollector::new(segment_limit);
1351 collect_segment_with_limit_sync(reader, query, &mut collector, segment_limit)?;
1352 Ok(collector.into_results_with_count())
1353}
1354
1355#[cfg(feature = "sync")]
1357pub fn search_segment_with_positions_and_count_sync(
1358 reader: &SegmentReader,
1359 query: &dyn Query,
1360 limit: usize,
1361) -> Result<(Vec<SearchResult>, u32)> {
1362 let segment_limit = limit.min(reader.num_docs() as usize);
1363 let mut collector = TopKCollector::with_positions(segment_limit);
1364 collect_segment_with_limit_sync(reader, query, &mut collector, segment_limit)?;
1365 Ok(collector.into_results_with_count())
1366}
1367
1368#[cfg(feature = "sync")]
1370pub fn collect_segment_with_limit_sync<C: Collector>(
1371 reader: &SegmentReader,
1372 query: &dyn Query,
1373 collector: &mut C,
1374 limit: usize,
1375) -> Result<()> {
1376 collect_segment_with_limit_seeded_sync(reader, query, collector, limit, 0.0)
1377}
1378
1379#[cfg(feature = "sync")]
1382pub fn collect_segment_with_limit_seeded_sync<C: Collector>(
1383 reader: &SegmentReader,
1384 query: &dyn Query,
1385 collector: &mut C,
1386 limit: usize,
1387 initial_threshold: f32,
1388) -> Result<()> {
1389 reader.check_posting_integrity()?;
1390 let mut options = super::ScorerOptions {
1391 physical_text_field: None,
1392 complete_text_matches: false,
1393 ranked_count_limit: None,
1394 skip_scoring_setup: !collector.needs_scores() && !collector.needs_positions(),
1395 eligibility: reader.alive_docs(),
1396 collect_positions: collector.needs_positions(),
1397 initial_threshold,
1398 shared_threshold: None,
1399 lsp_plan: None,
1400 global_stats: None,
1401 };
1402 let map = super::text_mapping::prepare(reader, query, &mut options, false);
1403 let scorer = query.scorer_sync_with_options(reader, limit, options)?;
1404 let mut scorer = super::text_mapping::filtered(scorer, reader.alive_docs(), map);
1405 drive_collected(scorer.as_mut(), collector, map, None);
1406 reader.check_posting_integrity()
1407}
1408
1409#[cfg(feature = "sync")]
1416pub fn search_segment_seeded_sync(
1417 reader: &SegmentReader,
1418 query: &dyn Query,
1419 limit: usize,
1420 collect_positions: bool,
1421 initial_threshold: f32,
1422) -> Result<(Vec<SearchResult>, u32)> {
1423 let segment_limit = limit.min(reader.num_docs() as usize);
1424 let mut collector = if collect_positions {
1425 TopKCollector::with_positions(segment_limit)
1426 } else {
1427 TopKCollector::new(segment_limit)
1428 };
1429 collect_segment_with_limit_seeded_sync(
1430 reader,
1431 query,
1432 &mut collector,
1433 segment_limit,
1434 initial_threshold,
1435 )?;
1436 Ok(collector.into_results_with_count())
1437}
1438
1439#[cfg(feature = "sync")]
1441pub fn search_segment_shared_sync(
1442 reader: &SegmentReader,
1443 query: &dyn Query,
1444 limit: usize,
1445 collect_positions: bool,
1446 shared_threshold: super::SharedThreshold,
1447) -> Result<(Vec<SearchResult>, u32)> {
1448 search_segment_shared_sync_planned(
1449 reader,
1450 query,
1451 limit,
1452 collect_positions,
1453 shared_threshold,
1454 None,
1455 None,
1456 )
1457}
1458
1459#[cfg(feature = "sync")]
1461pub(crate) fn search_segment_shared_sync_planned(
1462 reader: &SegmentReader,
1463 query: &dyn Query,
1464 limit: usize,
1465 collect_positions: bool,
1466 shared_threshold: super::SharedThreshold,
1467 lsp_plan: Option<std::sync::Arc<super::bmp::LspSegmentPlan>>,
1468 global_stats: Option<std::sync::Arc<super::GlobalStats>>,
1469) -> Result<(Vec<SearchResult>, u32)> {
1470 reader.check_posting_integrity()?;
1471 let segment_limit = limit.min(reader.num_docs() as usize);
1472 let mut options = super::ScorerOptions {
1473 physical_text_field: None,
1474 complete_text_matches: false,
1475 ranked_count_limit: None,
1476 skip_scoring_setup: false,
1477 eligibility: reader.alive_docs(),
1478 collect_positions,
1479 initial_threshold: shared_threshold.get(),
1480 shared_threshold: Some(shared_threshold.clone()),
1481 lsp_plan,
1482 global_stats,
1483 };
1484 let map = super::text_mapping::prepare(reader, query, &mut options, false);
1485 let scorer = query.scorer_sync_with_options(reader, segment_limit, options)?;
1486 let mut scorer = super::text_mapping::filtered(scorer, reader.alive_docs(), map);
1487 let results = top_k_from_mapped_scorer(
1488 scorer.as_mut(),
1489 segment_limit,
1490 collect_positions,
1491 Some(&shared_threshold),
1492 map,
1493 );
1494 reader.check_posting_integrity()?;
1495 Ok(results)
1496}
1497
1498#[cfg(test)]
1505fn top_k_from_scorer(
1506 scorer: &mut dyn super::Scorer,
1507 segment_limit: usize,
1508 collect_positions: bool,
1509 budget: Option<&super::SharedThreshold>,
1510) -> (Vec<SearchResult>, u32) {
1511 top_k_from_mapped_scorer(scorer, segment_limit, collect_positions, budget, None)
1512}
1513
1514fn top_k_from_mapped_scorer(
1515 scorer: &mut dyn super::Scorer,
1516 segment_limit: usize,
1517 collect_positions: bool,
1518 budget: Option<&super::SharedThreshold>,
1519 map: Option<&crate::segment::chunk_map::ChunkMap>,
1520) -> (Vec<SearchResult>, u32) {
1521 let handoff_limit = map.map_or(segment_limit, |map| map.num_chunks() as usize);
1525 if let Some((mut results, seen)) = scorer.precomputed_top_k(handoff_limit, collect_positions) {
1526 if let Some(map) = map {
1527 for result in &mut results {
1528 result.doc_id = map.doc_id(result.doc_id);
1529 }
1530 results.sort_unstable_by(|a, b| {
1531 b.score
1532 .total_cmp(&a.score)
1533 .then_with(|| a.doc_id.cmp(&b.doc_id))
1534 });
1535 results.truncate(segment_limit);
1536 }
1537 return (results, seen);
1538 }
1539 let mut collector = if collect_positions {
1540 TopKCollector::with_positions(segment_limit)
1541 } else {
1542 TopKCollector::new(segment_limit)
1543 };
1544 if segment_limit > 0 && scorer.supports_candidate_score_bounds() {
1545 drive_ranked_candidates(scorer, &mut collector, budget, map);
1546 } else {
1547 drive_collected(scorer, &mut collector, map, budget);
1548 }
1549 collector.into_results_with_count()
1550}
1551
1552fn drive_ranked_candidates(
1555 scorer: &mut dyn super::Scorer,
1556 collector: &mut TopKCollector,
1557 budget: Option<&super::SharedThreshold>,
1558 map: Option<&crate::segment::chunk_map::ChunkMap>,
1559) {
1560 let mut doc = scorer.doc();
1561 let mut next_block_check = 0;
1562 let mut confirmations = 0usize;
1563 let mut seed_attempted = false;
1564 let mut seeded_floor = Score::NEG_INFINITY;
1565 while doc != TERMINATED {
1566 if budget.is_some_and(super::SharedThreshold::stop_if_expired) {
1567 break;
1568 }
1569 if doc >= next_block_check && (collector.is_full() || seeded_floor.is_finite()) {
1570 if let Some((last, bound)) = scorer.candidate_block_upper_bound() {
1571 next_block_check = last.saturating_add(1);
1572 if bound < seeded_floor || !collector.would_collect(0, bound) {
1575 doc = scorer.seek_candidate(next_block_check);
1576 continue;
1577 }
1578 } else {
1579 next_block_check = TERMINATED;
1580 }
1581 }
1582 let result_doc = map.map_or(doc, |map| map.doc_id(doc));
1583 let competitive = if collector.is_full() || seeded_floor.is_finite() {
1585 let bound = scorer.candidate_score_upper_bound();
1586 bound >= seeded_floor && collector.would_collect(result_doc, bound)
1587 } else {
1588 true
1589 };
1590 if budget.is_some_and(super::SharedThreshold::stop_if_expired) {
1591 break;
1592 }
1593 confirmations += usize::from(competitive);
1594 if competitive && scorer.confirm_candidate() {
1595 if budget.is_some_and(super::SharedThreshold::stop_if_expired) {
1596 break;
1597 }
1598 let score = scorer.score();
1599 if budget.is_some_and(super::SharedThreshold::stop_if_expired) {
1600 break;
1601 }
1602 if collector.needs_positions() && collector.would_collect(result_doc, score) {
1603 let positions = scorer.matched_positions().unwrap_or_default();
1604 if budget.is_some_and(super::SharedThreshold::stop_if_expired) {
1605 break;
1606 }
1607 collector.collect_owned(result_doc, score, positions);
1608 } else {
1609 collector.collect(result_doc, score, &[]);
1610 }
1611 }
1612 if map.is_some() && confirmations >= 1024 && !seed_attempted {
1613 seed_attempted = true;
1614 if let Some(score) = scorer.seed_ranked_score(collector.k)
1615 && score.is_finite()
1616 {
1617 seeded_floor = score;
1618 }
1619 }
1620 doc = scorer.advance_competitive_candidate(
1624 collector.competitive_score().max(seeded_floor),
1625 map.is_some(),
1626 );
1627 }
1628}
1629
1630pub async fn search_segment_seeded(
1632 reader: &SegmentReader,
1633 query: &dyn Query,
1634 limit: usize,
1635 collect_positions: bool,
1636 initial_threshold: f32,
1637) -> Result<(Vec<SearchResult>, u32)> {
1638 let segment_limit = limit.min(reader.num_docs() as usize);
1639 let mut collector = if collect_positions {
1640 TopKCollector::with_positions(segment_limit)
1641 } else {
1642 TopKCollector::new(segment_limit)
1643 };
1644 collect_segment_with_limit_seeded(
1645 reader,
1646 query,
1647 &mut collector,
1648 segment_limit,
1649 initial_threshold,
1650 )
1651 .await?;
1652 Ok(collector.into_results_with_count())
1653}
1654
1655pub async fn search_segment_shared(
1657 reader: &SegmentReader,
1658 query: &dyn Query,
1659 limit: usize,
1660 collect_positions: bool,
1661 shared_threshold: super::SharedThreshold,
1662) -> Result<(Vec<SearchResult>, u32)> {
1663 search_segment_shared_planned(
1664 reader,
1665 query,
1666 limit,
1667 collect_positions,
1668 shared_threshold,
1669 None,
1670 None,
1671 )
1672 .await
1673}
1674
1675pub(crate) async fn search_segment_shared_planned(
1677 reader: &SegmentReader,
1678 query: &dyn Query,
1679 limit: usize,
1680 collect_positions: bool,
1681 shared_threshold: super::SharedThreshold,
1682 lsp_plan: Option<std::sync::Arc<super::bmp::LspSegmentPlan>>,
1683 global_stats: Option<std::sync::Arc<super::GlobalStats>>,
1684) -> Result<(Vec<SearchResult>, u32)> {
1685 reader.check_posting_integrity()?;
1686 let segment_limit = limit.min(reader.num_docs() as usize);
1687 let mut options = super::ScorerOptions {
1688 physical_text_field: None,
1689 complete_text_matches: false,
1690 ranked_count_limit: None,
1691 skip_scoring_setup: false,
1692 eligibility: reader.alive_docs(),
1693 collect_positions,
1694 initial_threshold: shared_threshold.get(),
1695 shared_threshold: Some(shared_threshold.clone()),
1696 lsp_plan,
1697 global_stats,
1698 };
1699 let map = super::text_mapping::prepare(reader, query, &mut options, false);
1700 let scorer = query
1701 .scorer_with_options(reader, segment_limit, options)
1702 .await?;
1703 let mut scorer = super::text_mapping::filtered(scorer, reader.alive_docs(), map);
1704 let results = top_k_from_mapped_scorer(
1705 scorer.as_mut(),
1706 segment_limit,
1707 collect_positions,
1708 Some(&shared_threshold),
1709 map,
1710 );
1711 reader.check_posting_integrity()?;
1712 Ok(results)
1713}
1714
1715impl From<&SearchResult> for SearchResult {
1717 fn from(value: &SearchResult) -> Self {
1718 value.clone()
1719 }
1720}
1721
1722#[cfg(test)]
1723mod tests {
1724 #[test]
1725 fn packed_score_heap_preserves_total_order_and_exact_bits() {
1726 let mut bits = vec![
1727 0, 1, 0x7f800000, 0xff800000, 0x80000000, 0x7fc00000, 0xffc00000, 0x7fffffff,
1728 0xffffffff, 0x7f800001, 0xff800001,
1729 ];
1730 let mut state = 317u32;
1731 for _ in 0..2048 {
1732 state = state.wrapping_mul(1664525).wrapping_add(1013904223);
1733 bits.push(state);
1734 }
1735 let mut actual = Vec::new();
1736 let mut expected = Vec::new();
1737 for &b in &bits {
1738 for doc in [0, 1, 713, u32::MAX] {
1739 let entry = super::ScoreOnlyResult::new(doc, f32::from_bits(b));
1740 assert_eq!((entry.doc_id(), entry.score().to_bits()), (doc, b));
1741 actual.push(entry);
1742 expected.push((doc, f32::from_bits(b)));
1743 }
1744 }
1745 actual.sort_unstable();
1746 expected.sort_unstable_by(|a, b| b.1.total_cmp(&a.1).then(a.0.cmp(&b.0)));
1747 assert_eq!(
1748 actual
1749 .iter()
1750 .map(|x| (x.doc_id(), x.score().to_bits()))
1751 .collect::<Vec<_>>(),
1752 expected
1753 .iter()
1754 .map(|x| (x.0, x.1.to_bits()))
1755 .collect::<Vec<_>>()
1756 );
1757 assert_eq!(std::mem::size_of::<super::ScoreOnlyResult>(), 8);
1758 }
1759
1760 use super::*;
1761
1762 struct BoundedCandidates {
1763 hits: Vec<(DocId, Score, Score, bool)>,
1764 at: usize,
1765 confirmations: std::sync::Arc<std::sync::Mutex<Vec<DocId>>>,
1766 scores: std::sync::Arc<std::sync::atomic::AtomicUsize>,
1767 bounds: std::sync::Arc<std::sync::atomic::AtomicUsize>,
1768 pause_confirmation: bool,
1769 seed_calls: usize,
1770 }
1771
1772 impl BoundedCandidates {
1773 fn fixture() -> Self {
1774 Self {
1775 hits: vec![
1776 (0, 5.0, 5.0, true),
1777 (1, 0.0, 9.0, false),
1778 (2, 1.0, 1.0, true),
1779 (3, 7.0, 7.0, true),
1780 (4, 7.0, 7.0, true),
1781 (5, 9.0, 9.0, true),
1782 ],
1783 at: 0,
1784 confirmations: Default::default(),
1785 scores: Default::default(),
1786 bounds: Default::default(),
1787 pause_confirmation: false,
1788 seed_calls: 0,
1789 }
1790 }
1791 }
1792
1793 impl super::super::DocSet for BoundedCandidates {
1794 fn doc(&self) -> DocId {
1795 self.hits.get(self.at).map_or(TERMINATED, |hit| hit.0)
1796 }
1797 fn advance(&mut self) -> DocId {
1798 self.at += 1;
1799 while self.at < self.hits.len() && !super::super::Scorer::confirm_candidate(self) {
1800 self.at += 1;
1801 }
1802 self.doc()
1803 }
1804 fn size_hint(&self) -> u32 {
1805 self.hits.len() as u32
1806 }
1807 }
1808
1809 impl super::super::Scorer for BoundedCandidates {
1810 fn seed_ranked_score(&mut self, limit: usize) -> Option<Score> {
1811 self.seed_calls += 1;
1812 let mut scores: Vec<_> = self.hits[self.at + 1..]
1813 .iter()
1814 .filter(|hit| hit.3)
1815 .map(|hit| hit.1)
1816 .collect();
1817 scores.sort_unstable_by(|a, b| b.total_cmp(a));
1818 scores.get(limit - 1).copied()
1819 }
1820 fn supports_candidate_score_bounds(&self) -> bool {
1821 true
1822 }
1823 fn candidate_score_upper_bound(&self) -> Score {
1824 self.bounds
1825 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1826 self.hits[self.at].2
1827 }
1828 fn advance_candidate(&mut self) -> DocId {
1829 self.at += 1;
1830 super::super::DocSet::doc(self)
1831 }
1832 fn confirm_candidate(&mut self) -> bool {
1833 self.confirmations
1834 .lock()
1835 .unwrap()
1836 .push(self.hits[self.at].0);
1837 if self.pause_confirmation {
1838 std::thread::sleep(std::time::Duration::from_millis(25));
1839 }
1840 self.hits[self.at].3
1841 }
1842 fn score(&self) -> Score {
1843 assert!(self.hits[self.at].3);
1844 self.scores
1845 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1846 self.hits[self.at].1
1847 }
1848 fn matched_positions(&self) -> Option<super::super::MatchedPositions> {
1849 Some(vec![(
1850 7,
1851 vec![ScoredPosition::new(
1852 self.hits[self.at].0,
1853 self.hits[self.at].1,
1854 )],
1855 )])
1856 }
1857 }
1858
1859 #[test]
1860 fn mapped_score_seeding_keeps_late_equal_score_winners_and_underfilled_heaps() {
1861 use crate::directories::OwnedBytes;
1862 use crate::segment::chunk_map::{ChunkMapBuilder, read_chunk_maps, write_chunk_maps};
1863 let mut map = ChunkMapBuilder::default();
1864 map.set_document_units(true);
1865 for doc in 0..2048 {
1866 map.push(2047 - doc, 0, 100).unwrap();
1867 }
1868 let mut bytes = Vec::new();
1869 write_chunk_maps(&mut bytes, &[(0, &map)], &[]).unwrap();
1870 let maps = read_chunk_maps(OwnedBytes::new(bytes)).unwrap();
1871 for prefix_matches in [false, true] {
1872 let mut scorer = BoundedCandidates::fixture();
1873 scorer.hits = (0..2048)
1874 .map(|doc| {
1875 if doc < 2000 {
1876 (doc, 1.0, 1.5, prefix_matches)
1877 } else {
1878 (doc, 2.0, 2.0, true)
1879 }
1880 })
1881 .collect();
1882 let (hits, _) =
1883 top_k_from_mapped_scorer(&mut scorer, 10, true, None, Some(&maps.chunk_maps[&0]));
1884 assert_eq!(
1885 hits.iter()
1886 .map(|hit| (hit.doc_id, hit.score))
1887 .collect::<Vec<_>>(),
1888 (0..10).map(|doc| (doc, 2.0)).collect::<Vec<_>>()
1889 );
1890 assert_eq!(scorer.seed_calls, 1);
1891 assert_eq!(scorer.confirmations.lock().unwrap().len(), 1072);
1892 }
1893 let mut scorer = BoundedCandidates::fixture();
1894 scorer.hits = (0..2048)
1895 .map(|doc| (doc, if doc < 2000 { 1.0 } else { 2.0 }, 2.0, true))
1896 .collect();
1897 let mut filtered = super::super::PredicatedScorer::new(
1898 Box::new(scorer),
1899 vec![Box::new(|doc| doc < 2000)],
1900 Vec::new(),
1901 Vec::new(),
1902 );
1903 assert_eq!(
1905 super::super::Scorer::seed_ranked_score(&mut filtered, 10),
1906 None
1907 );
1908 let (hits, _) =
1909 top_k_from_mapped_scorer(&mut filtered, 10, false, None, Some(&maps.chunk_maps[&0]));
1910 assert_eq!(
1911 hits.iter()
1912 .map(|hit| (hit.doc_id, hit.score))
1913 .collect::<Vec<_>>(),
1914 (48..58).map(|doc| (doc, 1.0)).collect::<Vec<_>>()
1915 );
1916 }
1917
1918 #[tokio::test]
1919 async fn two_term_union_counts_preserve_overlap_duplicates_fields_and_declining_collectors() {
1920 use crate::query::{BooleanQuery, TermQuery};
1921 use crate::segment::{SegmentBuilder, SegmentBuilderConfig, SegmentId};
1922 use crate::structures::PostingCodec;
1923 use std::sync::Arc;
1924 struct Ids(Vec<u32>);
1925 impl Collector for Ids {
1926 fn needs_scores(&self) -> bool {
1927 false
1928 }
1929 fn collect(&mut self, doc: u32, _: f32, _: &[(u32, Vec<ScoredPosition>)]) {
1930 self.0.push(doc);
1931 }
1932 }
1933 for codec in [
1934 PostingCodec::Rounded,
1935 PostingCodec::Packed,
1936 PostingCodec::Pfor,
1937 PostingCodec::Simd4x,
1938 ] {
1939 let dir = crate::RamDirectory::new();
1940 let mut schema = crate::SchemaBuilder::default();
1941 let a = schema.add_text_field("a", true, false);
1942 let b = schema.add_text_field("b", true, false);
1943 let schema = Arc::new(schema.build());
1944 let mut builder = SegmentBuilder::new(
1945 schema.clone(),
1946 SegmentBuilderConfig {
1947 posting_codec: codec,
1948 ..Default::default()
1949 },
1950 )
1951 .unwrap();
1952 for i in 0..1701 {
1953 let mut doc = crate::Document::new();
1954 doc.add_text(
1955 a,
1956 match (i % 2 == 0, i % 3 == 0) {
1957 (true, true) => "alpha beta alpha",
1958 (true, false) => "alpha",
1959 (false, true) => "beta",
1960 _ => "padding",
1961 },
1962 );
1963 doc.add_text(b, if i % 5 == 0 { "alpha" } else { "padding" });
1964 builder.add_document(doc).unwrap();
1965 }
1966 let id = SegmentId::new();
1967 builder.build(&dir, id, None).await.unwrap();
1968 let mut reader = SegmentReader::open(&dir, id, schema, 16).await.unwrap();
1969 let queries = [
1970 (
1971 BooleanQuery::new()
1972 .should(TermQuery::text(a, "alpha"))
1973 .should(TermQuery::text(a, "beta")),
1974 (0..1701)
1975 .filter(|i| i % 2 == 0 || i % 3 == 0)
1976 .collect::<Vec<u32>>(),
1977 true,
1978 ),
1979 (
1980 BooleanQuery::new()
1981 .should(TermQuery::text(a, "alpha"))
1982 .should(TermQuery::text(a, "alpha")),
1983 (0..1701).filter(|i| i % 2 == 0).collect(),
1984 true,
1985 ),
1986 (
1987 BooleanQuery::new()
1988 .should(TermQuery::text(a, "alpha"))
1989 .should(TermQuery::text(b, "alpha")),
1990 (0..1701).filter(|i| i % 2 == 0 || i % 5 == 0).collect(),
1991 true,
1992 ),
1993 (
1994 BooleanQuery::new()
1995 .should(TermQuery::text(a, "alpha"))
1996 .should(TermQuery::text(a, "missing")),
1997 (0..1701).filter(|i| i % 2 == 0).collect(),
1998 false,
1999 ),
2000 (
2001 BooleanQuery::new()
2002 .should(TermQuery::text(a, "missing"))
2003 .should(TermQuery::text(b, "missing")),
2004 Vec::new(),
2005 false,
2006 ),
2007 ];
2008 for (query, expected, admitted) in &queries {
2009 let known = two_term_union_count(&reader, query, 0).await.unwrap();
2010 assert_eq!(known, admitted.then_some(expected.len() as u64));
2011 let mut count = CountCollector::new();
2012 collect_segment(&reader, query, &mut count).await.unwrap();
2013 assert_eq!(count.count(), expected.len() as u64);
2014 let mut ids = Ids(Vec::new());
2015 collect_segment(&reader, query, &mut ids).await.unwrap();
2016 assert_eq!(&ids.0, expected);
2017 }
2018 let mut alive = crate::query::DocBitset::all(1701);
2019 alive.clear(0);
2020 alive.clear(6);
2021 let deletion = crate::segment::deletion::write(&dir, SegmentId::new(), 1701, &alive)
2022 .await
2023 .unwrap();
2024 reader.load_deletions(&dir, deletion).await.unwrap();
2025 for (query, expected, _) in &queries {
2026 assert_eq!(two_term_union_count(&reader, query, 0).await.unwrap(), None);
2027 let expected = expected.iter().filter(|&&d| d != 0 && d != 6).count() as u64;
2028 let mut count = CountCollector::new();
2029 collect_segment(&reader, query, &mut count).await.unwrap();
2030 assert_eq!(count.count(), expected);
2031 }
2032 }
2033 }
2034
2035 #[tokio::test]
2036 async fn union_count_metadata_shortcut_declines_mapped_and_fast_only_fields() {
2037 use crate::query::{BooleanQuery, TermQuery};
2038 use crate::segment::{SegmentBuilder, SegmentBuilderConfig, SegmentId};
2039 use std::sync::Arc;
2040 for chunked in [false, true] {
2041 let dir = crate::RamDirectory::new();
2042 let mut schema = crate::SchemaBuilder::default();
2043 let field = schema.add_text_field("text", chunked, false);
2044 if chunked {
2045 schema.set_chunked(field, true);
2046 } else {
2047 schema.set_fast(field, true);
2048 }
2049 let schema = Arc::new(schema.build());
2050 let mut builder =
2051 SegmentBuilder::new(schema.clone(), SegmentBuilderConfig::default()).unwrap();
2052 for values in [vec!["alpha", "beta", "alpha"], vec!["alpha"], vec!["gamma"]] {
2053 let mut doc = crate::Document::new();
2054 if chunked {
2055 for value in values {
2056 doc.add_text(field, value);
2057 }
2058 } else {
2059 doc.add_text(field, values[0]);
2060 }
2061 builder.add_document(doc).unwrap();
2062 }
2063 let id = SegmentId::new();
2064 builder.build(&dir, id, None).await.unwrap();
2065 let reader = SegmentReader::open(&dir, id, schema, 16).await.unwrap();
2066 let query = BooleanQuery::new()
2067 .should(TermQuery::text(field, "alpha"))
2068 .should(TermQuery::text(field, "beta"));
2069 assert_eq!(
2070 two_term_union_count(&reader, &query, 0).await.unwrap(),
2071 None
2072 );
2073 let mut count = CountCollector::new();
2074 collect_segment(&reader, &query, &mut count).await.unwrap();
2075 assert_eq!(count.count(), 2);
2076 }
2077 }
2078
2079 #[tokio::test]
2080 async fn fast_only_unions_keep_column_matches_in_count_and_ranked_collection() {
2081 use crate::query::{BooleanQuery, TermQuery};
2082 use crate::segment::{SegmentBuilder, SegmentBuilderConfig, SegmentId};
2083 use std::sync::Arc;
2084 let dir = crate::RamDirectory::new();
2085 let mut schema = crate::SchemaBuilder::default();
2086 let field = schema.add_text_field("text", false, false);
2087 schema.set_fast(field, true);
2088 let schema = Arc::new(schema.build());
2089 let mut builder =
2090 SegmentBuilder::new(schema.clone(), SegmentBuilderConfig::default()).unwrap();
2091 for text in ["alpha", "alpha", "beta", "gamma"] {
2092 let mut doc = crate::Document::new();
2093 doc.add_text(field, text);
2094 builder.add_document(doc).unwrap();
2095 }
2096 let id = SegmentId::new();
2097 builder.build(&dir, id, None).await.unwrap();
2098 let reader = SegmentReader::open(&dir, id, schema, 16).await.unwrap();
2099 let mut single = CountCollector::new();
2100 collect_segment(&reader, &TermQuery::text(field, "alpha"), &mut single)
2101 .await
2102 .unwrap();
2103 assert_eq!(single.count(), 2);
2104 let query = BooleanQuery::new()
2105 .should(TermQuery::text(field, "alpha"))
2106 .should(TermQuery::text(field, "beta"));
2107 let mut count = CountCollector::new();
2108 collect_segment(&reader, &query, &mut count).await.unwrap();
2109 assert_eq!(count.count(), 3);
2110 let mut top = TopKCollector::new(2);
2111 collect_segment_with_limit(&reader, &query, &mut top, 2)
2112 .await
2113 .unwrap();
2114 let hits = top.into_sorted_results();
2115 assert_eq!(
2116 hits.iter().map(|hit| hit.doc_id).collect::<Vec<_>>(),
2117 [0, 1]
2118 );
2119 assert!(hits.iter().all(|hit| hit.score == 1.0));
2120 #[cfg(feature = "sync")]
2121 {
2122 let mut top = TopKCollector::new(2);
2123 collect_segment_with_limit_sync(&reader, &query, &mut top, 2).unwrap();
2124 assert_eq!(top.into_sorted_results(), hits);
2125 }
2126 }
2127
2128 #[tokio::test]
2129 async fn separate_ranked_counts_preserve_ties_nested_collectors_and_existing_hits() {
2130 use crate::query::{BooleanQuery, TermQuery};
2131 use crate::segment::{SegmentBuilder, SegmentBuilderConfig, SegmentId};
2132 use crate::structures::postings::PostingCodec;
2133 use std::sync::Arc;
2134 struct Exhaustive<C>(C);
2135 impl<C: Collector> Collector for Exhaustive<C> {
2136 fn collect(&mut self, doc: u32, score: f32, positions: &[(u32, Vec<ScoredPosition>)]) {
2137 self.0.collect(doc, score, positions);
2138 }
2139 fn needs_scores(&self) -> bool {
2140 self.0.needs_scores()
2141 }
2142 fn needs_positions(&self) -> bool {
2143 self.0.needs_positions()
2144 }
2145 }
2146 fn hits(top: TopKCollector) -> Vec<(u32, u32)> {
2147 top.into_sorted_results()
2148 .into_iter()
2149 .map(|hit| (hit.doc_id, hit.score.to_bits()))
2150 .collect()
2151 }
2152 for codec in [
2153 PostingCodec::Rounded,
2154 PostingCodec::Packed,
2155 PostingCodec::Pfor,
2156 PostingCodec::Simd4x,
2157 ] {
2158 let dir = crate::RamDirectory::new();
2159 let mut schema = crate::SchemaBuilder::default();
2160 let field = schema.add_text_field("text", true, false);
2161 let schema = Arc::new(schema.build());
2162 let mut builder = SegmentBuilder::new(
2163 schema.clone(),
2164 SegmentBuilderConfig {
2165 posting_codec: codec,
2166 ..Default::default()
2167 },
2168 )
2169 .unwrap();
2170 for i in 0..24001 {
2171 let text = match i % 4 {
2172 0 => "alpha beta alpha",
2173 1 => "alpha",
2174 2 => "beta filler filler",
2175 _ => "gamma",
2176 };
2177 let mut doc = crate::Document::new();
2178 doc.add_text(field, text);
2179 builder.add_document(doc).unwrap();
2180 }
2181 let id = SegmentId::new();
2182 builder.build(&dir, id, None).await.unwrap();
2183 let reader = SegmentReader::open(&dir, id, schema, 16).await.unwrap();
2184 let queries: Vec<Box<dyn Query>> = vec![
2185 Box::new(super::super::BoostQuery::new(
2186 BooleanQuery::new()
2187 .must(TermQuery::text(field, "alpha"))
2188 .must(TermQuery::text(field, "beta")),
2189 1.0,
2190 )),
2191 Box::new(super::super::FilteredQuery::new(
2192 Arc::new(
2193 BooleanQuery::new()
2194 .must(TermQuery::text(field, "alpha"))
2195 .must(TermQuery::text(field, "beta")),
2196 ),
2197 vec![Arc::new(TermQuery::text(field, "beta"))],
2198 )),
2199 Box::new(
2200 BooleanQuery::new()
2201 .must(TermQuery::text(field, "alpha"))
2202 .must(TermQuery::text(field, "beta")),
2203 ),
2204 Box::new(
2205 BooleanQuery::new()
2206 .must(TermQuery::text(field, "beta"))
2207 .must(TermQuery::text(field, "alpha")),
2208 ),
2209 Box::new(
2210 BooleanQuery::new()
2211 .must(TermQuery::text(field, "alpha"))
2212 .must(TermQuery::text(field, "alpha")),
2213 ),
2214 Box::new(
2215 BooleanQuery::new()
2216 .must(TermQuery::text(field, "alpha"))
2217 .must(TermQuery::text(field, "missing")),
2218 ),
2219 Box::new(
2220 BooleanQuery::new()
2221 .must(TermQuery::text(field, "alpha"))
2222 .must(TermQuery::text(field, "beta"))
2223 .must(TermQuery::text(field, "gamma")),
2224 ),
2225 Box::new(
2226 BooleanQuery::new()
2227 .must(
2228 BooleanQuery::new()
2229 .must(TermQuery::text(field, "alpha"))
2230 .must(TermQuery::text(field, "beta")),
2231 )
2232 .should(TermQuery::text(field, "gamma")),
2233 ),
2234 Box::new(TermQuery::text(field, "alpha")),
2235 Box::new(
2236 BooleanQuery::new()
2237 .should(TermQuery::text(field, "alpha"))
2238 .should(TermQuery::text(field, "beta")),
2239 ),
2240 Box::new(
2241 BooleanQuery::new()
2242 .should(TermQuery::text(field, "alpha"))
2243 .should(TermQuery::text(field, "alpha")),
2244 ),
2245 Box::new(
2246 BooleanQuery::new()
2247 .should(TermQuery::text(field, "alpha"))
2248 .should(TermQuery::text(field, "missing")),
2249 ),
2250 Box::new(TermQuery::text(field, "missing")),
2251 Box::new(
2252 BooleanQuery::new()
2253 .must(TermQuery::text(field, "alpha"))
2254 .should(TermQuery::text(field, "beta")),
2255 ),
2256 Box::new(
2257 BooleanQuery::new()
2258 .must(TermQuery::text(field, "alpha"))
2259 .should(TermQuery::text(field, "alpha")),
2260 ),
2261 Box::new(
2262 BooleanQuery::new()
2263 .must(TermQuery::text(field, "alpha"))
2264 .should(TermQuery::text(field, "missing")),
2265 ),
2266 Box::new(
2267 BooleanQuery::new()
2268 .must(TermQuery::text(field, "alpha"))
2269 .should(TermQuery::text(field, "beta"))
2270 .should(TermQuery::text(field, "gamma")),
2271 ),
2272 ];
2273 let required = BooleanQuery::new()
2274 .must(TermQuery::text(field, "alpha"))
2275 .should(TermQuery::text(field, "beta"));
2276 assert_eq!(
2277 ranked_exact_count(&reader, &required, 17).await.unwrap(),
2278 Some(12001)
2279 );
2280 assert_eq!(
2281 ranked_exact_count(&reader, &required, 5000).await.unwrap(),
2282 None
2283 );
2284 let conjunction = BooleanQuery::new()
2285 .must(TermQuery::text(field, "alpha"))
2286 .must(TermQuery::text(field, "beta"));
2287 let counted = conjunction
2288 .scorer_with_options(
2289 &reader,
2290 usize::MAX / 2,
2291 super::super::ScorerOptions {
2292 complete_text_matches: true,
2293 ranked_count_limit: Some(17),
2294 ..Default::default()
2295 },
2296 )
2297 .await
2298 .unwrap();
2299 assert_eq!(counted.exact_ranked_count(), Some(6001));
2300 for deadline in [
2301 std::time::Instant::now(),
2302 std::time::Instant::now() + std::time::Duration::from_secs(60),
2303 ] {
2304 let bounded = conjunction
2305 .scorer_with_options(
2306 &reader,
2307 usize::MAX / 2,
2308 super::super::ScorerOptions {
2309 complete_text_matches: true,
2310 ranked_count_limit: Some(17),
2311 shared_threshold: Some(
2312 super::super::SharedThreshold::for_limit(17)
2313 .with_deadline(Some(deadline)),
2314 ),
2315 ..Default::default()
2316 },
2317 )
2318 .await
2319 .unwrap();
2320 assert_eq!(
2321 bounded.exact_ranked_count(),
2322 None,
2323 "a budgeted stream cannot promise a complete count"
2324 );
2325 }
2326
2327 for query in queries {
2328 for k in [0, 1, 17, 5000] {
2329 let mut actual = TopKCollector::new(k);
2330 let mut expected = TopKCollector::new(k);
2331 let mut a_count = CountCollector::new();
2332 let mut e_count = CountCollector::new();
2333 actual.collect(90000, 1e6, &[]);
2334 expected.collect(90000, 1e6, &[]);
2335 a_count.collect_count(9);
2336 e_count.collect_count(9);
2337 collect_segment(&reader, query.as_ref(), &mut (&mut actual, &mut a_count))
2338 .await
2339 .unwrap();
2340 collect_segment(
2341 &reader,
2342 query.as_ref(),
2343 &mut Exhaustive((&mut expected, &mut e_count)),
2344 )
2345 .await
2346 .unwrap();
2347 assert_eq!(a_count.count(), e_count.count(), "{codec:?} k={k}");
2348 assert_eq!(actual.total_seen(), expected.total_seen());
2349 assert_eq!(hits(actual), hits(expected), "{codec:?} k={k}");
2350 }
2351 }
2352 let query = BooleanQuery::new()
2353 .should(TermQuery::text(field, "alpha"))
2354 .should(TermQuery::text(field, "beta"));
2355 let mut small = TopKCollector::new(3);
2356 let mut large = TopKCollector::new(29);
2357 let mut count = CountCollector::new();
2358 let mut nested = (&mut small, &mut count);
2359 let mut tuple = (&mut nested, &mut large);
2360 assert_eq!(tuple.ranked_count_limit(), Some(29));
2361 collect_segment(&reader, &query, &mut tuple).await.unwrap();
2362 let mut expected = TopKCollector::new(29);
2363 let mut ignored_count = CountCollector::new();
2364 collect_segment(
2365 &reader,
2366 &query,
2367 &mut Exhaustive((&mut expected, &mut ignored_count)),
2368 )
2369 .await
2370 .unwrap();
2371 let expected = hits(expected);
2372 assert_eq!(hits(small), expected[..3]);
2373 assert_eq!(hits(large), expected);
2374 assert_eq!(count.count(), 18001);
2375 assert_eq!(TopKCollector::with_positions(3).ranked_count_limit(), None);
2376 }
2377 }
2378
2379 #[test]
2380 fn mapped_ranked_handoff_translates_before_truncating_stable_ties() {
2381 use crate::directories::OwnedBytes;
2382 use crate::query::docset::DocSet;
2383 use crate::segment::chunk_map::{ChunkMapBuilder, read_chunk_maps, write_chunk_maps};
2384 let mut builder = ChunkMapBuilder::default();
2385 builder.set_document_units(true);
2386 for doc in [3, 2, 0, 1] {
2387 builder.push(doc, 0, 1).unwrap();
2388 }
2389 let mut bytes = Vec::new();
2390 write_chunk_maps(&mut bytes, &[(0, &builder)], &[]).unwrap();
2391 let maps = read_chunk_maps(OwnedBytes::new(bytes)).unwrap();
2392 let map = &maps.chunk_maps[&0];
2393 for limit in [0, 1, 2, 4, 8] {
2394 for positions in [false, true] {
2395 let ranked = || {
2396 super::super::planner::TopKResultScorer::new(
2397 (0..4)
2398 .map(|doc_id| super::super::ScoredDoc {
2399 doc_id,
2400 score: 1.0,
2401 ordinal: 0,
2402 })
2403 .collect(),
2404 )
2405 };
2406 let mut scorer = ranked();
2407 let actual =
2408 top_k_from_mapped_scorer(&mut scorer, limit, positions, None, Some(map));
2409 let mut expected = TopKCollector::new(limit);
2410 drive_collected(&mut ranked(), &mut expected, Some(map), None);
2411 assert_eq!(actual, expected.into_results_with_count());
2412 assert_eq!(scorer.doc(), TERMINATED);
2414 }
2415 }
2416 }
2417
2418 #[tokio::test]
2419 async fn phrase_block_pruning_preserves_late_winners_score_bits_and_exact_counts() {
2420 use crate::query::PhraseQuery;
2421 use crate::segment::{SegmentBuilder, SegmentBuilderConfig, SegmentId};
2422 use crate::structures::PostingCodec;
2423 for (codec, posting_ratio_bounds, posting_impact_bounds) in [
2424 PostingCodec::Rounded,
2425 PostingCodec::Packed,
2426 PostingCodec::Pfor,
2427 PostingCodec::Simd4x,
2428 ]
2429 .into_iter()
2430 .flat_map(|codec| {
2431 [
2432 (codec, false, false),
2433 (codec, true, false),
2434 (codec, true, true),
2435 ]
2436 }) {
2437 let dir = crate::RamDirectory::new();
2438 let mut schema = crate::SchemaBuilder::default();
2439 let field = schema.add_text_field("body", true, false);
2440 schema.set_positions(field, crate::dsl::PositionMode::TokenPosition);
2441 let schema = std::sync::Arc::new(schema.build());
2442 let mut builder = SegmentBuilder::new(
2443 schema.clone(),
2444 SegmentBuilderConfig {
2445 posting_codec: codec,
2446 posting_ratio_bounds,
2447 posting_impact_bounds,
2448 ..Default::default()
2449 },
2450 )
2451 .unwrap();
2452 for id in 0..2053 {
2453 let text = match id {
2456 0..=3 => "alpha beta".to_owned(),
2457 2048.. => "alpha beta alpha beta".to_owned(),
2458 _ if id % 7 == 0 => format!("alpha {} beta", "filler ".repeat(80)),
2459 _ => format!("alpha beta {}", "filler ".repeat(80)),
2460 };
2461 let mut doc = crate::Document::new();
2462 doc.add_text(field, text);
2463 builder.add_document(doc).unwrap();
2464 }
2465 let id = SegmentId::new();
2466 builder.build(&dir, id, None).await.unwrap();
2467 let reader = SegmentReader::open(&dir, id, schema, 16).await.unwrap();
2468 for slop in [0, 4] {
2469 let query = PhraseQuery::new(field, vec![b"alpha".to_vec(), b"beta".to_vec()])
2470 .with_slop(slop);
2471 for k in [1, 3, 10, 100] {
2472 let mut complete = query.scorer(&reader, 0).await.unwrap();
2473 let mut top = TopKCollector::new(k);
2474 let mut count = CountCollector::new();
2475 drive_scorer(complete.as_mut(), &mut (&mut top, &mut count));
2476 let expected = top.into_sorted_results();
2477 let (actual, _) = search_segment_with_count(&reader, &query, k).await.unwrap();
2478 assert_eq!(actual, expected, "{codec:?}, slop={slop}, k={k}");
2479 #[cfg(feature = "sync")]
2480 assert_eq!(
2481 search_segment_with_count_sync(&reader, &query, k)
2482 .unwrap()
2483 .0,
2484 expected
2485 );
2486 let mut exact = CountCollector::new();
2487 collect_segment(&reader, &query, &mut exact).await.unwrap();
2488 assert_eq!(exact.count(), count.count());
2489 assert!(exact.count() > 1700);
2490 }
2491 }
2492 }
2493 }
2494
2495 #[test]
2496 fn ranked_bounds_skip_confirmation_but_preserve_late_winners_ties_and_complete_counts() {
2497 for positions in [false, true] {
2498 let mut ranked = BoundedCandidates::fixture();
2499 let (actual, seen) = top_k_from_scorer(&mut ranked, 1, positions, None);
2500 assert_eq!(actual[0].doc_id, 5);
2501 assert_eq!(seen, 3);
2502 assert_eq!(*ranked.confirmations.lock().unwrap(), [0, 1, 3, 5]);
2503 assert_eq!(ranked.scores.load(std::sync::atomic::Ordering::Relaxed), 3);
2504 let mut complete = BoundedCandidates::fixture();
2505 let mut top = if positions {
2506 TopKCollector::with_positions(1)
2507 } else {
2508 TopKCollector::new(1)
2509 };
2510 let mut count = CountCollector::new();
2511 drive_scorer(&mut complete, &mut (&mut top, &mut count));
2512 assert_eq!(actual, top.into_sorted_results());
2513 assert_eq!(count.count(), 5);
2514 assert_eq!(
2515 complete.bounds.load(std::sync::atomic::Ordering::Relaxed),
2516 0
2517 );
2518 let mut count_only = BoundedCandidates::fixture();
2519 let mut count = CountCollector::new();
2520 drive_scorer(&mut count_only, &mut count);
2521 assert_eq!(count.count(), 5);
2522 assert_eq!(
2523 count_only.bounds.load(std::sync::atomic::Ordering::Relaxed),
2524 0
2525 );
2526 assert_eq!(
2527 count_only.scores.load(std::sync::atomic::Ordering::Relaxed),
2528 0
2529 );
2530 }
2531 }
2532
2533 #[test]
2534 fn deadline_during_candidate_confirmation_prevents_scoring_and_collection() {
2535 let budget = super::super::SharedThreshold::for_limit(1).with_deadline(Some(
2536 std::time::Instant::now() + std::time::Duration::from_millis(20),
2537 ));
2538 let mut scorer = BoundedCandidates::fixture();
2539 scorer.pause_confirmation = true;
2540 let (hits, seen) = top_k_from_scorer(&mut scorer, 1, true, Some(&budget));
2541 assert!(hits.is_empty());
2542 assert_eq!(seen, 0);
2543 assert_eq!(scorer.scores.load(std::sync::atomic::Ordering::Relaxed), 0);
2544 assert!(budget.truncated());
2545 }
2546
2547 #[test]
2548 fn eligibility_wrappers_keep_exact_confirmation_and_do_not_inherit_leaf_bounds() {
2549 let candidates = BoundedCandidates::fixture();
2550 let bounds = candidates.bounds.clone();
2551 let mut alive = super::super::DocBitset::new(6);
2552 for doc in [2, 3, 4] {
2553 alive.set(doc);
2554 }
2555 let mut filtered = super::super::filtered::filtered(
2556 Box::new(candidates),
2557 Some(std::sync::Arc::new(alive)),
2558 );
2559 assert!(!filtered.supports_candidate_score_bounds());
2560 let (hits, seen) = top_k_from_scorer(filtered.as_mut(), 1, false, None);
2561 assert_eq!(hits[0].doc_id, 3);
2562 assert_eq!(seen, 3);
2563 assert_eq!(bounds.load(std::sync::atomic::Ordering::Relaxed), 0);
2564 }
2565
2566 #[test]
2567 fn scored_blocks_preserve_total_float_order_ties_and_exact_counts() {
2568 let special = [
2569 f32::NEG_INFINITY,
2570 -1.0,
2571 -0.0,
2572 0.0,
2573 1.0,
2574 f32::INFINITY,
2575 f32::from_bits(0xffc00001),
2576 f32::from_bits(0x7fc00001),
2577 f32::from_bits(0x7fc00002),
2578 ];
2579 for k in [0, 1, 10, 63, 64, 65, 127, 1000] {
2580 let mut blocks = TopKCollector::new(k);
2581 let mut scalar = TopKCollector::new(k);
2582 let mut count = CountCollector::new();
2583 let mut count2 = CountCollector::new();
2584 let mut expected = 0;
2585 for (round, base) in [128, 0, 256, 64, 320, u32::MAX - 64]
2586 .into_iter()
2587 .enumerate()
2588 {
2589 let scores = std::array::from_fn(|i| special[(i + round * 3) % special.len()]);
2590 for bits in [0, u64::MAX, 1, 1u64 << 63, 0xaaaaf00f800102a8] {
2591 let mut tuple = (&mut blocks, &mut count, &mut count2);
2592 assert!(tuple.supports_score_blocks());
2593 tuple.collect_score_block(base, &scores, bits);
2594 for (i, &score) in scores.iter().enumerate() {
2595 if bits & (1u64 << i) != 0 {
2596 scalar.collect(base + i as u32, score, &[]);
2597 expected += 1;
2598 }
2599 }
2600 }
2601 }
2602 assert_eq!(count.count(), expected);
2603 assert_eq!(count2.count(), expected);
2604 assert_eq!(blocks.total_seen(), expected as u32);
2605 assert_eq!(blocks.into_sorted_results(), scalar.into_sorted_results());
2606 }
2607 let mut top = TopKCollector::new(0);
2608 top.total_seen = u32::MAX - 5;
2609 top.collect_score_block(0, &[1.0; 64], u64::MAX);
2610 assert_eq!(top.total_seen(), u32::MAX);
2611 assert!(!TopKCollector::with_positions(10).supports_score_blocks());
2612 }
2613
2614 #[test]
2615 fn custom_collectors_keep_interleaved_callbacks_inside_score_windows() {
2616 use super::super::{DocSet, Scorer};
2617 use std::cell::RefCell;
2618 struct Records<'a> {
2619 child: u8,
2620 calls: &'a RefCell<Vec<(DocId, u8, u32)>>,
2621 }
2622 impl Collector for Records<'_> {
2623 fn collect(
2624 &mut self,
2625 doc: DocId,
2626 score: Score,
2627 positions: &[(u32, Vec<ScoredPosition>)],
2628 ) {
2629 assert!(positions.is_empty());
2630 self.calls
2631 .borrow_mut()
2632 .push((doc, self.child, score.to_bits()));
2633 }
2634 fn collect_score_block(&mut self, _: DocId, _: &[Score; 64], _: u64) {
2635 panic!("custom collector has not opted in");
2636 }
2637 }
2638 struct Window(bool);
2639 impl DocSet for Window {
2640 fn doc(&self) -> DocId {
2641 if self.0 { TERMINATED } else { 0 }
2642 }
2643 fn advance(&mut self) -> DocId {
2644 panic!("use score window")
2645 }
2646 fn size_hint(&self) -> u32 {
2647 4
2648 }
2649 }
2650 impl Scorer for Window {
2651 fn score(&self) -> Score {
2652 panic!("use score window")
2653 }
2654 fn supports_score_windows(&self) -> bool {
2655 true
2656 }
2657 fn fill_score_window(
2658 &mut self,
2659 _: DocId,
2660 scores: &mut [Score; super::super::docset::DOC_WINDOW_SIZE as usize],
2661 bits: &mut super::super::docset::DocWindow,
2662 ) {
2663 bits.fill(0);
2664 bits[0] = 3;
2665 bits[1] = 1 << 63;
2666 bits[63] = 1 << 63;
2667 for doc in [0, 1, 127, 4095] {
2668 scores[doc] = doc as f32;
2669 }
2670 self.0 = true;
2671 }
2672 }
2673 let calls = RefCell::new(Vec::new());
2674 let mut a = Records {
2675 child: 0,
2676 calls: &calls,
2677 };
2678 let mut b = Records {
2679 child: 1,
2680 calls: &calls,
2681 };
2682 let mut top = TopKCollector::new(2);
2683 let mut collector = (&mut a, &mut b, &mut top);
2684 assert!(!collector.supports_score_blocks());
2685 drive_scorer(&mut Window(false), &mut collector);
2686 let expected: Vec<_> = [0, 1, 127, 4095]
2687 .into_iter()
2688 .flat_map(|doc| {
2689 [
2690 (doc, 0, (doc as f32).to_bits()),
2691 (doc, 1, (doc as f32).to_bits()),
2692 ]
2693 })
2694 .collect();
2695 assert_eq!(*calls.borrow(), expected);
2696 assert_eq!(top.total_seen(), 4);
2697 assert_eq!(
2698 top.into_sorted_results()
2699 .iter()
2700 .map(|x| x.doc_id)
2701 .collect::<Vec<_>>(),
2702 [4095, 127]
2703 );
2704 let mut top = TopKCollector::new(2);
2705 let mut count = CountCollector::new();
2706 drive_scorer(&mut Window(false), &mut (&mut top, &mut count));
2707 assert_eq!(count.count(), 4);
2708 assert_eq!(top.total_seen(), 4);
2709 assert_eq!(
2710 top.into_sorted_results()
2711 .iter()
2712 .map(|x| x.doc_id)
2713 .collect::<Vec<_>>(),
2714 [4095, 127]
2715 );
2716 }
2717 use std::sync::Arc;
2718 use std::sync::atomic::{AtomicUsize, Ordering as AtomicOrdering};
2719
2720 #[test]
2721 fn expired_score_batch_is_discarded_before_any_collection() {
2722 use super::super::{DocSet, Scorer, SharedThreshold};
2723 struct ExpiresInScoreBatch {
2724 deadline: std::time::Instant,
2725 visited: bool,
2726 }
2727 impl DocSet for ExpiresInScoreBatch {
2728 fn doc(&self) -> u32 {
2729 if self.visited { TERMINATED } else { 0 }
2730 }
2731 fn advance(&mut self) -> u32 {
2732 panic!("must use batch")
2733 }
2734 fn size_hint(&self) -> u32 {
2735 4096
2736 }
2737 }
2738 impl Scorer for ExpiresInScoreBatch {
2739 fn supports_score_batches(&self) -> bool {
2740 true
2741 }
2742 fn fill_score_batch(
2743 &mut self,
2744 docs: &mut super::super::docset::DocBatch,
2745 scores: &mut super::super::ScoreBatch,
2746 ) -> usize {
2747 docs[0] = 0;
2748 scores[0] = 5.0;
2749 self.visited = true;
2750 std::thread::sleep(
2751 self.deadline
2752 .saturating_duration_since(std::time::Instant::now())
2753 + std::time::Duration::from_millis(1),
2754 );
2755 1
2756 }
2757 fn score(&self) -> f32 {
2758 panic!("count must not score")
2759 }
2760 }
2761 let deadline = std::time::Instant::now() + std::time::Duration::from_millis(100);
2762 let budget = SharedThreshold::new().with_deadline(Some(deadline));
2763 let mut scorer = ExpiresInScoreBatch {
2764 deadline,
2765 visited: false,
2766 };
2767 let mut count = CountCollector::new();
2768 let mut top = TopKCollector::new(10);
2769 drive_scorer_budgeted(&mut scorer, &mut (&mut top, &mut count), Some(&budget));
2770 assert!(scorer.visited);
2771 assert!(budget.truncated());
2772 assert_eq!(count.count(), 0);
2773 assert_eq!(top.total_seen(), 0);
2774 assert!(top.into_sorted_results().is_empty());
2775 }
2776
2777 #[test]
2778 fn expired_document_batch_is_discarded_before_count_collection() {
2779 use super::super::{DocSet, Scorer, SharedThreshold};
2780 struct ExpiresInBatch {
2781 deadline: std::time::Instant,
2782 visited: bool,
2783 }
2784 impl DocSet for ExpiresInBatch {
2785 fn doc(&self) -> u32 {
2786 if self.visited { TERMINATED } else { 0 }
2787 }
2788 fn advance(&mut self) -> u32 {
2789 panic!("must use batch")
2790 }
2791 fn size_hint(&self) -> u32 {
2792 4096
2793 }
2794 fn supports_doc_batches(&self) -> bool {
2795 true
2796 }
2797 fn fill_doc_batch(&mut self, docs: &mut super::super::docset::DocBatch) -> usize {
2798 docs[0] = 0;
2799 self.visited = true;
2800 std::thread::sleep(
2801 self.deadline
2802 .saturating_duration_since(std::time::Instant::now())
2803 + std::time::Duration::from_millis(1),
2804 );
2805 1
2806 }
2807 }
2808 impl Scorer for ExpiresInBatch {
2809 fn score(&self) -> f32 {
2810 panic!("count must not score")
2811 }
2812 }
2813 let deadline = std::time::Instant::now() + std::time::Duration::from_millis(100);
2814 let budget = SharedThreshold::new().with_deadline(Some(deadline));
2815 let mut scorer = ExpiresInBatch {
2816 deadline,
2817 visited: false,
2818 };
2819 let mut count = CountCollector::new();
2820 drive_scorer_budgeted(&mut scorer, &mut count, Some(&budget));
2821 assert!(scorer.visited);
2822 assert!(budget.truncated());
2823 assert_eq!(count.count(), 0);
2824 }
2825
2826 #[test]
2827 fn expired_document_window_is_discarded_before_count_collection() {
2828 use super::super::{DocSet, Scorer, SharedThreshold};
2829 struct ExpiresInWindow {
2830 deadline: std::time::Instant,
2831 visited: bool,
2832 }
2833 impl DocSet for ExpiresInWindow {
2834 fn doc(&self) -> u32 {
2835 if self.visited { TERMINATED } else { 0 }
2836 }
2837 fn advance(&mut self) -> u32 {
2838 panic!("must use window")
2839 }
2840 fn size_hint(&self) -> u32 {
2841 4096
2842 }
2843 fn supports_doc_windows(&self) -> bool {
2844 true
2845 }
2846 fn fill_doc_window(&mut self, _base: u32, bits: &mut super::super::docset::DocWindow) {
2847 bits.fill(u64::MAX);
2848 self.visited = true;
2849 std::thread::sleep(
2850 self.deadline
2851 .saturating_duration_since(std::time::Instant::now())
2852 + std::time::Duration::from_millis(1),
2853 );
2854 }
2855 }
2856 impl Scorer for ExpiresInWindow {
2857 fn score(&self) -> f32 {
2858 panic!("count must not score")
2859 }
2860 }
2861 let deadline = std::time::Instant::now() + std::time::Duration::from_millis(100);
2862 let budget = SharedThreshold::new().with_deadline(Some(deadline));
2863 let mut scorer = ExpiresInWindow {
2864 deadline,
2865 visited: false,
2866 };
2867 let mut count = CountCollector::new();
2868 drive_scorer_budgeted(&mut scorer, &mut count, Some(&budget));
2869 assert!(scorer.visited);
2870 assert!(budget.truncated());
2871 assert_eq!(count.count(), 0);
2872 }
2873
2874 #[test]
2875 fn expired_score_window_is_discarded_before_rank_and_count_collection() {
2876 use super::super::{DocSet, Scorer, SharedThreshold};
2877 struct ExpiresInWindow {
2878 deadline: std::time::Instant,
2879 visited: bool,
2880 }
2881 impl DocSet for ExpiresInWindow {
2882 fn doc(&self) -> u32 {
2883 if self.visited { TERMINATED } else { 0 }
2884 }
2885 fn advance(&mut self) -> u32 {
2886 panic!("must use score window")
2887 }
2888 fn size_hint(&self) -> u32 {
2889 4096
2890 }
2891 }
2892 impl Scorer for ExpiresInWindow {
2893 fn score(&self) -> f32 {
2894 panic!("must use score window")
2895 }
2896 fn supports_score_windows(&self) -> bool {
2897 true
2898 }
2899 fn fill_score_window(
2900 &mut self,
2901 _base: DocId,
2902 scores: &mut [Score; super::super::docset::DOC_WINDOW_SIZE as usize],
2903 bits: &mut super::super::docset::DocWindow,
2904 ) {
2905 bits.fill(u64::MAX);
2906 scores.fill(1.0);
2907 self.visited = true;
2908 std::thread::sleep(
2909 self.deadline
2910 .saturating_duration_since(std::time::Instant::now())
2911 + std::time::Duration::from_millis(1),
2912 );
2913 }
2914 }
2915 let deadline = std::time::Instant::now() + std::time::Duration::from_millis(20);
2916 let budget = SharedThreshold::new().with_deadline(Some(deadline));
2917 let mut scorer = ExpiresInWindow {
2918 deadline,
2919 visited: false,
2920 };
2921 let mut top = TopKCollector::new(10);
2922 let mut count = CountCollector::new();
2923 drive_scorer_budgeted(&mut scorer, &mut (&mut top, &mut count), Some(&budget));
2924 assert!(scorer.visited);
2925 assert!(budget.truncated());
2926 assert_eq!(count.count(), 0);
2927 assert!(top.into_sorted_results().is_empty());
2928 }
2929
2930 #[derive(Default)]
2931 struct OwnedPositionCollector {
2932 owned_calls: usize,
2933 borrowed_calls: usize,
2934 positions: super::super::MatchedPositions,
2935 }
2936
2937 impl Collector for OwnedPositionCollector {
2938 fn collect(
2939 &mut self,
2940 _doc_id: DocId,
2941 _score: Score,
2942 positions: &[(u32, Vec<ScoredPosition>)],
2943 ) {
2944 self.borrowed_calls += 1;
2945 self.positions = positions.to_vec();
2946 }
2947
2948 fn collect_owned(
2949 &mut self,
2950 _doc_id: DocId,
2951 _score: Score,
2952 positions: super::super::MatchedPositions,
2953 ) {
2954 self.owned_calls += 1;
2955 self.positions = positions;
2956 }
2957
2958 fn needs_positions(&self) -> bool {
2959 true
2960 }
2961 }
2962
2963 struct PositionCountingScorer {
2964 index: usize,
2965 position_calls: Arc<AtomicUsize>,
2966 }
2967
2968 impl super::super::DocSet for PositionCountingScorer {
2969 fn doc(&self) -> DocId {
2970 if self.index < 3 {
2971 self.index as DocId
2972 } else {
2973 TERMINATED
2974 }
2975 }
2976
2977 fn advance(&mut self) -> DocId {
2978 self.index += 1;
2979 self.doc()
2980 }
2981
2982 fn seek(&mut self, target: DocId) -> DocId {
2983 self.index = target.min(3) as usize;
2984 self.doc()
2985 }
2986
2987 fn size_hint(&self) -> u32 {
2988 3u32.saturating_sub(self.index as u32)
2989 }
2990 }
2991
2992 impl super::super::Scorer for PositionCountingScorer {
2993 fn score(&self) -> Score {
2994 [10.0, 1.0, 2.0][self.index]
2995 }
2996
2997 fn matched_positions(&self) -> Option<super::super::MatchedPositions> {
2998 self.position_calls.fetch_add(1, AtomicOrdering::Relaxed);
2999 Some(vec![(7, vec![ScoredPosition::new(self.index as u32, 1.0)])])
3000 }
3001 }
3002
3003 struct ScoreCountingScorer {
3004 doc: u32,
3005 calls: std::sync::Arc<AtomicUsize>,
3006 }
3007
3008 impl super::super::DocSet for ScoreCountingScorer {
3009 fn doc(&self) -> DocId {
3010 if self.doc < 3 { self.doc } else { TERMINATED }
3011 }
3012 fn advance(&mut self) -> DocId {
3013 self.doc += 1;
3014 self.doc()
3015 }
3016 fn size_hint(&self) -> u32 {
3017 3u32.saturating_sub(self.doc)
3018 }
3019 }
3020
3021 impl super::super::Scorer for ScoreCountingScorer {
3022 fn score(&self) -> Score {
3023 self.calls.fetch_add(1, AtomicOrdering::Relaxed);
3024 self.doc as f32 + 1.0
3025 }
3026 }
3027
3028 #[test]
3029 fn count_only_collection_skips_scores_but_ranked_tuples_compute_them() {
3030 let calls = std::sync::Arc::new(AtomicUsize::new(0));
3031 let mut scorer = ScoreCountingScorer {
3032 doc: 0,
3033 calls: calls.clone(),
3034 };
3035 let mut count = CountCollector::new();
3036 drive_scorer(&mut scorer, &mut count);
3037 assert_eq!(count.count(), 3);
3038 assert_eq!(calls.load(AtomicOrdering::Relaxed), 0);
3039
3040 let mut scorer = ScoreCountingScorer {
3041 doc: 0,
3042 calls: calls.clone(),
3043 };
3044 let mut count = CountCollector::new();
3045 let mut top = TopKCollector::new(1);
3046 drive_scorer(&mut scorer, &mut (&mut count, &mut top));
3047 assert_eq!(count.count(), 3);
3048 assert_eq!(calls.load(AtomicOrdering::Relaxed), 3);
3049 let hits = top.into_sorted_results();
3050 assert_eq!((hits[0].doc_id, hits[0].score), (2, 3.0));
3051 }
3052
3053 #[test]
3054 fn test_top_k_collector() {
3055 let mut collector = TopKCollector::new(3);
3056
3057 collector.collect(0, 1.0, &[]);
3058 collector.collect(1, 3.0, &[]);
3059 collector.collect(2, 2.0, &[]);
3060 collector.collect(3, 4.0, &[]);
3061 collector.collect(4, 0.5, &[]);
3062
3063 let results = collector.into_sorted_results();
3064
3065 assert_eq!(results.len(), 3);
3066 assert_eq!(results[0].doc_id, 3); assert_eq!(results[1].doc_id, 1); assert_eq!(results[2].doc_id, 2); }
3070
3071 #[test]
3072 fn top_k_zero_retains_no_results() {
3073 let mut collector = TopKCollector::new(0);
3074 collector.collect(1, 1.0, &[]);
3075
3076 assert!(collector.into_sorted_results().is_empty());
3077 }
3078
3079 #[test]
3080 fn huge_top_k_does_not_trigger_a_huge_initial_allocation() {
3081 let collector = TopKCollector::new(usize::MAX);
3082
3083 let TopKHeap::Scores(heap) = collector.heap else {
3084 panic!("score-only constructor selected the position heap");
3085 };
3086 assert!(heap.capacity() <= MAX_INITIAL_TOP_K_CAPACITY);
3087 }
3088
3089 #[test]
3090 fn score_only_heap_entry_stays_compact() {
3091 assert_eq!(std::mem::size_of::<ScoreOnlyResult>(), 8);
3092 assert!(std::mem::size_of::<SearchResult>() >= 4 * std::mem::size_of::<ScoreOnlyResult>());
3093 }
3094
3095 #[test]
3096 fn top_k_replacement_preserves_score_and_doc_ties() {
3097 let mut collector = TopKCollector::new(3);
3098 for (doc_id, score) in [(9, 2.0), (8, 2.0), (7, 2.0), (6, 2.0), (1, 1.0)] {
3099 collector.collect(doc_id, score, &[]);
3100 }
3101
3102 let results = collector.into_sorted_results();
3103 assert_eq!(
3104 results
3105 .iter()
3106 .map(|result| (result.doc_id, result.score))
3107 .collect::<Vec<_>>(),
3108 vec![(6, 2.0), (7, 2.0), (8, 2.0)]
3109 );
3110 }
3111
3112 #[test]
3113 fn extract_ordinals_sorts_and_deduplicates_without_hashing() {
3114 let result = SearchResult {
3115 doc_id: 1,
3116 score: 1.0,
3117 segment_id: 0,
3118 positions: vec![
3119 (
3120 3,
3121 vec![
3122 ScoredPosition::new(5 << 20, 1.0),
3123 ScoredPosition::new(2 << 20, 1.0),
3124 ScoredPosition::new(5 << 20, 2.0),
3125 ],
3126 ),
3127 (
3128 7,
3129 vec![
3130 ScoredPosition::new(4, 1.0),
3131 ScoredPosition::new(1, 1.0),
3132 ScoredPosition::new(4, 2.0),
3133 ],
3134 ),
3135 ],
3136 };
3137
3138 let fields = result.extract_ordinals();
3139 assert_eq!(fields[0].ordinals, vec![2, 5]);
3140 assert_eq!(fields[1].ordinals, vec![1, 4]);
3141 }
3142
3143 #[test]
3144 fn positions_are_only_materialized_for_competitive_hits() {
3145 let calls = Arc::new(AtomicUsize::new(0));
3146 let mut scorer = PositionCountingScorer {
3147 index: 0,
3148 position_calls: Arc::clone(&calls),
3149 };
3150 let mut collector = TopKCollector::with_positions(1);
3151
3152 drive_scorer(&mut scorer, &mut collector);
3153
3154 assert_eq!(calls.load(AtomicOrdering::Relaxed), 1);
3155 assert_eq!(collector.total_seen(), 3);
3156 let results = collector.into_sorted_results();
3157 assert_eq!(results.len(), 1);
3158 assert_eq!(results[0].doc_id, 0);
3159 assert_eq!(results[0].positions[0].0, 7);
3160 }
3161
3162 #[test]
3163 fn tuple_moves_owned_positions_to_single_position_collector() {
3164 let mut positions = OwnedPositionCollector::default();
3165 let mut count = CountCollector::new();
3166 let input = vec![(7, vec![ScoredPosition::new(3, 1.0)])];
3167 let input_ptr = input[0].1.as_ptr();
3168
3169 (&mut positions, &mut count).collect_owned(11, 2.0, input);
3170
3171 assert_eq!(positions.owned_calls, 1);
3172 assert_eq!(positions.borrowed_calls, 0);
3173 assert_eq!(positions.positions[0].1.as_ptr(), input_ptr);
3174 assert_eq!(count.count(), 1);
3175 }
3176
3177 #[test]
3178 fn tuple_clones_for_all_but_final_position_collector() {
3179 let mut first = OwnedPositionCollector::default();
3180 let mut second = OwnedPositionCollector::default();
3181 let mut count = CountCollector::new();
3182 let input = vec![(7, vec![ScoredPosition::new(3, 1.0)])];
3183 let input_ptr = input[0].1.as_ptr();
3184
3185 (&mut first, &mut count, &mut second).collect_owned(11, 2.0, input);
3186
3187 assert_eq!((first.owned_calls, first.borrowed_calls), (1, 0));
3188 assert_eq!((second.owned_calls, second.borrowed_calls), (1, 0));
3189 assert_ne!(first.positions[0].1.as_ptr(), input_ptr);
3190 assert_eq!(second.positions[0].1.as_ptr(), input_ptr);
3191 assert_eq!(count.count(), 1);
3192 }
3193
3194 #[test]
3195 fn test_count_collector() {
3196 let mut collector = CountCollector::new();
3197
3198 collector.collect(0, 1.0, &[]);
3199 collector.collect(1, 2.0, &[]);
3200 collector.collect(2, 3.0, &[]);
3201
3202 assert_eq!(collector.count(), 3);
3203 }
3204
3205 #[test]
3206 fn test_multi_collector() {
3207 let mut top_k = TopKCollector::new(2);
3208 let mut count = CountCollector::new();
3209
3210 for (doc_id, score) in [(0, 1.0), (1, 3.0), (2, 2.0), (3, 4.0), (4, 0.5)] {
3212 top_k.collect(doc_id, score, &[]);
3213 count.collect(doc_id, score, &[]);
3214 }
3215
3216 assert_eq!(count.count(), 5);
3218
3219 let results = top_k.into_sorted_results();
3221 assert_eq!(results.len(), 2);
3222 assert_eq!(results[0].doc_id, 3); assert_eq!(results[1].doc_id, 1); }
3225}