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(Debug, Clone, Copy)]
212pub struct ScoredDoc {
213 pub doc_id: DocId,
214 pub score: f32,
215 pub ordinal: u16,
217}
218
219pub struct MaxScoreExecutor<'a> {
230 metric_index: &'a str,
234 metric_field: &'a str,
235 cursors: Vec<TermCursor<'a>>,
236 prefix_sums: Vec<f32>,
237 collector: ScoreCollector,
238 inv_heap_factor: f32,
239 predicate: Option<super::DocPredicate<'a>>,
240}
241
242pub(crate) struct TermCursor<'a> {
251 pub max_score: f32,
252 num_blocks: usize,
253 block_idx: usize,
255 doc_ids: Vec<u32>,
256 scores: Vec<f32>,
257 ordinals: Vec<u16>,
258 pos: usize,
259 block_loaded: bool,
260 exhausted: bool,
261 lazy_ordinals: bool,
265 ordinals_loaded: bool,
267 current_sparse_block: Option<crate::structures::SparseBlock>,
269 variant: CursorVariant<'a>,
271}
272
273enum CursorVariant<'a> {
274 Text {
276 list: crate::structures::BlockPostingList,
277 idf: f32,
278 idf_times_k1_plus_1: f32,
280 denom_tf_coeff: f32,
282 denom_const: f32,
284 tfs: Vec<u32>,
285 deferred_tf: Option<(usize, usize, usize)>,
288 },
289 Sparse {
291 si: &'a crate::segment::SparseIndex,
292 query_weight: f32,
293 skip_start: usize,
294 block_data_offset: u64,
295 },
296}
297
298macro_rules! cursor_ensure_block {
306 ($self:ident, $load_block_fn:ident, $($aw:tt)*) => {{
307 if $self.exhausted || $self.block_loaded {
308 return Ok(!$self.exhausted);
309 }
310 match &mut $self.variant {
311 CursorVariant::Text {
312 list,
313 deferred_tf,
314 ..
315 } => {
316 if let Some(state) = list.decode_block_doc_ids_only($self.block_idx, &mut $self.doc_ids) {
317 *deferred_tf = Some(state);
318 $self.scores.clear();
319 $self.pos = 0;
320 $self.block_loaded = true;
321 Ok(true)
322 } else {
323 $self.exhausted = true;
324 Ok(false)
325 }
326 }
327 CursorVariant::Sparse {
328 si,
329 query_weight,
330 skip_start,
331 block_data_offset,
332 ..
333 } => {
334 let block = si
335 .$load_block_fn(*skip_start, *block_data_offset, $self.block_idx)
336 $($aw)* ?;
337 match block {
338 Some(b) => {
339 b.decode_doc_ids_into(&mut $self.doc_ids);
340 b.decode_scored_weights_into(*query_weight, &mut $self.scores);
341 if $self.lazy_ordinals {
342 $self.current_sparse_block = Some(b);
345 $self.ordinals_loaded = false;
346 } else {
347 b.decode_ordinals_into(&mut $self.ordinals);
348 $self.ordinals_loaded = true;
349 $self.current_sparse_block = None;
350 }
351 $self.pos = 0;
352 $self.block_loaded = true;
353 Ok(true)
354 }
355 None => {
356 $self.exhausted = true;
357 Ok(false)
358 }
359 }
360 }
361 }
362 }};
363}
364
365macro_rules! cursor_advance {
366 ($self:ident, $ensure_fn:ident, $($aw:tt)*) => {{
367 if $self.exhausted {
368 return Ok(u32::MAX);
369 }
370 $self.$ensure_fn() $($aw)* ?;
371 if $self.exhausted {
372 return Ok(u32::MAX);
373 }
374 Ok($self.advance_pos())
375 }};
376}
377
378macro_rules! cursor_seek {
379 ($self:ident, $ensure_fn:ident, $target:expr, $($aw:tt)*) => {{
380 if let Some(doc) = $self.seek_prepare($target) {
381 return Ok(doc);
382 }
383 $self.$ensure_fn() $($aw)* ?;
384 if $self.seek_finish($target) {
385 $self.$ensure_fn() $($aw)* ?;
386 }
387 Ok($self.doc())
388 }};
389}
390
391impl<'a> TermCursor<'a> {
392 pub fn text(
394 posting_list: crate::structures::BlockPostingList,
395 idf: f32,
396 avg_field_len: f32,
397 ) -> Self {
398 let max_tf = posting_list.max_tf() as f32;
399 let max_score = super::bm25_upper_bound(max_tf.max(1.0), idf);
400 let num_blocks = posting_list.num_blocks();
401 let safe_avg = avg_field_len.max(1.0);
402 Self {
403 max_score,
404 num_blocks,
405 block_idx: 0,
406 doc_ids: Vec::with_capacity(128),
407 scores: Vec::with_capacity(128),
408 ordinals: Vec::new(),
409 pos: 0,
410 block_loaded: false,
411 exhausted: num_blocks == 0,
412 lazy_ordinals: false,
413 ordinals_loaded: true, current_sparse_block: None,
415 variant: CursorVariant::Text {
416 list: posting_list,
417 idf,
418 idf_times_k1_plus_1: idf * (super::BM25_K1 + 1.0),
419 denom_tf_coeff: 1.0 + super::BM25_K1 * (super::BM25_B / safe_avg),
420 denom_const: super::BM25_K1 * (1.0 - super::BM25_B),
421 tfs: Vec::with_capacity(128),
422 deferred_tf: None,
423 },
424 }
425 }
426
427 pub fn sparse(
430 si: &'a crate::segment::SparseIndex,
431 query_weight: f32,
432 skip_start: usize,
433 skip_count: usize,
434 global_max_weight: f32,
435 block_data_offset: u64,
436 ) -> Self {
437 Self {
438 max_score: query_weight.abs() * global_max_weight,
439 num_blocks: skip_count,
440 block_idx: 0,
441 doc_ids: Vec::with_capacity(256),
442 scores: Vec::with_capacity(256),
443 ordinals: Vec::with_capacity(256),
444 pos: 0,
445 block_loaded: false,
446 exhausted: skip_count == 0,
447 lazy_ordinals: false,
448 ordinals_loaded: true,
449 current_sparse_block: None,
450 variant: CursorVariant::Sparse {
451 si,
452 query_weight,
453 skip_start,
454 block_data_offset,
455 },
456 }
457 }
458
459 #[inline]
462 fn block_first_doc(&self, idx: usize) -> DocId {
463 match &self.variant {
464 CursorVariant::Text { list, .. } => list.block_first_doc(idx).unwrap_or(u32::MAX),
465 CursorVariant::Sparse { si, skip_start, .. } => {
466 si.read_skip_entry(*skip_start + idx).first_doc
467 }
468 }
469 }
470
471 #[inline]
472 fn block_last_doc(&self, idx: usize) -> DocId {
473 match &self.variant {
474 CursorVariant::Text { list, .. } => list.block_last_doc(idx).unwrap_or(0),
475 CursorVariant::Sparse { si, skip_start, .. } => {
476 si.read_skip_entry(*skip_start + idx).last_doc
477 }
478 }
479 }
480
481 #[inline]
484 pub fn doc(&self) -> DocId {
485 if self.exhausted {
486 return u32::MAX;
487 }
488 if self.block_loaded {
489 debug_assert!(self.pos < self.doc_ids.len());
490 unsafe { *self.doc_ids.get_unchecked(self.pos) }
492 } else {
493 self.block_first_doc(self.block_idx)
494 }
495 }
496
497 #[inline]
498 pub fn ordinal(&self) -> u16 {
499 if !self.block_loaded || self.ordinals.is_empty() {
500 return 0;
501 }
502 debug_assert!(self.pos < self.ordinals.len());
503 unsafe { *self.ordinals.get_unchecked(self.pos) }
505 }
506
507 #[inline]
513 pub fn ordinal_mut(&mut self) -> u16 {
514 if !self.block_loaded {
515 return 0;
516 }
517 if !self.ordinals_loaded {
518 if let Some(ref block) = self.current_sparse_block {
519 block.decode_ordinals_into(&mut self.ordinals);
520 }
521 self.ordinals_loaded = true;
522 }
523 if self.ordinals.is_empty() {
524 return 0;
525 }
526 debug_assert!(self.pos < self.ordinals.len());
527 unsafe { *self.ordinals.get_unchecked(self.pos) }
528 }
529
530 #[inline]
531 pub fn score(&self) -> f32 {
532 if !self.block_loaded {
533 return 0.0;
534 }
535 debug_assert!(self.pos < self.scores.len());
536 unsafe { *self.scores.get_unchecked(self.pos) }
538 }
539
540 #[inline]
546 pub fn ensure_scores(&mut self) {
547 if self.block_loaded && self.scores.is_empty() {
548 self.compute_deferred_scores();
549 }
550 }
551
552 #[inline]
553 pub fn current_block_max_score(&self) -> f32 {
554 if self.exhausted {
555 return 0.0;
556 }
557 match &self.variant {
558 CursorVariant::Text { list, idf, .. } => {
559 let block_max_tf = list.block_max_tf(self.block_idx).unwrap_or(0) as f32;
560 super::bm25_upper_bound(block_max_tf.max(1.0), *idf)
561 }
562 CursorVariant::Sparse {
563 si,
564 query_weight,
565 skip_start,
566 ..
567 } => query_weight.abs() * si.read_skip_entry(*skip_start + self.block_idx).max_weight,
568 }
569 }
570
571 pub fn skip_to_next_block(&mut self) -> DocId {
574 if self.exhausted {
575 return u32::MAX;
576 }
577 self.block_idx += 1;
578 self.block_loaded = false;
579 if self.block_idx >= self.num_blocks {
580 self.exhausted = true;
581 return u32::MAX;
582 }
583 self.block_first_doc(self.block_idx)
584 }
585
586 #[inline]
587 fn advance_pos(&mut self) -> DocId {
588 self.pos += 1;
589 if self.pos >= self.doc_ids.len() {
590 self.block_idx += 1;
591 self.block_loaded = false;
592 if self.block_idx >= self.num_blocks {
593 self.exhausted = true;
594 return u32::MAX;
595 }
596 }
597 self.doc()
598 }
599
600 #[inline(never)]
602 fn compute_deferred_scores(&mut self) {
603 if let CursorVariant::Text {
604 list,
605 idf_times_k1_plus_1,
606 denom_tf_coeff,
607 denom_const,
608 tfs,
609 deferred_tf,
610 ..
611 } = &mut self.variant
612 && let Some((block_offset, tf_start, count)) = deferred_tf.take()
613 {
614 list.decode_block_tfs_deferred(block_offset, tf_start, count, tfs);
615 let num_scale = *idf_times_k1_plus_1;
616 let d_tf = *denom_tf_coeff;
617 let d_const = *denom_const;
618 self.scores.clear();
619 self.scores.resize(count, 0.0);
620 for i in 0..count {
621 let tf = unsafe { *tfs.get_unchecked(i) } as f32;
622 let score = (num_scale * tf) / (d_tf * tf + d_const);
623 unsafe {
624 *self.scores.get_unchecked_mut(i) = score;
625 }
626 }
627 }
628 }
629
630 pub async fn ensure_block_loaded(&mut self) -> crate::Result<bool> {
636 cursor_ensure_block!(self, load_block_direct, .await)
637 }
638
639 pub fn ensure_block_loaded_sync(&mut self) -> crate::Result<bool> {
640 cursor_ensure_block!(self, load_block_direct_sync,)
641 }
642
643 pub async fn advance(&mut self) -> crate::Result<DocId> {
644 cursor_advance!(self, ensure_block_loaded, .await)
645 }
646
647 pub fn advance_sync(&mut self) -> crate::Result<DocId> {
648 cursor_advance!(self, ensure_block_loaded_sync,)
649 }
650
651 pub async fn seek(&mut self, target: DocId) -> crate::Result<DocId> {
652 cursor_seek!(self, ensure_block_loaded, target, .await)
653 }
654
655 pub fn seek_sync(&mut self, target: DocId) -> crate::Result<DocId> {
656 cursor_seek!(self, ensure_block_loaded_sync, target,)
657 }
658
659 fn seek_prepare(&mut self, target: DocId) -> Option<DocId> {
660 if self.exhausted {
661 return Some(u32::MAX);
662 }
663
664 if self.block_loaded
666 && let Some(&last) = self.doc_ids.last()
667 {
668 if last >= target && self.doc_ids[self.pos] < target {
669 let remaining = &self.doc_ids[self.pos..];
670 self.pos += crate::structures::simd::find_first_ge_u32(remaining, target);
671 if self.pos >= self.doc_ids.len() {
672 self.block_idx += 1;
673 self.block_loaded = false;
674 if self.block_idx >= self.num_blocks {
675 self.exhausted = true;
676 return Some(u32::MAX);
677 }
678 }
679 return Some(self.doc());
680 }
681 if self.doc_ids[self.pos] >= target {
682 return Some(self.doc());
683 }
684 }
685
686 let lo = match &self.variant {
688 CursorVariant::Text { list, .. } => match list.seek_block(target, self.block_idx) {
690 Some(idx) => idx,
691 None => {
692 self.exhausted = true;
693 return Some(u32::MAX);
694 }
695 },
696 CursorVariant::Sparse { .. } => {
698 let mut lo = self.block_idx;
699 let mut hi = self.num_blocks;
700 while lo < hi {
701 let mid = lo + (hi - lo) / 2;
702 if self.block_last_doc(mid) < target {
703 lo = mid + 1;
704 } else {
705 hi = mid;
706 }
707 }
708 lo
709 }
710 };
711 if lo >= self.num_blocks {
712 self.exhausted = true;
713 return Some(u32::MAX);
714 }
715 if lo != self.block_idx || !self.block_loaded {
716 self.block_idx = lo;
717 self.block_loaded = false;
718 }
719 None
720 }
721
722 #[inline]
723 fn seek_finish(&mut self, target: DocId) -> bool {
724 if self.exhausted {
725 return false;
726 }
727 self.pos = crate::structures::simd::find_first_ge_u32(&self.doc_ids, target);
728 if self.pos >= self.doc_ids.len() {
729 self.block_idx += 1;
730 self.block_loaded = false;
731 if self.block_idx >= self.num_blocks {
732 self.exhausted = true;
733 return false;
734 }
735 return true;
736 }
737 false
738 }
739}
740
741macro_rules! bms_execute_loop {
746 ($self:ident, $ensure:ident, $advance:ident, $seek:ident, $($aw:tt)*) => {{
747 let n = $self.cursors.len();
748
749 for cursor in &mut $self.cursors {
751 cursor.$ensure() $($aw)* ?;
752 }
753
754 let mut docs_scored = 0u64;
755 let mut docs_skipped = 0u64;
756 let mut blocks_skipped = 0u64;
757 let mut conjunction_skipped = 0u64;
758 let mut ordinal_scores: Vec<(u16, f32)> = Vec::with_capacity(n * 2);
759 let _bms_start = std::time::Instant::now();
760
761 let inv_heap_factor = $self.inv_heap_factor;
762 let mut adjusted_threshold = $self.collector.threshold() * inv_heap_factor - 1e-6;
763
764 loop {
765 let partition = $self.find_partition();
766 if partition >= n {
767 break;
768 }
769
770 let mut min_doc = u32::MAX;
774 let mut at_min_mask = 0u64; for i in partition..n {
776 let doc = $self.cursors[i].doc();
777 match doc.cmp(&min_doc) {
778 std::cmp::Ordering::Less => {
779 min_doc = doc;
780 at_min_mask = 1u64 << (i as u32);
781 }
782 std::cmp::Ordering::Equal => {
783 at_min_mask |= 1u64 << (i as u32);
784 }
785 _ => {}
786 }
787 }
788 if min_doc == u32::MAX {
789 break;
790 }
791
792 let non_essential_upper = if partition > 0 {
793 $self.prefix_sums[partition - 1]
794 } else {
795 0.0
796 };
797
798 if $self.collector.len() >= $self.collector.k {
800 let mut present_upper: f32 = 0.0;
801 let mut mask = at_min_mask;
802 while mask != 0 {
803 let i = mask.trailing_zeros() as usize;
804 present_upper += $self.cursors[i].max_score;
805 mask &= mask - 1;
806 }
807
808 if present_upper + non_essential_upper < adjusted_threshold {
809 let mut mask = at_min_mask;
810 while mask != 0 {
811 let i = mask.trailing_zeros() as usize;
812 $self.cursors[i].$ensure() $($aw)* ?;
813 $self.cursors[i].$advance() $($aw)* ?;
814 mask &= mask - 1;
815 }
816 conjunction_skipped += 1;
817 continue;
818 }
819 }
820
821 if $self.collector.len() >= $self.collector.k {
823 let mut block_max_sum: f32 = 0.0;
824 let mut mask = at_min_mask;
825 while mask != 0 {
826 let i = mask.trailing_zeros() as usize;
827 block_max_sum += $self.cursors[i].current_block_max_score();
828 mask &= mask - 1;
829 }
830
831 if block_max_sum + non_essential_upper < adjusted_threshold {
832 let mut mask = at_min_mask;
833 while mask != 0 {
834 let i = mask.trailing_zeros() as usize;
835 $self.cursors[i].skip_to_next_block();
836 $self.cursors[i].$ensure() $($aw)* ?;
837 mask &= mask - 1;
838 }
839 blocks_skipped += 1;
840 continue;
841 }
842 }
843
844 if let Some(ref pred) = $self.predicate {
846 if !pred(min_doc) {
847 let mut mask = at_min_mask;
848 while mask != 0 {
849 let i = mask.trailing_zeros() as usize;
850 $self.cursors[i].$ensure() $($aw)* ?;
851 $self.cursors[i].$advance() $($aw)* ?;
852 mask &= mask - 1;
853 }
854 continue;
855 }
856 }
857
858 ordinal_scores.clear();
860 {
861 let mut mask = at_min_mask;
862 while mask != 0 {
863 let i = mask.trailing_zeros() as usize;
864 $self.cursors[i].$ensure() $($aw)* ?;
865 $self.cursors[i].ensure_scores();
866 while $self.cursors[i].doc() == min_doc {
867 let ord = $self.cursors[i].ordinal_mut();
868 let sc = $self.cursors[i].score();
869 ordinal_scores.push((ord, sc));
870 $self.cursors[i].$advance() $($aw)* ?;
871 }
872 mask &= mask - 1;
873 }
874 }
875
876 let essential_total: f32 = ordinal_scores.iter().map(|(_, s)| *s).sum();
877 if $self.collector.len() >= $self.collector.k
878 && essential_total + non_essential_upper < adjusted_threshold
879 {
880 docs_skipped += 1;
881 continue;
882 }
883
884 let mut running_total = essential_total;
886 for i in (0..partition).rev() {
887 if $self.collector.len() >= $self.collector.k
888 && running_total + $self.prefix_sums[i] < adjusted_threshold
889 {
890 break;
891 }
892
893 let doc = $self.cursors[i].$seek(min_doc) $($aw)* ?;
894 if doc == min_doc {
895 $self.cursors[i].ensure_scores();
896 while $self.cursors[i].doc() == min_doc {
897 let s = $self.cursors[i].score();
898 running_total += s;
899 let ord = $self.cursors[i].ordinal_mut();
900 ordinal_scores.push((ord, s));
901 $self.cursors[i].$advance() $($aw)* ?;
902 }
903 }
904 }
905
906 if ordinal_scores.len() == 1 {
909 let (ord, score) = ordinal_scores[0];
910 if $self.collector.insert_with_ordinal(min_doc, score, ord) {
911 docs_scored += 1;
912 adjusted_threshold = $self.collector.threshold() * inv_heap_factor - 1e-6;
913 } else {
914 docs_skipped += 1;
915 }
916 } else if !ordinal_scores.is_empty() {
917 if ordinal_scores.len() > 2 {
918 ordinal_scores.sort_unstable_by_key(|(ord, _)| *ord);
919 } else if ordinal_scores.len() == 2 && ordinal_scores[0].0 > ordinal_scores[1].0 {
920 ordinal_scores.swap(0, 1);
921 }
922 let mut j = 0;
923 while j < ordinal_scores.len() {
924 let current_ord = ordinal_scores[j].0;
925 let mut score = 0.0f32;
926 while j < ordinal_scores.len() && ordinal_scores[j].0 == current_ord {
927 score += ordinal_scores[j].1;
928 j += 1;
929 }
930 if $self
931 .collector
932 .insert_with_ordinal(min_doc, score, current_ord)
933 {
934 docs_scored += 1;
935 adjusted_threshold = $self.collector.threshold() * inv_heap_factor - 1e-6;
936 } else {
937 docs_skipped += 1;
938 }
939 }
940 }
941 }
942
943 let results: Vec<ScoredDoc> = $self
944 .collector
945 .into_sorted_results()
946 .into_iter()
947 .map(|(doc_id, score, ordinal)| ScoredDoc {
948 doc_id,
949 score,
950 ordinal,
951 })
952 .collect();
953
954 let _bms_elapsed_ms = _bms_start.elapsed().as_millis() as u64;
955 if _bms_elapsed_ms > 500 {
956 warn!(
957 "slow MaxScore: {}ms, cursors={}, scored={}, skipped={}, blocks_skipped={}, conjunction_skipped={}, returned={}, top_score={:.4}",
958 _bms_elapsed_ms,
959 n,
960 docs_scored,
961 docs_skipped,
962 blocks_skipped,
963 conjunction_skipped,
964 results.len(),
965 results.first().map(|r| r.score).unwrap_or(0.0)
966 );
967 } else {
968 debug!(
969 "MaxScoreExecutor: {}ms, scored={}, skipped={}, blocks_skipped={}, conjunction_skipped={}, returned={}, top_score={:.4}",
970 _bms_elapsed_ms,
971 docs_scored,
972 docs_skipped,
973 blocks_skipped,
974 conjunction_skipped,
975 results.len(),
976 results.first().map(|r| r.score).unwrap_or(0.0)
977 );
978 }
979
980 Ok(results)
981 }};
982}
983
984impl<'a> MaxScoreExecutor<'a> {
985 pub(crate) fn new(mut cursors: Vec<TermCursor<'a>>, k: usize, heap_factor: f32) -> Self {
990 if cursors.len() > super::MAX_QUERY_TERMS {
994 cursors.sort_unstable_by(|a, b| b.max_score.total_cmp(&a.max_score));
995 cursors.truncate(super::MAX_QUERY_TERMS);
996 log::warn!(
997 "MaxScore cursor count exceeded {}; retaining the strongest cursors",
998 super::MAX_QUERY_TERMS
999 );
1000 }
1001
1002 for c in &mut cursors {
1005 c.lazy_ordinals = true;
1006 }
1007
1008 cursors.sort_by(|a, b| {
1010 a.max_score
1011 .partial_cmp(&b.max_score)
1012 .unwrap_or(Ordering::Equal)
1013 });
1014
1015 let mut prefix_sums = Vec::with_capacity(cursors.len());
1016 let mut cumsum = 0.0f32;
1017 for c in &cursors {
1018 cumsum += c.max_score;
1019 prefix_sums.push(cumsum);
1020 }
1021
1022 let clamped_heap_factor = heap_factor.clamp(0.01, 1.0);
1023
1024 debug!(
1025 "Creating MaxScoreExecutor: num_cursors={}, k={}, total_upper={:.4}, heap_factor={:.2}",
1026 cursors.len(),
1027 k,
1028 cumsum,
1029 clamped_heap_factor
1030 );
1031
1032 Self {
1033 cursors,
1034 prefix_sums,
1035 collector: ScoreCollector::new(k),
1036 inv_heap_factor: 1.0 / clamped_heap_factor,
1037 predicate: None,
1038 metric_index: "unknown",
1039 metric_field: "unknown",
1040 }
1041 }
1042
1043 pub fn with_metric_labels(mut self, index: &'a str, field: &'a str) -> Self {
1045 self.metric_index = index;
1046 self.metric_field = field;
1047 self
1048 }
1049
1050 pub fn sparse(
1054 sparse_index: &'a crate::segment::SparseIndex,
1055 query_terms: Vec<(u32, f32)>,
1056 k: usize,
1057 heap_factor: f32,
1058 ) -> Self {
1059 let cursors: Vec<TermCursor<'a>> = query_terms
1060 .iter()
1061 .filter_map(|&(dim_id, qw)| {
1062 let (skip_start, skip_count, global_max, block_data_offset) =
1063 sparse_index.get_skip_range_full(dim_id)?;
1064 Some(TermCursor::sparse(
1065 sparse_index,
1066 qw,
1067 skip_start,
1068 skip_count,
1069 global_max,
1070 block_data_offset,
1071 ))
1072 })
1073 .collect();
1074 Self::new(cursors, k, heap_factor)
1075 }
1076
1077 pub fn text(
1081 posting_lists: Vec<(crate::structures::BlockPostingList, f32)>,
1082 avg_field_len: f32,
1083 k: usize,
1084 ) -> Self {
1085 let cursors: Vec<TermCursor<'a>> = posting_lists
1086 .into_iter()
1087 .map(|(pl, idf)| TermCursor::text(pl, idf, avg_field_len))
1088 .collect();
1089 Self::new(cursors, k, 1.0)
1090 }
1091
1092 #[inline]
1093 fn find_partition(&self) -> usize {
1094 let threshold = self.collector.threshold() * self.inv_heap_factor;
1098 self.prefix_sums.partition_point(|&sum| sum < threshold)
1101 }
1102
1103 pub fn with_predicate(mut self, predicate: super::DocPredicate<'a>) -> Self {
1109 self.predicate = Some(predicate);
1110 self
1111 }
1112
1113 pub fn seed_threshold(&mut self, initial_threshold: f32) {
1115 self.collector.seed_threshold(initial_threshold);
1116 }
1117
1118 pub async fn execute(mut self) -> crate::Result<Vec<ScoredDoc>> {
1120 if self.cursors.is_empty() {
1121 return Ok(Vec::new());
1122 }
1123 let t = crate::observe::Timer::start();
1124 let results = bms_execute_loop!(self, ensure_block_loaded, advance, seek, .await);
1125 if let Ok(r) = &results {
1126 crate::observe::maxscore_query(self.metric_index, self.metric_field, t.secs(), r.len());
1127 }
1128 results
1129 }
1130
1131 pub fn execute_sync(mut self) -> crate::Result<Vec<ScoredDoc>> {
1133 if self.cursors.is_empty() {
1134 return Ok(Vec::new());
1135 }
1136 let t = crate::observe::Timer::start();
1137 let results = bms_execute_loop!(self, ensure_block_loaded_sync, advance_sync, seek_sync,);
1138 if let Ok(r) = &results {
1139 crate::observe::maxscore_query(self.metric_index, self.metric_field, t.secs(), r.len());
1140 }
1141 results
1142 }
1143}
1144
1145#[cfg(test)]
1146mod tests {
1147 use super::*;
1148
1149 #[test]
1150 fn test_score_collector_basic() {
1151 let mut collector = ScoreCollector::new(3);
1152
1153 collector.insert(1, 1.0);
1154 collector.insert(2, 2.0);
1155 collector.insert(3, 3.0);
1156 assert_eq!(collector.threshold(), 1.0);
1157
1158 collector.insert(4, 4.0);
1159 assert_eq!(collector.threshold(), 2.0);
1160
1161 let results = collector.into_sorted_results();
1162 assert_eq!(results.len(), 3);
1163 assert_eq!(results[0].0, 4); assert_eq!(results[1].0, 3);
1165 assert_eq!(results[2].0, 2);
1166 }
1167
1168 #[test]
1169 fn test_score_collector_threshold() {
1170 let mut collector = ScoreCollector::new(2);
1171
1172 collector.insert(1, 5.0);
1173 collector.insert(2, 3.0);
1174 assert_eq!(collector.threshold(), 3.0);
1175
1176 assert!(!collector.would_enter(2.0));
1178 assert!(!collector.insert(3, 2.0));
1179
1180 assert!(collector.would_enter(4.0));
1182 assert!(collector.insert(4, 4.0));
1183 assert_eq!(collector.threshold(), 4.0);
1184 }
1185
1186 #[test]
1187 fn test_heap_entry_ordering() {
1188 let mut heap = BinaryHeap::new();
1189 heap.push(HeapEntry {
1190 doc_id: 1,
1191 score: 3.0,
1192 ordinal: 0,
1193 });
1194 heap.push(HeapEntry {
1195 doc_id: 2,
1196 score: 1.0,
1197 ordinal: 0,
1198 });
1199 heap.push(HeapEntry {
1200 doc_id: 3,
1201 score: 2.0,
1202 ordinal: 0,
1203 });
1204
1205 assert_eq!(heap.pop().unwrap().score, 1.0);
1207 assert_eq!(heap.pop().unwrap().score, 2.0);
1208 assert_eq!(heap.pop().unwrap().score, 3.0);
1209 }
1210}