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 == other.score && self.doc_id == other.doc_id
27 }
28}
29
30impl Eq for HeapEntry {}
31
32impl Ord for HeapEntry {
33 fn cmp(&self, other: &Self) -> Ordering {
34 other
37 .score
38 .total_cmp(&self.score)
39 .then(self.doc_id.cmp(&other.doc_id))
40 }
41}
42
43impl PartialOrd for HeapEntry {
44 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
45 Some(self.cmp(other))
46 }
47}
48
49pub struct ScoreCollector {
62 heap: BinaryHeap<HeapEntry>,
64 pub k: usize,
65 cached_threshold: f32,
68}
69
70impl ScoreCollector {
71 pub fn new(k: usize) -> Self {
73 let capacity = k.saturating_add(1).min(1_000_000);
75 Self {
76 heap: BinaryHeap::with_capacity(capacity),
77 k,
78 cached_threshold: 0.0,
79 }
80 }
81
82 #[inline]
84 pub fn threshold(&self) -> f32 {
85 self.cached_threshold
86 }
87
88 #[inline]
90 fn update_threshold(&mut self) {
91 self.cached_threshold = if self.heap.len() >= self.k {
92 self.heap.peek().map(|e| e.score).unwrap_or(0.0)
93 } else {
94 0.0
95 };
96 }
97
98 #[inline]
101 pub fn insert(&mut self, doc_id: DocId, score: f32) -> bool {
102 self.insert_with_ordinal(doc_id, score, 0)
103 }
104
105 #[inline]
108 pub fn insert_with_ordinal(&mut self, doc_id: DocId, score: f32, ordinal: u16) -> bool {
109 if self.heap.len() < self.k {
110 self.heap.push(HeapEntry {
111 doc_id,
112 score,
113 ordinal,
114 });
115 if self.heap.len() == self.k {
117 self.update_threshold();
118 }
119 true
120 } else if score > self.cached_threshold {
121 self.heap.push(HeapEntry {
122 doc_id,
123 score,
124 ordinal,
125 });
126 self.heap.pop(); self.update_threshold();
128 true
129 } else {
130 false
131 }
132 }
133
134 #[inline]
136 pub fn would_enter(&self, score: f32) -> bool {
137 self.heap.len() < self.k || score > self.cached_threshold
138 }
139
140 #[inline]
142 pub fn len(&self) -> usize {
143 self.heap.len()
144 }
145
146 #[inline]
148 pub fn is_empty(&self) -> bool {
149 self.heap.is_empty()
150 }
151
152 pub fn seed_threshold(&mut self, initial_threshold: f32) {
158 if initial_threshold > 0.0 && self.heap.is_empty() {
159 for _ in 0..self.k {
160 self.heap.push(HeapEntry {
161 doc_id: u32::MAX,
162 score: initial_threshold,
163 ordinal: 0,
164 });
165 }
166 self.update_threshold();
167 }
168 }
169
170 pub fn into_sorted_results(self) -> Vec<(DocId, f32, u16)> {
173 let mut results: Vec<(DocId, f32, u16)> = self
174 .heap
175 .into_vec()
176 .into_iter()
177 .filter(|e| e.doc_id != u32::MAX)
178 .map(|e| (e.doc_id, e.score, e.ordinal))
179 .collect();
180
181 results.sort_unstable_by(|a, b| b.1.total_cmp(&a.1).then(a.0.cmp(&b.0)));
183
184 results
185 }
186}
187
188#[derive(Debug, Clone, Copy)]
190pub struct ScoredDoc {
191 pub doc_id: DocId,
192 pub score: f32,
193 pub ordinal: u16,
195}
196
197pub struct MaxScoreExecutor<'a> {
208 metric_index: &'a str,
212 metric_field: &'a str,
213 cursors: Vec<TermCursor<'a>>,
214 prefix_sums: Vec<f32>,
215 collector: ScoreCollector,
216 inv_heap_factor: f32,
217 predicate: Option<super::DocPredicate<'a>>,
218}
219
220pub(crate) struct TermCursor<'a> {
229 pub max_score: f32,
230 num_blocks: usize,
231 block_idx: usize,
233 doc_ids: Vec<u32>,
234 scores: Vec<f32>,
235 ordinals: Vec<u16>,
236 pos: usize,
237 block_loaded: bool,
238 exhausted: bool,
239 lazy_ordinals: bool,
243 ordinals_loaded: bool,
245 current_sparse_block: Option<crate::structures::SparseBlock>,
247 variant: CursorVariant<'a>,
249}
250
251enum CursorVariant<'a> {
252 Text {
254 list: crate::structures::BlockPostingList,
255 idf: f32,
256 idf_times_k1_plus_1: f32,
258 denom_tf_coeff: f32,
260 denom_const: f32,
262 tfs: Vec<u32>,
263 deferred_tf: Option<(usize, usize, usize)>,
266 },
267 Sparse {
269 si: &'a crate::segment::SparseIndex,
270 query_weight: f32,
271 skip_start: usize,
272 block_data_offset: u64,
273 },
274}
275
276macro_rules! cursor_ensure_block {
284 ($self:ident, $load_block_fn:ident, $($aw:tt)*) => {{
285 if $self.exhausted || $self.block_loaded {
286 return Ok(!$self.exhausted);
287 }
288 match &mut $self.variant {
289 CursorVariant::Text {
290 list,
291 deferred_tf,
292 ..
293 } => {
294 if let Some(state) = list.decode_block_doc_ids_only($self.block_idx, &mut $self.doc_ids) {
295 *deferred_tf = Some(state);
296 $self.scores.clear();
297 $self.pos = 0;
298 $self.block_loaded = true;
299 Ok(true)
300 } else {
301 $self.exhausted = true;
302 Ok(false)
303 }
304 }
305 CursorVariant::Sparse {
306 si,
307 query_weight,
308 skip_start,
309 block_data_offset,
310 ..
311 } => {
312 let block = si
313 .$load_block_fn(*skip_start, *block_data_offset, $self.block_idx)
314 $($aw)* ?;
315 match block {
316 Some(b) => {
317 b.decode_doc_ids_into(&mut $self.doc_ids);
318 b.decode_scored_weights_into(*query_weight, &mut $self.scores);
319 if $self.lazy_ordinals {
320 $self.current_sparse_block = Some(b);
323 $self.ordinals_loaded = false;
324 } else {
325 b.decode_ordinals_into(&mut $self.ordinals);
326 $self.ordinals_loaded = true;
327 $self.current_sparse_block = None;
328 }
329 $self.pos = 0;
330 $self.block_loaded = true;
331 Ok(true)
332 }
333 None => {
334 $self.exhausted = true;
335 Ok(false)
336 }
337 }
338 }
339 }
340 }};
341}
342
343macro_rules! cursor_advance {
344 ($self:ident, $ensure_fn:ident, $($aw:tt)*) => {{
345 if $self.exhausted {
346 return Ok(u32::MAX);
347 }
348 $self.$ensure_fn() $($aw)* ?;
349 if $self.exhausted {
350 return Ok(u32::MAX);
351 }
352 Ok($self.advance_pos())
353 }};
354}
355
356macro_rules! cursor_seek {
357 ($self:ident, $ensure_fn:ident, $target:expr, $($aw:tt)*) => {{
358 if let Some(doc) = $self.seek_prepare($target) {
359 return Ok(doc);
360 }
361 $self.$ensure_fn() $($aw)* ?;
362 if $self.seek_finish($target) {
363 $self.$ensure_fn() $($aw)* ?;
364 }
365 Ok($self.doc())
366 }};
367}
368
369impl<'a> TermCursor<'a> {
370 pub fn text(
372 posting_list: crate::structures::BlockPostingList,
373 idf: f32,
374 avg_field_len: f32,
375 ) -> Self {
376 let max_tf = posting_list.max_tf() as f32;
377 let max_score = super::bm25_upper_bound(max_tf.max(1.0), idf);
378 let num_blocks = posting_list.num_blocks();
379 let safe_avg = avg_field_len.max(1.0);
380 Self {
381 max_score,
382 num_blocks,
383 block_idx: 0,
384 doc_ids: Vec::with_capacity(128),
385 scores: Vec::with_capacity(128),
386 ordinals: Vec::new(),
387 pos: 0,
388 block_loaded: false,
389 exhausted: num_blocks == 0,
390 lazy_ordinals: false,
391 ordinals_loaded: true, current_sparse_block: None,
393 variant: CursorVariant::Text {
394 list: posting_list,
395 idf,
396 idf_times_k1_plus_1: idf * (super::BM25_K1 + 1.0),
397 denom_tf_coeff: 1.0 + super::BM25_K1 * (super::BM25_B / safe_avg),
398 denom_const: super::BM25_K1 * (1.0 - super::BM25_B),
399 tfs: Vec::with_capacity(128),
400 deferred_tf: None,
401 },
402 }
403 }
404
405 pub fn sparse(
408 si: &'a crate::segment::SparseIndex,
409 query_weight: f32,
410 skip_start: usize,
411 skip_count: usize,
412 global_max_weight: f32,
413 block_data_offset: u64,
414 ) -> Self {
415 Self {
416 max_score: query_weight.abs() * global_max_weight,
417 num_blocks: skip_count,
418 block_idx: 0,
419 doc_ids: Vec::with_capacity(256),
420 scores: Vec::with_capacity(256),
421 ordinals: Vec::with_capacity(256),
422 pos: 0,
423 block_loaded: false,
424 exhausted: skip_count == 0,
425 lazy_ordinals: false,
426 ordinals_loaded: true,
427 current_sparse_block: None,
428 variant: CursorVariant::Sparse {
429 si,
430 query_weight,
431 skip_start,
432 block_data_offset,
433 },
434 }
435 }
436
437 #[inline]
440 fn block_first_doc(&self, idx: usize) -> DocId {
441 match &self.variant {
442 CursorVariant::Text { list, .. } => list.block_first_doc(idx).unwrap_or(u32::MAX),
443 CursorVariant::Sparse { si, skip_start, .. } => {
444 si.read_skip_entry(*skip_start + idx).first_doc
445 }
446 }
447 }
448
449 #[inline]
450 fn block_last_doc(&self, idx: usize) -> DocId {
451 match &self.variant {
452 CursorVariant::Text { list, .. } => list.block_last_doc(idx).unwrap_or(0),
453 CursorVariant::Sparse { si, skip_start, .. } => {
454 si.read_skip_entry(*skip_start + idx).last_doc
455 }
456 }
457 }
458
459 #[inline]
462 pub fn doc(&self) -> DocId {
463 if self.exhausted {
464 return u32::MAX;
465 }
466 if self.block_loaded {
467 debug_assert!(self.pos < self.doc_ids.len());
468 unsafe { *self.doc_ids.get_unchecked(self.pos) }
470 } else {
471 self.block_first_doc(self.block_idx)
472 }
473 }
474
475 #[inline]
476 pub fn ordinal(&self) -> u16 {
477 if !self.block_loaded || self.ordinals.is_empty() {
478 return 0;
479 }
480 debug_assert!(self.pos < self.ordinals.len());
481 unsafe { *self.ordinals.get_unchecked(self.pos) }
483 }
484
485 #[inline]
491 pub fn ordinal_mut(&mut self) -> u16 {
492 if !self.block_loaded {
493 return 0;
494 }
495 if !self.ordinals_loaded {
496 if let Some(ref block) = self.current_sparse_block {
497 block.decode_ordinals_into(&mut self.ordinals);
498 }
499 self.ordinals_loaded = true;
500 }
501 if self.ordinals.is_empty() {
502 return 0;
503 }
504 debug_assert!(self.pos < self.ordinals.len());
505 unsafe { *self.ordinals.get_unchecked(self.pos) }
506 }
507
508 #[inline]
509 pub fn score(&self) -> f32 {
510 if !self.block_loaded {
511 return 0.0;
512 }
513 debug_assert!(self.pos < self.scores.len());
514 unsafe { *self.scores.get_unchecked(self.pos) }
516 }
517
518 #[inline]
524 pub fn ensure_scores(&mut self) {
525 if self.block_loaded && self.scores.is_empty() {
526 self.compute_deferred_scores();
527 }
528 }
529
530 #[inline]
531 pub fn current_block_max_score(&self) -> f32 {
532 if self.exhausted {
533 return 0.0;
534 }
535 match &self.variant {
536 CursorVariant::Text { list, idf, .. } => {
537 let block_max_tf = list.block_max_tf(self.block_idx).unwrap_or(0) as f32;
538 super::bm25_upper_bound(block_max_tf.max(1.0), *idf)
539 }
540 CursorVariant::Sparse {
541 si,
542 query_weight,
543 skip_start,
544 ..
545 } => query_weight.abs() * si.read_skip_entry(*skip_start + self.block_idx).max_weight,
546 }
547 }
548
549 pub fn skip_to_next_block(&mut self) -> DocId {
552 if self.exhausted {
553 return u32::MAX;
554 }
555 self.block_idx += 1;
556 self.block_loaded = false;
557 if self.block_idx >= self.num_blocks {
558 self.exhausted = true;
559 return u32::MAX;
560 }
561 self.block_first_doc(self.block_idx)
562 }
563
564 #[inline]
565 fn advance_pos(&mut self) -> DocId {
566 self.pos += 1;
567 if self.pos >= self.doc_ids.len() {
568 self.block_idx += 1;
569 self.block_loaded = false;
570 if self.block_idx >= self.num_blocks {
571 self.exhausted = true;
572 return u32::MAX;
573 }
574 }
575 self.doc()
576 }
577
578 #[inline(never)]
580 fn compute_deferred_scores(&mut self) {
581 if let CursorVariant::Text {
582 list,
583 idf_times_k1_plus_1,
584 denom_tf_coeff,
585 denom_const,
586 tfs,
587 deferred_tf,
588 ..
589 } = &mut self.variant
590 && let Some((block_offset, tf_start, count)) = deferred_tf.take()
591 {
592 list.decode_block_tfs_deferred(block_offset, tf_start, count, tfs);
593 let num_scale = *idf_times_k1_plus_1;
594 let d_tf = *denom_tf_coeff;
595 let d_const = *denom_const;
596 self.scores.clear();
597 self.scores.resize(count, 0.0);
598 for i in 0..count {
599 let tf = unsafe { *tfs.get_unchecked(i) } as f32;
600 let score = (num_scale * tf) / (d_tf * tf + d_const);
601 unsafe {
602 *self.scores.get_unchecked_mut(i) = score;
603 }
604 }
605 }
606 }
607
608 pub async fn ensure_block_loaded(&mut self) -> crate::Result<bool> {
614 cursor_ensure_block!(self, load_block_direct, .await)
615 }
616
617 pub fn ensure_block_loaded_sync(&mut self) -> crate::Result<bool> {
618 cursor_ensure_block!(self, load_block_direct_sync,)
619 }
620
621 pub async fn advance(&mut self) -> crate::Result<DocId> {
622 cursor_advance!(self, ensure_block_loaded, .await)
623 }
624
625 pub fn advance_sync(&mut self) -> crate::Result<DocId> {
626 cursor_advance!(self, ensure_block_loaded_sync,)
627 }
628
629 pub async fn seek(&mut self, target: DocId) -> crate::Result<DocId> {
630 cursor_seek!(self, ensure_block_loaded, target, .await)
631 }
632
633 pub fn seek_sync(&mut self, target: DocId) -> crate::Result<DocId> {
634 cursor_seek!(self, ensure_block_loaded_sync, target,)
635 }
636
637 fn seek_prepare(&mut self, target: DocId) -> Option<DocId> {
638 if self.exhausted {
639 return Some(u32::MAX);
640 }
641
642 if self.block_loaded
644 && let Some(&last) = self.doc_ids.last()
645 {
646 if last >= target && self.doc_ids[self.pos] < target {
647 let remaining = &self.doc_ids[self.pos..];
648 self.pos += crate::structures::simd::find_first_ge_u32(remaining, target);
649 if self.pos >= self.doc_ids.len() {
650 self.block_idx += 1;
651 self.block_loaded = false;
652 if self.block_idx >= self.num_blocks {
653 self.exhausted = true;
654 return Some(u32::MAX);
655 }
656 }
657 return Some(self.doc());
658 }
659 if self.doc_ids[self.pos] >= target {
660 return Some(self.doc());
661 }
662 }
663
664 let lo = match &self.variant {
666 CursorVariant::Text { list, .. } => match list.seek_block(target, self.block_idx) {
668 Some(idx) => idx,
669 None => {
670 self.exhausted = true;
671 return Some(u32::MAX);
672 }
673 },
674 CursorVariant::Sparse { .. } => {
676 let mut lo = self.block_idx;
677 let mut hi = self.num_blocks;
678 while lo < hi {
679 let mid = lo + (hi - lo) / 2;
680 if self.block_last_doc(mid) < target {
681 lo = mid + 1;
682 } else {
683 hi = mid;
684 }
685 }
686 lo
687 }
688 };
689 if lo >= self.num_blocks {
690 self.exhausted = true;
691 return Some(u32::MAX);
692 }
693 if lo != self.block_idx || !self.block_loaded {
694 self.block_idx = lo;
695 self.block_loaded = false;
696 }
697 None
698 }
699
700 #[inline]
701 fn seek_finish(&mut self, target: DocId) -> bool {
702 if self.exhausted {
703 return false;
704 }
705 self.pos = crate::structures::simd::find_first_ge_u32(&self.doc_ids, target);
706 if self.pos >= self.doc_ids.len() {
707 self.block_idx += 1;
708 self.block_loaded = false;
709 if self.block_idx >= self.num_blocks {
710 self.exhausted = true;
711 return false;
712 }
713 return true;
714 }
715 false
716 }
717}
718
719macro_rules! bms_execute_loop {
724 ($self:ident, $ensure:ident, $advance:ident, $seek:ident, $($aw:tt)*) => {{
725 let n = $self.cursors.len();
726
727 for cursor in &mut $self.cursors {
729 cursor.$ensure() $($aw)* ?;
730 }
731
732 let mut docs_scored = 0u64;
733 let mut docs_skipped = 0u64;
734 let mut blocks_skipped = 0u64;
735 let mut conjunction_skipped = 0u64;
736 let mut ordinal_scores: Vec<(u16, f32)> = Vec::with_capacity(n * 2);
737 let _bms_start = std::time::Instant::now();
738
739 let inv_heap_factor = $self.inv_heap_factor;
740 let mut adjusted_threshold = $self.collector.threshold() * inv_heap_factor - 1e-6;
741
742 loop {
743 let partition = $self.find_partition();
744 if partition >= n {
745 break;
746 }
747
748 let mut min_doc = u32::MAX;
752 let mut at_min_mask = 0u64; for i in partition..n {
754 let doc = $self.cursors[i].doc();
755 match doc.cmp(&min_doc) {
756 std::cmp::Ordering::Less => {
757 min_doc = doc;
758 at_min_mask = 1u64 << (i as u32);
759 }
760 std::cmp::Ordering::Equal => {
761 at_min_mask |= 1u64 << (i as u32);
762 }
763 _ => {}
764 }
765 }
766 if min_doc == u32::MAX {
767 break;
768 }
769
770 let non_essential_upper = if partition > 0 {
771 $self.prefix_sums[partition - 1]
772 } else {
773 0.0
774 };
775
776 if $self.collector.len() >= $self.collector.k {
778 let mut present_upper: f32 = 0.0;
779 let mut mask = at_min_mask;
780 while mask != 0 {
781 let i = mask.trailing_zeros() as usize;
782 present_upper += $self.cursors[i].max_score;
783 mask &= mask - 1;
784 }
785
786 if present_upper + non_essential_upper <= adjusted_threshold {
787 let mut mask = at_min_mask;
788 while mask != 0 {
789 let i = mask.trailing_zeros() as usize;
790 $self.cursors[i].$ensure() $($aw)* ?;
791 $self.cursors[i].$advance() $($aw)* ?;
792 mask &= mask - 1;
793 }
794 conjunction_skipped += 1;
795 continue;
796 }
797 }
798
799 if $self.collector.len() >= $self.collector.k {
801 let mut block_max_sum: f32 = 0.0;
802 let mut mask = at_min_mask;
803 while mask != 0 {
804 let i = mask.trailing_zeros() as usize;
805 block_max_sum += $self.cursors[i].current_block_max_score();
806 mask &= mask - 1;
807 }
808
809 if block_max_sum + non_essential_upper <= adjusted_threshold {
810 let mut mask = at_min_mask;
811 while mask != 0 {
812 let i = mask.trailing_zeros() as usize;
813 $self.cursors[i].skip_to_next_block();
814 $self.cursors[i].$ensure() $($aw)* ?;
815 mask &= mask - 1;
816 }
817 blocks_skipped += 1;
818 continue;
819 }
820 }
821
822 if let Some(ref pred) = $self.predicate {
824 if !pred(min_doc) {
825 let mut mask = at_min_mask;
826 while mask != 0 {
827 let i = mask.trailing_zeros() as usize;
828 $self.cursors[i].$ensure() $($aw)* ?;
829 $self.cursors[i].$advance() $($aw)* ?;
830 mask &= mask - 1;
831 }
832 continue;
833 }
834 }
835
836 ordinal_scores.clear();
838 {
839 let mut mask = at_min_mask;
840 while mask != 0 {
841 let i = mask.trailing_zeros() as usize;
842 $self.cursors[i].$ensure() $($aw)* ?;
843 $self.cursors[i].ensure_scores();
844 while $self.cursors[i].doc() == min_doc {
845 let ord = $self.cursors[i].ordinal_mut();
846 let sc = $self.cursors[i].score();
847 ordinal_scores.push((ord, sc));
848 $self.cursors[i].$advance() $($aw)* ?;
849 }
850 mask &= mask - 1;
851 }
852 }
853
854 let essential_total: f32 = ordinal_scores.iter().map(|(_, s)| *s).sum();
855 if $self.collector.len() >= $self.collector.k
856 && essential_total + non_essential_upper <= adjusted_threshold
857 {
858 docs_skipped += 1;
859 continue;
860 }
861
862 let mut running_total = essential_total;
864 for i in (0..partition).rev() {
865 if $self.collector.len() >= $self.collector.k
866 && running_total + $self.prefix_sums[i] <= adjusted_threshold
867 {
868 break;
869 }
870
871 let doc = $self.cursors[i].$seek(min_doc) $($aw)* ?;
872 if doc == min_doc {
873 $self.cursors[i].ensure_scores();
874 while $self.cursors[i].doc() == min_doc {
875 let s = $self.cursors[i].score();
876 running_total += s;
877 let ord = $self.cursors[i].ordinal_mut();
878 ordinal_scores.push((ord, s));
879 $self.cursors[i].$advance() $($aw)* ?;
880 }
881 }
882 }
883
884 if ordinal_scores.len() == 1 {
887 let (ord, score) = ordinal_scores[0];
888 if $self.collector.insert_with_ordinal(min_doc, score, ord) {
889 docs_scored += 1;
890 adjusted_threshold = $self.collector.threshold() * inv_heap_factor - 1e-6;
891 } else {
892 docs_skipped += 1;
893 }
894 } else if !ordinal_scores.is_empty() {
895 if ordinal_scores.len() > 2 {
896 ordinal_scores.sort_unstable_by_key(|(ord, _)| *ord);
897 } else if ordinal_scores.len() == 2 && ordinal_scores[0].0 > ordinal_scores[1].0 {
898 ordinal_scores.swap(0, 1);
899 }
900 let mut j = 0;
901 while j < ordinal_scores.len() {
902 let current_ord = ordinal_scores[j].0;
903 let mut score = 0.0f32;
904 while j < ordinal_scores.len() && ordinal_scores[j].0 == current_ord {
905 score += ordinal_scores[j].1;
906 j += 1;
907 }
908 if $self
909 .collector
910 .insert_with_ordinal(min_doc, score, current_ord)
911 {
912 docs_scored += 1;
913 adjusted_threshold = $self.collector.threshold() * inv_heap_factor - 1e-6;
914 } else {
915 docs_skipped += 1;
916 }
917 }
918 }
919 }
920
921 let results: Vec<ScoredDoc> = $self
922 .collector
923 .into_sorted_results()
924 .into_iter()
925 .map(|(doc_id, score, ordinal)| ScoredDoc {
926 doc_id,
927 score,
928 ordinal,
929 })
930 .collect();
931
932 let _bms_elapsed_ms = _bms_start.elapsed().as_millis() as u64;
933 if _bms_elapsed_ms > 500 {
934 warn!(
935 "slow MaxScore: {}ms, cursors={}, scored={}, skipped={}, blocks_skipped={}, conjunction_skipped={}, returned={}, top_score={:.4}",
936 _bms_elapsed_ms,
937 n,
938 docs_scored,
939 docs_skipped,
940 blocks_skipped,
941 conjunction_skipped,
942 results.len(),
943 results.first().map(|r| r.score).unwrap_or(0.0)
944 );
945 } else {
946 debug!(
947 "MaxScoreExecutor: {}ms, scored={}, skipped={}, blocks_skipped={}, conjunction_skipped={}, returned={}, top_score={:.4}",
948 _bms_elapsed_ms,
949 docs_scored,
950 docs_skipped,
951 blocks_skipped,
952 conjunction_skipped,
953 results.len(),
954 results.first().map(|r| r.score).unwrap_or(0.0)
955 );
956 }
957
958 Ok(results)
959 }};
960}
961
962impl<'a> MaxScoreExecutor<'a> {
963 pub(crate) fn new(mut cursors: Vec<TermCursor<'a>>, k: usize, heap_factor: f32) -> Self {
968 for c in &mut cursors {
971 c.lazy_ordinals = true;
972 }
973
974 cursors.sort_by(|a, b| {
976 a.max_score
977 .partial_cmp(&b.max_score)
978 .unwrap_or(Ordering::Equal)
979 });
980
981 let mut prefix_sums = Vec::with_capacity(cursors.len());
982 let mut cumsum = 0.0f32;
983 for c in &cursors {
984 cumsum += c.max_score;
985 prefix_sums.push(cumsum);
986 }
987
988 let clamped_heap_factor = heap_factor.clamp(0.01, 1.0);
989
990 debug!(
991 "Creating MaxScoreExecutor: num_cursors={}, k={}, total_upper={:.4}, heap_factor={:.2}",
992 cursors.len(),
993 k,
994 cumsum,
995 clamped_heap_factor
996 );
997
998 Self {
999 cursors,
1000 prefix_sums,
1001 collector: ScoreCollector::new(k),
1002 inv_heap_factor: 1.0 / clamped_heap_factor,
1003 predicate: None,
1004 metric_index: "unknown",
1005 metric_field: "unknown",
1006 }
1007 }
1008
1009 pub fn with_metric_labels(mut self, index: &'a str, field: &'a str) -> Self {
1011 self.metric_index = index;
1012 self.metric_field = field;
1013 self
1014 }
1015
1016 pub fn sparse(
1020 sparse_index: &'a crate::segment::SparseIndex,
1021 query_terms: Vec<(u32, f32)>,
1022 k: usize,
1023 heap_factor: f32,
1024 ) -> Self {
1025 let cursors: Vec<TermCursor<'a>> = query_terms
1026 .iter()
1027 .filter_map(|&(dim_id, qw)| {
1028 let (skip_start, skip_count, global_max, block_data_offset) =
1029 sparse_index.get_skip_range_full(dim_id)?;
1030 Some(TermCursor::sparse(
1031 sparse_index,
1032 qw,
1033 skip_start,
1034 skip_count,
1035 global_max,
1036 block_data_offset,
1037 ))
1038 })
1039 .collect();
1040 Self::new(cursors, k, heap_factor)
1041 }
1042
1043 pub fn text(
1047 posting_lists: Vec<(crate::structures::BlockPostingList, f32)>,
1048 avg_field_len: f32,
1049 k: usize,
1050 ) -> Self {
1051 let cursors: Vec<TermCursor<'a>> = posting_lists
1052 .into_iter()
1053 .map(|(pl, idf)| TermCursor::text(pl, idf, avg_field_len))
1054 .collect();
1055 Self::new(cursors, k, 1.0)
1056 }
1057
1058 #[inline]
1059 fn find_partition(&self) -> usize {
1060 let threshold = self.collector.threshold() * self.inv_heap_factor;
1064 self.prefix_sums.partition_point(|&sum| sum <= threshold)
1065 }
1066
1067 pub fn with_predicate(mut self, predicate: super::DocPredicate<'a>) -> Self {
1073 self.predicate = Some(predicate);
1074 self
1075 }
1076
1077 pub fn seed_threshold(&mut self, initial_threshold: f32) {
1079 self.collector.seed_threshold(initial_threshold);
1080 }
1081
1082 pub async fn execute(mut self) -> crate::Result<Vec<ScoredDoc>> {
1084 if self.cursors.is_empty() {
1085 return Ok(Vec::new());
1086 }
1087 let t = crate::observe::Timer::start();
1088 let results = bms_execute_loop!(self, ensure_block_loaded, advance, seek, .await);
1089 if let Ok(r) = &results {
1090 crate::observe::maxscore_query(self.metric_index, self.metric_field, t.secs(), r.len());
1091 }
1092 results
1093 }
1094
1095 pub fn execute_sync(mut self) -> crate::Result<Vec<ScoredDoc>> {
1097 if self.cursors.is_empty() {
1098 return Ok(Vec::new());
1099 }
1100 let t = crate::observe::Timer::start();
1101 let results = bms_execute_loop!(self, ensure_block_loaded_sync, advance_sync, seek_sync,);
1102 if let Ok(r) = &results {
1103 crate::observe::maxscore_query(self.metric_index, self.metric_field, t.secs(), r.len());
1104 }
1105 results
1106 }
1107}
1108
1109#[cfg(test)]
1110mod tests {
1111 use super::*;
1112
1113 #[test]
1114 fn test_score_collector_basic() {
1115 let mut collector = ScoreCollector::new(3);
1116
1117 collector.insert(1, 1.0);
1118 collector.insert(2, 2.0);
1119 collector.insert(3, 3.0);
1120 assert_eq!(collector.threshold(), 1.0);
1121
1122 collector.insert(4, 4.0);
1123 assert_eq!(collector.threshold(), 2.0);
1124
1125 let results = collector.into_sorted_results();
1126 assert_eq!(results.len(), 3);
1127 assert_eq!(results[0].0, 4); assert_eq!(results[1].0, 3);
1129 assert_eq!(results[2].0, 2);
1130 }
1131
1132 #[test]
1133 fn test_score_collector_threshold() {
1134 let mut collector = ScoreCollector::new(2);
1135
1136 collector.insert(1, 5.0);
1137 collector.insert(2, 3.0);
1138 assert_eq!(collector.threshold(), 3.0);
1139
1140 assert!(!collector.would_enter(2.0));
1142 assert!(!collector.insert(3, 2.0));
1143
1144 assert!(collector.would_enter(4.0));
1146 assert!(collector.insert(4, 4.0));
1147 assert_eq!(collector.threshold(), 4.0);
1148 }
1149
1150 #[test]
1151 fn test_heap_entry_ordering() {
1152 let mut heap = BinaryHeap::new();
1153 heap.push(HeapEntry {
1154 doc_id: 1,
1155 score: 3.0,
1156 ordinal: 0,
1157 });
1158 heap.push(HeapEntry {
1159 doc_id: 2,
1160 score: 1.0,
1161 ordinal: 0,
1162 });
1163 heap.push(HeapEntry {
1164 doc_id: 3,
1165 score: 2.0,
1166 ordinal: 0,
1167 });
1168
1169 assert_eq!(heap.pop().unwrap().score, 1.0);
1171 assert_eq!(heap.pop().unwrap().score, 2.0);
1172 assert_eq!(heap.pop().unwrap().score, 3.0);
1173 }
1174}