1use std::cmp::Ordering;
10use std::collections::BinaryHeap;
11
12use log::{debug, warn};
13
14use crate::DocId;
15
16const MAX_INITIAL_SCORE_COLLECTOR_CAPACITY: usize = 8 * 1024;
20
21#[derive(Clone, Copy)]
23pub struct HeapEntry {
24 pub doc_id: DocId,
25 pub score: f32,
26 pub ordinal: u16,
27}
28
29impl PartialEq for HeapEntry {
30 fn eq(&self, other: &Self) -> bool {
31 self.score.to_bits() == other.score.to_bits()
32 && self.doc_id == other.doc_id
33 && self.ordinal == other.ordinal
34 }
35}
36
37impl Eq for HeapEntry {}
38
39impl Ord for HeapEntry {
40 fn cmp(&self, other: &Self) -> Ordering {
41 other
44 .score
45 .total_cmp(&self.score)
46 .then_with(|| self.doc_id.cmp(&other.doc_id))
47 .then_with(|| self.ordinal.cmp(&other.ordinal))
48 }
49}
50
51impl PartialOrd for HeapEntry {
52 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
53 Some(self.cmp(other))
54 }
55}
56
57pub struct ScoreCollector {
70 heap: BinaryHeap<HeapEntry>,
72 pub k: usize,
73 cached_threshold: f32,
76 virtual_threshold: Option<f32>,
80}
81
82impl ScoreCollector {
83 pub fn new(k: usize) -> Self {
85 Self {
86 heap: BinaryHeap::with_capacity(k.min(MAX_INITIAL_SCORE_COLLECTOR_CAPACITY)),
87 k,
88 cached_threshold: 0.0,
89 virtual_threshold: None,
90 }
91 }
92
93 #[inline]
95 pub fn threshold(&self) -> f32 {
96 self.cached_threshold
97 }
98
99 #[inline]
101 fn update_threshold(&mut self) {
102 self.cached_threshold = if let Some(threshold) = self.virtual_threshold {
103 threshold
104 } else if self.heap.len() >= self.k {
105 self.heap.peek().map(|e| e.score).unwrap_or(0.0)
106 } else {
107 0.0
108 };
109 }
110
111 #[inline]
114 pub fn insert(&mut self, doc_id: DocId, score: f32) -> bool {
115 self.insert_with_ordinal(doc_id, score, 0)
116 }
117
118 #[inline]
121 pub fn insert_with_ordinal(&mut self, doc_id: DocId, score: f32, ordinal: u16) -> bool {
122 if self.k == 0 {
123 return false;
124 }
125 let entry = HeapEntry {
126 doc_id,
127 score,
128 ordinal,
129 };
130 if self.heap.len() < self.k {
131 if let Some(threshold) = self.virtual_threshold {
132 let sentinel = HeapEntry {
133 doc_id: u32::MAX,
134 score: threshold,
135 ordinal: 0,
136 };
137 if entry >= sentinel {
138 return false;
139 }
140 }
141
142 self.heap.push(entry);
143 if self.heap.len() == self.k {
145 self.virtual_threshold = None;
146 self.update_threshold();
147 }
148 true
149 } else if self.heap.peek().is_some_and(|worst| entry < *worst) {
150 {
151 let mut worst = self.heap.peek_mut().expect("full heap has a root");
152 *worst = entry;
153 }
154 self.update_threshold();
155 true
156 } else {
157 false
158 }
159 }
160
161 #[inline]
163 pub fn would_enter(&self, score: f32) -> bool {
164 self.len() < self.k || score > self.cached_threshold
165 }
166
167 #[inline]
170 pub fn would_enter_candidate(&self, doc_id: DocId, score: f32, ordinal: u16) -> bool {
171 if self.k == 0 {
172 return false;
173 }
174 let entry = HeapEntry {
175 doc_id,
176 score,
177 ordinal,
178 };
179 if let Some(threshold) = self.virtual_threshold {
180 let sentinel = HeapEntry {
181 doc_id: u32::MAX,
182 score: threshold,
183 ordinal: 0,
184 };
185 entry < sentinel
186 } else {
187 self.heap.len() < self.k || self.heap.peek().is_some_and(|worst| entry < *worst)
188 }
189 }
190
191 #[inline]
193 pub fn len(&self) -> usize {
194 if self.virtual_threshold.is_some() {
195 self.k
196 } else {
197 self.heap.len()
198 }
199 }
200
201 #[inline]
203 pub fn real_len(&self) -> usize {
204 self.heap.len()
205 }
206
207 #[inline]
209 pub fn is_empty(&self) -> bool {
210 self.len() == 0
211 }
212
213 pub fn seed_threshold(&mut self, initial_threshold: f32) {
220 if initial_threshold <= 0.0
221 || self.k == 0
222 || (self.len() >= self.k && initial_threshold <= self.cached_threshold)
223 {
224 return;
225 }
226
227 let sentinel = HeapEntry {
228 doc_id: u32::MAX,
229 score: initial_threshold,
230 ordinal: 0,
231 };
232
233 if let Some(current_threshold) = self.virtual_threshold {
237 let current = HeapEntry {
238 doc_id: u32::MAX,
239 score: current_threshold,
240 ordinal: 0,
241 };
242 if sentinel >= current {
243 return;
244 }
245 } else if self.heap.len() >= self.k
246 && !self.heap.peek().is_some_and(|worst| sentinel < *worst)
247 {
248 return;
249 }
250
251 self.virtual_threshold = Some(initial_threshold);
252 while self.heap.peek().is_some_and(|worst| sentinel < *worst) {
253 self.heap.pop();
254 }
255 self.update_threshold();
256 }
257
258 pub fn into_sorted_results(self) -> Vec<(DocId, f32, u16)> {
261 let mut results: Vec<(DocId, f32, u16)> = self
262 .heap
263 .into_vec()
264 .into_iter()
265 .filter(|e| e.doc_id != u32::MAX)
266 .map(|e| (e.doc_id, e.score, e.ordinal))
267 .collect();
268
269 results.sort_unstable_by(|a, b| {
271 b.1.total_cmp(&a.1)
272 .then_with(|| a.0.cmp(&b.0))
273 .then_with(|| a.2.cmp(&b.2))
274 });
275
276 results
277 }
278}
279
280#[derive(Clone, Debug)]
303pub struct SharedThreshold {
304 floor: std::sync::Arc<std::sync::atomic::AtomicU32>,
305 k: usize,
308 deadline: Option<std::time::Instant>,
311 truncated: std::sync::Arc<std::sync::atomic::AtomicBool>,
313}
314
315impl Default for SharedThreshold {
316 fn default() -> Self {
317 Self::new()
318 }
319}
320
321impl SharedThreshold {
322 pub fn new() -> Self {
326 Self::with_depth(usize::MAX)
327 }
328
329 pub fn for_limit(limit: usize) -> Self {
331 Self::with_depth(limit)
332 }
333
334 fn with_depth(k: usize) -> Self {
335 Self {
336 floor: std::sync::Arc::new(std::sync::atomic::AtomicU32::new(0)),
338 k,
339 deadline: None,
340 truncated: std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)),
341 }
342 }
343
344 pub fn with_deadline(mut self, deadline: Option<std::time::Instant>) -> Self {
346 self.deadline = deadline;
347 self
348 }
349
350 pub fn deadline(&self) -> Option<std::time::Instant> {
352 self.deadline
353 }
354
355 #[inline]
357 pub fn expired(&self) -> bool {
358 self.deadline
359 .is_some_and(|deadline| std::time::Instant::now() >= deadline)
360 }
361
362 pub fn mark_truncated(&self) {
364 self.truncated
365 .store(true, std::sync::atomic::Ordering::Relaxed);
366 }
367
368 pub fn truncated(&self) -> bool {
370 self.truncated.load(std::sync::atomic::Ordering::Relaxed)
371 }
372
373 #[inline]
376 pub(crate) fn covers(&self, heap_depth: usize) -> bool {
377 heap_depth >= self.k
378 }
379
380 #[inline]
382 pub fn get(&self) -> f32 {
383 f32::from_bits(self.floor.load(std::sync::atomic::Ordering::Relaxed))
384 }
385
386 pub fn raise(&self, score: f32) {
391 if score <= 0.0 {
394 return;
395 }
396 use std::sync::atomic::Ordering::Relaxed;
397 let bits = score.to_bits();
398 let mut cur = self.floor.load(Relaxed);
399 while f32::from_bits(cur) < score {
400 match self
401 .floor
402 .compare_exchange_weak(cur, bits, Relaxed, Relaxed)
403 {
404 Ok(_) => break,
405 Err(actual) => cur = actual,
406 }
407 }
408 }
409}
410
411#[derive(Debug, Clone, Copy)]
413pub struct ScoredDoc {
414 pub doc_id: DocId,
415 pub score: f32,
416 pub ordinal: u16,
418}
419
420pub struct MaxScoreExecutor<'a> {
431 metric_index: &'a str,
435 metric_field: &'a str,
436 cursors: Vec<TermCursor<'a>>,
437 prefix_sums: Vec<f32>,
438 collector: ScoreCollector,
439 inv_heap_factor: f32,
440 predicate: Option<super::DocPredicate<'a>>,
441 budget: Option<SharedThreshold>,
444}
445
446#[derive(Clone, Copy)]
450pub enum LengthSource<'a> {
451 Chunks(&'a crate::segment::chunk_map::ChunkMap),
452 Docs(&'a crate::segment::chunk_map::DocLengths),
453}
454
455impl LengthSource<'_> {
456 #[inline]
457 pub fn length(&self, id: u32) -> u32 {
458 match self {
459 LengthSource::Chunks(map) => map.length(id),
460 LengthSource::Docs(lengths) => lengths.length(id),
461 }
462 }
463}
464
465pub(crate) struct TermCursor<'a> {
474 pub max_score: f32,
475 num_blocks: usize,
476 block_idx: usize,
478 doc_ids: Vec<u32>,
479 scores: Vec<f32>,
480 ordinals: Vec<u16>,
481 pos: usize,
482 block_loaded: bool,
483 exhausted: bool,
484 lazy_ordinals: bool,
488 ordinals_loaded: bool,
490 current_sparse_block: Option<crate::structures::SparseBlock>,
492 variant: CursorVariant<'a>,
494}
495
496#[allow(clippy::large_enum_variant)]
499enum CursorVariant<'a> {
500 Text {
502 list: crate::structures::BlockPostingList,
503 idf: f32,
504 idf_times_k1_plus_1: f32,
506 denom_tf_coeff: f32,
508 denom_const: f32,
510 denom_len_coeff: f32,
513 lengths: Option<LengthSource<'a>>,
516 length_bounds: bool,
520 avg_len: f32,
522 params: super::Bm25Params,
524 tfs: Vec<u32>,
525 deferred_tf: Option<(usize, usize, usize)>,
528 },
529 Sparse {
531 si: &'a crate::segment::SparseIndex,
532 query_weight: f32,
533 skip_start: usize,
534 block_data_offset: u64,
535 },
536}
537
538macro_rules! cursor_ensure_block {
546 ($self:ident, $load_block_fn:ident, $($aw:tt)*) => {{
547 if $self.exhausted || $self.block_loaded {
548 return Ok(!$self.exhausted);
549 }
550 match &mut $self.variant {
551 CursorVariant::Text {
552 list,
553 deferred_tf,
554 ..
555 } => {
556 if let Some(state) = list.decode_block_doc_ids_only($self.block_idx, &mut $self.doc_ids) {
557 *deferred_tf = Some(state);
558 $self.scores.clear();
559 $self.pos = 0;
560 $self.block_loaded = true;
561 Ok(true)
562 } else {
563 $self.exhausted = true;
564 Ok(false)
565 }
566 }
567 CursorVariant::Sparse {
568 si,
569 query_weight,
570 skip_start,
571 block_data_offset,
572 ..
573 } => {
574 let block = si
575 .$load_block_fn(*skip_start, *block_data_offset, $self.block_idx)
576 $($aw)* ?;
577 match block {
578 Some(b) => {
579 b.decode_doc_ids_into(&mut $self.doc_ids);
580 b.decode_scored_weights_into(*query_weight, &mut $self.scores);
581 if $self.lazy_ordinals {
582 $self.current_sparse_block = Some(b);
585 $self.ordinals_loaded = false;
586 } else {
587 b.decode_ordinals_into(&mut $self.ordinals);
588 $self.ordinals_loaded = true;
589 $self.current_sparse_block = None;
590 }
591 $self.pos = 0;
592 $self.block_loaded = true;
593 Ok(true)
594 }
595 None => {
596 $self.exhausted = true;
597 Ok(false)
598 }
599 }
600 }
601 }
602 }};
603}
604
605macro_rules! cursor_advance {
606 ($self:ident, $ensure_fn:ident, $($aw:tt)*) => {{
607 if $self.exhausted {
608 return Ok(u32::MAX);
609 }
610 $self.$ensure_fn() $($aw)* ?;
611 if $self.exhausted {
612 return Ok(u32::MAX);
613 }
614 Ok($self.advance_pos())
615 }};
616}
617
618macro_rules! cursor_seek {
619 ($self:ident, $ensure_fn:ident, $target:expr, $($aw:tt)*) => {{
620 if let Some(doc) = $self.seek_prepare($target) {
621 return Ok(doc);
622 }
623 $self.$ensure_fn() $($aw)* ?;
624 if $self.seek_finish($target) {
625 $self.$ensure_fn() $($aw)* ?;
626 }
627 Ok($self.doc())
628 }};
629}
630
631impl<'a> TermCursor<'a> {
632 pub fn text_with_params(
634 posting_list: crate::structures::BlockPostingList,
635 idf: f32,
636 avg_field_len: f32,
637 lengths: Option<LengthSource<'a>>,
638 params: super::Bm25Params,
639 ) -> Self {
640 Self::text_with_lengths(posting_list, idf, avg_field_len, lengths, params)
641 }
642
643 fn text_with_lengths(
644 posting_list: crate::structures::BlockPostingList,
645 idf: f32,
646 avg_field_len: f32,
647 lengths: Option<LengthSource<'a>>,
648 params: super::Bm25Params,
649 ) -> Self {
650 let max_tf = posting_list.max_tf() as f32;
651 let safe_avg = avg_field_len.max(1.0);
652 let length_bounds = lengths.is_some() && posting_list.min_len().is_some();
653 let max_score = match posting_list.min_len() {
654 Some(min_len) if length_bounds => {
655 params.upper_bound_with_len(max_tf.max(1.0), idf, min_len as f32, safe_avg)
656 }
657 _ => params.upper_bound(max_tf.max(1.0), idf),
658 };
659 let num_blocks = posting_list.num_blocks();
660 Self {
661 max_score,
662 num_blocks,
663 block_idx: 0,
664 doc_ids: Vec::with_capacity(128),
665 scores: Vec::with_capacity(128),
666 ordinals: Vec::new(),
667 pos: 0,
668 block_loaded: false,
669 exhausted: num_blocks == 0,
670 lazy_ordinals: false,
671 ordinals_loaded: true, current_sparse_block: None,
673 variant: CursorVariant::Text {
674 list: posting_list,
675 idf,
676 idf_times_k1_plus_1: idf * (params.k1 + 1.0),
677 denom_tf_coeff: 1.0 + params.k1 * (params.b / safe_avg),
678 denom_const: params.k1 * (1.0 - params.b),
679 denom_len_coeff: params.k1 * params.b / safe_avg,
680 lengths,
681 length_bounds,
682 avg_len: safe_avg,
683 params,
684 tfs: Vec::with_capacity(128),
685 deferred_tf: None,
686 },
687 }
688 }
689
690 pub fn sparse(
693 si: &'a crate::segment::SparseIndex,
694 query_weight: f32,
695 skip_start: usize,
696 skip_count: usize,
697 global_max_weight: f32,
698 block_data_offset: u64,
699 ) -> Self {
700 Self {
701 max_score: query_weight.abs() * global_max_weight,
702 num_blocks: skip_count,
703 block_idx: 0,
704 doc_ids: Vec::with_capacity(256),
705 scores: Vec::with_capacity(256),
706 ordinals: Vec::with_capacity(256),
707 pos: 0,
708 block_loaded: false,
709 exhausted: skip_count == 0,
710 lazy_ordinals: false,
711 ordinals_loaded: true,
712 current_sparse_block: None,
713 variant: CursorVariant::Sparse {
714 si,
715 query_weight,
716 skip_start,
717 block_data_offset,
718 },
719 }
720 }
721
722 #[inline]
725 fn block_first_doc(&self, idx: usize) -> DocId {
726 match &self.variant {
727 CursorVariant::Text { list, .. } => list.block_first_doc(idx).unwrap_or(u32::MAX),
728 CursorVariant::Sparse { si, skip_start, .. } => {
729 si.read_skip_entry(*skip_start + idx).first_doc
730 }
731 }
732 }
733
734 #[inline]
735 fn block_last_doc(&self, idx: usize) -> DocId {
736 match &self.variant {
737 CursorVariant::Text { list, .. } => list.block_last_doc(idx).unwrap_or(0),
738 CursorVariant::Sparse { si, skip_start, .. } => {
739 si.read_skip_entry(*skip_start + idx).last_doc
740 }
741 }
742 }
743
744 #[inline]
747 pub fn doc(&self) -> DocId {
748 if self.exhausted {
749 return u32::MAX;
750 }
751 if self.block_loaded {
752 debug_assert!(self.pos < self.doc_ids.len());
753 unsafe { *self.doc_ids.get_unchecked(self.pos) }
755 } else {
756 self.block_first_doc(self.block_idx)
757 }
758 }
759
760 #[inline]
761 pub fn ordinal(&self) -> u16 {
762 if !self.block_loaded || self.ordinals.is_empty() {
763 return 0;
764 }
765 debug_assert!(self.pos < self.ordinals.len());
766 unsafe { *self.ordinals.get_unchecked(self.pos) }
768 }
769
770 #[inline]
776 pub fn ordinal_mut(&mut self) -> u16 {
777 if !self.block_loaded {
778 return 0;
779 }
780 if !self.ordinals_loaded {
781 if let Some(ref block) = self.current_sparse_block {
782 block.decode_ordinals_into(&mut self.ordinals);
783 }
784 self.ordinals_loaded = true;
785 }
786 if self.ordinals.is_empty() {
787 return 0;
788 }
789 debug_assert!(self.pos < self.ordinals.len());
790 unsafe { *self.ordinals.get_unchecked(self.pos) }
791 }
792
793 #[inline]
794 pub fn score(&self) -> f32 {
795 if !self.block_loaded {
796 return 0.0;
797 }
798 debug_assert!(self.pos < self.scores.len());
799 unsafe { *self.scores.get_unchecked(self.pos) }
801 }
802
803 #[inline]
809 pub fn ensure_scores(&mut self) {
810 if self.block_loaded && self.scores.is_empty() {
811 self.compute_deferred_scores();
812 }
813 }
814
815 #[inline]
816 pub fn current_block_max_score(&self) -> f32 {
817 if self.exhausted {
818 return 0.0;
819 }
820 match &self.variant {
821 CursorVariant::Text { .. } => self.text_block_bound(self.block_idx),
822 CursorVariant::Sparse {
823 si,
824 query_weight,
825 skip_start,
826 ..
827 } => query_weight.abs() * si.read_skip_entry(*skip_start + self.block_idx).max_weight,
828 }
829 }
830
831 #[inline]
835 pub fn current_group_max_score(&self) -> Option<f32> {
836 if self.exhausted {
837 return Some(0.0);
838 }
839 match &self.variant {
840 CursorVariant::Text { .. } => self.text_group_bound(self.block_idx),
841 CursorVariant::Sparse { .. } => None,
842 }
843 }
844
845 #[inline]
848 pub(crate) fn is_text(&self) -> bool {
849 matches!(self.variant, CursorVariant::Text { .. })
850 }
851
852 fn text_block_bound(&self, idx: usize) -> f32 {
854 match &self.variant {
855 CursorVariant::Text {
856 list,
857 idf,
858 length_bounds,
859 avg_len,
860 params,
861 ..
862 } => {
863 let (max_tf, min_len) = list.block_bounds(idx).unwrap_or((0, None));
864 match min_len {
865 Some(min_len) if *length_bounds => params.upper_bound_with_len(
866 (max_tf as f32).max(1.0),
867 *idf,
868 min_len as f32,
869 *avg_len,
870 ),
871 _ => params.upper_bound((max_tf as f32).max(1.0), *idf),
872 }
873 }
874 CursorVariant::Sparse { .. } => self.max_score,
875 }
876 }
877
878 fn text_group_bound(&self, idx: usize) -> Option<f32> {
880 match &self.variant {
881 CursorVariant::Text {
882 list,
883 idf,
884 length_bounds,
885 avg_len,
886 params,
887 ..
888 } => {
889 let (max_tf, min_len) = list.group_bounds(idx)?;
890 Some(if *length_bounds {
891 params.upper_bound_with_len(
892 (max_tf as f32).max(1.0),
893 *idf,
894 min_len as f32,
895 *avg_len,
896 )
897 } else {
898 params.upper_bound((max_tf as f32).max(1.0), *idf)
899 })
900 }
901 CursorVariant::Sparse { .. } => None,
902 }
903 }
904
905 pub(crate) fn window_upper_bound(&self, from: DocId, to: DocId) -> f32 {
911 if self.exhausted {
912 return 0.0;
913 }
914 let CursorVariant::Text { list, .. } = &self.variant else {
915 return self.max_score;
916 };
917 let start = from.max(self.doc());
920 if start > to {
921 return 0.0;
922 }
923 let Some(mut idx) = list.seek_block(start, self.block_idx) else {
924 return 0.0;
925 };
926 let mut bound = 0.0f32;
927 while idx < self.num_blocks {
928 if list.block_first_doc(idx).unwrap_or(u32::MAX) > to {
929 break;
930 }
931 if list.is_group_start(idx)
932 && list.group_last_doc(idx).is_some_and(|last| last <= to)
933 && let Some(group_bound) = self.text_group_bound(idx)
934 {
935 bound = bound.max(group_bound);
936 idx = list.next_group_block(idx);
937 continue;
938 }
939 bound = bound.max(self.text_block_bound(idx));
940 idx += 1;
941 }
942 bound
943 }
944
945 pub(crate) fn score_window_sync(
950 &mut self,
951 from: DocId,
952 to: DocId,
953 scores: &mut [f32],
954 mask: &mut [u64],
955 ) -> crate::Result<u32> {
956 let mut matched = 0u32;
957 loop {
958 if self.exhausted {
959 return Ok(matched);
960 }
961 if !self.block_loaded {
962 if self.block_first_doc(self.block_idx) > to {
963 return Ok(matched);
964 }
965 self.ensure_block_loaded_sync()?;
966 if self.exhausted {
967 return Ok(matched);
968 }
969 }
970 if self.doc_ids[self.pos] > to {
971 return Ok(matched);
972 }
973 self.ensure_scores();
974 let remaining = &self.doc_ids[self.pos..];
975 let end = if to == u32::MAX {
976 remaining.len()
977 } else {
978 crate::structures::simd::find_first_ge_u32(remaining, to + 1)
979 };
980 let block_scores = &self.scores[self.pos..self.pos + end];
981 for (doc, score) in remaining[..end].iter().zip(block_scores) {
982 let slot = (doc - from) as usize;
983 scores[slot] += score;
984 mask[slot >> 6] |= 1u64 << (slot & 63);
985 }
986 matched += end as u32;
987 self.pos += end;
988 if self.pos >= self.doc_ids.len() {
989 self.block_idx += 1;
990 self.block_loaded = false;
991 if self.block_idx >= self.num_blocks {
992 self.exhausted = true;
993 return Ok(matched);
994 }
995 } else {
996 return Ok(matched);
997 }
998 }
999 }
1000
1001 pub(crate) fn skip_past_sync(&mut self, to: DocId) -> crate::Result<()> {
1004 if to == u32::MAX {
1005 self.exhausted = true;
1006 return Ok(());
1007 }
1008 while !self.exhausted && self.block_last_doc(self.block_idx) <= to {
1009 self.skip_to_next_block();
1010 }
1011 if !self.exhausted && self.doc() <= to {
1012 self.seek_sync(to + 1)?;
1013 }
1014 Ok(())
1015 }
1016
1017 #[inline]
1019 pub fn current_group_last_doc(&self) -> DocId {
1020 match &self.variant {
1021 CursorVariant::Text { list, .. } => list.group_last_doc(self.block_idx).unwrap_or(0),
1022 CursorVariant::Sparse { .. } => self.block_last_doc(self.block_idx),
1023 }
1024 }
1025
1026 pub fn skip_to_next_group(&mut self) -> DocId {
1028 if self.exhausted {
1029 return u32::MAX;
1030 }
1031 let next = match &self.variant {
1032 CursorVariant::Text { list, .. } => list.next_group_block(self.block_idx),
1033 CursorVariant::Sparse { .. } => self.block_idx + 1,
1034 };
1035 self.block_idx = next;
1036 self.block_loaded = false;
1037 if self.block_idx >= self.num_blocks {
1038 self.exhausted = true;
1039 return u32::MAX;
1040 }
1041 self.block_first_doc(self.block_idx)
1042 }
1043
1044 pub fn skip_to_next_block(&mut self) -> DocId {
1047 if self.exhausted {
1048 return u32::MAX;
1049 }
1050 self.block_idx += 1;
1051 self.block_loaded = false;
1052 if self.block_idx >= self.num_blocks {
1053 self.exhausted = true;
1054 return u32::MAX;
1055 }
1056 self.block_first_doc(self.block_idx)
1057 }
1058
1059 #[inline]
1060 fn advance_pos(&mut self) -> DocId {
1061 self.pos += 1;
1062 if self.pos >= self.doc_ids.len() {
1063 self.block_idx += 1;
1064 self.block_loaded = false;
1065 if self.block_idx >= self.num_blocks {
1066 self.exhausted = true;
1067 return u32::MAX;
1068 }
1069 }
1070 self.doc()
1071 }
1072
1073 #[inline(never)]
1075 fn compute_deferred_scores(&mut self) {
1076 if let CursorVariant::Text {
1077 list,
1078 idf_times_k1_plus_1,
1079 denom_tf_coeff,
1080 denom_const,
1081 denom_len_coeff,
1082 lengths,
1083 tfs,
1084 deferred_tf,
1085 ..
1086 } = &mut self.variant
1087 && let Some((block_offset, tf_start, count)) = deferred_tf.take()
1088 {
1089 list.decode_block_tfs_deferred(block_offset, tf_start, count, tfs);
1090 let num_scale = *idf_times_k1_plus_1;
1091 let d_tf = *denom_tf_coeff;
1092 let d_const = *denom_const;
1093 let d_len = *denom_len_coeff;
1094 self.scores.clear();
1095 self.scores.resize(count, 0.0);
1096 match lengths {
1097 Some(source) => {
1099 for i in 0..count {
1100 let tf = unsafe { *tfs.get_unchecked(i) } as f32;
1101 let vid = unsafe { *self.doc_ids.get_unchecked(i) };
1102 let len = source.length(vid) as f32;
1103 let score = (num_scale * tf) / (tf + d_const + d_len * len);
1104 unsafe {
1105 *self.scores.get_unchecked_mut(i) = score;
1106 }
1107 }
1108 }
1109 None => {
1110 for i in 0..count {
1111 let tf = unsafe { *tfs.get_unchecked(i) } as f32;
1112 let score = (num_scale * tf) / (d_tf * tf + d_const);
1113 unsafe {
1114 *self.scores.get_unchecked_mut(i) = score;
1115 }
1116 }
1117 }
1118 }
1119 }
1120 }
1121
1122 pub async fn ensure_block_loaded(&mut self) -> crate::Result<bool> {
1128 cursor_ensure_block!(self, load_block_direct, .await)
1129 }
1130
1131 pub fn ensure_block_loaded_sync(&mut self) -> crate::Result<bool> {
1132 cursor_ensure_block!(self, load_block_direct_sync,)
1133 }
1134
1135 pub async fn advance(&mut self) -> crate::Result<DocId> {
1136 cursor_advance!(self, ensure_block_loaded, .await)
1137 }
1138
1139 pub fn advance_sync(&mut self) -> crate::Result<DocId> {
1140 cursor_advance!(self, ensure_block_loaded_sync,)
1141 }
1142
1143 pub async fn seek(&mut self, target: DocId) -> crate::Result<DocId> {
1144 cursor_seek!(self, ensure_block_loaded, target, .await)
1145 }
1146
1147 pub fn seek_sync(&mut self, target: DocId) -> crate::Result<DocId> {
1148 cursor_seek!(self, ensure_block_loaded_sync, target,)
1149 }
1150
1151 fn seek_prepare(&mut self, target: DocId) -> Option<DocId> {
1152 if self.exhausted {
1153 return Some(u32::MAX);
1154 }
1155
1156 if self.block_loaded
1158 && let Some(&last) = self.doc_ids.last()
1159 {
1160 if last >= target && self.doc_ids[self.pos] < target {
1161 let remaining = &self.doc_ids[self.pos..];
1162 self.pos += crate::structures::simd::find_first_ge_u32(remaining, target);
1163 if self.pos >= self.doc_ids.len() {
1164 self.block_idx += 1;
1165 self.block_loaded = false;
1166 if self.block_idx >= self.num_blocks {
1167 self.exhausted = true;
1168 return Some(u32::MAX);
1169 }
1170 }
1171 return Some(self.doc());
1172 }
1173 if self.doc_ids[self.pos] >= target {
1174 return Some(self.doc());
1175 }
1176 }
1177
1178 let lo = match &self.variant {
1180 CursorVariant::Text { list, .. } => match list.seek_block(target, self.block_idx) {
1182 Some(idx) => idx,
1183 None => {
1184 self.exhausted = true;
1185 return Some(u32::MAX);
1186 }
1187 },
1188 CursorVariant::Sparse { .. } => {
1190 let mut lo = self.block_idx;
1191 let mut hi = self.num_blocks;
1192 while lo < hi {
1193 let mid = lo + (hi - lo) / 2;
1194 if self.block_last_doc(mid) < target {
1195 lo = mid + 1;
1196 } else {
1197 hi = mid;
1198 }
1199 }
1200 lo
1201 }
1202 };
1203 if lo >= self.num_blocks {
1204 self.exhausted = true;
1205 return Some(u32::MAX);
1206 }
1207 if lo != self.block_idx || !self.block_loaded {
1208 self.block_idx = lo;
1209 self.block_loaded = false;
1210 }
1211 None
1212 }
1213
1214 #[inline]
1215 fn seek_finish(&mut self, target: DocId) -> bool {
1216 if self.exhausted {
1217 return false;
1218 }
1219 self.pos = crate::structures::simd::find_first_ge_u32(&self.doc_ids, target);
1220 if self.pos >= self.doc_ids.len() {
1221 self.block_idx += 1;
1222 self.block_loaded = false;
1223 if self.block_idx >= self.num_blocks {
1224 self.exhausted = true;
1225 return false;
1226 }
1227 return true;
1228 }
1229 false
1230 }
1231}
1232
1233macro_rules! bms_execute_loop {
1238 ($self:ident, $ensure:ident, $advance:ident, $seek:ident, $($aw:tt)*) => {{
1239 let n = $self.cursors.len();
1240
1241 for cursor in &mut $self.cursors {
1243 cursor.$ensure() $($aw)* ?;
1244 }
1245
1246 let mut docs_scored = 0u64;
1247 let mut docs_skipped = 0u64;
1248 let mut blocks_skipped = 0u64;
1249 let mut groups_skipped = 0u64;
1250 let mut conjunction_skipped = 0u64;
1251 let mut ordinal_scores: Vec<(u16, f32)> = Vec::with_capacity(n * 2);
1252 let _bms_start = std::time::Instant::now();
1253
1254 let inv_heap_factor = $self.inv_heap_factor;
1255 let mut adjusted_threshold = $self.collector.threshold() * inv_heap_factor - 1e-6;
1256 let mut iterations: u64 = 0;
1257
1258 loop {
1259 iterations += 1;
1263 if iterations & 0xFFF == 0
1264 && let Some(budget) = &$self.budget
1265 && budget.expired()
1266 {
1267 budget.mark_truncated();
1268 log::debug!(
1269 "MaxScoreExecutor: deadline reached after {} iterations, {} scored",
1270 iterations,
1271 docs_scored
1272 );
1273 break;
1274 }
1275 let partition = $self.find_partition();
1276 if partition >= n {
1277 break;
1278 }
1279
1280 let mut min_doc = u32::MAX;
1284 let mut next_other = u32::MAX;
1288 let mut at_min_mask = 0u64; for i in partition..n {
1290 let doc = $self.cursors[i].doc();
1291 match doc.cmp(&min_doc) {
1292 std::cmp::Ordering::Less => {
1293 next_other = min_doc;
1294 min_doc = doc;
1295 at_min_mask = 1u64 << (i as u32);
1296 }
1297 std::cmp::Ordering::Equal => {
1298 at_min_mask |= 1u64 << (i as u32);
1299 }
1300 std::cmp::Ordering::Greater => {
1301 if doc < next_other {
1302 next_other = doc;
1303 }
1304 }
1305 }
1306 }
1307 if min_doc == u32::MAX {
1308 break;
1309 }
1310
1311 let non_essential_upper = if partition > 0 {
1312 $self.prefix_sums[partition - 1]
1313 } else {
1314 0.0
1315 };
1316
1317 if $self.collector.len() >= $self.collector.k {
1319 let mut present_upper: f32 = 0.0;
1320 let mut mask = at_min_mask;
1321 while mask != 0 {
1322 let i = mask.trailing_zeros() as usize;
1323 present_upper += $self.cursors[i].max_score;
1324 mask &= mask - 1;
1325 }
1326
1327 if present_upper + non_essential_upper < adjusted_threshold {
1328 let mut mask = at_min_mask;
1329 while mask != 0 {
1330 let i = mask.trailing_zeros() as usize;
1331 $self.cursors[i].$ensure() $($aw)* ?;
1332 $self.cursors[i].$advance() $($aw)* ?;
1333 mask &= mask - 1;
1334 }
1335 conjunction_skipped += 1;
1336 continue;
1337 }
1338 }
1339
1340 if $self.collector.len() >= $self.collector.k {
1342 let mut block_max_sum: f32 = 0.0;
1343 let mut mask = at_min_mask;
1344 while mask != 0 {
1345 let i = mask.trailing_zeros() as usize;
1346 block_max_sum += $self.cursors[i].current_block_max_score();
1347 mask &= mask - 1;
1348 }
1349
1350 if block_max_sum + non_essential_upper < adjusted_threshold {
1351 let mut group_sum: f32 = 0.0;
1367 let mut mask = at_min_mask;
1368 while mask != 0 {
1369 let i = mask.trailing_zeros() as usize;
1370 group_sum += $self.cursors[i]
1371 .current_group_max_score()
1372 .unwrap_or_else(|| $self.cursors[i].current_block_max_score());
1373 mask &= mask - 1;
1374 }
1375 let group_prunable = group_sum + non_essential_upper < adjusted_threshold;
1376 let mut mask = at_min_mask;
1377 while mask != 0 {
1378 let i = mask.trailing_zeros() as usize;
1379 let by_group =
1380 group_prunable && $self.cursors[i].current_group_max_score().is_some();
1381 let boundary = if by_group {
1382 $self.cursors[i].current_group_last_doc()
1383 } else {
1384 $self.cursors[i].block_last_doc($self.cursors[i].block_idx)
1385 };
1386 if next_other > boundary {
1387 if by_group {
1388 $self.cursors[i].skip_to_next_group();
1389 groups_skipped += 1;
1390 } else {
1391 $self.cursors[i].skip_to_next_block();
1392 }
1393 $self.cursors[i].$ensure() $($aw)* ?;
1394 } else {
1395 $self.cursors[i].$seek(next_other) $($aw)* ?;
1396 }
1397 mask &= mask - 1;
1398 }
1399 blocks_skipped += 1;
1400 continue;
1401 }
1402 }
1403
1404 if let Some(ref pred) = $self.predicate {
1406 if !pred(min_doc) {
1407 let mut mask = at_min_mask;
1408 while mask != 0 {
1409 let i = mask.trailing_zeros() as usize;
1410 $self.cursors[i].$ensure() $($aw)* ?;
1411 $self.cursors[i].$advance() $($aw)* ?;
1412 mask &= mask - 1;
1413 }
1414 continue;
1415 }
1416 }
1417
1418 ordinal_scores.clear();
1420 {
1421 let mut mask = at_min_mask;
1422 while mask != 0 {
1423 let i = mask.trailing_zeros() as usize;
1424 $self.cursors[i].$ensure() $($aw)* ?;
1425 $self.cursors[i].ensure_scores();
1426 while $self.cursors[i].doc() == min_doc {
1427 let ord = $self.cursors[i].ordinal_mut();
1428 let sc = $self.cursors[i].score();
1429 ordinal_scores.push((ord, sc));
1430 $self.cursors[i].$advance() $($aw)* ?;
1431 }
1432 mask &= mask - 1;
1433 }
1434 }
1435
1436 let essential_total: f32 = ordinal_scores.iter().map(|(_, s)| *s).sum();
1437 if $self.collector.len() >= $self.collector.k
1438 && essential_total + non_essential_upper < adjusted_threshold
1439 {
1440 docs_skipped += 1;
1441 continue;
1442 }
1443
1444 let mut running_total = essential_total;
1446 for i in (0..partition).rev() {
1447 if $self.collector.len() >= $self.collector.k
1448 && running_total + $self.prefix_sums[i] < adjusted_threshold
1449 {
1450 break;
1451 }
1452
1453 let doc = $self.cursors[i].$seek(min_doc) $($aw)* ?;
1454 if doc == min_doc {
1455 $self.cursors[i].ensure_scores();
1456 while $self.cursors[i].doc() == min_doc {
1457 let s = $self.cursors[i].score();
1458 running_total += s;
1459 let ord = $self.cursors[i].ordinal_mut();
1460 ordinal_scores.push((ord, s));
1461 $self.cursors[i].$advance() $($aw)* ?;
1462 }
1463 }
1464 }
1465
1466 if ordinal_scores.len() == 1 {
1469 let (ord, score) = ordinal_scores[0];
1470 if $self.collector.insert_with_ordinal(min_doc, score, ord) {
1471 docs_scored += 1;
1472 adjusted_threshold = $self.collector.threshold() * inv_heap_factor - 1e-6;
1473 } else {
1474 docs_skipped += 1;
1475 }
1476 } else if !ordinal_scores.is_empty() {
1477 if ordinal_scores.len() > 2 {
1478 ordinal_scores.sort_unstable_by_key(|(ord, _)| *ord);
1479 } else if ordinal_scores.len() == 2 && ordinal_scores[0].0 > ordinal_scores[1].0 {
1480 ordinal_scores.swap(0, 1);
1481 }
1482 let mut j = 0;
1483 while j < ordinal_scores.len() {
1484 let current_ord = ordinal_scores[j].0;
1485 let mut score = 0.0f32;
1486 while j < ordinal_scores.len() && ordinal_scores[j].0 == current_ord {
1487 score += ordinal_scores[j].1;
1488 j += 1;
1489 }
1490 if $self
1491 .collector
1492 .insert_with_ordinal(min_doc, score, current_ord)
1493 {
1494 docs_scored += 1;
1495 adjusted_threshold = $self.collector.threshold() * inv_heap_factor - 1e-6;
1496 } else {
1497 docs_skipped += 1;
1498 }
1499 }
1500 }
1501 }
1502
1503 let results: Vec<ScoredDoc> = $self
1504 .collector
1505 .into_sorted_results()
1506 .into_iter()
1507 .map(|(doc_id, score, ordinal)| ScoredDoc {
1508 doc_id,
1509 score,
1510 ordinal,
1511 })
1512 .collect();
1513
1514 let _bms_elapsed_ms = _bms_start.elapsed().as_millis() as u64;
1515 if _bms_elapsed_ms > 500 {
1516 warn!(
1517 "slow MaxScore: {}ms, cursors={}, scored={}, skipped={}, blocks_skipped={}, groups_skipped={}, conjunction_skipped={}, returned={}, top_score={:.4}",
1518 _bms_elapsed_ms,
1519 n,
1520 docs_scored,
1521 docs_skipped,
1522 blocks_skipped,
1523 groups_skipped,
1524 conjunction_skipped,
1525 results.len(),
1526 results.first().map(|r| r.score).unwrap_or(0.0)
1527 );
1528 } else {
1529 debug!(
1530 "MaxScoreExecutor: {}ms, scored={}, skipped={}, blocks_skipped={}, groups_skipped={}, conjunction_skipped={}, returned={}, top_score={:.4}",
1531 _bms_elapsed_ms,
1532 docs_scored,
1533 docs_skipped,
1534 blocks_skipped,
1535 groups_skipped,
1536 conjunction_skipped,
1537 results.len(),
1538 results.first().map(|r| r.score).unwrap_or(0.0)
1539 );
1540 }
1541
1542 Ok(results)
1543 }};
1544}
1545
1546impl<'a> MaxScoreExecutor<'a> {
1547 pub(crate) fn new(mut cursors: Vec<TermCursor<'a>>, k: usize, heap_factor: f32) -> Self {
1552 if cursors.len() > super::MAX_QUERY_TERMS {
1556 cursors.sort_unstable_by(|a, b| b.max_score.total_cmp(&a.max_score));
1557 cursors.truncate(super::MAX_QUERY_TERMS);
1558 log::warn!(
1559 "MaxScore cursor count exceeded {}; retaining the strongest cursors",
1560 super::MAX_QUERY_TERMS
1561 );
1562 }
1563
1564 for c in &mut cursors {
1567 c.lazy_ordinals = true;
1568 }
1569
1570 cursors.sort_by(|a, b| {
1572 a.max_score
1573 .partial_cmp(&b.max_score)
1574 .unwrap_or(Ordering::Equal)
1575 });
1576
1577 let mut prefix_sums = Vec::with_capacity(cursors.len());
1578 let mut cumsum = 0.0f32;
1579 for c in &cursors {
1580 cumsum += c.max_score;
1581 prefix_sums.push(cumsum);
1582 }
1583
1584 let clamped_heap_factor = heap_factor.clamp(0.01, 1.0);
1585
1586 debug!(
1587 "Creating MaxScoreExecutor: num_cursors={}, k={}, total_upper={:.4}, heap_factor={:.2}",
1588 cursors.len(),
1589 k,
1590 cumsum,
1591 clamped_heap_factor
1592 );
1593
1594 Self {
1595 cursors,
1596 prefix_sums,
1597 collector: ScoreCollector::new(k),
1598 inv_heap_factor: 1.0 / clamped_heap_factor,
1599 predicate: None,
1600 budget: None,
1601 metric_index: "unknown",
1602 metric_field: "unknown",
1603 }
1604 }
1605
1606 pub fn with_budget(mut self, budget: Option<SharedThreshold>) -> Self {
1608 self.budget = budget.filter(|b| b.deadline().is_some());
1609 self
1610 }
1611
1612 pub fn with_metric_labels(mut self, index: &'a str, field: &'a str) -> Self {
1614 self.metric_index = index;
1615 self.metric_field = field;
1616 self
1617 }
1618
1619 pub fn sparse(
1623 sparse_index: &'a crate::segment::SparseIndex,
1624 query_terms: Vec<(u32, f32)>,
1625 k: usize,
1626 heap_factor: f32,
1627 ) -> Self {
1628 let cursors: Vec<TermCursor<'a>> = query_terms
1629 .iter()
1630 .filter_map(|&(dim_id, qw)| {
1631 let (skip_start, skip_count, global_max, block_data_offset) =
1632 sparse_index.get_skip_range_full(dim_id)?;
1633 Some(TermCursor::sparse(
1634 sparse_index,
1635 qw,
1636 skip_start,
1637 skip_count,
1638 global_max,
1639 block_data_offset,
1640 ))
1641 })
1642 .collect();
1643 Self::new(cursors, k, heap_factor)
1644 }
1645
1646 pub fn text(
1650 posting_lists: Vec<(crate::structures::BlockPostingList, f32)>,
1651 avg_field_len: f32,
1652 k: usize,
1653 lengths: Option<&'a crate::segment::chunk_map::DocLengths>,
1654 params: super::Bm25Params,
1655 heap_factor: f32,
1656 ) -> Self {
1657 let cursors: Vec<TermCursor<'a>> = posting_lists
1658 .into_iter()
1659 .map(|(pl, idf)| {
1660 TermCursor::text_with_params(
1661 pl,
1662 idf,
1663 avg_field_len,
1664 lengths.map(LengthSource::Docs),
1665 params,
1666 )
1667 })
1668 .collect();
1669 Self::new(cursors, k, heap_factor)
1670 }
1671
1672 pub fn text_chunked(
1676 posting_lists: Vec<(crate::structures::BlockPostingList, f32)>,
1677 avg_chunk_len: f32,
1678 k: usize,
1679 lengths: &'a crate::segment::chunk_map::ChunkMap,
1680 params: super::Bm25Params,
1681 heap_factor: f32,
1682 ) -> Self {
1683 let cursors: Vec<TermCursor<'a>> = posting_lists
1684 .into_iter()
1685 .map(|(pl, idf)| {
1686 TermCursor::text_with_params(
1687 pl,
1688 idf,
1689 avg_chunk_len,
1690 Some(LengthSource::Chunks(lengths)),
1691 params,
1692 )
1693 })
1694 .collect();
1695 Self::new(cursors, k, heap_factor)
1696 }
1697
1698 #[inline]
1699 fn find_partition(&self) -> usize {
1700 let threshold = self.collector.threshold() * self.inv_heap_factor;
1704 self.prefix_sums.partition_point(|&sum| sum < threshold)
1707 }
1708
1709 pub fn with_predicate(mut self, predicate: super::DocPredicate<'a>) -> Self {
1715 self.predicate = Some(predicate);
1716 self
1717 }
1718
1719 pub fn seed_threshold(&mut self, initial_threshold: f32) {
1721 self.collector.seed_threshold(initial_threshold);
1722 }
1723
1724 pub async fn execute(mut self) -> crate::Result<Vec<ScoredDoc>> {
1730 if self.cursors.is_empty() {
1731 return Ok(Vec::new());
1732 }
1733 let t = crate::observe::Timer::start();
1734 let results = if self.all_text() {
1735 self.execute_windowed()
1736 } else {
1737 bms_execute_loop!(self, ensure_block_loaded, advance, seek, .await)
1738 };
1739 if let Ok(r) = &results {
1740 crate::observe::maxscore_query(self.metric_index, self.metric_field, t.secs(), r.len());
1741 }
1742 results
1743 }
1744
1745 pub fn execute_sync(mut self) -> crate::Result<Vec<ScoredDoc>> {
1747 if self.cursors.is_empty() {
1748 return Ok(Vec::new());
1749 }
1750 let t = crate::observe::Timer::start();
1751 let results = if self.all_text() {
1752 self.execute_windowed()
1753 } else {
1754 bms_execute_loop!(self, ensure_block_loaded_sync, advance_sync, seek_sync,)
1755 };
1756 if let Ok(r) = &results {
1757 crate::observe::maxscore_query(self.metric_index, self.metric_field, t.secs(), r.len());
1758 }
1759 results
1760 }
1761
1762 #[cfg(test)]
1765 pub(crate) fn execute_doc_at_a_time_sync(mut self) -> crate::Result<Vec<ScoredDoc>> {
1766 if self.cursors.is_empty() {
1767 return Ok(Vec::new());
1768 }
1769 bms_execute_loop!(self, ensure_block_loaded_sync, advance_sync, seek_sync,)
1770 }
1771
1772 fn all_text(&self) -> bool {
1773 self.cursors.iter().all(TermCursor::is_text)
1774 }
1775
1776 pub(crate) fn execute_windowed(&mut self) -> crate::Result<Vec<ScoredDoc>> {
1804 let n = self.cursors.len();
1805 for cursor in &mut self.cursors {
1806 cursor.ensure_block_loaded_sync()?;
1807 }
1808 let inv_heap_factor = self.inv_heap_factor;
1809 let mut window_scores = vec![0.0f32; WINDOW_IDS];
1810 let mut window_mask = vec![0u64; WINDOW_IDS / 64];
1811 let mut cand_docs: Vec<u32> = Vec::with_capacity(WINDOW_IDS);
1812 let mut cand_scores: Vec<f32> = Vec::with_capacity(WINDOW_IDS);
1813 let mut wmax = vec![0.0f32; n];
1814 let mut order: Vec<usize> = (0..n).collect();
1815 let mut wprefix = vec![0.0f32; n];
1816 let mut windows = 0u64;
1817 let mut windows_skipped = 0u64;
1818 let mut candidates = 0u64;
1819 let mut docs_scored = 0u64;
1820 let started = std::time::Instant::now();
1821
1822 loop {
1823 windows += 1;
1824 if windows & 0x3F == 0
1825 && let Some(budget) = &self.budget
1826 && budget.expired()
1827 {
1828 budget.mark_truncated();
1829 log::debug!(
1830 "MaxScoreExecutor(windowed): deadline reached after {} windows, {} scored",
1831 windows,
1832 docs_scored
1833 );
1834 break;
1835 }
1836 let partition = self.find_partition();
1837 if partition >= n {
1838 break;
1839 }
1840 let mut from = u32::MAX;
1843 let mut to = u32::MAX;
1844 for cursor in &self.cursors[partition..] {
1845 if cursor.exhausted {
1846 continue;
1847 }
1848 from = from.min(cursor.doc());
1849 to = to.min(cursor.block_last_doc(cursor.block_idx));
1850 }
1851 if from == u32::MAX {
1852 break;
1853 }
1854 let to = to.max(from).min(from.saturating_add(WINDOW_IDS as u32 - 1));
1855 let width = (to - from) as usize + 1;
1856 let words = width.div_ceil(64);
1857
1858 let heap_full = self.collector.len() >= self.collector.k;
1860 let threshold = if heap_full {
1861 self.collector.threshold() * inv_heap_factor - 1e-6
1862 } else {
1863 0.0
1864 };
1865 for (i, bound) in wmax.iter_mut().enumerate() {
1866 *bound = self.cursors[i].window_upper_bound(from, to);
1867 }
1868 order.sort_unstable_by(|&a, &b| wmax[a].total_cmp(&wmax[b]));
1869 let mut sum = 0.0f32;
1870 for (rank, &i) in order.iter().enumerate() {
1871 sum += wmax[i];
1872 wprefix[rank] = sum;
1873 }
1874 let wpartition = if heap_full {
1875 wprefix.partition_point(|&s| s < threshold)
1876 } else {
1877 0
1878 };
1879 if wpartition >= n {
1880 for cursor in &mut self.cursors {
1882 if !cursor.exhausted && cursor.doc() <= to {
1883 cursor.skip_past_sync(to)?;
1884 }
1885 }
1886 windows_skipped += 1;
1887 continue;
1888 }
1889
1890 window_scores[..width].fill(0.0);
1892 window_mask[..words].fill(0);
1893 for &i in &order[wpartition..] {
1894 let cursor = &mut self.cursors[i];
1895 if cursor.exhausted {
1896 continue;
1897 }
1898 if cursor.doc() < from {
1899 cursor.seek_sync(from)?;
1900 }
1901 if cursor.exhausted || cursor.doc() > to {
1902 continue;
1903 }
1904 cursor.score_window_sync(
1905 from,
1906 to,
1907 &mut window_scores[..width],
1908 &mut window_mask[..words],
1909 )?;
1910 }
1911
1912 cand_docs.clear();
1914 cand_scores.clear();
1915 for (word_idx, word) in window_mask[..words].iter().enumerate() {
1916 let mut bits = *word;
1917 while bits != 0 {
1918 let slot = (word_idx << 6) | bits.trailing_zeros() as usize;
1919 bits &= bits - 1;
1920 cand_docs.push(from + slot as u32);
1921 cand_scores.push(window_scores[slot]);
1922 }
1923 }
1924 if let Some(pred) = &self.predicate {
1925 let mut kept = 0usize;
1926 for j in 0..cand_docs.len() {
1927 let doc = cand_docs[j];
1928 cand_docs[kept] = doc;
1929 cand_scores[kept] = cand_scores[j];
1930 kept += pred(doc) as usize;
1931 }
1932 cand_docs.truncate(kept);
1933 cand_scores.truncate(kept);
1934 }
1935
1936 let mut remaining = if wpartition > 0 {
1938 wprefix[wpartition - 1]
1939 } else {
1940 0.0
1941 };
1942 for rank in (0..wpartition).rev() {
1943 let i = order[rank];
1944 if heap_full {
1945 filter_competitive(&mut cand_docs, &mut cand_scores, remaining, threshold);
1946 }
1947 if cand_docs.is_empty() {
1948 break;
1949 }
1950 if wmax[i] > 0.0 {
1951 let cursor = &mut self.cursors[i];
1952 for (doc, score) in cand_docs.iter().zip(cand_scores.iter_mut()) {
1953 if cursor.seek_sync(*doc)? == *doc {
1954 cursor.ensure_scores();
1955 *score += cursor.score();
1956 }
1957 }
1958 }
1959 remaining -= wmax[i];
1960 }
1961 if heap_full {
1962 filter_competitive(&mut cand_docs, &mut cand_scores, 0.0, threshold);
1963 }
1964 candidates += cand_docs.len() as u64;
1965 for (doc, score) in cand_docs.iter().zip(&cand_scores) {
1966 if self.collector.insert_with_ordinal(*doc, *score, 0) {
1967 docs_scored += 1;
1968 }
1969 }
1970 }
1971
1972 let collector = std::mem::replace(&mut self.collector, ScoreCollector::new(0));
1973 let results: Vec<ScoredDoc> = collector
1974 .into_sorted_results()
1975 .into_iter()
1976 .map(|(doc_id, score, ordinal)| ScoredDoc {
1977 doc_id,
1978 score,
1979 ordinal,
1980 })
1981 .collect();
1982 let elapsed_ms = started.elapsed().as_millis() as u64;
1983 if elapsed_ms > 500 {
1984 warn!(
1985 "slow windowed MaxScore: {}ms, cursors={}, windows={}, windows_skipped={}, candidates={}, scored={}, returned={}, top_score={:.4}",
1986 elapsed_ms,
1987 n,
1988 windows,
1989 windows_skipped,
1990 candidates,
1991 docs_scored,
1992 results.len(),
1993 results.first().map(|r| r.score).unwrap_or(0.0)
1994 );
1995 } else {
1996 debug!(
1997 "MaxScoreExecutor(windowed): {}ms, cursors={}, windows={}, windows_skipped={}, candidates={}, scored={}, returned={}, top_score={:.4}",
1998 elapsed_ms,
1999 n,
2000 windows,
2001 windows_skipped,
2002 candidates,
2003 docs_scored,
2004 results.len(),
2005 results.first().map(|r| r.score).unwrap_or(0.0)
2006 );
2007 }
2008 Ok(results)
2009 }
2010}
2011
2012const WINDOW_IDS: usize = 4096;
2014
2015fn filter_competitive(docs: &mut Vec<u32>, scores: &mut Vec<f32>, remaining: f32, threshold: f32) {
2019 let mut kept = 0usize;
2020 for j in 0..docs.len() {
2021 let doc = docs[j];
2022 let score = scores[j];
2023 docs[kept] = doc;
2024 scores[kept] = score;
2025 kept += (score + remaining >= threshold) as usize;
2026 }
2027 docs.truncate(kept);
2028 scores.truncate(kept);
2029}
2030
2031#[cfg(test)]
2032mod tests {
2033 use super::*;
2034
2035 struct Corpus {
2038 postings: Vec<Vec<(u32, u32)>>,
2040 lengths: Vec<u16>,
2041 n_docs: u32,
2042 }
2043
2044 fn xorshift(state: &mut u64) -> u64 {
2045 *state ^= *state << 13;
2046 *state ^= *state >> 7;
2047 *state ^= *state << 17;
2048 *state
2049 }
2050
2051 fn random_corpus(seed: u64, n_docs: u32, n_terms: usize) -> Corpus {
2054 let mut state = seed.wrapping_mul(0x9E37_79B9_7F4A_7C15) | 1;
2055 let lengths: Vec<u16> = (0..n_docs)
2056 .map(|_| 1 + (xorshift(&mut state) % 400) as u16)
2057 .collect();
2058 let densities = [0.6, 0.25, 0.1, 0.03, 0.005];
2059 let postings = (0..n_terms)
2060 .map(|t| {
2061 let density = densities[t % densities.len()];
2062 let cutoff = (density * u32::MAX as f64) as u64;
2063 let mut postings = Vec::new();
2064 for doc in 0..n_docs {
2065 if (xorshift(&mut state) & 0xFFFF_FFFF) >= cutoff {
2066 continue;
2067 }
2068 let r = xorshift(&mut state) % 100;
2069 let tf = if r < 70 {
2070 1
2071 } else if r < 90 {
2072 2
2073 } else {
2074 3 + (r % 6) as u32
2075 };
2076 postings.push((doc, tf));
2077 }
2078 postings
2079 })
2080 .collect();
2081 Corpus {
2082 postings,
2083 lengths,
2084 n_docs,
2085 }
2086 }
2087
2088 fn build_lists(
2089 corpus: &Corpus,
2090 lengths: Option<&crate::segment::chunk_map::DocLengths>,
2091 ) -> Vec<(crate::structures::BlockPostingList, f32)> {
2092 corpus
2093 .postings
2094 .iter()
2095 .map(|postings| {
2096 let mut list = crate::structures::PostingList::new();
2097 for &(doc, tf) in postings {
2098 list.push(doc, tf);
2099 }
2100 let length_of = lengths.map(|l| move |doc: DocId| l.length(doc));
2101 let block_list = crate::structures::BlockPostingList::from_posting_list_with(
2102 &list,
2103 false,
2104 length_of.as_ref().map(|f| f as &dyn Fn(DocId) -> u32),
2105 )
2106 .unwrap();
2107 let idf = super::super::bm25_idf(postings.len() as f32, corpus.n_docs as f32);
2108 (block_list, idf)
2109 })
2110 .collect()
2111 }
2112
2113 fn exhaustive(
2115 corpus: &Corpus,
2116 lists: &[(crate::structures::BlockPostingList, f32)],
2117 real_lengths: bool,
2118 avg: f32,
2119 params: super::super::Bm25Params,
2120 ) -> std::collections::HashMap<u32, f32> {
2121 let mut scores: std::collections::HashMap<u32, f32> = std::collections::HashMap::new();
2122 for (postings, (_, idf)) in corpus.postings.iter().zip(lists) {
2123 for &(doc, tf) in postings {
2124 let len = if real_lengths {
2125 corpus.lengths[doc as usize] as f32
2126 } else {
2127 tf as f32
2128 };
2129 *scores.entry(doc).or_insert(0.0) += params.score(tf as f32, *idf, len, avg);
2130 }
2131 }
2132 scores
2133 }
2134
2135 fn check_top_k(
2136 label: &str,
2137 results: &[ScoredDoc],
2138 exhaustive: &std::collections::HashMap<u32, f32>,
2139 k: usize,
2140 predicate: Option<&dyn Fn(u32) -> bool>,
2141 ) {
2142 let mut expected: Vec<(u32, f32)> = exhaustive
2143 .iter()
2144 .filter(|(doc, _)| predicate.is_none_or(|p| p(**doc)))
2145 .map(|(doc, score)| (*doc, *score))
2146 .collect();
2147 expected.sort_by(|a, b| b.1.total_cmp(&a.1).then(a.0.cmp(&b.0)));
2148 let want = k.min(expected.len());
2149 assert_eq!(results.len(), want, "{label}: result count");
2150 for (rank, (got, exp)) in results.iter().zip(&expected).enumerate() {
2151 let tolerance = 1e-4 * exp.1.abs().max(1.0);
2152 assert!(
2153 (got.score - exp.1).abs() <= tolerance,
2154 "{label}: rank {rank} score {} vs exhaustive {} (doc {} vs {})",
2155 got.score,
2156 exp.1,
2157 got.doc_id,
2158 exp.0
2159 );
2160 let own = exhaustive[&got.doc_id];
2161 assert!(
2162 (got.score - own).abs() <= tolerance,
2163 "{label}: doc {} scored {} but exhaustive says {}",
2164 got.doc_id,
2165 got.score,
2166 own
2167 );
2168 if let Some(p) = predicate {
2169 assert!(
2170 p(got.doc_id),
2171 "{label}: doc {} fails the predicate",
2172 got.doc_id
2173 );
2174 }
2175 }
2176 for pair in results.windows(2) {
2177 assert!(
2178 pair[0].score >= pair[1].score,
2179 "{label}: results not sorted"
2180 );
2181 }
2182 }
2183
2184 #[test]
2189 fn windowed_text_maxscore_matches_exhaustive_and_doc_at_a_time() {
2190 let params = super::super::Bm25Params::default();
2191 let predicate_fn = |doc: u32| !doc.is_multiple_of(3);
2192 let mut cases = 0usize;
2193 for seed in 1..=6u64 {
2194 for &n_docs in &[300u32, 2_500, 12_000] {
2195 for &n_terms in &[1usize, 2, 4, 9] {
2196 let corpus = random_corpus(seed, n_docs, n_terms);
2197 let doc_lengths =
2198 crate::segment::chunk_map::DocLengths::from_lengths(&corpus.lengths);
2199 for real_lengths in [true, false] {
2200 let lengths = real_lengths.then_some(&doc_lengths);
2201 let lists = build_lists(&corpus, lengths);
2202 let avg = if real_lengths {
2203 doc_lengths.avg_len()
2204 } else {
2205 1.0
2206 };
2207 let truth = exhaustive(&corpus, &lists, real_lengths, avg, params);
2208 for &k in &[1usize, 10, 100] {
2209 for with_predicate in [false, true] {
2210 let label = format!(
2211 "seed={seed} docs={n_docs} terms={n_terms} lengths={real_lengths} k={k} pred={with_predicate}"
2212 );
2213 let pred: Option<&dyn Fn(u32) -> bool> =
2214 with_predicate.then_some(&predicate_fn);
2215 let make = |seeded: f32| {
2216 let mut executor = MaxScoreExecutor::text(
2217 lists.clone(),
2218 avg,
2219 k,
2220 lengths,
2221 params,
2222 1.0,
2223 );
2224 if with_predicate {
2225 executor = executor.with_predicate(Box::new(predicate_fn));
2226 }
2227 if seeded > 0.0 {
2228 executor.seed_threshold(seeded);
2229 }
2230 executor
2231 };
2232 let windowed = make(0.0).execute_windowed().unwrap();
2233 check_top_k(
2234 &format!("windowed {label}"),
2235 &windowed,
2236 &truth,
2237 k,
2238 pred,
2239 );
2240 let reference = make(0.0).execute_doc_at_a_time_sync().unwrap();
2241 check_top_k(
2242 &format!("reference {label}"),
2243 &reference,
2244 &truth,
2245 k,
2246 pred,
2247 );
2248 if let Some(kth) = windowed.last().map(|r| r.score)
2250 && windowed.len() == k
2251 {
2252 let seeded = make(kth * 0.9).execute_windowed().unwrap();
2253 check_top_k(
2254 &format!("seeded {label}"),
2255 &seeded,
2256 &truth,
2257 k,
2258 pred,
2259 );
2260 let above =
2262 make(windowed[0].score * 1.5).execute_windowed().unwrap();
2263 assert!(above.is_empty(), "{label}: floor above all scores");
2264 }
2265 cases += 1;
2266 }
2267 }
2268 }
2269 }
2270 }
2271 }
2272 assert!(cases > 400);
2273 }
2274
2275 #[test]
2278 fn windowed_text_maxscore_heap_factor_is_a_subset_with_exact_scores() {
2279 let params = super::super::Bm25Params::default();
2280 let corpus = random_corpus(7, 20_000, 6);
2281 let doc_lengths = crate::segment::chunk_map::DocLengths::from_lengths(&corpus.lengths);
2282 let lists = build_lists(&corpus, Some(&doc_lengths));
2283 let avg = doc_lengths.avg_len();
2284 let truth = exhaustive(&corpus, &lists, true, avg, params);
2285 let exact = MaxScoreExecutor::text(lists.clone(), avg, 50, Some(&doc_lengths), params, 1.0)
2286 .execute_windowed()
2287 .unwrap();
2288 check_top_k("exact", &exact, &truth, 50, None);
2289 let approx = MaxScoreExecutor::text(lists, avg, 50, Some(&doc_lengths), params, 0.6)
2290 .execute_windowed()
2291 .unwrap();
2292 assert_eq!(approx.len(), 50);
2293 for hit in &approx {
2294 let own = truth[&hit.doc_id];
2295 assert!((hit.score - own).abs() <= 1e-4 * own.max(1.0));
2296 }
2297 let exact_kth = exact.last().unwrap().score;
2300 assert!(approx.iter().all(|hit| hit.score >= exact_kth * 0.6 - 1e-4));
2301 assert_eq!(approx[0].doc_id, exact[0].doc_id);
2302 let overlap = approx
2303 .iter()
2304 .filter(|hit| exact.iter().any(|e| e.doc_id == hit.doc_id))
2305 .count();
2306 assert!(overlap >= 25, "overlap {overlap} of 50");
2307 }
2308
2309 #[test]
2310 fn test_shared_threshold_monotonic_raise() {
2311 let shared = SharedThreshold::new();
2312 assert_eq!(shared.get(), 0.0);
2313
2314 shared.raise(2.5);
2315 assert_eq!(shared.get(), 2.5);
2316
2317 shared.raise(1.0);
2319 assert_eq!(shared.get(), 2.5);
2320
2321 shared.raise(4.0);
2323 assert_eq!(shared.get(), 4.0);
2324
2325 shared.raise(0.0);
2327 shared.raise(-3.0);
2328 shared.raise(f32::NAN);
2329 assert_eq!(shared.get(), 4.0);
2330
2331 let clone = shared.clone();
2333 clone.raise(9.0);
2334 assert_eq!(shared.get(), 9.0);
2335 }
2336
2337 #[test]
2338 fn test_shared_threshold_seed_matches_manual() {
2339 let mut seeded = ScoreCollector::new(2);
2342 seeded.seed_threshold(3.0);
2343 assert_eq!(seeded.threshold(), 3.0);
2344 assert!(!seeded.would_enter(3.0));
2346 assert!(seeded.would_enter(3.5));
2347 seeded.insert(1, 5.0);
2350 seeded.insert(2, 4.0);
2351 let results = seeded.into_sorted_results();
2352 assert_eq!(results.len(), 2);
2353 assert_eq!(results[0].0, 1);
2354 assert_eq!(results[1].0, 2);
2355 }
2356
2357 #[test]
2358 fn test_shared_threshold_can_raise_after_real_inserts() {
2359 let mut collector = ScoreCollector::new(3);
2360 collector.insert(1, 10.0);
2361 collector.insert(2, 4.0);
2362 assert_eq!(collector.real_len(), 2);
2363
2364 collector.seed_threshold(6.0);
2367 assert_eq!(collector.threshold(), 6.0);
2368 assert_eq!(collector.real_len(), 1);
2369
2370 assert!(collector.would_enter_candidate(3, 6.0, 0));
2373 assert!(collector.insert(3, 6.0));
2374 assert_eq!(collector.real_len(), 2);
2375 let results = collector.into_sorted_results();
2376 assert_eq!(results, vec![(1, 10.0, 0), (3, 6.0, 0)]);
2377 }
2378
2379 #[test]
2380 fn test_large_seed_uses_virtual_sentinels() {
2381 let k = 1_000_000_000;
2382 let mut collector = ScoreCollector::new(k);
2383 assert!(collector.heap.capacity() <= MAX_INITIAL_SCORE_COLLECTOR_CAPACITY);
2384
2385 collector.seed_threshold(42.0);
2386
2387 assert_eq!(collector.heap.len(), 0);
2390 assert!(collector.heap.capacity() <= MAX_INITIAL_SCORE_COLLECTOR_CAPACITY);
2391 assert_eq!(collector.len(), k);
2392 assert_eq!(collector.real_len(), 0);
2393 assert_eq!(collector.threshold(), 42.0);
2394 assert!(!collector.is_empty());
2395
2396 assert!(collector.insert_with_ordinal(9, 42.0, 7));
2399 assert!(!collector.insert(10, 41.0));
2400 assert!(collector.insert(11, 43.0));
2401 assert_eq!(collector.len(), k);
2402 assert_eq!(collector.real_len(), 2);
2403 assert_eq!(
2404 collector.into_sorted_results(),
2405 vec![(11, 43.0, 0), (9, 42.0, 7)]
2406 );
2407 }
2408
2409 #[test]
2410 fn test_virtual_sentinels_preserve_tie_order_when_filled() {
2411 let mut collector = ScoreCollector::new(3);
2412 collector.seed_threshold(5.0);
2413
2414 assert!(collector.insert_with_ordinal(3, 5.0, 2));
2415 assert!(collector.insert_with_ordinal(2, 5.0, 8));
2416 assert!(collector.insert_with_ordinal(1, 5.0, 4));
2417 assert_eq!(collector.real_len(), 3);
2418 assert!(collector.virtual_threshold.is_none());
2419
2420 assert!(collector.insert_with_ordinal(2, 5.0, 1));
2423 assert!(!collector.insert_with_ordinal(4, 5.0, 0));
2424 assert_eq!(
2425 collector.into_sorted_results(),
2426 vec![(1, 5.0, 4), (2, 5.0, 1), (2, 5.0, 8)]
2427 );
2428 }
2429
2430 #[test]
2431 fn test_score_collector_basic() {
2432 let mut collector = ScoreCollector::new(3);
2433
2434 collector.insert(1, 1.0);
2435 collector.insert(2, 2.0);
2436 collector.insert(3, 3.0);
2437 assert_eq!(collector.threshold(), 1.0);
2438
2439 collector.insert(4, 4.0);
2440 assert_eq!(collector.threshold(), 2.0);
2441
2442 let results = collector.into_sorted_results();
2443 assert_eq!(results.len(), 3);
2444 assert_eq!(results[0].0, 4); assert_eq!(results[1].0, 3);
2446 assert_eq!(results[2].0, 2);
2447 }
2448
2449 #[test]
2450 fn test_score_collector_threshold() {
2451 let mut collector = ScoreCollector::new(2);
2452
2453 collector.insert(1, 5.0);
2454 collector.insert(2, 3.0);
2455 assert_eq!(collector.threshold(), 3.0);
2456
2457 assert!(!collector.would_enter(2.0));
2459 assert!(!collector.insert(3, 2.0));
2460
2461 assert!(collector.would_enter(4.0));
2463 assert!(collector.insert(4, 4.0));
2464 assert_eq!(collector.threshold(), 4.0);
2465 }
2466
2467 #[test]
2468 fn test_heap_entry_ordering() {
2469 let mut heap = BinaryHeap::new();
2470 heap.push(HeapEntry {
2471 doc_id: 1,
2472 score: 3.0,
2473 ordinal: 0,
2474 });
2475 heap.push(HeapEntry {
2476 doc_id: 2,
2477 score: 1.0,
2478 ordinal: 0,
2479 });
2480 heap.push(HeapEntry {
2481 doc_id: 3,
2482 score: 2.0,
2483 ordinal: 0,
2484 });
2485
2486 assert_eq!(heap.pop().unwrap().score, 1.0);
2488 assert_eq!(heap.pop().unwrap().score, 2.0);
2489 assert_eq!(heap.pop().unwrap().score, 3.0);
2490 }
2491}