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}
72
73impl ScoreCollector {
74 pub fn new(k: usize) -> Self {
76 let capacity = k.saturating_add(1).min(1_000_000);
78 Self {
79 heap: BinaryHeap::with_capacity(capacity),
80 k,
81 cached_threshold: 0.0,
82 }
83 }
84
85 #[inline]
87 pub fn threshold(&self) -> f32 {
88 self.cached_threshold
89 }
90
91 #[inline]
93 fn update_threshold(&mut self) {
94 self.cached_threshold = if self.heap.len() >= self.k {
95 self.heap.peek().map(|e| e.score).unwrap_or(0.0)
96 } else {
97 0.0
98 };
99 }
100
101 #[inline]
104 pub fn insert(&mut self, doc_id: DocId, score: f32) -> bool {
105 self.insert_with_ordinal(doc_id, score, 0)
106 }
107
108 #[inline]
111 pub fn insert_with_ordinal(&mut self, doc_id: DocId, score: f32, ordinal: u16) -> bool {
112 if self.k == 0 {
113 return false;
114 }
115 let entry = HeapEntry {
116 doc_id,
117 score,
118 ordinal,
119 };
120 if self.heap.len() < self.k {
121 self.heap.push(entry);
122 if self.heap.len() == self.k {
124 self.update_threshold();
125 }
126 true
127 } else if self.heap.peek().is_some_and(|worst| entry < *worst) {
128 self.heap.push(entry);
129 self.heap.pop(); self.update_threshold();
131 true
132 } else {
133 false
134 }
135 }
136
137 #[inline]
139 pub fn would_enter(&self, score: f32) -> bool {
140 self.heap.len() < self.k || score > self.cached_threshold
141 }
142
143 #[inline]
146 pub fn would_enter_candidate(&self, doc_id: DocId, score: f32, ordinal: u16) -> bool {
147 if self.k == 0 {
148 return false;
149 }
150 let entry = HeapEntry {
151 doc_id,
152 score,
153 ordinal,
154 };
155 self.heap.len() < self.k || self.heap.peek().is_some_and(|worst| entry < *worst)
156 }
157
158 #[inline]
160 pub fn len(&self) -> usize {
161 self.heap.len()
162 }
163
164 #[inline]
166 pub fn is_empty(&self) -> bool {
167 self.heap.is_empty()
168 }
169
170 pub fn seed_threshold(&mut self, initial_threshold: f32) {
176 if initial_threshold > 0.0 && self.heap.is_empty() {
177 for _ in 0..self.k {
178 self.heap.push(HeapEntry {
179 doc_id: u32::MAX,
180 score: initial_threshold,
181 ordinal: 0,
182 });
183 }
184 self.update_threshold();
185 }
186 }
187
188 pub fn into_sorted_results(self) -> Vec<(DocId, f32, u16)> {
191 let mut results: Vec<(DocId, f32, u16)> = self
192 .heap
193 .into_vec()
194 .into_iter()
195 .filter(|e| e.doc_id != u32::MAX)
196 .map(|e| (e.doc_id, e.score, e.ordinal))
197 .collect();
198
199 results.sort_unstable_by(|a, b| {
201 b.1.total_cmp(&a.1)
202 .then_with(|| a.0.cmp(&b.0))
203 .then_with(|| a.2.cmp(&b.2))
204 });
205
206 results
207 }
208}
209
210#[derive(Clone, Default)]
226pub struct SharedThreshold(std::sync::Arc<std::sync::atomic::AtomicU32>);
227
228impl SharedThreshold {
229 pub fn new() -> Self {
231 Self(std::sync::Arc::new(std::sync::atomic::AtomicU32::new(0)))
233 }
234
235 #[inline]
237 pub fn get(&self) -> f32 {
238 f32::from_bits(self.0.load(std::sync::atomic::Ordering::Relaxed))
239 }
240
241 pub fn raise(&self, score: f32) {
246 if score <= 0.0 {
249 return;
250 }
251 use std::sync::atomic::Ordering::Relaxed;
252 let bits = score.to_bits();
253 let mut cur = self.0.load(Relaxed);
254 while f32::from_bits(cur) < score {
255 match self.0.compare_exchange_weak(cur, bits, Relaxed, Relaxed) {
256 Ok(_) => break,
257 Err(actual) => cur = actual,
258 }
259 }
260 }
261}
262
263#[derive(Debug, Clone, Copy)]
265pub struct ScoredDoc {
266 pub doc_id: DocId,
267 pub score: f32,
268 pub ordinal: u16,
270}
271
272pub struct MaxScoreExecutor<'a> {
283 metric_index: &'a str,
287 metric_field: &'a str,
288 cursors: Vec<TermCursor<'a>>,
289 prefix_sums: Vec<f32>,
290 collector: ScoreCollector,
291 inv_heap_factor: f32,
292 predicate: Option<super::DocPredicate<'a>>,
293}
294
295pub(crate) struct TermCursor<'a> {
304 pub max_score: f32,
305 num_blocks: usize,
306 block_idx: usize,
308 doc_ids: Vec<u32>,
309 scores: Vec<f32>,
310 ordinals: Vec<u16>,
311 pos: usize,
312 block_loaded: bool,
313 exhausted: bool,
314 lazy_ordinals: bool,
318 ordinals_loaded: bool,
320 current_sparse_block: Option<crate::structures::SparseBlock>,
322 variant: CursorVariant<'a>,
324}
325
326enum CursorVariant<'a> {
327 Text {
329 list: crate::structures::BlockPostingList,
330 idf: f32,
331 idf_times_k1_plus_1: f32,
333 denom_tf_coeff: f32,
335 denom_const: f32,
337 tfs: Vec<u32>,
338 deferred_tf: Option<(usize, usize, usize)>,
341 },
342 Sparse {
344 si: &'a crate::segment::SparseIndex,
345 query_weight: f32,
346 skip_start: usize,
347 block_data_offset: u64,
348 },
349}
350
351macro_rules! cursor_ensure_block {
359 ($self:ident, $load_block_fn:ident, $($aw:tt)*) => {{
360 if $self.exhausted || $self.block_loaded {
361 return Ok(!$self.exhausted);
362 }
363 match &mut $self.variant {
364 CursorVariant::Text {
365 list,
366 deferred_tf,
367 ..
368 } => {
369 if let Some(state) = list.decode_block_doc_ids_only($self.block_idx, &mut $self.doc_ids) {
370 *deferred_tf = Some(state);
371 $self.scores.clear();
372 $self.pos = 0;
373 $self.block_loaded = true;
374 Ok(true)
375 } else {
376 $self.exhausted = true;
377 Ok(false)
378 }
379 }
380 CursorVariant::Sparse {
381 si,
382 query_weight,
383 skip_start,
384 block_data_offset,
385 ..
386 } => {
387 let block = si
388 .$load_block_fn(*skip_start, *block_data_offset, $self.block_idx)
389 $($aw)* ?;
390 match block {
391 Some(b) => {
392 b.decode_doc_ids_into(&mut $self.doc_ids);
393 b.decode_scored_weights_into(*query_weight, &mut $self.scores);
394 if $self.lazy_ordinals {
395 $self.current_sparse_block = Some(b);
398 $self.ordinals_loaded = false;
399 } else {
400 b.decode_ordinals_into(&mut $self.ordinals);
401 $self.ordinals_loaded = true;
402 $self.current_sparse_block = None;
403 }
404 $self.pos = 0;
405 $self.block_loaded = true;
406 Ok(true)
407 }
408 None => {
409 $self.exhausted = true;
410 Ok(false)
411 }
412 }
413 }
414 }
415 }};
416}
417
418macro_rules! cursor_advance {
419 ($self:ident, $ensure_fn:ident, $($aw:tt)*) => {{
420 if $self.exhausted {
421 return Ok(u32::MAX);
422 }
423 $self.$ensure_fn() $($aw)* ?;
424 if $self.exhausted {
425 return Ok(u32::MAX);
426 }
427 Ok($self.advance_pos())
428 }};
429}
430
431macro_rules! cursor_seek {
432 ($self:ident, $ensure_fn:ident, $target:expr, $($aw:tt)*) => {{
433 if let Some(doc) = $self.seek_prepare($target) {
434 return Ok(doc);
435 }
436 $self.$ensure_fn() $($aw)* ?;
437 if $self.seek_finish($target) {
438 $self.$ensure_fn() $($aw)* ?;
439 }
440 Ok($self.doc())
441 }};
442}
443
444impl<'a> TermCursor<'a> {
445 pub fn text(
447 posting_list: crate::structures::BlockPostingList,
448 idf: f32,
449 avg_field_len: f32,
450 ) -> Self {
451 let max_tf = posting_list.max_tf() as f32;
452 let max_score = super::bm25_upper_bound(max_tf.max(1.0), idf);
453 let num_blocks = posting_list.num_blocks();
454 let safe_avg = avg_field_len.max(1.0);
455 Self {
456 max_score,
457 num_blocks,
458 block_idx: 0,
459 doc_ids: Vec::with_capacity(128),
460 scores: Vec::with_capacity(128),
461 ordinals: Vec::new(),
462 pos: 0,
463 block_loaded: false,
464 exhausted: num_blocks == 0,
465 lazy_ordinals: false,
466 ordinals_loaded: true, current_sparse_block: None,
468 variant: CursorVariant::Text {
469 list: posting_list,
470 idf,
471 idf_times_k1_plus_1: idf * (super::BM25_K1 + 1.0),
472 denom_tf_coeff: 1.0 + super::BM25_K1 * (super::BM25_B / safe_avg),
473 denom_const: super::BM25_K1 * (1.0 - super::BM25_B),
474 tfs: Vec::with_capacity(128),
475 deferred_tf: None,
476 },
477 }
478 }
479
480 pub fn sparse(
483 si: &'a crate::segment::SparseIndex,
484 query_weight: f32,
485 skip_start: usize,
486 skip_count: usize,
487 global_max_weight: f32,
488 block_data_offset: u64,
489 ) -> Self {
490 Self {
491 max_score: query_weight.abs() * global_max_weight,
492 num_blocks: skip_count,
493 block_idx: 0,
494 doc_ids: Vec::with_capacity(256),
495 scores: Vec::with_capacity(256),
496 ordinals: Vec::with_capacity(256),
497 pos: 0,
498 block_loaded: false,
499 exhausted: skip_count == 0,
500 lazy_ordinals: false,
501 ordinals_loaded: true,
502 current_sparse_block: None,
503 variant: CursorVariant::Sparse {
504 si,
505 query_weight,
506 skip_start,
507 block_data_offset,
508 },
509 }
510 }
511
512 #[inline]
515 fn block_first_doc(&self, idx: usize) -> DocId {
516 match &self.variant {
517 CursorVariant::Text { list, .. } => list.block_first_doc(idx).unwrap_or(u32::MAX),
518 CursorVariant::Sparse { si, skip_start, .. } => {
519 si.read_skip_entry(*skip_start + idx).first_doc
520 }
521 }
522 }
523
524 #[inline]
525 fn block_last_doc(&self, idx: usize) -> DocId {
526 match &self.variant {
527 CursorVariant::Text { list, .. } => list.block_last_doc(idx).unwrap_or(0),
528 CursorVariant::Sparse { si, skip_start, .. } => {
529 si.read_skip_entry(*skip_start + idx).last_doc
530 }
531 }
532 }
533
534 #[inline]
537 pub fn doc(&self) -> DocId {
538 if self.exhausted {
539 return u32::MAX;
540 }
541 if self.block_loaded {
542 debug_assert!(self.pos < self.doc_ids.len());
543 unsafe { *self.doc_ids.get_unchecked(self.pos) }
545 } else {
546 self.block_first_doc(self.block_idx)
547 }
548 }
549
550 #[inline]
551 pub fn ordinal(&self) -> u16 {
552 if !self.block_loaded || self.ordinals.is_empty() {
553 return 0;
554 }
555 debug_assert!(self.pos < self.ordinals.len());
556 unsafe { *self.ordinals.get_unchecked(self.pos) }
558 }
559
560 #[inline]
566 pub fn ordinal_mut(&mut self) -> u16 {
567 if !self.block_loaded {
568 return 0;
569 }
570 if !self.ordinals_loaded {
571 if let Some(ref block) = self.current_sparse_block {
572 block.decode_ordinals_into(&mut self.ordinals);
573 }
574 self.ordinals_loaded = true;
575 }
576 if self.ordinals.is_empty() {
577 return 0;
578 }
579 debug_assert!(self.pos < self.ordinals.len());
580 unsafe { *self.ordinals.get_unchecked(self.pos) }
581 }
582
583 #[inline]
584 pub fn score(&self) -> f32 {
585 if !self.block_loaded {
586 return 0.0;
587 }
588 debug_assert!(self.pos < self.scores.len());
589 unsafe { *self.scores.get_unchecked(self.pos) }
591 }
592
593 #[inline]
599 pub fn ensure_scores(&mut self) {
600 if self.block_loaded && self.scores.is_empty() {
601 self.compute_deferred_scores();
602 }
603 }
604
605 #[inline]
606 pub fn current_block_max_score(&self) -> f32 {
607 if self.exhausted {
608 return 0.0;
609 }
610 match &self.variant {
611 CursorVariant::Text { list, idf, .. } => {
612 let block_max_tf = list.block_max_tf(self.block_idx).unwrap_or(0) as f32;
613 super::bm25_upper_bound(block_max_tf.max(1.0), *idf)
614 }
615 CursorVariant::Sparse {
616 si,
617 query_weight,
618 skip_start,
619 ..
620 } => query_weight.abs() * si.read_skip_entry(*skip_start + self.block_idx).max_weight,
621 }
622 }
623
624 pub fn skip_to_next_block(&mut self) -> DocId {
627 if self.exhausted {
628 return u32::MAX;
629 }
630 self.block_idx += 1;
631 self.block_loaded = false;
632 if self.block_idx >= self.num_blocks {
633 self.exhausted = true;
634 return u32::MAX;
635 }
636 self.block_first_doc(self.block_idx)
637 }
638
639 #[inline]
640 fn advance_pos(&mut self) -> DocId {
641 self.pos += 1;
642 if self.pos >= self.doc_ids.len() {
643 self.block_idx += 1;
644 self.block_loaded = false;
645 if self.block_idx >= self.num_blocks {
646 self.exhausted = true;
647 return u32::MAX;
648 }
649 }
650 self.doc()
651 }
652
653 #[inline(never)]
655 fn compute_deferred_scores(&mut self) {
656 if let CursorVariant::Text {
657 list,
658 idf_times_k1_plus_1,
659 denom_tf_coeff,
660 denom_const,
661 tfs,
662 deferred_tf,
663 ..
664 } = &mut self.variant
665 && let Some((block_offset, tf_start, count)) = deferred_tf.take()
666 {
667 list.decode_block_tfs_deferred(block_offset, tf_start, count, tfs);
668 let num_scale = *idf_times_k1_plus_1;
669 let d_tf = *denom_tf_coeff;
670 let d_const = *denom_const;
671 self.scores.clear();
672 self.scores.resize(count, 0.0);
673 for i in 0..count {
674 let tf = unsafe { *tfs.get_unchecked(i) } as f32;
675 let score = (num_scale * tf) / (d_tf * tf + d_const);
676 unsafe {
677 *self.scores.get_unchecked_mut(i) = score;
678 }
679 }
680 }
681 }
682
683 pub async fn ensure_block_loaded(&mut self) -> crate::Result<bool> {
689 cursor_ensure_block!(self, load_block_direct, .await)
690 }
691
692 pub fn ensure_block_loaded_sync(&mut self) -> crate::Result<bool> {
693 cursor_ensure_block!(self, load_block_direct_sync,)
694 }
695
696 pub async fn advance(&mut self) -> crate::Result<DocId> {
697 cursor_advance!(self, ensure_block_loaded, .await)
698 }
699
700 pub fn advance_sync(&mut self) -> crate::Result<DocId> {
701 cursor_advance!(self, ensure_block_loaded_sync,)
702 }
703
704 pub async fn seek(&mut self, target: DocId) -> crate::Result<DocId> {
705 cursor_seek!(self, ensure_block_loaded, target, .await)
706 }
707
708 pub fn seek_sync(&mut self, target: DocId) -> crate::Result<DocId> {
709 cursor_seek!(self, ensure_block_loaded_sync, target,)
710 }
711
712 fn seek_prepare(&mut self, target: DocId) -> Option<DocId> {
713 if self.exhausted {
714 return Some(u32::MAX);
715 }
716
717 if self.block_loaded
719 && let Some(&last) = self.doc_ids.last()
720 {
721 if last >= target && self.doc_ids[self.pos] < target {
722 let remaining = &self.doc_ids[self.pos..];
723 self.pos += crate::structures::simd::find_first_ge_u32(remaining, target);
724 if self.pos >= self.doc_ids.len() {
725 self.block_idx += 1;
726 self.block_loaded = false;
727 if self.block_idx >= self.num_blocks {
728 self.exhausted = true;
729 return Some(u32::MAX);
730 }
731 }
732 return Some(self.doc());
733 }
734 if self.doc_ids[self.pos] >= target {
735 return Some(self.doc());
736 }
737 }
738
739 let lo = match &self.variant {
741 CursorVariant::Text { list, .. } => match list.seek_block(target, self.block_idx) {
743 Some(idx) => idx,
744 None => {
745 self.exhausted = true;
746 return Some(u32::MAX);
747 }
748 },
749 CursorVariant::Sparse { .. } => {
751 let mut lo = self.block_idx;
752 let mut hi = self.num_blocks;
753 while lo < hi {
754 let mid = lo + (hi - lo) / 2;
755 if self.block_last_doc(mid) < target {
756 lo = mid + 1;
757 } else {
758 hi = mid;
759 }
760 }
761 lo
762 }
763 };
764 if lo >= self.num_blocks {
765 self.exhausted = true;
766 return Some(u32::MAX);
767 }
768 if lo != self.block_idx || !self.block_loaded {
769 self.block_idx = lo;
770 self.block_loaded = false;
771 }
772 None
773 }
774
775 #[inline]
776 fn seek_finish(&mut self, target: DocId) -> bool {
777 if self.exhausted {
778 return false;
779 }
780 self.pos = crate::structures::simd::find_first_ge_u32(&self.doc_ids, target);
781 if self.pos >= self.doc_ids.len() {
782 self.block_idx += 1;
783 self.block_loaded = false;
784 if self.block_idx >= self.num_blocks {
785 self.exhausted = true;
786 return false;
787 }
788 return true;
789 }
790 false
791 }
792}
793
794macro_rules! bms_execute_loop {
799 ($self:ident, $ensure:ident, $advance:ident, $seek:ident, $($aw:tt)*) => {{
800 let n = $self.cursors.len();
801
802 for cursor in &mut $self.cursors {
804 cursor.$ensure() $($aw)* ?;
805 }
806
807 let mut docs_scored = 0u64;
808 let mut docs_skipped = 0u64;
809 let mut blocks_skipped = 0u64;
810 let mut conjunction_skipped = 0u64;
811 let mut ordinal_scores: Vec<(u16, f32)> = Vec::with_capacity(n * 2);
812 let _bms_start = std::time::Instant::now();
813
814 let inv_heap_factor = $self.inv_heap_factor;
815 let mut adjusted_threshold = $self.collector.threshold() * inv_heap_factor - 1e-6;
816
817 loop {
818 let partition = $self.find_partition();
819 if partition >= n {
820 break;
821 }
822
823 let mut min_doc = u32::MAX;
827 let mut at_min_mask = 0u64; for i in partition..n {
829 let doc = $self.cursors[i].doc();
830 match doc.cmp(&min_doc) {
831 std::cmp::Ordering::Less => {
832 min_doc = doc;
833 at_min_mask = 1u64 << (i as u32);
834 }
835 std::cmp::Ordering::Equal => {
836 at_min_mask |= 1u64 << (i as u32);
837 }
838 _ => {}
839 }
840 }
841 if min_doc == u32::MAX {
842 break;
843 }
844
845 let non_essential_upper = if partition > 0 {
846 $self.prefix_sums[partition - 1]
847 } else {
848 0.0
849 };
850
851 if $self.collector.len() >= $self.collector.k {
853 let mut present_upper: f32 = 0.0;
854 let mut mask = at_min_mask;
855 while mask != 0 {
856 let i = mask.trailing_zeros() as usize;
857 present_upper += $self.cursors[i].max_score;
858 mask &= mask - 1;
859 }
860
861 if present_upper + non_essential_upper < adjusted_threshold {
862 let mut mask = at_min_mask;
863 while mask != 0 {
864 let i = mask.trailing_zeros() as usize;
865 $self.cursors[i].$ensure() $($aw)* ?;
866 $self.cursors[i].$advance() $($aw)* ?;
867 mask &= mask - 1;
868 }
869 conjunction_skipped += 1;
870 continue;
871 }
872 }
873
874 if $self.collector.len() >= $self.collector.k {
876 let mut block_max_sum: f32 = 0.0;
877 let mut mask = at_min_mask;
878 while mask != 0 {
879 let i = mask.trailing_zeros() as usize;
880 block_max_sum += $self.cursors[i].current_block_max_score();
881 mask &= mask - 1;
882 }
883
884 if block_max_sum + non_essential_upper < adjusted_threshold {
885 let mut mask = at_min_mask;
886 while mask != 0 {
887 let i = mask.trailing_zeros() as usize;
888 $self.cursors[i].skip_to_next_block();
889 $self.cursors[i].$ensure() $($aw)* ?;
890 mask &= mask - 1;
891 }
892 blocks_skipped += 1;
893 continue;
894 }
895 }
896
897 if let Some(ref pred) = $self.predicate {
899 if !pred(min_doc) {
900 let mut mask = at_min_mask;
901 while mask != 0 {
902 let i = mask.trailing_zeros() as usize;
903 $self.cursors[i].$ensure() $($aw)* ?;
904 $self.cursors[i].$advance() $($aw)* ?;
905 mask &= mask - 1;
906 }
907 continue;
908 }
909 }
910
911 ordinal_scores.clear();
913 {
914 let mut mask = at_min_mask;
915 while mask != 0 {
916 let i = mask.trailing_zeros() as usize;
917 $self.cursors[i].$ensure() $($aw)* ?;
918 $self.cursors[i].ensure_scores();
919 while $self.cursors[i].doc() == min_doc {
920 let ord = $self.cursors[i].ordinal_mut();
921 let sc = $self.cursors[i].score();
922 ordinal_scores.push((ord, sc));
923 $self.cursors[i].$advance() $($aw)* ?;
924 }
925 mask &= mask - 1;
926 }
927 }
928
929 let essential_total: f32 = ordinal_scores.iter().map(|(_, s)| *s).sum();
930 if $self.collector.len() >= $self.collector.k
931 && essential_total + non_essential_upper < adjusted_threshold
932 {
933 docs_skipped += 1;
934 continue;
935 }
936
937 let mut running_total = essential_total;
939 for i in (0..partition).rev() {
940 if $self.collector.len() >= $self.collector.k
941 && running_total + $self.prefix_sums[i] < adjusted_threshold
942 {
943 break;
944 }
945
946 let doc = $self.cursors[i].$seek(min_doc) $($aw)* ?;
947 if doc == min_doc {
948 $self.cursors[i].ensure_scores();
949 while $self.cursors[i].doc() == min_doc {
950 let s = $self.cursors[i].score();
951 running_total += s;
952 let ord = $self.cursors[i].ordinal_mut();
953 ordinal_scores.push((ord, s));
954 $self.cursors[i].$advance() $($aw)* ?;
955 }
956 }
957 }
958
959 if ordinal_scores.len() == 1 {
962 let (ord, score) = ordinal_scores[0];
963 if $self.collector.insert_with_ordinal(min_doc, score, ord) {
964 docs_scored += 1;
965 adjusted_threshold = $self.collector.threshold() * inv_heap_factor - 1e-6;
966 } else {
967 docs_skipped += 1;
968 }
969 } else if !ordinal_scores.is_empty() {
970 if ordinal_scores.len() > 2 {
971 ordinal_scores.sort_unstable_by_key(|(ord, _)| *ord);
972 } else if ordinal_scores.len() == 2 && ordinal_scores[0].0 > ordinal_scores[1].0 {
973 ordinal_scores.swap(0, 1);
974 }
975 let mut j = 0;
976 while j < ordinal_scores.len() {
977 let current_ord = ordinal_scores[j].0;
978 let mut score = 0.0f32;
979 while j < ordinal_scores.len() && ordinal_scores[j].0 == current_ord {
980 score += ordinal_scores[j].1;
981 j += 1;
982 }
983 if $self
984 .collector
985 .insert_with_ordinal(min_doc, score, current_ord)
986 {
987 docs_scored += 1;
988 adjusted_threshold = $self.collector.threshold() * inv_heap_factor - 1e-6;
989 } else {
990 docs_skipped += 1;
991 }
992 }
993 }
994 }
995
996 let results: Vec<ScoredDoc> = $self
997 .collector
998 .into_sorted_results()
999 .into_iter()
1000 .map(|(doc_id, score, ordinal)| ScoredDoc {
1001 doc_id,
1002 score,
1003 ordinal,
1004 })
1005 .collect();
1006
1007 let _bms_elapsed_ms = _bms_start.elapsed().as_millis() as u64;
1008 if _bms_elapsed_ms > 500 {
1009 warn!(
1010 "slow MaxScore: {}ms, cursors={}, scored={}, skipped={}, blocks_skipped={}, conjunction_skipped={}, returned={}, top_score={:.4}",
1011 _bms_elapsed_ms,
1012 n,
1013 docs_scored,
1014 docs_skipped,
1015 blocks_skipped,
1016 conjunction_skipped,
1017 results.len(),
1018 results.first().map(|r| r.score).unwrap_or(0.0)
1019 );
1020 } else {
1021 debug!(
1022 "MaxScoreExecutor: {}ms, scored={}, skipped={}, blocks_skipped={}, conjunction_skipped={}, returned={}, top_score={:.4}",
1023 _bms_elapsed_ms,
1024 docs_scored,
1025 docs_skipped,
1026 blocks_skipped,
1027 conjunction_skipped,
1028 results.len(),
1029 results.first().map(|r| r.score).unwrap_or(0.0)
1030 );
1031 }
1032
1033 Ok(results)
1034 }};
1035}
1036
1037impl<'a> MaxScoreExecutor<'a> {
1038 pub(crate) fn new(mut cursors: Vec<TermCursor<'a>>, k: usize, heap_factor: f32) -> Self {
1043 if cursors.len() > super::MAX_QUERY_TERMS {
1047 cursors.sort_unstable_by(|a, b| b.max_score.total_cmp(&a.max_score));
1048 cursors.truncate(super::MAX_QUERY_TERMS);
1049 log::warn!(
1050 "MaxScore cursor count exceeded {}; retaining the strongest cursors",
1051 super::MAX_QUERY_TERMS
1052 );
1053 }
1054
1055 for c in &mut cursors {
1058 c.lazy_ordinals = true;
1059 }
1060
1061 cursors.sort_by(|a, b| {
1063 a.max_score
1064 .partial_cmp(&b.max_score)
1065 .unwrap_or(Ordering::Equal)
1066 });
1067
1068 let mut prefix_sums = Vec::with_capacity(cursors.len());
1069 let mut cumsum = 0.0f32;
1070 for c in &cursors {
1071 cumsum += c.max_score;
1072 prefix_sums.push(cumsum);
1073 }
1074
1075 let clamped_heap_factor = heap_factor.clamp(0.01, 1.0);
1076
1077 debug!(
1078 "Creating MaxScoreExecutor: num_cursors={}, k={}, total_upper={:.4}, heap_factor={:.2}",
1079 cursors.len(),
1080 k,
1081 cumsum,
1082 clamped_heap_factor
1083 );
1084
1085 Self {
1086 cursors,
1087 prefix_sums,
1088 collector: ScoreCollector::new(k),
1089 inv_heap_factor: 1.0 / clamped_heap_factor,
1090 predicate: None,
1091 metric_index: "unknown",
1092 metric_field: "unknown",
1093 }
1094 }
1095
1096 pub fn with_metric_labels(mut self, index: &'a str, field: &'a str) -> Self {
1098 self.metric_index = index;
1099 self.metric_field = field;
1100 self
1101 }
1102
1103 pub fn sparse(
1107 sparse_index: &'a crate::segment::SparseIndex,
1108 query_terms: Vec<(u32, f32)>,
1109 k: usize,
1110 heap_factor: f32,
1111 ) -> Self {
1112 let cursors: Vec<TermCursor<'a>> = query_terms
1113 .iter()
1114 .filter_map(|&(dim_id, qw)| {
1115 let (skip_start, skip_count, global_max, block_data_offset) =
1116 sparse_index.get_skip_range_full(dim_id)?;
1117 Some(TermCursor::sparse(
1118 sparse_index,
1119 qw,
1120 skip_start,
1121 skip_count,
1122 global_max,
1123 block_data_offset,
1124 ))
1125 })
1126 .collect();
1127 Self::new(cursors, k, heap_factor)
1128 }
1129
1130 pub fn text(
1134 posting_lists: Vec<(crate::structures::BlockPostingList, f32)>,
1135 avg_field_len: f32,
1136 k: usize,
1137 ) -> Self {
1138 let cursors: Vec<TermCursor<'a>> = posting_lists
1139 .into_iter()
1140 .map(|(pl, idf)| TermCursor::text(pl, idf, avg_field_len))
1141 .collect();
1142 Self::new(cursors, k, 1.0)
1143 }
1144
1145 #[inline]
1146 fn find_partition(&self) -> usize {
1147 let threshold = self.collector.threshold() * self.inv_heap_factor;
1151 self.prefix_sums.partition_point(|&sum| sum < threshold)
1154 }
1155
1156 pub fn with_predicate(mut self, predicate: super::DocPredicate<'a>) -> Self {
1162 self.predicate = Some(predicate);
1163 self
1164 }
1165
1166 pub fn seed_threshold(&mut self, initial_threshold: f32) {
1168 self.collector.seed_threshold(initial_threshold);
1169 }
1170
1171 pub async fn execute(mut self) -> crate::Result<Vec<ScoredDoc>> {
1173 if self.cursors.is_empty() {
1174 return Ok(Vec::new());
1175 }
1176 let t = crate::observe::Timer::start();
1177 let results = bms_execute_loop!(self, ensure_block_loaded, advance, seek, .await);
1178 if let Ok(r) = &results {
1179 crate::observe::maxscore_query(self.metric_index, self.metric_field, t.secs(), r.len());
1180 }
1181 results
1182 }
1183
1184 pub fn execute_sync(mut self) -> crate::Result<Vec<ScoredDoc>> {
1186 if self.cursors.is_empty() {
1187 return Ok(Vec::new());
1188 }
1189 let t = crate::observe::Timer::start();
1190 let results = bms_execute_loop!(self, ensure_block_loaded_sync, advance_sync, seek_sync,);
1191 if let Ok(r) = &results {
1192 crate::observe::maxscore_query(self.metric_index, self.metric_field, t.secs(), r.len());
1193 }
1194 results
1195 }
1196}
1197
1198#[cfg(test)]
1199mod tests {
1200 use super::*;
1201
1202 #[test]
1203 fn test_shared_threshold_monotonic_raise() {
1204 let shared = SharedThreshold::new();
1205 assert_eq!(shared.get(), 0.0);
1206
1207 shared.raise(2.5);
1208 assert_eq!(shared.get(), 2.5);
1209
1210 shared.raise(1.0);
1212 assert_eq!(shared.get(), 2.5);
1213
1214 shared.raise(4.0);
1216 assert_eq!(shared.get(), 4.0);
1217
1218 shared.raise(0.0);
1220 shared.raise(-3.0);
1221 shared.raise(f32::NAN);
1222 assert_eq!(shared.get(), 4.0);
1223
1224 let clone = shared.clone();
1226 clone.raise(9.0);
1227 assert_eq!(shared.get(), 9.0);
1228 }
1229
1230 #[test]
1231 fn test_shared_threshold_seed_matches_manual() {
1232 let mut seeded = ScoreCollector::new(2);
1235 seeded.seed_threshold(3.0);
1236 assert_eq!(seeded.threshold(), 3.0);
1237 assert!(!seeded.would_enter(3.0));
1239 assert!(seeded.would_enter(3.5));
1240 seeded.insert(1, 5.0);
1243 seeded.insert(2, 4.0);
1244 let results = seeded.into_sorted_results();
1245 assert_eq!(results.len(), 2);
1246 assert_eq!(results[0].0, 1);
1247 assert_eq!(results[1].0, 2);
1248 }
1249
1250 #[test]
1251 fn test_score_collector_basic() {
1252 let mut collector = ScoreCollector::new(3);
1253
1254 collector.insert(1, 1.0);
1255 collector.insert(2, 2.0);
1256 collector.insert(3, 3.0);
1257 assert_eq!(collector.threshold(), 1.0);
1258
1259 collector.insert(4, 4.0);
1260 assert_eq!(collector.threshold(), 2.0);
1261
1262 let results = collector.into_sorted_results();
1263 assert_eq!(results.len(), 3);
1264 assert_eq!(results[0].0, 4); assert_eq!(results[1].0, 3);
1266 assert_eq!(results[2].0, 2);
1267 }
1268
1269 #[test]
1270 fn test_score_collector_threshold() {
1271 let mut collector = ScoreCollector::new(2);
1272
1273 collector.insert(1, 5.0);
1274 collector.insert(2, 3.0);
1275 assert_eq!(collector.threshold(), 3.0);
1276
1277 assert!(!collector.would_enter(2.0));
1279 assert!(!collector.insert(3, 2.0));
1280
1281 assert!(collector.would_enter(4.0));
1283 assert!(collector.insert(4, 4.0));
1284 assert_eq!(collector.threshold(), 4.0);
1285 }
1286
1287 #[test]
1288 fn test_heap_entry_ordering() {
1289 let mut heap = BinaryHeap::new();
1290 heap.push(HeapEntry {
1291 doc_id: 1,
1292 score: 3.0,
1293 ordinal: 0,
1294 });
1295 heap.push(HeapEntry {
1296 doc_id: 2,
1297 score: 1.0,
1298 ordinal: 0,
1299 });
1300 heap.push(HeapEntry {
1301 doc_id: 3,
1302 score: 2.0,
1303 ordinal: 0,
1304 });
1305
1306 assert_eq!(heap.pop().unwrap().score, 1.0);
1308 assert_eq!(heap.pop().unwrap().score, 2.0);
1309 assert_eq!(heap.pop().unwrap().score, 3.0);
1310 }
1311}