1use std::cmp::Ordering;
10use std::collections::BinaryHeap;
11
12use log::{debug, warn};
13
14use crate::DocId;
15
16#[derive(Clone, Copy)]
18pub struct HeapEntry {
19 pub doc_id: DocId,
20 pub score: f32,
21 pub ordinal: u16,
22}
23
24impl PartialEq for HeapEntry {
25 fn eq(&self, other: &Self) -> bool {
26 self.score.to_bits() == other.score.to_bits()
27 && self.doc_id == other.doc_id
28 && self.ordinal == other.ordinal
29 }
30}
31
32impl Eq for HeapEntry {}
33
34impl Ord for HeapEntry {
35 fn cmp(&self, other: &Self) -> Ordering {
36 other
39 .score
40 .total_cmp(&self.score)
41 .then_with(|| self.doc_id.cmp(&other.doc_id))
42 .then_with(|| self.ordinal.cmp(&other.ordinal))
43 }
44}
45
46impl PartialOrd for HeapEntry {
47 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
48 Some(self.cmp(other))
49 }
50}
51
52pub struct ScoreCollector {
65 heap: BinaryHeap<HeapEntry>,
67 pub k: usize,
68 cached_threshold: f32,
71 real_len: usize,
74}
75
76impl ScoreCollector {
77 pub fn new(k: usize) -> Self {
79 let capacity = k.saturating_add(1).min(1_000_000);
81 Self {
82 heap: BinaryHeap::with_capacity(capacity),
83 k,
84 cached_threshold: 0.0,
85 real_len: 0,
86 }
87 }
88
89 #[inline]
91 pub fn threshold(&self) -> f32 {
92 self.cached_threshold
93 }
94
95 #[inline]
97 fn update_threshold(&mut self) {
98 self.cached_threshold = if self.heap.len() >= self.k {
99 self.heap.peek().map(|e| e.score).unwrap_or(0.0)
100 } else {
101 0.0
102 };
103 }
104
105 #[inline]
108 pub fn insert(&mut self, doc_id: DocId, score: f32) -> bool {
109 self.insert_with_ordinal(doc_id, score, 0)
110 }
111
112 #[inline]
115 pub fn insert_with_ordinal(&mut self, doc_id: DocId, score: f32, ordinal: u16) -> bool {
116 if self.k == 0 {
117 return false;
118 }
119 let entry = HeapEntry {
120 doc_id,
121 score,
122 ordinal,
123 };
124 if self.heap.len() < self.k {
125 self.heap.push(entry);
126 self.real_len += 1;
127 if self.heap.len() == self.k {
129 self.update_threshold();
130 }
131 true
132 } else if self.heap.peek().is_some_and(|worst| entry < *worst) {
133 self.heap.push(entry);
134 if self
135 .heap
136 .pop()
137 .is_some_and(|evicted| evicted.doc_id == u32::MAX)
138 {
139 self.real_len += 1;
140 }
141 self.update_threshold();
142 true
143 } else {
144 false
145 }
146 }
147
148 #[inline]
150 pub fn would_enter(&self, score: f32) -> bool {
151 self.heap.len() < self.k || score > self.cached_threshold
152 }
153
154 #[inline]
157 pub fn would_enter_candidate(&self, doc_id: DocId, score: f32, ordinal: u16) -> bool {
158 if self.k == 0 {
159 return false;
160 }
161 let entry = HeapEntry {
162 doc_id,
163 score,
164 ordinal,
165 };
166 self.heap.len() < self.k || self.heap.peek().is_some_and(|worst| entry < *worst)
167 }
168
169 #[inline]
171 pub fn len(&self) -> usize {
172 self.heap.len()
173 }
174
175 #[inline]
177 pub fn real_len(&self) -> usize {
178 self.real_len
179 }
180
181 #[inline]
183 pub fn is_empty(&self) -> bool {
184 self.heap.is_empty()
185 }
186
187 pub fn seed_threshold(&mut self, initial_threshold: f32) {
194 if initial_threshold <= 0.0
195 || self.k == 0
196 || (self.heap.len() >= self.k && initial_threshold <= self.cached_threshold)
197 {
198 return;
199 }
200
201 let sentinel = HeapEntry {
202 doc_id: u32::MAX,
203 score: initial_threshold,
204 ordinal: 0,
205 };
206 while self.heap.len() < self.k {
207 self.heap.push(sentinel);
208 }
209 while self.heap.peek().is_some_and(|worst| sentinel < *worst) {
210 self.heap.push(sentinel);
211 if self
212 .heap
213 .pop()
214 .is_some_and(|evicted| evicted.doc_id != u32::MAX)
215 {
216 self.real_len -= 1;
217 }
218 }
219 self.update_threshold();
220 }
221
222 pub fn into_sorted_results(self) -> Vec<(DocId, f32, u16)> {
225 let mut results: Vec<(DocId, f32, u16)> = self
226 .heap
227 .into_vec()
228 .into_iter()
229 .filter(|e| e.doc_id != u32::MAX)
230 .map(|e| (e.doc_id, e.score, e.ordinal))
231 .collect();
232
233 results.sort_unstable_by(|a, b| {
235 b.1.total_cmp(&a.1)
236 .then_with(|| a.0.cmp(&b.0))
237 .then_with(|| a.2.cmp(&b.2))
238 });
239
240 results
241 }
242}
243
244#[derive(Clone, Debug, Default)]
260pub struct SharedThreshold(std::sync::Arc<std::sync::atomic::AtomicU32>);
261
262impl SharedThreshold {
263 pub fn new() -> Self {
265 Self(std::sync::Arc::new(std::sync::atomic::AtomicU32::new(0)))
267 }
268
269 #[inline]
271 pub fn get(&self) -> f32 {
272 f32::from_bits(self.0.load(std::sync::atomic::Ordering::Relaxed))
273 }
274
275 pub fn raise(&self, score: f32) {
280 if score <= 0.0 {
283 return;
284 }
285 use std::sync::atomic::Ordering::Relaxed;
286 let bits = score.to_bits();
287 let mut cur = self.0.load(Relaxed);
288 while f32::from_bits(cur) < score {
289 match self.0.compare_exchange_weak(cur, bits, Relaxed, Relaxed) {
290 Ok(_) => break,
291 Err(actual) => cur = actual,
292 }
293 }
294 }
295}
296
297#[derive(Debug, Clone, Copy)]
299pub struct ScoredDoc {
300 pub doc_id: DocId,
301 pub score: f32,
302 pub ordinal: u16,
304}
305
306pub struct MaxScoreExecutor<'a> {
317 metric_index: &'a str,
321 metric_field: &'a str,
322 cursors: Vec<TermCursor<'a>>,
323 prefix_sums: Vec<f32>,
324 collector: ScoreCollector,
325 inv_heap_factor: f32,
326 predicate: Option<super::DocPredicate<'a>>,
327}
328
329pub(crate) struct TermCursor<'a> {
338 pub max_score: f32,
339 num_blocks: usize,
340 block_idx: usize,
342 doc_ids: Vec<u32>,
343 scores: Vec<f32>,
344 ordinals: Vec<u16>,
345 pos: usize,
346 block_loaded: bool,
347 exhausted: bool,
348 lazy_ordinals: bool,
352 ordinals_loaded: bool,
354 current_sparse_block: Option<crate::structures::SparseBlock>,
356 variant: CursorVariant<'a>,
358}
359
360enum CursorVariant<'a> {
361 Text {
363 list: crate::structures::BlockPostingList,
364 idf: f32,
365 idf_times_k1_plus_1: f32,
367 denom_tf_coeff: f32,
369 denom_const: f32,
371 tfs: Vec<u32>,
372 deferred_tf: Option<(usize, usize, usize)>,
375 },
376 Sparse {
378 si: &'a crate::segment::SparseIndex,
379 query_weight: f32,
380 skip_start: usize,
381 block_data_offset: u64,
382 },
383}
384
385macro_rules! cursor_ensure_block {
393 ($self:ident, $load_block_fn:ident, $($aw:tt)*) => {{
394 if $self.exhausted || $self.block_loaded {
395 return Ok(!$self.exhausted);
396 }
397 match &mut $self.variant {
398 CursorVariant::Text {
399 list,
400 deferred_tf,
401 ..
402 } => {
403 if let Some(state) = list.decode_block_doc_ids_only($self.block_idx, &mut $self.doc_ids) {
404 *deferred_tf = Some(state);
405 $self.scores.clear();
406 $self.pos = 0;
407 $self.block_loaded = true;
408 Ok(true)
409 } else {
410 $self.exhausted = true;
411 Ok(false)
412 }
413 }
414 CursorVariant::Sparse {
415 si,
416 query_weight,
417 skip_start,
418 block_data_offset,
419 ..
420 } => {
421 let block = si
422 .$load_block_fn(*skip_start, *block_data_offset, $self.block_idx)
423 $($aw)* ?;
424 match block {
425 Some(b) => {
426 b.decode_doc_ids_into(&mut $self.doc_ids);
427 b.decode_scored_weights_into(*query_weight, &mut $self.scores);
428 if $self.lazy_ordinals {
429 $self.current_sparse_block = Some(b);
432 $self.ordinals_loaded = false;
433 } else {
434 b.decode_ordinals_into(&mut $self.ordinals);
435 $self.ordinals_loaded = true;
436 $self.current_sparse_block = None;
437 }
438 $self.pos = 0;
439 $self.block_loaded = true;
440 Ok(true)
441 }
442 None => {
443 $self.exhausted = true;
444 Ok(false)
445 }
446 }
447 }
448 }
449 }};
450}
451
452macro_rules! cursor_advance {
453 ($self:ident, $ensure_fn:ident, $($aw:tt)*) => {{
454 if $self.exhausted {
455 return Ok(u32::MAX);
456 }
457 $self.$ensure_fn() $($aw)* ?;
458 if $self.exhausted {
459 return Ok(u32::MAX);
460 }
461 Ok($self.advance_pos())
462 }};
463}
464
465macro_rules! cursor_seek {
466 ($self:ident, $ensure_fn:ident, $target:expr, $($aw:tt)*) => {{
467 if let Some(doc) = $self.seek_prepare($target) {
468 return Ok(doc);
469 }
470 $self.$ensure_fn() $($aw)* ?;
471 if $self.seek_finish($target) {
472 $self.$ensure_fn() $($aw)* ?;
473 }
474 Ok($self.doc())
475 }};
476}
477
478impl<'a> TermCursor<'a> {
479 pub fn text(
481 posting_list: crate::structures::BlockPostingList,
482 idf: f32,
483 avg_field_len: f32,
484 ) -> Self {
485 let max_tf = posting_list.max_tf() as f32;
486 let max_score = super::bm25_upper_bound(max_tf.max(1.0), idf);
487 let num_blocks = posting_list.num_blocks();
488 let safe_avg = avg_field_len.max(1.0);
489 Self {
490 max_score,
491 num_blocks,
492 block_idx: 0,
493 doc_ids: Vec::with_capacity(128),
494 scores: Vec::with_capacity(128),
495 ordinals: Vec::new(),
496 pos: 0,
497 block_loaded: false,
498 exhausted: num_blocks == 0,
499 lazy_ordinals: false,
500 ordinals_loaded: true, current_sparse_block: None,
502 variant: CursorVariant::Text {
503 list: posting_list,
504 idf,
505 idf_times_k1_plus_1: idf * (super::BM25_K1 + 1.0),
506 denom_tf_coeff: 1.0 + super::BM25_K1 * (super::BM25_B / safe_avg),
507 denom_const: super::BM25_K1 * (1.0 - super::BM25_B),
508 tfs: Vec::with_capacity(128),
509 deferred_tf: None,
510 },
511 }
512 }
513
514 pub fn sparse(
517 si: &'a crate::segment::SparseIndex,
518 query_weight: f32,
519 skip_start: usize,
520 skip_count: usize,
521 global_max_weight: f32,
522 block_data_offset: u64,
523 ) -> Self {
524 Self {
525 max_score: query_weight.abs() * global_max_weight,
526 num_blocks: skip_count,
527 block_idx: 0,
528 doc_ids: Vec::with_capacity(256),
529 scores: Vec::with_capacity(256),
530 ordinals: Vec::with_capacity(256),
531 pos: 0,
532 block_loaded: false,
533 exhausted: skip_count == 0,
534 lazy_ordinals: false,
535 ordinals_loaded: true,
536 current_sparse_block: None,
537 variant: CursorVariant::Sparse {
538 si,
539 query_weight,
540 skip_start,
541 block_data_offset,
542 },
543 }
544 }
545
546 #[inline]
549 fn block_first_doc(&self, idx: usize) -> DocId {
550 match &self.variant {
551 CursorVariant::Text { list, .. } => list.block_first_doc(idx).unwrap_or(u32::MAX),
552 CursorVariant::Sparse { si, skip_start, .. } => {
553 si.read_skip_entry(*skip_start + idx).first_doc
554 }
555 }
556 }
557
558 #[inline]
559 fn block_last_doc(&self, idx: usize) -> DocId {
560 match &self.variant {
561 CursorVariant::Text { list, .. } => list.block_last_doc(idx).unwrap_or(0),
562 CursorVariant::Sparse { si, skip_start, .. } => {
563 si.read_skip_entry(*skip_start + idx).last_doc
564 }
565 }
566 }
567
568 #[inline]
571 pub fn doc(&self) -> DocId {
572 if self.exhausted {
573 return u32::MAX;
574 }
575 if self.block_loaded {
576 debug_assert!(self.pos < self.doc_ids.len());
577 unsafe { *self.doc_ids.get_unchecked(self.pos) }
579 } else {
580 self.block_first_doc(self.block_idx)
581 }
582 }
583
584 #[inline]
585 pub fn ordinal(&self) -> u16 {
586 if !self.block_loaded || self.ordinals.is_empty() {
587 return 0;
588 }
589 debug_assert!(self.pos < self.ordinals.len());
590 unsafe { *self.ordinals.get_unchecked(self.pos) }
592 }
593
594 #[inline]
600 pub fn ordinal_mut(&mut self) -> u16 {
601 if !self.block_loaded {
602 return 0;
603 }
604 if !self.ordinals_loaded {
605 if let Some(ref block) = self.current_sparse_block {
606 block.decode_ordinals_into(&mut self.ordinals);
607 }
608 self.ordinals_loaded = true;
609 }
610 if self.ordinals.is_empty() {
611 return 0;
612 }
613 debug_assert!(self.pos < self.ordinals.len());
614 unsafe { *self.ordinals.get_unchecked(self.pos) }
615 }
616
617 #[inline]
618 pub fn score(&self) -> f32 {
619 if !self.block_loaded {
620 return 0.0;
621 }
622 debug_assert!(self.pos < self.scores.len());
623 unsafe { *self.scores.get_unchecked(self.pos) }
625 }
626
627 #[inline]
633 pub fn ensure_scores(&mut self) {
634 if self.block_loaded && self.scores.is_empty() {
635 self.compute_deferred_scores();
636 }
637 }
638
639 #[inline]
640 pub fn current_block_max_score(&self) -> f32 {
641 if self.exhausted {
642 return 0.0;
643 }
644 match &self.variant {
645 CursorVariant::Text { list, idf, .. } => {
646 let block_max_tf = list.block_max_tf(self.block_idx).unwrap_or(0) as f32;
647 super::bm25_upper_bound(block_max_tf.max(1.0), *idf)
648 }
649 CursorVariant::Sparse {
650 si,
651 query_weight,
652 skip_start,
653 ..
654 } => query_weight.abs() * si.read_skip_entry(*skip_start + self.block_idx).max_weight,
655 }
656 }
657
658 pub fn skip_to_next_block(&mut self) -> DocId {
661 if self.exhausted {
662 return u32::MAX;
663 }
664 self.block_idx += 1;
665 self.block_loaded = false;
666 if self.block_idx >= self.num_blocks {
667 self.exhausted = true;
668 return u32::MAX;
669 }
670 self.block_first_doc(self.block_idx)
671 }
672
673 #[inline]
674 fn advance_pos(&mut self) -> DocId {
675 self.pos += 1;
676 if self.pos >= self.doc_ids.len() {
677 self.block_idx += 1;
678 self.block_loaded = false;
679 if self.block_idx >= self.num_blocks {
680 self.exhausted = true;
681 return u32::MAX;
682 }
683 }
684 self.doc()
685 }
686
687 #[inline(never)]
689 fn compute_deferred_scores(&mut self) {
690 if let CursorVariant::Text {
691 list,
692 idf_times_k1_plus_1,
693 denom_tf_coeff,
694 denom_const,
695 tfs,
696 deferred_tf,
697 ..
698 } = &mut self.variant
699 && let Some((block_offset, tf_start, count)) = deferred_tf.take()
700 {
701 list.decode_block_tfs_deferred(block_offset, tf_start, count, tfs);
702 let num_scale = *idf_times_k1_plus_1;
703 let d_tf = *denom_tf_coeff;
704 let d_const = *denom_const;
705 self.scores.clear();
706 self.scores.resize(count, 0.0);
707 for i in 0..count {
708 let tf = unsafe { *tfs.get_unchecked(i) } as f32;
709 let score = (num_scale * tf) / (d_tf * tf + d_const);
710 unsafe {
711 *self.scores.get_unchecked_mut(i) = score;
712 }
713 }
714 }
715 }
716
717 pub async fn ensure_block_loaded(&mut self) -> crate::Result<bool> {
723 cursor_ensure_block!(self, load_block_direct, .await)
724 }
725
726 pub fn ensure_block_loaded_sync(&mut self) -> crate::Result<bool> {
727 cursor_ensure_block!(self, load_block_direct_sync,)
728 }
729
730 pub async fn advance(&mut self) -> crate::Result<DocId> {
731 cursor_advance!(self, ensure_block_loaded, .await)
732 }
733
734 pub fn advance_sync(&mut self) -> crate::Result<DocId> {
735 cursor_advance!(self, ensure_block_loaded_sync,)
736 }
737
738 pub async fn seek(&mut self, target: DocId) -> crate::Result<DocId> {
739 cursor_seek!(self, ensure_block_loaded, target, .await)
740 }
741
742 pub fn seek_sync(&mut self, target: DocId) -> crate::Result<DocId> {
743 cursor_seek!(self, ensure_block_loaded_sync, target,)
744 }
745
746 fn seek_prepare(&mut self, target: DocId) -> Option<DocId> {
747 if self.exhausted {
748 return Some(u32::MAX);
749 }
750
751 if self.block_loaded
753 && let Some(&last) = self.doc_ids.last()
754 {
755 if last >= target && self.doc_ids[self.pos] < target {
756 let remaining = &self.doc_ids[self.pos..];
757 self.pos += crate::structures::simd::find_first_ge_u32(remaining, target);
758 if self.pos >= self.doc_ids.len() {
759 self.block_idx += 1;
760 self.block_loaded = false;
761 if self.block_idx >= self.num_blocks {
762 self.exhausted = true;
763 return Some(u32::MAX);
764 }
765 }
766 return Some(self.doc());
767 }
768 if self.doc_ids[self.pos] >= target {
769 return Some(self.doc());
770 }
771 }
772
773 let lo = match &self.variant {
775 CursorVariant::Text { list, .. } => match list.seek_block(target, self.block_idx) {
777 Some(idx) => idx,
778 None => {
779 self.exhausted = true;
780 return Some(u32::MAX);
781 }
782 },
783 CursorVariant::Sparse { .. } => {
785 let mut lo = self.block_idx;
786 let mut hi = self.num_blocks;
787 while lo < hi {
788 let mid = lo + (hi - lo) / 2;
789 if self.block_last_doc(mid) < target {
790 lo = mid + 1;
791 } else {
792 hi = mid;
793 }
794 }
795 lo
796 }
797 };
798 if lo >= self.num_blocks {
799 self.exhausted = true;
800 return Some(u32::MAX);
801 }
802 if lo != self.block_idx || !self.block_loaded {
803 self.block_idx = lo;
804 self.block_loaded = false;
805 }
806 None
807 }
808
809 #[inline]
810 fn seek_finish(&mut self, target: DocId) -> bool {
811 if self.exhausted {
812 return false;
813 }
814 self.pos = crate::structures::simd::find_first_ge_u32(&self.doc_ids, target);
815 if self.pos >= self.doc_ids.len() {
816 self.block_idx += 1;
817 self.block_loaded = false;
818 if self.block_idx >= self.num_blocks {
819 self.exhausted = true;
820 return false;
821 }
822 return true;
823 }
824 false
825 }
826}
827
828macro_rules! bms_execute_loop {
833 ($self:ident, $ensure:ident, $advance:ident, $seek:ident, $($aw:tt)*) => {{
834 let n = $self.cursors.len();
835
836 for cursor in &mut $self.cursors {
838 cursor.$ensure() $($aw)* ?;
839 }
840
841 let mut docs_scored = 0u64;
842 let mut docs_skipped = 0u64;
843 let mut blocks_skipped = 0u64;
844 let mut conjunction_skipped = 0u64;
845 let mut ordinal_scores: Vec<(u16, f32)> = Vec::with_capacity(n * 2);
846 let _bms_start = std::time::Instant::now();
847
848 let inv_heap_factor = $self.inv_heap_factor;
849 let mut adjusted_threshold = $self.collector.threshold() * inv_heap_factor - 1e-6;
850
851 loop {
852 let partition = $self.find_partition();
853 if partition >= n {
854 break;
855 }
856
857 let mut min_doc = u32::MAX;
861 let mut at_min_mask = 0u64; for i in partition..n {
863 let doc = $self.cursors[i].doc();
864 match doc.cmp(&min_doc) {
865 std::cmp::Ordering::Less => {
866 min_doc = doc;
867 at_min_mask = 1u64 << (i as u32);
868 }
869 std::cmp::Ordering::Equal => {
870 at_min_mask |= 1u64 << (i as u32);
871 }
872 _ => {}
873 }
874 }
875 if min_doc == u32::MAX {
876 break;
877 }
878
879 let non_essential_upper = if partition > 0 {
880 $self.prefix_sums[partition - 1]
881 } else {
882 0.0
883 };
884
885 if $self.collector.len() >= $self.collector.k {
887 let mut present_upper: f32 = 0.0;
888 let mut mask = at_min_mask;
889 while mask != 0 {
890 let i = mask.trailing_zeros() as usize;
891 present_upper += $self.cursors[i].max_score;
892 mask &= mask - 1;
893 }
894
895 if present_upper + non_essential_upper < adjusted_threshold {
896 let mut mask = at_min_mask;
897 while mask != 0 {
898 let i = mask.trailing_zeros() as usize;
899 $self.cursors[i].$ensure() $($aw)* ?;
900 $self.cursors[i].$advance() $($aw)* ?;
901 mask &= mask - 1;
902 }
903 conjunction_skipped += 1;
904 continue;
905 }
906 }
907
908 if $self.collector.len() >= $self.collector.k {
910 let mut block_max_sum: f32 = 0.0;
911 let mut mask = at_min_mask;
912 while mask != 0 {
913 let i = mask.trailing_zeros() as usize;
914 block_max_sum += $self.cursors[i].current_block_max_score();
915 mask &= mask - 1;
916 }
917
918 if block_max_sum + non_essential_upper < adjusted_threshold {
919 let mut mask = at_min_mask;
920 while mask != 0 {
921 let i = mask.trailing_zeros() as usize;
922 $self.cursors[i].skip_to_next_block();
923 $self.cursors[i].$ensure() $($aw)* ?;
924 mask &= mask - 1;
925 }
926 blocks_skipped += 1;
927 continue;
928 }
929 }
930
931 if let Some(ref pred) = $self.predicate {
933 if !pred(min_doc) {
934 let mut mask = at_min_mask;
935 while mask != 0 {
936 let i = mask.trailing_zeros() as usize;
937 $self.cursors[i].$ensure() $($aw)* ?;
938 $self.cursors[i].$advance() $($aw)* ?;
939 mask &= mask - 1;
940 }
941 continue;
942 }
943 }
944
945 ordinal_scores.clear();
947 {
948 let mut mask = at_min_mask;
949 while mask != 0 {
950 let i = mask.trailing_zeros() as usize;
951 $self.cursors[i].$ensure() $($aw)* ?;
952 $self.cursors[i].ensure_scores();
953 while $self.cursors[i].doc() == min_doc {
954 let ord = $self.cursors[i].ordinal_mut();
955 let sc = $self.cursors[i].score();
956 ordinal_scores.push((ord, sc));
957 $self.cursors[i].$advance() $($aw)* ?;
958 }
959 mask &= mask - 1;
960 }
961 }
962
963 let essential_total: f32 = ordinal_scores.iter().map(|(_, s)| *s).sum();
964 if $self.collector.len() >= $self.collector.k
965 && essential_total + non_essential_upper < adjusted_threshold
966 {
967 docs_skipped += 1;
968 continue;
969 }
970
971 let mut running_total = essential_total;
973 for i in (0..partition).rev() {
974 if $self.collector.len() >= $self.collector.k
975 && running_total + $self.prefix_sums[i] < adjusted_threshold
976 {
977 break;
978 }
979
980 let doc = $self.cursors[i].$seek(min_doc) $($aw)* ?;
981 if doc == min_doc {
982 $self.cursors[i].ensure_scores();
983 while $self.cursors[i].doc() == min_doc {
984 let s = $self.cursors[i].score();
985 running_total += s;
986 let ord = $self.cursors[i].ordinal_mut();
987 ordinal_scores.push((ord, s));
988 $self.cursors[i].$advance() $($aw)* ?;
989 }
990 }
991 }
992
993 if ordinal_scores.len() == 1 {
996 let (ord, score) = ordinal_scores[0];
997 if $self.collector.insert_with_ordinal(min_doc, score, ord) {
998 docs_scored += 1;
999 adjusted_threshold = $self.collector.threshold() * inv_heap_factor - 1e-6;
1000 } else {
1001 docs_skipped += 1;
1002 }
1003 } else if !ordinal_scores.is_empty() {
1004 if ordinal_scores.len() > 2 {
1005 ordinal_scores.sort_unstable_by_key(|(ord, _)| *ord);
1006 } else if ordinal_scores.len() == 2 && ordinal_scores[0].0 > ordinal_scores[1].0 {
1007 ordinal_scores.swap(0, 1);
1008 }
1009 let mut j = 0;
1010 while j < ordinal_scores.len() {
1011 let current_ord = ordinal_scores[j].0;
1012 let mut score = 0.0f32;
1013 while j < ordinal_scores.len() && ordinal_scores[j].0 == current_ord {
1014 score += ordinal_scores[j].1;
1015 j += 1;
1016 }
1017 if $self
1018 .collector
1019 .insert_with_ordinal(min_doc, score, current_ord)
1020 {
1021 docs_scored += 1;
1022 adjusted_threshold = $self.collector.threshold() * inv_heap_factor - 1e-6;
1023 } else {
1024 docs_skipped += 1;
1025 }
1026 }
1027 }
1028 }
1029
1030 let results: Vec<ScoredDoc> = $self
1031 .collector
1032 .into_sorted_results()
1033 .into_iter()
1034 .map(|(doc_id, score, ordinal)| ScoredDoc {
1035 doc_id,
1036 score,
1037 ordinal,
1038 })
1039 .collect();
1040
1041 let _bms_elapsed_ms = _bms_start.elapsed().as_millis() as u64;
1042 if _bms_elapsed_ms > 500 {
1043 warn!(
1044 "slow MaxScore: {}ms, cursors={}, scored={}, skipped={}, blocks_skipped={}, conjunction_skipped={}, returned={}, top_score={:.4}",
1045 _bms_elapsed_ms,
1046 n,
1047 docs_scored,
1048 docs_skipped,
1049 blocks_skipped,
1050 conjunction_skipped,
1051 results.len(),
1052 results.first().map(|r| r.score).unwrap_or(0.0)
1053 );
1054 } else {
1055 debug!(
1056 "MaxScoreExecutor: {}ms, scored={}, skipped={}, blocks_skipped={}, conjunction_skipped={}, returned={}, top_score={:.4}",
1057 _bms_elapsed_ms,
1058 docs_scored,
1059 docs_skipped,
1060 blocks_skipped,
1061 conjunction_skipped,
1062 results.len(),
1063 results.first().map(|r| r.score).unwrap_or(0.0)
1064 );
1065 }
1066
1067 Ok(results)
1068 }};
1069}
1070
1071impl<'a> MaxScoreExecutor<'a> {
1072 pub(crate) fn new(mut cursors: Vec<TermCursor<'a>>, k: usize, heap_factor: f32) -> Self {
1077 if cursors.len() > super::MAX_QUERY_TERMS {
1081 cursors.sort_unstable_by(|a, b| b.max_score.total_cmp(&a.max_score));
1082 cursors.truncate(super::MAX_QUERY_TERMS);
1083 log::warn!(
1084 "MaxScore cursor count exceeded {}; retaining the strongest cursors",
1085 super::MAX_QUERY_TERMS
1086 );
1087 }
1088
1089 for c in &mut cursors {
1092 c.lazy_ordinals = true;
1093 }
1094
1095 cursors.sort_by(|a, b| {
1097 a.max_score
1098 .partial_cmp(&b.max_score)
1099 .unwrap_or(Ordering::Equal)
1100 });
1101
1102 let mut prefix_sums = Vec::with_capacity(cursors.len());
1103 let mut cumsum = 0.0f32;
1104 for c in &cursors {
1105 cumsum += c.max_score;
1106 prefix_sums.push(cumsum);
1107 }
1108
1109 let clamped_heap_factor = heap_factor.clamp(0.01, 1.0);
1110
1111 debug!(
1112 "Creating MaxScoreExecutor: num_cursors={}, k={}, total_upper={:.4}, heap_factor={:.2}",
1113 cursors.len(),
1114 k,
1115 cumsum,
1116 clamped_heap_factor
1117 );
1118
1119 Self {
1120 cursors,
1121 prefix_sums,
1122 collector: ScoreCollector::new(k),
1123 inv_heap_factor: 1.0 / clamped_heap_factor,
1124 predicate: None,
1125 metric_index: "unknown",
1126 metric_field: "unknown",
1127 }
1128 }
1129
1130 pub fn with_metric_labels(mut self, index: &'a str, field: &'a str) -> Self {
1132 self.metric_index = index;
1133 self.metric_field = field;
1134 self
1135 }
1136
1137 pub fn sparse(
1141 sparse_index: &'a crate::segment::SparseIndex,
1142 query_terms: Vec<(u32, f32)>,
1143 k: usize,
1144 heap_factor: f32,
1145 ) -> Self {
1146 let cursors: Vec<TermCursor<'a>> = query_terms
1147 .iter()
1148 .filter_map(|&(dim_id, qw)| {
1149 let (skip_start, skip_count, global_max, block_data_offset) =
1150 sparse_index.get_skip_range_full(dim_id)?;
1151 Some(TermCursor::sparse(
1152 sparse_index,
1153 qw,
1154 skip_start,
1155 skip_count,
1156 global_max,
1157 block_data_offset,
1158 ))
1159 })
1160 .collect();
1161 Self::new(cursors, k, heap_factor)
1162 }
1163
1164 pub fn text(
1168 posting_lists: Vec<(crate::structures::BlockPostingList, f32)>,
1169 avg_field_len: f32,
1170 k: usize,
1171 ) -> Self {
1172 let cursors: Vec<TermCursor<'a>> = posting_lists
1173 .into_iter()
1174 .map(|(pl, idf)| TermCursor::text(pl, idf, avg_field_len))
1175 .collect();
1176 Self::new(cursors, k, 1.0)
1177 }
1178
1179 #[inline]
1180 fn find_partition(&self) -> usize {
1181 let threshold = self.collector.threshold() * self.inv_heap_factor;
1185 self.prefix_sums.partition_point(|&sum| sum < threshold)
1188 }
1189
1190 pub fn with_predicate(mut self, predicate: super::DocPredicate<'a>) -> Self {
1196 self.predicate = Some(predicate);
1197 self
1198 }
1199
1200 pub fn seed_threshold(&mut self, initial_threshold: f32) {
1202 self.collector.seed_threshold(initial_threshold);
1203 }
1204
1205 pub async fn execute(mut self) -> crate::Result<Vec<ScoredDoc>> {
1207 if self.cursors.is_empty() {
1208 return Ok(Vec::new());
1209 }
1210 let t = crate::observe::Timer::start();
1211 let results = bms_execute_loop!(self, ensure_block_loaded, advance, seek, .await);
1212 if let Ok(r) = &results {
1213 crate::observe::maxscore_query(self.metric_index, self.metric_field, t.secs(), r.len());
1214 }
1215 results
1216 }
1217
1218 pub fn execute_sync(mut self) -> crate::Result<Vec<ScoredDoc>> {
1220 if self.cursors.is_empty() {
1221 return Ok(Vec::new());
1222 }
1223 let t = crate::observe::Timer::start();
1224 let results = bms_execute_loop!(self, ensure_block_loaded_sync, advance_sync, seek_sync,);
1225 if let Ok(r) = &results {
1226 crate::observe::maxscore_query(self.metric_index, self.metric_field, t.secs(), r.len());
1227 }
1228 results
1229 }
1230}
1231
1232#[cfg(test)]
1233mod tests {
1234 use super::*;
1235
1236 #[test]
1237 fn test_shared_threshold_monotonic_raise() {
1238 let shared = SharedThreshold::new();
1239 assert_eq!(shared.get(), 0.0);
1240
1241 shared.raise(2.5);
1242 assert_eq!(shared.get(), 2.5);
1243
1244 shared.raise(1.0);
1246 assert_eq!(shared.get(), 2.5);
1247
1248 shared.raise(4.0);
1250 assert_eq!(shared.get(), 4.0);
1251
1252 shared.raise(0.0);
1254 shared.raise(-3.0);
1255 shared.raise(f32::NAN);
1256 assert_eq!(shared.get(), 4.0);
1257
1258 let clone = shared.clone();
1260 clone.raise(9.0);
1261 assert_eq!(shared.get(), 9.0);
1262 }
1263
1264 #[test]
1265 fn test_shared_threshold_seed_matches_manual() {
1266 let mut seeded = ScoreCollector::new(2);
1269 seeded.seed_threshold(3.0);
1270 assert_eq!(seeded.threshold(), 3.0);
1271 assert!(!seeded.would_enter(3.0));
1273 assert!(seeded.would_enter(3.5));
1274 seeded.insert(1, 5.0);
1277 seeded.insert(2, 4.0);
1278 let results = seeded.into_sorted_results();
1279 assert_eq!(results.len(), 2);
1280 assert_eq!(results[0].0, 1);
1281 assert_eq!(results[1].0, 2);
1282 }
1283
1284 #[test]
1285 fn test_shared_threshold_can_raise_after_real_inserts() {
1286 let mut collector = ScoreCollector::new(3);
1287 collector.insert(1, 10.0);
1288 collector.insert(2, 4.0);
1289 assert_eq!(collector.real_len(), 2);
1290
1291 collector.seed_threshold(6.0);
1294 assert_eq!(collector.threshold(), 6.0);
1295 assert_eq!(collector.real_len(), 1);
1296
1297 assert!(collector.would_enter_candidate(3, 6.0, 0));
1300 assert!(collector.insert(3, 6.0));
1301 assert_eq!(collector.real_len(), 2);
1302 let results = collector.into_sorted_results();
1303 assert_eq!(results, vec![(1, 10.0, 0), (3, 6.0, 0)]);
1304 }
1305
1306 #[test]
1307 fn test_score_collector_basic() {
1308 let mut collector = ScoreCollector::new(3);
1309
1310 collector.insert(1, 1.0);
1311 collector.insert(2, 2.0);
1312 collector.insert(3, 3.0);
1313 assert_eq!(collector.threshold(), 1.0);
1314
1315 collector.insert(4, 4.0);
1316 assert_eq!(collector.threshold(), 2.0);
1317
1318 let results = collector.into_sorted_results();
1319 assert_eq!(results.len(), 3);
1320 assert_eq!(results[0].0, 4); assert_eq!(results[1].0, 3);
1322 assert_eq!(results[2].0, 2);
1323 }
1324
1325 #[test]
1326 fn test_score_collector_threshold() {
1327 let mut collector = ScoreCollector::new(2);
1328
1329 collector.insert(1, 5.0);
1330 collector.insert(2, 3.0);
1331 assert_eq!(collector.threshold(), 3.0);
1332
1333 assert!(!collector.would_enter(2.0));
1335 assert!(!collector.insert(3, 2.0));
1336
1337 assert!(collector.would_enter(4.0));
1339 assert!(collector.insert(4, 4.0));
1340 assert_eq!(collector.threshold(), 4.0);
1341 }
1342
1343 #[test]
1344 fn test_heap_entry_ordering() {
1345 let mut heap = BinaryHeap::new();
1346 heap.push(HeapEntry {
1347 doc_id: 1,
1348 score: 3.0,
1349 ordinal: 0,
1350 });
1351 heap.push(HeapEntry {
1352 doc_id: 2,
1353 score: 1.0,
1354 ordinal: 0,
1355 });
1356 heap.push(HeapEntry {
1357 doc_id: 3,
1358 score: 2.0,
1359 ordinal: 0,
1360 });
1361
1362 assert_eq!(heap.pop().unwrap().score, 1.0);
1364 assert_eq!(heap.pop().unwrap().score, 2.0);
1365 assert_eq!(heap.pop().unwrap().score, 3.0);
1366 }
1367}