1use std::cmp::Ordering;
10use std::collections::BinaryHeap;
11
12use log::{debug, warn};
13
14use crate::DocId;
15
16mod conjunction;
17mod windows;
18
19const MAX_INITIAL_SCORE_COLLECTOR_CAPACITY: usize = 8 * 1024;
23
24#[derive(Clone, Copy)]
26pub struct HeapEntry {
27 pub doc_id: DocId,
28 pub score: f32,
29 pub ordinal: u16,
30}
31
32impl PartialEq for HeapEntry {
33 fn eq(&self, other: &Self) -> bool {
34 self.score.to_bits() == other.score.to_bits()
35 && self.doc_id == other.doc_id
36 && self.ordinal == other.ordinal
37 }
38}
39
40impl Eq for HeapEntry {}
41
42impl Ord for HeapEntry {
43 fn cmp(&self, other: &Self) -> Ordering {
44 other
48 .score
49 .total_cmp(&self.score)
50 .then_with(|| self.doc_id.cmp(&other.doc_id))
51 .then_with(|| self.ordinal.cmp(&other.ordinal))
52 }
53}
54
55impl PartialOrd for HeapEntry {
56 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
57 Some(self.cmp(other))
58 }
59}
60
61pub struct ScoreCollector {
74 heap: BinaryHeap<HeapEntry>,
76 pub k: usize,
77 cached_threshold: f32,
80 virtual_threshold: Option<f32>,
84}
85
86impl ScoreCollector {
87 pub fn new(k: usize) -> Self {
89 Self {
90 heap: BinaryHeap::with_capacity(k.min(MAX_INITIAL_SCORE_COLLECTOR_CAPACITY)),
91 k,
92 cached_threshold: 0.0,
93 virtual_threshold: None,
94 }
95 }
96
97 #[inline]
99 pub fn threshold(&self) -> f32 {
100 self.cached_threshold
101 }
102
103 #[inline]
105 fn update_threshold(&mut self) {
106 self.cached_threshold = if let Some(threshold) = self.virtual_threshold {
107 threshold
108 } else if self.heap.len() >= self.k {
109 self.heap.peek().map(|e| e.score).unwrap_or(0.0)
110 } else {
111 0.0
112 };
113 }
114
115 #[inline]
118 pub fn insert(&mut self, doc_id: DocId, score: f32) -> bool {
119 self.insert_with_ordinal(doc_id, score, 0)
120 }
121
122 #[inline]
125 pub fn insert_with_ordinal(&mut self, doc_id: DocId, score: f32, ordinal: u16) -> bool {
126 if self.k == 0 {
127 return false;
128 }
129 let entry = HeapEntry {
130 doc_id,
131 score,
132 ordinal,
133 };
134 if self.heap.len() < self.k {
135 if let Some(threshold) = self.virtual_threshold {
136 let sentinel = HeapEntry {
137 doc_id: u32::MAX,
138 score: threshold,
139 ordinal: 0,
140 };
141 if entry >= sentinel {
142 return false;
143 }
144 }
145
146 self.heap.push(entry);
147 crate::observe::search_work!(maxscore_heap_updates += 1);
148 if self.heap.len() == self.k {
150 self.virtual_threshold = None;
151 self.update_threshold();
152 }
153 true
154 } else if score < self.cached_threshold {
155 false
156 } else if self.heap.peek().is_some_and(|worst| entry < *worst) {
157 {
158 let mut worst = self.heap.peek_mut().expect("full heap has a root");
159 *worst = entry;
160 }
161 self.update_threshold();
162 crate::observe::search_work!(maxscore_heap_updates += 1);
163 true
164 } else {
165 false
166 }
167 }
168
169 fn insert_text_run(&mut self, docs: &[DocId], scores: &[f32]) {
173 self.insert_text_run_with_mapping(docs, scores, |doc| doc);
174 }
175
176 fn insert_text_run_with_mapping(
178 &mut self,
179 docs: &[DocId],
180 scores: &[f32],
181 resolve: impl Fn(DocId) -> DocId,
182 ) {
183 debug_assert_eq!(docs.len(), scores.len());
184 crate::observe::search_work!(score_batches += 1);
185 let (blocks, tail) = scores.as_chunks::<8>();
186 for (docs, scores) in docs.chunks_exact(8).zip(blocks) {
187 let threshold = if self.heap.len() >= self.k {
188 self.cached_threshold
189 } else {
190 f32::NEG_INFINITY
191 };
192 let mut candidates = 0u8;
193 for (i, &score) in scores.iter().enumerate() {
194 let eligible = if score < threshold { 0 } else { 1 };
195 candidates |= eligible << i;
196 }
197 while candidates != 0 {
198 let i = candidates.trailing_zeros() as usize;
199 self.insert(resolve(docs[i]), 0.0 + scores[i]);
200 candidates &= candidates - 1;
201 }
202 }
203 for (&doc, &score) in docs[blocks.len() * 8..].iter().zip(tail) {
204 if self.heap.len() >= self.k && score < self.cached_threshold {
205 continue;
206 }
207 self.insert(resolve(doc), 0.0 + score);
208 }
209 }
210
211 #[cfg(test)]
213 pub fn would_enter(&self, score: f32) -> bool {
214 self.len() < self.k || score > self.cached_threshold
215 }
216
217 #[cfg(test)]
220 pub fn would_enter_candidate(&self, doc_id: DocId, score: f32, ordinal: u16) -> bool {
221 if self.k == 0 {
222 return false;
223 }
224 let entry = HeapEntry {
225 doc_id,
226 score,
227 ordinal,
228 };
229 if let Some(threshold) = self.virtual_threshold {
230 let sentinel = HeapEntry {
231 doc_id: u32::MAX,
232 score: threshold,
233 ordinal: 0,
234 };
235 entry < sentinel
236 } else {
237 self.heap.len() < self.k || self.heap.peek().is_some_and(|worst| entry < *worst)
238 }
239 }
240
241 #[inline]
243 pub fn len(&self) -> usize {
244 if self.virtual_threshold.is_some() {
245 self.k
246 } else {
247 self.heap.len()
248 }
249 }
250
251 #[inline]
253 pub fn real_len(&self) -> usize {
254 self.heap.len()
255 }
256
257 #[inline]
259 pub fn is_empty(&self) -> bool {
260 self.len() == 0
261 }
262
263 pub fn seed_threshold(&mut self, initial_threshold: f32) {
270 if initial_threshold <= 0.0
271 || self.k == 0
272 || (self.len() >= self.k && initial_threshold <= self.cached_threshold)
273 {
274 return;
275 }
276
277 let sentinel = HeapEntry {
278 doc_id: u32::MAX,
279 score: initial_threshold,
280 ordinal: 0,
281 };
282
283 if let Some(current_threshold) = self.virtual_threshold {
287 let current = HeapEntry {
288 doc_id: u32::MAX,
289 score: current_threshold,
290 ordinal: 0,
291 };
292 if sentinel >= current {
293 return;
294 }
295 } else if self.heap.len() >= self.k
296 && !self.heap.peek().is_some_and(|worst| sentinel < *worst)
297 {
298 return;
299 }
300
301 self.virtual_threshold = Some(initial_threshold);
302 while self.heap.peek().is_some_and(|worst| sentinel < *worst) {
303 self.heap.pop();
304 }
305 self.update_threshold();
306 }
307
308 pub fn into_sorted_results(self) -> Vec<(DocId, f32, u16)> {
311 let mut results: Vec<(DocId, f32, u16)> = self
312 .heap
313 .into_vec()
314 .into_iter()
315 .filter(|e| e.doc_id != u32::MAX)
316 .map(|e| (e.doc_id, e.score, e.ordinal))
317 .collect();
318
319 results.sort_unstable_by(|a, b| {
321 b.1.total_cmp(&a.1)
322 .then_with(|| a.0.cmp(&b.0))
323 .then_with(|| a.2.cmp(&b.2))
324 });
325
326 results
327 }
328}
329
330#[derive(Clone, Debug)]
353pub struct SharedThreshold {
354 floor: std::sync::Arc<std::sync::atomic::AtomicU32>,
355 k: usize,
358 deadline: Option<std::time::Instant>,
361 truncated: std::sync::Arc<std::sync::atomic::AtomicBool>,
363}
364
365impl Default for SharedThreshold {
366 fn default() -> Self {
367 Self::new()
368 }
369}
370
371impl SharedThreshold {
372 pub fn new() -> Self {
376 Self::with_depth(usize::MAX)
377 }
378
379 pub fn for_limit(limit: usize) -> Self {
381 Self::with_depth(limit)
382 }
383
384 fn with_depth(k: usize) -> Self {
385 Self {
386 floor: std::sync::Arc::new(std::sync::atomic::AtomicU32::new(0)),
388 k,
389 deadline: None,
390 truncated: std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)),
391 }
392 }
393
394 pub fn with_deadline(mut self, deadline: Option<std::time::Instant>) -> Self {
396 self.deadline = deadline;
397 self
398 }
399
400 pub fn deadline(&self) -> Option<std::time::Instant> {
402 self.deadline
403 }
404
405 pub(crate) fn budget_only(&self) -> Self {
407 Self {
408 floor: std::sync::Arc::new(std::sync::atomic::AtomicU32::new(0)),
409 k: usize::MAX,
410 deadline: self.deadline,
411 truncated: self.truncated.clone(),
412 }
413 }
414
415 #[inline]
416 pub(crate) fn stop_if_expired(&self) -> bool {
417 if self.expired() {
418 self.mark_truncated();
419 true
420 } else {
421 false
422 }
423 }
424
425 #[inline]
427 pub fn expired(&self) -> bool {
428 self.deadline
429 .is_some_and(|deadline| std::time::Instant::now() >= deadline)
430 }
431
432 pub fn mark_truncated(&self) {
434 self.truncated
435 .store(true, std::sync::atomic::Ordering::Relaxed);
436 }
437
438 pub fn truncated(&self) -> bool {
440 self.truncated.load(std::sync::atomic::Ordering::Relaxed)
441 }
442
443 #[inline]
446 pub(crate) fn covers(&self, heap_depth: usize) -> bool {
447 heap_depth >= self.k
448 }
449
450 #[inline]
452 pub fn get(&self) -> f32 {
453 f32::from_bits(self.floor.load(std::sync::atomic::Ordering::Relaxed))
454 }
455
456 pub fn raise(&self, score: f32) {
461 if score <= 0.0 {
464 return;
465 }
466 use std::sync::atomic::Ordering::Relaxed;
467 let bits = score.to_bits();
468 let mut cur = self.floor.load(Relaxed);
469 while f32::from_bits(cur) < score {
470 match self
471 .floor
472 .compare_exchange_weak(cur, bits, Relaxed, Relaxed)
473 {
474 Ok(_) => break,
475 Err(actual) => cur = actual,
476 }
477 }
478 }
479}
480
481#[derive(Debug, Clone, Copy)]
483pub struct ScoredDoc {
484 pub doc_id: DocId,
485 pub score: f32,
486 pub ordinal: u16,
488}
489
490pub struct MaxScoreExecutor<'a> {
501 metric_index: &'a str,
505 metric_field: &'a str,
506 cursors: Vec<TermCursor<'a>>,
507 prefix_sums: Vec<f32>,
508 score_order: Vec<usize>,
510 all_required: bool,
512 required_mask: u64,
514 collector: ScoreCollector,
515 document_map: Option<&'a crate::segment::chunk_map::ChunkMap>,
517 inv_heap_factor: f32,
518 predicate: Option<super::DocPredicate<'a>>,
519 budget: Option<SharedThreshold>,
522 dropped_cursors: usize,
526 configuration_error: Option<String>,
529 stats: ExecutorStats,
531}
532
533#[derive(Clone, Copy, Debug, Default)]
536pub(crate) struct ExecutorStats {
537 pub windows: u64,
540 pub windows_skipped: u64,
541 pub groups_skipped: u64,
542 pub candidates: u64,
545 pub docs_scored: u64,
546 pub optional_leads: u64,
549 pub blocks_scored: u64,
551 pub blocks_skipped: u64,
552}
553
554#[derive(Clone, Copy)]
558pub enum LengthSource<'a> {
559 Chunks(&'a crate::segment::chunk_map::ChunkMap),
560 Docs(&'a crate::segment::chunk_map::DocLengths),
561}
562
563impl LengthSource<'_> {
564 #[inline]
565 pub fn length(&self, id: u32) -> u32 {
566 match self {
567 LengthSource::Chunks(map) => map.bm25_length(id),
568 LengthSource::Docs(lengths) => lengths.length(id),
569 }
570 }
571
572 pub(crate) fn gather_lengths(&self, ids: &[u32], out: &mut [u32]) {
573 match self {
574 LengthSource::Chunks(map) => map.gather_bm25_lengths(ids, out),
575 LengthSource::Docs(lengths) => lengths.gather_lengths(ids, out),
576 }
577 }
578}
579
580pub(crate) struct TermCursor<'a> {
589 pub max_score: f32,
590 num_blocks: usize,
591 block_idx: usize,
593 doc_ids: Vec<u32>,
596 scores: Vec<f32>,
599 ordinals: Vec<u16>,
600 tfs: Vec<u32>,
602 pos: usize,
603 block_loaded: bool,
604 exhausted: bool,
605 lazy_ordinals: bool,
609 ordinals_loaded: bool,
611 current_sparse_block: Option<crate::structures::SparseBlock>,
613 variant: CursorVariant<'a>,
615}
616
617#[allow(clippy::large_enum_variant)]
620enum CursorVariant<'a> {
621 Text {
623 list: crate::structures::BlockPostingList,
624 idf: f32,
625 lengths: Option<LengthSource<'a>>,
628 length_bounds: bool,
632 length_floor: u32,
633 block_bound: CachedScoreBound,
634 group_bound: CachedScoreBound,
635 prepared_bounds: Option<super::bm25::PreparedBounds>,
636 avg_len: f32,
638 params: super::Bm25Params,
640 normalization: Option<Box<super::bm25::NormTable>>,
641 deferred_tf: Option<(usize, usize, usize)>,
645 },
646 Sparse {
648 si: &'a crate::segment::SparseIndex,
649 query_weight: f32,
650 skip_start: usize,
651 block_data_offset: u64,
652 },
653}
654
655#[derive(Default)]
660struct CursorBuffers {
661 doc_ids: Vec<u32>,
662 scores: Vec<f32>,
663 ordinals: Vec<u16>,
664 tfs: Vec<u32>,
665}
666
667const CURSOR_BUFFER_POOL_LIMIT: usize = 2 * super::MAX_QUERY_TERMS;
669
670thread_local! {
671 static CURSOR_BUFFERS: std::cell::RefCell<Vec<CursorBuffers>> =
672 const { std::cell::RefCell::new(Vec::new()) };
673}
674
675impl CursorBuffers {
676 fn take() -> Self {
678 CURSOR_BUFFERS
679 .with(|pool| pool.borrow_mut().pop())
680 .unwrap_or_else(|| {
681 const BLOCK: usize = if crate::structures::postings::POSTING_BLOCK_SIZE > 256 {
686 crate::structures::postings::POSTING_BLOCK_SIZE
687 } else {
688 256
689 };
690 Self {
691 doc_ids: Vec::with_capacity(BLOCK),
692 scores: Vec::with_capacity(BLOCK),
693 ordinals: Vec::new(),
694 tfs: Vec::with_capacity(BLOCK),
695 }
696 })
697 }
698
699 fn recycle(mut self) {
701 self.doc_ids.clear();
702 self.scores.clear();
703 self.ordinals.clear();
704 self.tfs.clear();
705 CURSOR_BUFFERS.with(|pool| {
706 let mut pool = pool.borrow_mut();
707 if pool.len() < CURSOR_BUFFER_POOL_LIMIT {
708 pool.push(self);
709 }
710 });
711 }
712}
713
714impl Drop for TermCursor<'_> {
715 fn drop(&mut self) {
716 CursorBuffers {
717 doc_ids: std::mem::take(&mut self.doc_ids),
718 scores: std::mem::take(&mut self.scores),
719 ordinals: std::mem::take(&mut self.ordinals),
720 tfs: std::mem::take(&mut self.tfs),
721 }
722 .recycle();
723 }
724}
725
726#[allow(clippy::too_many_arguments)]
727pub(super) fn score_text_run(
728 params: super::Bm25Params,
729 idf: f32,
730 avg_len: f32,
731 lengths: Option<LengthSource<'_>>,
732 normalization: Option<&super::bm25::NormTable>,
733 docs: &[DocId],
734 tfs: &[u32],
735 scores: &mut [f32],
736) {
737 debug_assert_eq!(docs.len(), tfs.len());
738 debug_assert_eq!(docs.len(), scores.len());
739 crate::observe::search_work!(score_batches += 1);
740 if let (Some(LengthSource::Docs(lengths)), Some(table)) = (lengths, normalization) {
741 crate::observe::search_work!(lookup_score_units += docs.len());
742 table.score_batch(
743 params,
744 idf,
745 avg_len,
746 1.0,
747 docs.iter().map(|&doc| lengths.norm_code(doc)),
748 tfs,
749 scores,
750 );
751 return;
752 }
753 let mut gathered = [0; crate::structures::postings::POSTING_BLOCK_SIZE];
754 crate::observe::search_work!(exact_score_units += docs.len());
755 if let Some(source) = lengths {
756 source.gather_lengths(docs, &mut gathered[..docs.len()]);
757 }
758 for ((&tf, &len), score) in tfs.iter().zip(&gathered[..docs.len()]).zip(scores) {
759 let tf = tf as f32;
760 let len = if len == 0 { tf } else { len as f32 };
761 *score = params.score(tf, idf, len, avg_len);
762 }
763}
764
765struct CachedScoreBound(std::sync::atomic::AtomicU64);
768
769impl CachedScoreBound {
770 fn new() -> Self {
771 Self(std::sync::atomic::AtomicU64::new(u64::MAX))
772 }
773
774 fn get_or_compute(&self, key: usize, compute: impl FnOnce() -> f32) -> f32 {
775 use std::sync::atomic::Ordering::Relaxed;
776 let key = key as u64;
778 let entry = self.0.load(Relaxed);
779 if entry >> 32 == key {
780 return f32::from_bits(entry as u32);
781 }
782 let score = compute();
783 self.0
784 .store((key << 32) | u64::from(score.to_bits()), Relaxed);
785 score
786 }
787}
788
789macro_rules! cursor_ensure_block {
797 ($self:ident, $load_block_fn:ident, $($aw:tt)*) => {{
798 if $self.exhausted || $self.block_loaded {
799 return Ok(!$self.exhausted);
800 }
801 match &mut $self.variant {
802 CursorVariant::Text {
803 list,
804 deferred_tf,
805 ..
806 } => {
807 if let Some(state) = list.decode_block_doc_ids_only($self.block_idx, &mut $self.doc_ids) {
808 *deferred_tf = Some(state);
809 $self.scores.clear();
810 $self.pos = 0;
811 $self.block_loaded = true;
812 Ok(true)
813 } else {
814 $self.exhausted = true;
819 Err(crate::Error::Corruption(format!(
820 "text posting block {} of {} failed to decode",
821 $self.block_idx, $self.num_blocks
822 )))
823 }
824 }
825 CursorVariant::Sparse {
826 si,
827 query_weight,
828 skip_start,
829 block_data_offset,
830 ..
831 } => {
832 let block = si
833 .$load_block_fn(*skip_start, *block_data_offset, $self.block_idx)
834 $($aw)* ?;
835 match block {
836 Some(b) => {
837 b.decode_doc_ids_into(&mut $self.doc_ids);
838 b.decode_scored_weights_into(*query_weight, &mut $self.scores);
839 if $self.lazy_ordinals {
840 $self.current_sparse_block = Some(b);
843 $self.ordinals_loaded = false;
844 } else {
845 b.decode_ordinals_into(&mut $self.ordinals);
846 $self.ordinals_loaded = true;
847 $self.current_sparse_block = None;
848 }
849 $self.pos = 0;
850 $self.block_loaded = true;
851 Ok(true)
852 }
853 None => {
854 $self.exhausted = true;
855 Ok(false)
856 }
857 }
858 }
859 }
860 }};
861}
862
863macro_rules! cursor_advance {
864 ($self:ident, $ensure_fn:ident, $($aw:tt)*) => {{
865 if $self.exhausted {
866 return Ok(u32::MAX);
867 }
868 $self.$ensure_fn() $($aw)* ?;
869 if $self.exhausted {
870 return Ok(u32::MAX);
871 }
872 Ok($self.advance_pos())
873 }};
874}
875
876macro_rules! cursor_seek {
877 ($self:ident, $ensure_fn:ident, $target:expr, $($aw:tt)*) => {{
878 if let Some(doc) = $self.seek_prepare($target) {
879 return Ok(doc);
880 }
881 $self.$ensure_fn() $($aw)* ?;
882 if $self.seek_finish($target) {
883 $self.$ensure_fn() $($aw)* ?;
884 }
885 Ok($self.doc())
886 }};
887}
888
889impl<'a> TermCursor<'a> {
890 pub fn text_with_params(
894 posting_list: crate::structures::BlockPostingList,
895 idf: f32,
896 avg_field_len: f32,
897 lengths: Option<LengthSource<'a>>,
898 params: super::Bm25Params,
899 ) -> Self {
900 let posting_max_tf = posting_list.max_tf();
901 let max_tf = posting_max_tf as f32;
902 let safe_avg = avg_field_len.max(1.0);
903 let length_bounds = lengths.is_some() && posting_list.min_len().is_some();
904 let length_floor = match lengths {
905 Some(LengthSource::Chunks(map)) => map.length_floor(),
906 _ => 0,
907 };
908 let max_score = match posting_list.min_len() {
909 Some(min_len) if length_bounds => params.upper_bound_with_len(
910 max_tf.max(1.0),
911 idf,
912 min_len.max(length_floor) as f32,
913 safe_avg,
914 ),
915 _ => params.upper_bound(max_tf.max(1.0), idf),
916 };
917 let num_blocks = posting_list.num_blocks();
918 let buffers = CursorBuffers::take();
919 Self {
920 max_score,
921 num_blocks,
922 block_idx: 0,
923 doc_ids: buffers.doc_ids,
924 scores: buffers.scores,
925 ordinals: buffers.ordinals,
926 tfs: buffers.tfs,
927 pos: 0,
928 block_loaded: false,
929 exhausted: num_blocks == 0,
930 lazy_ordinals: false,
931 ordinals_loaded: true, current_sparse_block: None,
933 variant: CursorVariant::Text {
934 list: posting_list,
935 idf,
936 lengths,
937 length_bounds,
938 length_floor,
939 block_bound: CachedScoreBound::new(),
940 group_bound: CachedScoreBound::new(),
941 prepared_bounds: super::bm25::PreparedBounds::new(
942 params,
943 posting_max_tf,
944 idf,
945 safe_avg,
946 ),
947 avg_len: safe_avg,
948 params,
949 normalization: match lengths {
950 Some(LengthSource::Docs(lengths)) if lengths.is_quantized() => {
951 Some(Box::new(super::bm25::NormTable::new(params, safe_avg)))
952 }
953 _ => None,
954 },
955 deferred_tf: None,
956 },
957 }
958 }
959
960 pub fn sparse(
963 si: &'a crate::segment::SparseIndex,
964 query_weight: f32,
965 skip_start: usize,
966 skip_count: usize,
967 global_max_weight: f32,
968 block_data_offset: u64,
969 ) -> Self {
970 let buffers = CursorBuffers::take();
971 Self {
972 max_score: query_weight.abs() * global_max_weight,
973 num_blocks: skip_count,
974 block_idx: 0,
975 doc_ids: buffers.doc_ids,
976 scores: buffers.scores,
977 ordinals: buffers.ordinals,
978 tfs: buffers.tfs,
979 pos: 0,
980 block_loaded: false,
981 exhausted: skip_count == 0,
982 lazy_ordinals: false,
983 ordinals_loaded: true,
984 current_sparse_block: None,
985 variant: CursorVariant::Sparse {
986 si,
987 query_weight,
988 skip_start,
989 block_data_offset,
990 },
991 }
992 }
993
994 #[inline]
997 fn block_first_doc(&self, idx: usize) -> DocId {
998 match &self.variant {
999 CursorVariant::Text { list, .. } => list.block_first_doc(idx).unwrap_or(u32::MAX),
1000 CursorVariant::Sparse { si, skip_start, .. } => {
1001 si.read_skip_entry(*skip_start + idx).first_doc
1002 }
1003 }
1004 }
1005
1006 #[inline]
1007 fn block_last_doc(&self, idx: usize) -> DocId {
1008 match &self.variant {
1009 CursorVariant::Text { list, .. } => list.block_last_doc(idx).unwrap_or(0),
1010 CursorVariant::Sparse { si, skip_start, .. } => {
1011 si.read_skip_entry(*skip_start + idx).last_doc
1012 }
1013 }
1014 }
1015
1016 #[inline]
1019 pub fn doc(&self) -> DocId {
1020 if self.exhausted {
1021 return u32::MAX;
1022 }
1023 if self.block_loaded {
1024 debug_assert!(self.pos < self.doc_ids.len());
1025 unsafe { *self.doc_ids.get_unchecked(self.pos) }
1027 } else {
1028 self.block_first_doc(self.block_idx)
1029 }
1030 }
1031
1032 #[inline]
1033 pub fn ordinal(&self) -> u16 {
1034 if !self.block_loaded || self.ordinals.is_empty() {
1035 return 0;
1036 }
1037 debug_assert!(self.pos < self.ordinals.len());
1038 unsafe { *self.ordinals.get_unchecked(self.pos) }
1040 }
1041
1042 #[inline]
1048 fn ordinal_mut(&mut self) -> u16 {
1049 if !self.block_loaded {
1050 return 0;
1051 }
1052 if !self.ordinals_loaded {
1053 if let Some(ref block) = self.current_sparse_block {
1054 block.decode_ordinals_into(&mut self.ordinals);
1055 }
1056 self.ordinals_loaded = true;
1057 }
1058 if self.ordinals.is_empty() {
1059 return 0;
1060 }
1061 debug_assert!(self.pos < self.ordinals.len());
1062 unsafe { *self.ordinals.get_unchecked(self.pos) }
1063 }
1064
1065 #[inline]
1066 pub fn score(&self) -> f32 {
1067 if !self.block_loaded {
1068 return 0.0;
1069 }
1070 debug_assert!(self.pos < self.scores.len());
1071 unsafe { *self.scores.get_unchecked(self.pos) }
1073 }
1074
1075 #[inline]
1081 fn ensure_scores(&mut self) {
1082 if self.block_loaded && self.scores.is_empty() {
1083 self.compute_deferred_scores();
1084 }
1085 }
1086
1087 #[inline]
1088 fn current_block_max_score(&self) -> f32 {
1089 if self.exhausted {
1090 return 0.0;
1091 }
1092 match &self.variant {
1093 CursorVariant::Text { .. } => self.text_block_bound(self.block_idx),
1094 CursorVariant::Sparse {
1095 si,
1096 query_weight,
1097 skip_start,
1098 ..
1099 } => query_weight.abs() * si.read_skip_entry(*skip_start + self.block_idx).max_weight,
1100 }
1101 }
1102
1103 #[inline]
1107 fn current_group_max_score(&self) -> Option<f32> {
1108 if self.exhausted {
1109 return Some(0.0);
1110 }
1111 match &self.variant {
1112 CursorVariant::Text { .. } => self.text_group_bound(self.block_idx),
1113 CursorVariant::Sparse { .. } => None,
1114 }
1115 }
1116
1117 #[inline]
1120 fn is_text(&self) -> bool {
1121 matches!(self.variant, CursorVariant::Text { .. })
1122 }
1123
1124 fn supports_text_block_pruning(&self) -> bool {
1126 if !self.max_score.is_finite() {
1127 return false;
1128 }
1129 match &self.variant {
1130 CursorVariant::Text {
1131 length_bounds,
1132 prepared_bounds,
1133 ..
1134 } => *length_bounds && prepared_bounds.is_some(),
1135 CursorVariant::Sparse { .. } => false,
1136 }
1137 }
1138
1139 fn text_block_bound(&self, idx: usize) -> f32 {
1141 self.text_block_bound_for_threshold(idx, f32::NEG_INFINITY)
1142 }
1143
1144 fn text_block_bound_for_threshold(&self, idx: usize, threshold: f32) -> f32 {
1147 crate::observe::search_work!(block_bound_calls += 1);
1148 match &self.variant {
1149 CursorVariant::Text {
1150 list,
1151 idf,
1152 length_bounds,
1153 length_floor,
1154 prepared_bounds,
1155 block_bound,
1156 avg_len,
1157 params,
1158 ..
1159 } => block_bound.get_or_compute(idx, || {
1160 let (max_tf, min_len) = list.block_bounds(idx).unwrap_or((0, None));
1161 let bound = match min_len {
1162 Some(min_len) if *length_bounds => params.upper_bound_with_len(
1163 (max_tf as f32).max(1.0),
1164 *idf,
1165 min_len.max(*length_floor) as f32,
1166 *avg_len,
1167 ),
1168 _ => params.upper_bound((max_tf as f32).max(1.0), *idf),
1169 };
1170 if *length_bounds {
1171 let bound =
1172 bound.min(prepared_bounds.as_ref().map_or(f32::INFINITY, |bounds| {
1173 bounds.ratio(max_tf, list.block_length_ratio(idx))
1174 }));
1175 if bound >= threshold && list.has_impact_bounds() {
1176 bound.min(prepared_bounds.as_ref().map_or(f32::INFINITY, |bounds| {
1177 bounds.impacts(|a, b| list.block_impact_minimum(idx, a, b))
1178 }))
1179 } else {
1180 bound
1181 }
1182 } else {
1183 bound
1184 }
1185 }),
1186 CursorVariant::Sparse { .. } => self.max_score,
1187 }
1188 }
1189
1190 fn text_group_bound(&self, idx: usize) -> Option<f32> {
1192 self.text_group_bound_for_threshold(idx, f32::NEG_INFINITY)
1193 }
1194
1195 fn text_group_bound_for_threshold(&self, idx: usize, threshold: f32) -> Option<f32> {
1196 crate::observe::search_work!(group_bound_calls += 1);
1197 match &self.variant {
1198 CursorVariant::Text {
1199 list,
1200 idf,
1201 length_bounds,
1202 length_floor,
1203 prepared_bounds,
1204 group_bound,
1205 avg_len,
1206 params,
1207 ..
1208 } => {
1209 let (max_tf, min_len) = list.group_bounds(idx)?;
1210 Some(group_bound.get_or_compute(list.next_group_block(idx), || {
1211 if *length_bounds {
1212 let bound = params
1213 .upper_bound_with_len(
1214 (max_tf as f32).max(1.0),
1215 *idf,
1216 min_len.max(*length_floor) as f32,
1217 *avg_len,
1218 )
1219 .min(prepared_bounds.as_ref().map_or(f32::INFINITY, |bounds| {
1220 bounds.ratio(max_tf, list.group_length_ratio(idx))
1221 }));
1222 if bound >= threshold && list.has_group_impact_bounds() {
1223 bound.min(prepared_bounds.as_ref().map_or(f32::INFINITY, |bounds| {
1224 bounds.impacts(|a, b| list.group_impact_minimum(idx, a, b))
1225 }))
1226 } else {
1227 bound
1228 }
1229 } else {
1230 params.upper_bound((max_tf as f32).max(1.0), *idf)
1231 }
1232 }))
1233 }
1234 CursorVariant::Sparse { .. } => None,
1235 }
1236 }
1237
1238 fn has_group_impacts(&self) -> bool {
1239 matches!(&self.variant, CursorVariant::Text { list, .. } if list.has_group_impact_bounds())
1240 }
1241
1242 fn text_group_span_from(&self, from: DocId) -> Option<(DocId, DocId, f32)> {
1245 if self.exhausted {
1246 return None;
1247 }
1248 let CursorVariant::Text { list, .. } = &self.variant else {
1249 return None;
1250 };
1251 let start = from.max(self.doc());
1252 let idx = list.seek_block(start, self.block_idx)?;
1253 let first = start.max(list.block_first_doc(idx)?);
1254 let (last, bound) = if let Some(bound) = self.text_group_bound(idx) {
1255 (list.group_last_doc(idx)?, bound)
1256 } else {
1257 (list.block_last_doc(idx)?, self.text_block_bound(idx))
1258 };
1259 Some((first, last, bound))
1260 }
1261
1262 fn window_upper_bound(&self, from: DocId, to: DocId) -> f32 {
1268 if self.exhausted {
1269 return 0.0;
1270 }
1271 let CursorVariant::Text { list, .. } = &self.variant else {
1272 return self.max_score;
1273 };
1274 let start = from.max(self.doc());
1277 if start > to {
1278 return 0.0;
1279 }
1280 if self.block_last_doc(self.block_idx) >= to
1283 && !(list.is_group_start(self.block_idx)
1284 && list
1285 .group_last_doc(self.block_idx)
1286 .is_some_and(|last| last <= to))
1287 {
1288 return 0.0f32.max(self.text_block_bound(self.block_idx));
1289 }
1290 let Some(mut idx) = list.seek_block(start, self.block_idx) else {
1291 return 0.0;
1292 };
1293 let mut bound = 0.0f32;
1294 while idx < self.num_blocks {
1295 if list.block_first_doc(idx).unwrap_or(u32::MAX) > to {
1296 break;
1297 }
1298 if list.is_group_start(idx)
1299 && list.group_last_doc(idx).is_some_and(|last| last <= to)
1300 && let Some(group_bound) = self.text_group_bound(idx)
1301 {
1302 bound = bound.max(group_bound);
1303 idx = list.next_group_block(idx);
1304 continue;
1305 }
1306 bound = bound.max(self.text_block_bound(idx));
1307 idx += 1;
1308 }
1309 bound
1310 }
1311
1312 fn score_window_sync(
1320 &mut self,
1321 from: DocId,
1322 to: DocId,
1323 scores: &mut [f32],
1324 mask: &mut [u64],
1325 mut contributions: Option<(&mut [f32], &mut [u64])>,
1326 ) -> crate::Result<u32> {
1327 self.visit_scored_window_sync(to, |docs, block_scores| {
1328 for (doc, score) in docs.iter().zip(block_scores) {
1329 let slot = (doc - from) as usize;
1330 scores[slot] += score;
1331 if let Some((values, present)) = contributions.as_mut() {
1332 values[slot] = *score;
1333 present[slot >> 6] |= 1u64 << (slot & 63);
1334 }
1335 mask[slot >> 6] |= 1u64 << (slot & 63);
1336 }
1337 })
1338 }
1339
1340 fn append_scored_window_sync(
1350 &mut self,
1351 from: DocId,
1352 to: DocId,
1353 docs: &mut Vec<DocId>,
1354 scores: &mut Vec<f32>,
1355 mut contributions: Option<(&mut [f32], &mut [u64])>,
1356 ) -> crate::Result<u32> {
1357 self.visit_scored_window_sync(to, |run_docs, run_scores| {
1358 docs.extend_from_slice(run_docs);
1359 scores.extend(run_scores.iter().map(|score| 0.0 + score));
1360 if let Some((values, present)) = contributions.as_mut() {
1361 for (&doc, &score) in run_docs.iter().zip(run_scores) {
1362 let slot = (doc - from) as usize;
1363 values[slot] = score;
1364 present[slot >> 6] |= 1u64 << (slot & 63);
1365 }
1366 }
1367 })
1368 }
1369
1370 fn visit_scored_window_sync(
1373 &mut self,
1374 to: DocId,
1375 mut visit: impl FnMut(&[DocId], &[f32]),
1376 ) -> crate::Result<u32> {
1377 let mut matched = 0u32;
1378 loop {
1379 if self.exhausted {
1380 return Ok(matched);
1381 }
1382 if !self.block_loaded {
1383 if self.block_first_doc(self.block_idx) > to {
1384 return Ok(matched);
1385 }
1386 self.ensure_block_loaded_sync()?;
1387 if self.exhausted {
1388 return Ok(matched);
1389 }
1390 }
1391 if self.doc_ids[self.pos] > to {
1392 return Ok(matched);
1393 }
1394 self.ensure_scores();
1395 let remaining = &self.doc_ids[self.pos..];
1396 let end = if to == u32::MAX {
1397 remaining.len()
1398 } else {
1399 crate::structures::simd::find_first_ge_u32(remaining, to + 1)
1400 };
1401 let block_scores = &self.scores[self.pos..self.pos + end];
1402 visit(&remaining[..end], block_scores);
1403 matched += end as u32;
1404 self.pos += end;
1405 if self.pos >= self.doc_ids.len() {
1406 self.block_idx += 1;
1407 self.block_loaded = false;
1408 if self.block_idx >= self.num_blocks {
1409 self.exhausted = true;
1410 return Ok(matched);
1411 }
1412 } else {
1413 return Ok(matched);
1414 }
1415 }
1416 }
1417
1418 fn skip_past_sync(&mut self, to: DocId) -> crate::Result<()> {
1421 if to == u32::MAX {
1422 self.exhausted = true;
1423 return Ok(());
1424 }
1425 while !self.exhausted && self.block_last_doc(self.block_idx) <= to {
1426 if self.current_group_last_doc() <= to {
1427 self.skip_to_next_group();
1428 } else {
1429 self.skip_to_next_block();
1430 }
1431 }
1432 if !self.exhausted && self.doc() <= to {
1433 self.seek_sync(to + 1)?;
1434 }
1435 Ok(())
1436 }
1437
1438 fn score_candidates_sync(
1442 &mut self,
1443 from: DocId,
1444 docs: &mut Vec<DocId>,
1445 scores: &mut Vec<f32>,
1446 required: bool,
1447 contributions: Option<(&mut [f32], &mut [u64])>,
1448 budget: Option<&SharedThreshold>,
1449 ) -> crate::Result<bool> {
1450 if required {
1451 self.score_candidate_membership::<true>(from, docs, scores, contributions, budget)
1452 } else {
1453 self.score_candidate_membership::<false>(from, docs, scores, contributions, budget)
1454 }
1455 }
1456
1457 fn score_candidate_membership<const REQUIRED: bool>(
1458 &mut self,
1459 from: DocId,
1460 docs: &mut Vec<DocId>,
1461 scores: &mut Vec<f32>,
1462 mut contributions: Option<(&mut [f32], &mut [u64])>,
1463 budget: Option<&SharedThreshold>,
1464 ) -> crate::Result<bool> {
1465 const RUN: usize = crate::structures::postings::POSTING_BLOCK_SIZE;
1466 let mut matched_docs = [0; RUN];
1467 const { assert!(RUN <= u8::MAX as usize + 1) };
1469 let mut posting_slots = [0u8; RUN];
1470 let mut output_slots = [0; RUN];
1471 let mut values = [0.0; RUN];
1472 let mut input = 0;
1473 let mut kept = 0;
1474 while input < docs.len() {
1475 if budget.is_some_and(SharedThreshold::stop_if_expired) {
1476 return Ok(false);
1477 }
1478 if self.seek_sync(docs[input])? == u32::MAX {
1481 break;
1482 }
1483 let block_last = *self.doc_ids.last().expect("loaded posting block");
1484 let mut matched = 0;
1485 while input < docs.len() && docs[input] <= block_last && matched < RUN {
1486 if input.is_multiple_of(64) && budget.is_some_and(SharedThreshold::stop_if_expired)
1487 {
1488 return Ok(false);
1489 }
1490 let doc = docs[input];
1491 if self.doc_ids[self.pos] < doc {
1492 self.pos +=
1493 crate::structures::simd::find_first_ge_u32(&self.doc_ids[self.pos..], doc);
1494 }
1495 let present = self.doc_ids[self.pos] == doc;
1496 if present || !REQUIRED {
1497 if REQUIRED {
1498 docs[kept] = doc;
1499 scores[kept] = scores[input];
1500 }
1501 if present {
1502 matched_docs[matched] = doc;
1503 posting_slots[matched] = self.pos as u8;
1504 output_slots[matched] = kept;
1505 matched += 1;
1506 }
1507 kept += 1;
1508 }
1509 input += 1;
1510 }
1511 if matched > 0 {
1512 self.score_candidate_block(
1513 &matched_docs[..matched],
1514 &posting_slots[..matched],
1515 &mut values[..matched],
1516 );
1517 for i in 0..matched {
1518 scores[output_slots[i]] += values[i];
1519 if let Some((stored, present)) = contributions.as_mut() {
1520 let slot = (matched_docs[i] - from) as usize;
1521 stored[slot] = values[i];
1522 present[slot >> 6] |= 1u64 << (slot & 63);
1523 }
1524 }
1525 }
1526 }
1527 if REQUIRED {
1528 docs.truncate(kept);
1529 scores.truncate(kept);
1530 }
1531 Ok(true)
1532 }
1533
1534 fn score_candidate_block(&mut self, docs: &[DocId], slots: &[u8], values: &mut [f32]) {
1537 if !self.scores.is_empty() {
1538 for (&slot, value) in slots.iter().zip(values) {
1539 *value = self.scores[usize::from(slot)];
1540 }
1541 return;
1542 }
1543 self.decode_deferred_tfs();
1544 let CursorVariant::Text {
1545 idf,
1546 avg_len,
1547 params,
1548 lengths,
1549 normalization,
1550 ..
1551 } = &self.variant
1552 else {
1553 unreachable!("loaded sparse blocks already contain scores");
1554 };
1555 let mut frequencies = [0; crate::structures::postings::POSTING_BLOCK_SIZE];
1556 for (tf, &slot) in frequencies.iter_mut().zip(slots) {
1557 *tf = self.tfs[usize::from(slot)];
1558 }
1559 score_text_run(
1560 *params,
1561 *idf,
1562 *avg_len,
1563 *lengths,
1564 normalization.as_deref(),
1565 docs,
1566 &frequencies[..docs.len()],
1567 values,
1568 );
1569 }
1570
1571 #[inline]
1573 fn current_group_last_doc(&self) -> DocId {
1574 match &self.variant {
1575 CursorVariant::Text { list, .. } => list.group_last_doc(self.block_idx).unwrap_or(0),
1576 CursorVariant::Sparse { .. } => self.block_last_doc(self.block_idx),
1577 }
1578 }
1579
1580 fn skip_to_next_group(&mut self) -> DocId {
1582 if self.exhausted {
1583 return u32::MAX;
1584 }
1585 let next = match &self.variant {
1586 CursorVariant::Text { list, .. } => list.next_group_block(self.block_idx),
1587 CursorVariant::Sparse { .. } => self.block_idx + 1,
1588 };
1589 self.block_idx = next;
1590 self.block_loaded = false;
1591 if self.block_idx >= self.num_blocks {
1592 self.exhausted = true;
1593 return u32::MAX;
1594 }
1595 self.block_first_doc(self.block_idx)
1596 }
1597
1598 fn skip_to_next_block(&mut self) -> DocId {
1601 if self.exhausted {
1602 return u32::MAX;
1603 }
1604 self.block_idx += 1;
1605 self.block_loaded = false;
1606 if self.block_idx >= self.num_blocks {
1607 self.exhausted = true;
1608 return u32::MAX;
1609 }
1610 self.block_first_doc(self.block_idx)
1611 }
1612
1613 #[inline]
1614 fn advance_pos(&mut self) -> DocId {
1615 self.pos += 1;
1616 if self.pos >= self.doc_ids.len() {
1617 self.block_idx += 1;
1618 self.block_loaded = false;
1619 if self.block_idx >= self.num_blocks {
1620 self.exhausted = true;
1621 return u32::MAX;
1622 }
1623 }
1624 self.doc()
1625 }
1626
1627 #[inline(never)]
1629 fn decode_deferred_tfs(&mut self) {
1630 if let CursorVariant::Text {
1631 list, deferred_tf, ..
1632 } = &mut self.variant
1633 && let Some((block_offset, tf_start, count)) = deferred_tf.take()
1634 {
1635 list.decode_block_tfs_deferred(block_offset, tf_start, count, &mut self.tfs);
1636 }
1637 }
1638
1639 fn compute_deferred_scores(&mut self) {
1640 self.decode_deferred_tfs();
1641 if let CursorVariant::Text {
1642 idf,
1643 avg_len,
1644 params,
1645 lengths,
1646 normalization,
1647 ..
1648 } = &self.variant
1649 {
1650 self.scores.resize(self.doc_ids.len(), 0.0);
1651 score_text_run(
1652 *params,
1653 *idf,
1654 *avg_len,
1655 *lengths,
1656 normalization.as_deref(),
1657 &self.doc_ids,
1658 &self.tfs,
1659 &mut self.scores,
1660 );
1661 }
1662 }
1663
1664 pub async fn ensure_block_loaded(&mut self) -> crate::Result<bool> {
1670 cursor_ensure_block!(self, load_block_direct, .await)
1671 }
1672
1673 pub fn ensure_block_loaded_sync(&mut self) -> crate::Result<bool> {
1674 cursor_ensure_block!(self, load_block_direct_sync,)
1675 }
1676
1677 pub async fn advance(&mut self) -> crate::Result<DocId> {
1678 cursor_advance!(self, ensure_block_loaded, .await)
1679 }
1680
1681 pub fn advance_sync(&mut self) -> crate::Result<DocId> {
1682 cursor_advance!(self, ensure_block_loaded_sync,)
1683 }
1684
1685 pub async fn seek(&mut self, target: DocId) -> crate::Result<DocId> {
1686 cursor_seek!(self, ensure_block_loaded, target, .await)
1687 }
1688
1689 pub fn seek_sync(&mut self, target: DocId) -> crate::Result<DocId> {
1690 cursor_seek!(self, ensure_block_loaded_sync, target,)
1691 }
1692
1693 #[inline]
1694 fn seek_prepare(&mut self, target: DocId) -> Option<DocId> {
1695 crate::observe::search_work!(posting_seeks += 1);
1696 if self.exhausted {
1697 return Some(u32::MAX);
1698 }
1699
1700 if self.block_loaded
1702 && let Some(&last) = self.doc_ids.last()
1703 {
1704 if last >= target && self.doc_ids[self.pos] < target {
1705 self.pos = crate::structures::simd::find_first_ge_block_from(
1706 &self.doc_ids,
1707 self.pos,
1708 target,
1709 );
1710 if self.pos >= self.doc_ids.len() {
1711 self.block_idx += 1;
1712 self.block_loaded = false;
1713 if self.block_idx >= self.num_blocks {
1714 self.exhausted = true;
1715 return Some(u32::MAX);
1716 }
1717 }
1718 return Some(self.doc());
1719 }
1720 if self.doc_ids[self.pos] >= target {
1721 return Some(self.doc());
1722 }
1723 }
1724
1725 self.seek_directory(target)
1726 }
1727
1728 #[inline(never)]
1731 fn seek_directory(&mut self, target: DocId) -> Option<DocId> {
1732 let lo = match &self.variant {
1733 CursorVariant::Text { list, .. } => match list.seek_block(target, self.block_idx) {
1735 Some(idx) => idx,
1736 None => {
1737 self.exhausted = true;
1738 return Some(u32::MAX);
1739 }
1740 },
1741 CursorVariant::Sparse { .. } => {
1743 let mut lo = self.block_idx;
1744 let mut hi = self.num_blocks;
1745 while lo < hi {
1746 let mid = lo + (hi - lo) / 2;
1747 if self.block_last_doc(mid) < target {
1748 lo = mid + 1;
1749 } else {
1750 hi = mid;
1751 }
1752 }
1753 lo
1754 }
1755 };
1756 if lo >= self.num_blocks {
1757 self.exhausted = true;
1758 return Some(u32::MAX);
1759 }
1760 if lo != self.block_idx || !self.block_loaded {
1761 self.block_idx = lo;
1762 self.block_loaded = false;
1763 }
1764 None
1765 }
1766
1767 #[inline]
1768 fn seek_finish(&mut self, target: DocId) -> bool {
1769 if self.exhausted {
1770 return false;
1771 }
1772 self.pos = crate::structures::simd::find_first_ge_block_from(&self.doc_ids, 0, target);
1773 if self.pos >= self.doc_ids.len() {
1774 self.block_idx += 1;
1775 self.block_loaded = false;
1776 if self.block_idx >= self.num_blocks {
1777 self.exhausted = true;
1778 return false;
1779 }
1780 return true;
1781 }
1782 false
1783 }
1784}
1785
1786macro_rules! bms_execute_loop {
1791 ($self:ident, $ensure:ident, $advance:ident, $seek:ident, $($aw:tt)*) => {{
1792 let n = $self.cursors.len();
1793
1794 for cursor in &mut $self.cursors {
1796 cursor.$ensure() $($aw)* ?;
1797 }
1798
1799 let mut docs_scored = 0u64;
1800 let mut docs_skipped = 0u64;
1801 let mut blocks_skipped = 0u64;
1802 let mut groups_skipped = 0u64;
1803 let mut conjunction_skipped = 0u64;
1804 let mut ordinal_scores: Vec<(u16, f32)> = Vec::with_capacity(n * 2);
1805 let started = crate::observe::WallTimer::start();
1806
1807 let mut adjusted_threshold = $self.pruning_threshold();
1811 let mut iterations: u64 = 0;
1812
1813 loop {
1814 iterations += 1;
1818 if iterations & 0xFFF == 0
1819 && let Some(budget) = &$self.budget
1820 && budget.expired()
1821 {
1822 budget.mark_truncated();
1823 log::debug!(
1824 "MaxScoreExecutor: deadline reached after {} iterations, {} scored",
1825 iterations,
1826 docs_scored
1827 );
1828 break;
1829 }
1830 let partition = $self.find_partition();
1831 if partition >= n {
1832 break;
1833 }
1834
1835 let mut min_doc = u32::MAX;
1839 let mut next_other = u32::MAX;
1843 let mut at_min_mask = 0u64; for i in partition..n {
1845 let doc = $self.cursors[i].doc();
1846 match doc.cmp(&min_doc) {
1847 std::cmp::Ordering::Less => {
1848 next_other = min_doc;
1849 min_doc = doc;
1850 at_min_mask = 1u64 << (i as u32);
1851 }
1852 std::cmp::Ordering::Equal => {
1853 at_min_mask |= 1u64 << (i as u32);
1854 }
1855 std::cmp::Ordering::Greater => {
1856 if doc < next_other {
1857 next_other = doc;
1858 }
1859 }
1860 }
1861 }
1862 if min_doc == u32::MAX {
1863 break;
1864 }
1865
1866 let non_essential_upper = if partition > 0 {
1867 $self.prefix_sums[partition - 1]
1868 } else {
1869 0.0
1870 };
1871
1872 if $self.collector.len() >= $self.collector.k {
1874 let mut present_upper: f32 = 0.0;
1875 let mut mask = at_min_mask;
1876 while mask != 0 {
1877 let i = mask.trailing_zeros() as usize;
1878 present_upper += $self.cursors[i].max_score;
1879 mask &= mask - 1;
1880 }
1881
1882 if present_upper + non_essential_upper < adjusted_threshold {
1883 let mut mask = at_min_mask;
1884 while mask != 0 {
1885 let i = mask.trailing_zeros() as usize;
1886 $self.cursors[i].$ensure() $($aw)* ?;
1887 $self.cursors[i].$advance() $($aw)* ?;
1888 mask &= mask - 1;
1889 }
1890 conjunction_skipped += 1;
1891 continue;
1892 }
1893 }
1894
1895 if $self.collector.len() >= $self.collector.k {
1897 let mut block_max_sum: f32 = 0.0;
1898 let mut mask = at_min_mask;
1899 while mask != 0 {
1900 let i = mask.trailing_zeros() as usize;
1901 block_max_sum += $self.cursors[i].current_block_max_score();
1902 mask &= mask - 1;
1903 }
1904
1905 if block_max_sum + non_essential_upper < adjusted_threshold {
1906 let mut group_sum: f32 = 0.0;
1922 let mut mask = at_min_mask;
1923 while mask != 0 {
1924 let i = mask.trailing_zeros() as usize;
1925 group_sum += $self.cursors[i]
1926 .current_group_max_score()
1927 .unwrap_or_else(|| $self.cursors[i].current_block_max_score());
1928 mask &= mask - 1;
1929 }
1930 let group_prunable = group_sum + non_essential_upper < adjusted_threshold;
1931 let mut mask = at_min_mask;
1932 while mask != 0 {
1933 let i = mask.trailing_zeros() as usize;
1934 let by_group =
1935 group_prunable && $self.cursors[i].current_group_max_score().is_some();
1936 let boundary = if by_group {
1937 $self.cursors[i].current_group_last_doc()
1938 } else {
1939 $self.cursors[i].block_last_doc($self.cursors[i].block_idx)
1940 };
1941 if next_other > boundary {
1942 if by_group {
1943 $self.cursors[i].skip_to_next_group();
1944 groups_skipped += 1;
1945 } else {
1946 $self.cursors[i].skip_to_next_block();
1947 }
1948 $self.cursors[i].$ensure() $($aw)* ?;
1949 } else {
1950 $self.cursors[i].$seek(next_other) $($aw)* ?;
1951 }
1952 mask &= mask - 1;
1953 }
1954 blocks_skipped += 1;
1955 continue;
1956 }
1957 }
1958
1959 if let Some(ref pred) = $self.predicate {
1961 if !pred(min_doc) {
1962 let mut mask = at_min_mask;
1963 while mask != 0 {
1964 let i = mask.trailing_zeros() as usize;
1965 $self.cursors[i].$ensure() $($aw)* ?;
1966 $self.cursors[i].$advance() $($aw)* ?;
1967 mask &= mask - 1;
1968 }
1969 continue;
1970 }
1971 }
1972
1973 ordinal_scores.clear();
1975 {
1976 let mut mask = at_min_mask;
1977 while mask != 0 {
1978 let i = mask.trailing_zeros() as usize;
1979 $self.cursors[i].$ensure() $($aw)* ?;
1980 $self.cursors[i].ensure_scores();
1981 while $self.cursors[i].doc() == min_doc {
1982 let ord = $self.cursors[i].ordinal_mut();
1983 let sc = $self.cursors[i].score();
1984 ordinal_scores.push((ord, sc));
1985 $self.cursors[i].$advance() $($aw)* ?;
1986 }
1987 mask &= mask - 1;
1988 }
1989 }
1990
1991 let essential_total: f32 = ordinal_scores.iter().map(|(_, s)| *s).sum();
1992 if $self.collector.len() >= $self.collector.k
1993 && essential_total + non_essential_upper < adjusted_threshold
1994 {
1995 docs_skipped += 1;
1996 continue;
1997 }
1998
1999 let mut running_total = essential_total;
2001 for i in (0..partition).rev() {
2002 if $self.collector.len() >= $self.collector.k
2003 && running_total + $self.prefix_sums[i] < adjusted_threshold
2004 {
2005 break;
2006 }
2007
2008 let doc = $self.cursors[i].$seek(min_doc) $($aw)* ?;
2009 if doc == min_doc {
2010 $self.cursors[i].ensure_scores();
2011 while $self.cursors[i].doc() == min_doc {
2012 let s = $self.cursors[i].score();
2013 running_total += s;
2014 let ord = $self.cursors[i].ordinal_mut();
2015 ordinal_scores.push((ord, s));
2016 $self.cursors[i].$advance() $($aw)* ?;
2017 }
2018 }
2019 }
2020
2021 if ordinal_scores.len() == 1 {
2024 let (ord, score) = ordinal_scores[0];
2025 if $self.collector.insert_with_ordinal(min_doc, score, ord) {
2026 docs_scored += 1;
2027 adjusted_threshold = $self.pruning_threshold();
2028 } else {
2029 docs_skipped += 1;
2030 }
2031 } else if !ordinal_scores.is_empty() {
2032 if ordinal_scores.len() > 2 {
2033 ordinal_scores.sort_unstable_by_key(|(ord, _)| *ord);
2034 } else if ordinal_scores.len() == 2 && ordinal_scores[0].0 > ordinal_scores[1].0 {
2035 ordinal_scores.swap(0, 1);
2036 }
2037 let mut j = 0;
2038 while j < ordinal_scores.len() {
2039 let current_ord = ordinal_scores[j].0;
2040 let mut score = 0.0f32;
2041 while j < ordinal_scores.len() && ordinal_scores[j].0 == current_ord {
2042 score += ordinal_scores[j].1;
2043 j += 1;
2044 }
2045 if $self
2046 .collector
2047 .insert_with_ordinal(min_doc, score, current_ord)
2048 {
2049 docs_scored += 1;
2050 adjusted_threshold = $self.pruning_threshold();
2051 } else {
2052 docs_skipped += 1;
2053 }
2054 }
2055 }
2056 }
2057
2058 let results = $self.finish();
2059
2060 let elapsed_ms = (started.secs() * 1000.0) as u64;
2061 if elapsed_ms > 500 {
2062 warn!(
2063 "slow MaxScore: {}ms, cursors={}, scored={}, skipped={}, blocks_skipped={}, groups_skipped={}, conjunction_skipped={}, returned={}, top_score={:.4}",
2064 elapsed_ms,
2065 n,
2066 docs_scored,
2067 docs_skipped,
2068 blocks_skipped,
2069 groups_skipped,
2070 conjunction_skipped,
2071 results.len(),
2072 results.first().map(|r| r.score).unwrap_or(0.0)
2073 );
2074 } else {
2075 debug!(
2076 "MaxScoreExecutor: {}ms, scored={}, skipped={}, blocks_skipped={}, groups_skipped={}, conjunction_skipped={}, returned={}, top_score={:.4}",
2077 elapsed_ms,
2078 docs_scored,
2079 docs_skipped,
2080 blocks_skipped,
2081 groups_skipped,
2082 conjunction_skipped,
2083 results.len(),
2084 results.first().map(|r| r.score).unwrap_or(0.0)
2085 );
2086 }
2087
2088 Ok(results)
2089 }};
2090}
2091
2092impl<'a> MaxScoreExecutor<'a> {
2093 pub(crate) fn new(mut cursors: Vec<TermCursor<'a>>, k: usize, heap_factor: f32) -> Self {
2098 let dropped_cursors = cursors.len().saturating_sub(super::MAX_QUERY_TERMS);
2105 if dropped_cursors > 0 {
2106 log::warn!(
2107 "MaxScoreExecutor: {} cursors exceed the {}-term limit; dropping the {} with the lowest upper bounds (input order is lost, required-term semantics will be refused)",
2108 cursors.len(),
2109 super::MAX_QUERY_TERMS,
2110 dropped_cursors
2111 );
2112 cursors.sort_unstable_by(|a, b| b.max_score.total_cmp(&a.max_score));
2113 cursors.truncate(super::MAX_QUERY_TERMS);
2114 }
2115
2116 for c in &mut cursors {
2119 c.lazy_ordinals = true;
2120 }
2121
2122 let mut numbered: Vec<_> = cursors.into_iter().enumerate().collect();
2124 numbered.sort_by(|a, b| a.1.max_score.total_cmp(&b.1.max_score));
2125 let mut score_order: Vec<_> = (0..numbered.len()).collect();
2126 score_order.sort_unstable_by_key(|&i| numbered[i].0);
2127 let cursors: Vec<_> = numbered.into_iter().map(|(_, cursor)| cursor).collect();
2128
2129 let mut prefix_sums = Vec::with_capacity(cursors.len());
2130 let mut cumsum = 0.0f32;
2131 for c in &cursors {
2132 cumsum += c.max_score;
2133 prefix_sums.push(cumsum);
2134 }
2135
2136 let clamped_heap_factor = heap_factor.clamp(0.01, 1.0);
2137
2138 log::trace!(
2139 "Creating MaxScoreExecutor: num_cursors={}, k={}, total_upper={:.4}, heap_factor={:.2}",
2140 cursors.len(),
2141 k,
2142 cumsum,
2143 clamped_heap_factor
2144 );
2145
2146 Self {
2147 cursors,
2148 prefix_sums,
2149 score_order,
2150 all_required: false,
2151 required_mask: 0,
2152 collector: ScoreCollector::new(k),
2153 document_map: None,
2154 inv_heap_factor: 1.0 / clamped_heap_factor,
2155 predicate: None,
2156 budget: None,
2157 metric_index: "unknown",
2158 metric_field: "unknown",
2159 dropped_cursors,
2160 configuration_error: None,
2161 stats: ExecutorStats::default(),
2162 }
2163 }
2164
2165 fn reject_configuration(&mut self, message: String) {
2169 log::error!("MaxScoreExecutor: {message}; the query fails instead of mis-ranking");
2170 self.configuration_error.get_or_insert(message);
2171 }
2172
2173 pub fn with_budget(mut self, budget: Option<SharedThreshold>) -> Self {
2175 self.budget = budget.filter(|b| b.deadline().is_some());
2176 self
2177 }
2178
2179 pub(crate) fn require_all_terms(mut self) -> Self {
2185 if self.dropped_cursors > 0 {
2186 self.reject_configuration(format!(
2187 "require_all_terms after {} cursors were dropped at the {}-term limit",
2188 self.dropped_cursors,
2189 super::MAX_QUERY_TERMS
2190 ));
2191 } else if !self.all_text() || self.cursors.len() < 2 {
2192 self.reject_configuration(format!(
2193 "require_all_terms needs at least two text cursors (got {} cursors, all_text={})",
2194 self.cursors.len(),
2195 self.all_text()
2196 ));
2197 } else {
2198 self.all_required = true;
2199 }
2200 self
2201 }
2202
2203 pub(crate) fn require_prefix_terms(mut self, count: usize) -> Self {
2210 if self.dropped_cursors > 0 {
2211 self.reject_configuration(format!(
2212 "require_prefix_terms({count}) after {} cursors were dropped at the {}-term limit",
2213 self.dropped_cursors,
2214 super::MAX_QUERY_TERMS
2215 ));
2216 } else if !self.all_text() || count == 0 || count > self.cursors.len() {
2217 self.reject_configuration(format!(
2218 "require_prefix_terms({count}) needs 1..={} text cursors (all_text={})",
2219 self.cursors.len(),
2220 self.all_text()
2221 ));
2222 } else {
2223 for &index in &self.score_order[..count] {
2224 self.required_mask |= 1u64 << index;
2225 }
2226 }
2227 self
2228 }
2229
2230 pub fn with_metric_labels(mut self, index: &'a str, field: &'a str) -> Self {
2232 self.metric_index = index;
2233 self.metric_field = field;
2234 self
2235 }
2236
2237 pub fn sparse(
2241 sparse_index: &'a crate::segment::SparseIndex,
2242 query_terms: Vec<(u32, f32)>,
2243 k: usize,
2244 heap_factor: f32,
2245 ) -> Self {
2246 let cursors: Vec<TermCursor<'a>> = query_terms
2247 .iter()
2248 .filter_map(|&(dim_id, qw)| {
2249 let (skip_start, skip_count, global_max, block_data_offset) =
2250 sparse_index.get_skip_range_full(dim_id)?;
2251 Some(TermCursor::sparse(
2252 sparse_index,
2253 qw,
2254 skip_start,
2255 skip_count,
2256 global_max,
2257 block_data_offset,
2258 ))
2259 })
2260 .collect();
2261 Self::new(cursors, k, heap_factor)
2262 }
2263
2264 pub fn text_with_lengths(
2268 posting_lists: Vec<(crate::structures::BlockPostingList, f32)>,
2269 avg_len: f32,
2270 k: usize,
2271 lengths: Option<LengthSource<'a>>,
2272 params: super::Bm25Params,
2273 heap_factor: f32,
2274 ) -> Self {
2275 let cursors: Vec<TermCursor<'a>> = posting_lists
2276 .into_iter()
2277 .map(|(pl, idf)| TermCursor::text_with_params(pl, idf, avg_len, lengths, params))
2278 .collect();
2279 Self::new(cursors, k, heap_factor)
2280 }
2281
2282 pub fn text(
2285 posting_lists: Vec<(crate::structures::BlockPostingList, f32)>,
2286 avg_field_len: f32,
2287 k: usize,
2288 lengths: Option<&'a crate::segment::chunk_map::DocLengths>,
2289 params: super::Bm25Params,
2290 heap_factor: f32,
2291 ) -> Self {
2292 Self::text_with_lengths(
2293 posting_lists,
2294 avg_field_len,
2295 k,
2296 lengths.map(LengthSource::Docs),
2297 params,
2298 heap_factor,
2299 )
2300 }
2301
2302 pub fn text_chunked(
2306 posting_lists: Vec<(crate::structures::BlockPostingList, f32)>,
2307 avg_chunk_len: f32,
2308 k: usize,
2309 lengths: &'a crate::segment::chunk_map::ChunkMap,
2310 params: super::Bm25Params,
2311 heap_factor: f32,
2312 ) -> Self {
2313 Self::text_with_lengths(
2314 posting_lists,
2315 avg_chunk_len,
2316 k,
2317 Some(LengthSource::Chunks(lengths)),
2318 params,
2319 heap_factor,
2320 )
2321 }
2322
2323 #[inline]
2324 fn find_partition(&self) -> usize {
2325 let threshold = self.pruning_threshold();
2329 self.prefix_sums.partition_point(|&sum| sum < threshold)
2332 }
2333
2334 fn pruning_threshold(&self) -> f32 {
2341 let threshold = self.collector.threshold() * self.inv_heap_factor;
2342 threshold - threshold.abs() * (4.0 * self.cursors.len() as f32 * f32::EPSILON) - 1e-6
2343 }
2344
2345 pub(crate) fn with_document_map(
2346 mut self,
2347 map: &'a crate::segment::chunk_map::ChunkMap,
2348 ) -> Self {
2349 self.document_map = Some(map);
2350 self
2351 }
2352
2353 fn result_doc(&self, physical: DocId) -> DocId {
2354 self.document_map
2355 .map_or(physical, |map| map.doc_id(physical))
2356 }
2357
2358 pub fn with_predicate(mut self, predicate: super::DocPredicate<'a>) -> Self {
2364 self.predicate = Some(predicate);
2365 self
2366 }
2367
2368 pub fn seed_threshold(&mut self, initial_threshold: f32) {
2370 self.collector.seed_threshold(initial_threshold);
2371 }
2372
2373 pub async fn execute(mut self) -> crate::Result<Vec<ScoredDoc>> {
2379 let Some(timer) = self.begin()? else {
2380 return Ok(Vec::new());
2381 };
2382 let results = if self.all_text() {
2383 self.dispatch_sync()
2384 } else {
2385 bms_execute_loop!(self, ensure_block_loaded, advance, seek, .await)
2386 };
2387 self.record(timer, &results);
2388 results
2389 }
2390
2391 pub fn execute_sync(mut self) -> crate::Result<Vec<ScoredDoc>> {
2393 let Some(timer) = self.begin()? else {
2394 return Ok(Vec::new());
2395 };
2396 let results = self.dispatch_sync();
2397 self.record(timer, &results);
2398 results
2399 }
2400
2401 fn begin(&mut self) -> crate::Result<Option<crate::observe::Timer>> {
2405 if let Some(error) = self.configuration_error.take() {
2406 return Err(crate::Error::Query(error));
2407 }
2408 if self.cursors.is_empty() {
2409 return Ok(None);
2410 }
2411 Ok(Some(crate::observe::Timer::start()))
2412 }
2413
2414 fn dispatch_sync(&mut self) -> crate::Result<Vec<ScoredDoc>> {
2418 if self.all_required {
2419 self.execute_conjunction()
2420 } else if self.required_mask != 0 {
2421 self.execute_text_windows::<true>()
2422 } else if self.single_text_with_block_bounds() {
2423 self.execute_single_text()
2424 } else if self.all_text() {
2425 self.execute_windowed()
2426 } else {
2427 bms_execute_loop!(self, ensure_block_loaded_sync, advance_sync, seek_sync,)
2428 }
2429 }
2430
2431 fn record(&self, timer: crate::observe::Timer, results: &crate::Result<Vec<ScoredDoc>>) {
2432 crate::observe::search_work!(
2433 executor_windows += self.stats.windows,
2434 executor_windows_skipped += self.stats.windows_skipped,
2435 executor_groups_skipped += self.stats.groups_skipped,
2436 executor_candidates += self.stats.candidates,
2437 executor_heap_admissions += self.stats.docs_scored,
2438 single_blocks_scored += self.stats.blocks_scored,
2439 single_blocks_skipped += self.stats.blocks_skipped
2440 );
2441 if let Ok(r) = results {
2442 crate::observe::maxscore_query(
2443 self.metric_index,
2444 self.metric_field,
2445 timer.secs(),
2446 r.len(),
2447 );
2448 }
2449 }
2450
2451 fn finish(&mut self) -> Vec<ScoredDoc> {
2453 let collector = std::mem::replace(&mut self.collector, ScoreCollector::new(0));
2454 collector
2455 .into_sorted_results()
2456 .into_iter()
2457 .map(|(doc_id, score, ordinal)| ScoredDoc {
2458 doc_id,
2459 score,
2460 ordinal,
2461 })
2462 .collect()
2463 }
2464
2465 #[cfg(test)]
2468 pub(crate) fn execute_doc_at_a_time_sync(mut self) -> crate::Result<Vec<ScoredDoc>> {
2469 if self.cursors.is_empty() {
2470 return Ok(Vec::new());
2471 }
2472 bms_execute_loop!(self, ensure_block_loaded_sync, advance_sync, seek_sync,)
2473 }
2474
2475 fn single_text_with_block_bounds(&self) -> bool {
2477 matches!(self.cursors.as_slice(), [cursor] if cursor.supports_text_block_pruning())
2478 }
2479
2480 fn execute_single_text(&mut self) -> crate::Result<Vec<ScoredDoc>> {
2483 if self.collector.k == 0 {
2484 return Ok(Vec::new());
2485 }
2486 let mut blocks_scored = 0u64;
2487 let mut blocks_skipped = 0u64;
2488 let mut groups_skipped = 0u64;
2489 let mut decisions = 0u64;
2490 let started = crate::observe::WallTimer::start();
2491 while !self.cursors[0].exhausted {
2492 if decisions & 0x3F == 0
2494 && self
2495 .budget
2496 .as_ref()
2497 .is_some_and(SharedThreshold::stop_if_expired)
2498 {
2499 break;
2500 }
2501 decisions += 1;
2502 let threshold = self.pruning_threshold();
2503 let full = self.collector.len() >= self.collector.k;
2504 let cursor = &mut self.cursors[0];
2505 if full
2506 && cursor
2507 .text_group_bound_for_threshold(cursor.block_idx, threshold)
2508 .is_some_and(|bound| bound < threshold)
2509 {
2510 let before = cursor.block_idx;
2511 cursor.skip_to_next_group();
2512 blocks_skipped += (cursor.block_idx - before) as u64;
2513 groups_skipped += 1;
2514 continue;
2515 }
2516 if full
2517 && cursor.text_block_bound_for_threshold(cursor.block_idx, threshold) < threshold
2518 {
2519 cursor.skip_to_next_block();
2520 blocks_skipped += 1;
2521 continue;
2522 }
2523 if !cursor.ensure_block_loaded_sync()? {
2524 break;
2525 }
2526 cursor.ensure_scores();
2527 blocks_scored += 1;
2528 if self.predicate.is_none() {
2529 let docs = &cursor.doc_ids[cursor.pos..];
2530 let scores = &cursor.scores[cursor.pos..];
2531 if let Some(map) = self.document_map {
2532 self.collector
2533 .insert_text_run_with_mapping(docs, scores, |doc| map.doc_id(doc));
2534 } else {
2535 self.collector.insert_text_run(docs, scores);
2536 }
2537 } else {
2538 for (&doc, &score) in cursor.doc_ids[cursor.pos..]
2539 .iter()
2540 .zip(&cursor.scores[cursor.pos..])
2541 {
2542 if self
2543 .predicate
2544 .as_ref()
2545 .is_none_or(|predicate| predicate(doc))
2546 {
2547 self.collector.insert_with_ordinal(
2549 self.document_map.map_or(doc, |map| map.doc_id(doc)),
2550 0.0 + score,
2551 0,
2552 );
2553 }
2554 }
2555 }
2556 cursor.skip_to_next_block();
2557 }
2558 let results = self.finish();
2559 self.stats = ExecutorStats {
2560 blocks_scored,
2561 blocks_skipped,
2562 groups_skipped,
2563 ..ExecutorStats::default()
2564 };
2565 debug!(
2566 "MaxScoreExecutor(single): {}ms, blocks_scored={}, blocks_skipped={}, groups_skipped={}, returned={}",
2567 (started.secs() * 1000.0) as u64,
2568 self.stats.blocks_scored,
2569 self.stats.blocks_skipped,
2570 self.stats.groups_skipped,
2571 results.len()
2572 );
2573 Ok(results)
2574 }
2575
2576 fn all_text(&self) -> bool {
2577 self.cursors.iter().all(TermCursor::is_text)
2578 }
2579}
2580
2581const WINDOW_IDS: usize = 4096;
2583
2584#[derive(Default)]
2589struct WindowScratch {
2590 window_scores: Vec<f32>,
2592 window_mask: Vec<u64>,
2594 contributions: Vec<f32>,
2598 contribution_masks: Vec<u64>,
2599 cand_docs: Vec<u32>,
2601 cand_scores: Vec<f32>,
2602 wmax: Vec<f32>,
2604 order: Vec<usize>,
2605 wprefix: Vec<f32>,
2606 conjunction_tfs: Vec<u32>,
2608}
2609
2610impl WindowScratch {
2611 fn prepare_windows(&mut self, n: usize) {
2614 debug_assert!(n <= super::MAX_QUERY_TERMS);
2615 grow(&mut self.window_scores, WINDOW_IDS, 0.0);
2616 grow(&mut self.window_mask, WINDOW_IDS / 64, 0);
2617 if n > 1 {
2618 grow(&mut self.contributions, n * WINDOW_IDS, 0.0);
2619 grow(&mut self.contribution_masks, n * (WINDOW_IDS / 64), 0);
2620 }
2621 self.cand_docs.clear();
2622 self.cand_scores.clear();
2623 self.cand_docs.reserve(WINDOW_IDS);
2624 self.cand_scores.reserve(WINDOW_IDS);
2625 self.wmax.clear();
2626 self.wmax.resize(n, 0.0);
2627 self.wprefix.clear();
2628 self.wprefix.resize(n, 0.0);
2629 self.order.clear();
2630 self.order.extend(0..n);
2631 }
2632
2633 fn prepare_conjunction(&mut self, n: usize) {
2634 debug_assert!(n <= super::MAX_QUERY_TERMS);
2635 grow(
2636 &mut self.conjunction_tfs,
2637 n * crate::structures::postings::POSTING_BLOCK_SIZE,
2638 0,
2639 );
2640 }
2641}
2642
2643fn grow<T: Copy>(buffer: &mut Vec<T>, len: usize, fill: T) {
2645 if buffer.len() < len {
2646 buffer.resize(len, fill);
2647 }
2648}
2649
2650thread_local! {
2651 static WINDOW_SCRATCH: std::cell::RefCell<WindowScratch> =
2652 std::cell::RefCell::new(WindowScratch::default());
2653}
2654
2655fn with_window_scratch<R>(run: impl FnOnce(&mut WindowScratch) -> R) -> R {
2660 WINDOW_SCRATCH.with(|cell| match cell.try_borrow_mut() {
2661 Ok(mut scratch) => run(&mut scratch),
2662 Err(_) => {
2663 log::warn!(
2664 "MaxScoreExecutor: window scratch already in use on this thread (nested execution); allocating a private scratch"
2665 );
2666 run(&mut WindowScratch::default())
2667 }
2668 })
2669}
2670
2671fn filter_competitive(docs: &mut Vec<u32>, scores: &mut Vec<f32>, remaining: f32, threshold: f32) {
2675 let mut kept = 0usize;
2676 for j in 0..docs.len() {
2677 let doc = docs[j];
2678 let score = scores[j];
2679 docs[kept] = doc;
2680 scores[kept] = score;
2681 kept += (score + remaining >= threshold) as usize;
2682 }
2683 docs.truncate(kept);
2684 scores.truncate(kept);
2685}
2686
2687#[cfg(test)]
2688mod tests;