1use std::convert::Infallible;
4
5use crate::kernels::dot_f32;
6use crate::scan::ScanCandidate;
7use crate::scan::topk::BoundedTopK;
8
9#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
11pub struct SearchByteCounts {
12 pub coarse: usize,
14 pub rescore: usize,
16 total: usize,
17}
18
19impl SearchByteCounts {
20 #[must_use]
22 pub const fn total(self) -> usize {
23 self.total
24 }
25}
26
27#[derive(Clone, Copy, Debug, PartialEq)]
29pub struct RescoreHit {
30 pub row_index: usize,
32 pub score: f64,
34}
35
36#[derive(Clone, Copy, Debug, Eq, PartialEq)]
38pub enum RescoreMetric {
39 InnerProduct,
41 SquaredL2,
43}
44
45#[derive(Clone, Copy, Debug)]
46enum PoolWidth {
47 DenseOversample(usize),
48 Retained,
49}
50
51#[derive(Clone, Copy, Debug)]
53pub struct RescorePool<'a> {
54 row_indices: Option<&'a [u32]>,
55 coarse_scores: &'a [f32],
56 width: PoolWidth,
57 metric: RescoreMetric,
58 coarse_rows_touched: usize,
59 coarse_bytes_per_row: usize,
60 prefetch: bool,
61}
62
63impl<'a> RescorePool<'a> {
64 #[must_use]
66 pub const fn dense(
67 coarse_scores: &'a [f32],
68 oversample: usize,
69 coarse_bytes_per_row: usize,
70 ) -> Self {
71 Self {
72 row_indices: None,
73 coarse_scores,
74 width: PoolWidth::DenseOversample(oversample),
75 metric: RescoreMetric::InnerProduct,
76 coarse_rows_touched: coarse_scores.len(),
77 coarse_bytes_per_row,
78 prefetch: false,
79 }
80 }
81
82 #[must_use]
84 pub const fn retained(
85 row_indices: &'a [u32],
86 coarse_scores: &'a [f32],
87 metric: RescoreMetric,
88 coarse_rows_touched: usize,
89 coarse_bytes_per_row: usize,
90 ) -> Self {
91 Self {
92 row_indices: Some(row_indices),
93 coarse_scores,
94 width: PoolWidth::Retained,
95 metric,
96 coarse_rows_touched,
97 coarse_bytes_per_row,
98 prefetch: false,
99 }
100 }
101
102 #[must_use]
104 pub const fn with_prefetch(mut self, prefetch: bool) -> Self {
105 self.prefetch = prefetch;
106 self
107 }
108}
109
110#[derive(Clone, Debug, PartialEq)]
112pub struct RescoreResult {
113 pub hits: Vec<RescoreHit>,
115 pub candidates_rescored: usize,
117 pub bytes: SearchByteCounts,
119}
120
121#[derive(Clone, Debug, Eq, PartialEq)]
123pub enum RescoreError {
124 ZeroDimension,
126 QueryDimension {
128 expected: usize,
130 actual: usize,
132 },
133 RowDataLength {
135 dimension: usize,
137 actual: usize,
139 },
140 CoarseScoreCount {
142 expected: usize,
144 actual: usize,
146 },
147 CandidateRowCount {
149 expected: usize,
151 actual: usize,
153 },
154 CandidateRowOutOfRange {
156 position: usize,
158 row_index: usize,
160 row_count: usize,
162 },
163 ZeroK,
165 ZeroOversample,
167 ZeroCandidateLimit,
169 CandidateLimitExceeded {
171 requested: usize,
173 maximum: usize,
175 },
176 NonFiniteCoarseScore {
178 index: usize,
180 },
181 NonFiniteExactScore {
183 row_index: usize,
185 },
186 InsufficientCandidates {
188 k: usize,
190 candidates: usize,
192 },
193 ArithmeticOverflow,
195}
196
197impl std::fmt::Display for RescoreError {
198 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
199 match self {
200 Self::ZeroDimension => formatter.write_str("rescore dimension must not be zero"),
201 Self::QueryDimension { expected, actual } => write!(
202 formatter,
203 "rescore query dimension mismatch: expected {expected}, got {actual}"
204 ),
205 Self::RowDataLength { dimension, actual } => write!(
206 formatter,
207 "rescore row data length {actual} is not divisible by dimension {dimension}"
208 ),
209 Self::CoarseScoreCount { expected, actual } => write!(
210 formatter,
211 "rescore coarse score count mismatch: expected {expected}, got {actual}"
212 ),
213 Self::CandidateRowCount { expected, actual } => write!(
214 formatter,
215 "rescore retained row id count mismatch: expected {expected}, got {actual}"
216 ),
217 Self::CandidateRowOutOfRange {
218 position,
219 row_index,
220 row_count,
221 } => write!(
222 formatter,
223 "rescore retained row at position {position} is {row_index}, outside {row_count} rows"
224 ),
225 Self::ZeroK => formatter.write_str("rescore top-k must not be zero"),
226 Self::ZeroOversample => formatter.write_str("rescore oversample must not be zero"),
227 Self::ZeroCandidateLimit => {
228 formatter.write_str("rescore candidate limit must not be zero")
229 }
230 Self::CandidateLimitExceeded { requested, maximum } => write!(
231 formatter,
232 "rescore candidate count {requested} exceeds per-segment limit {maximum}"
233 ),
234 Self::NonFiniteCoarseScore { index } => {
235 write!(
236 formatter,
237 "rescore coarse score is non-finite at row {index}"
238 )
239 }
240 Self::NonFiniteExactScore { row_index } => {
241 write!(
242 formatter,
243 "rescore exact score is non-finite at row {row_index}"
244 )
245 }
246 Self::InsufficientCandidates { k, candidates } => write!(
247 formatter,
248 "rescore pool has {candidates} candidates, fewer than requested top-k {k}"
249 ),
250 Self::ArithmeticOverflow => formatter.write_str("rescore byte count overflowed usize"),
251 }
252 }
253}
254
255impl std::error::Error for RescoreError {}
256
257#[derive(Clone, Debug, Eq, PartialEq)]
258pub(crate) enum RescoreCheckError<E> {
259 Rescore(RescoreError),
260 Check(E),
261}
262
263impl<E> From<RescoreError> for RescoreCheckError<E> {
264 fn from(error: RescoreError) -> Self {
265 Self::Rescore(error)
266 }
267}
268
269fn validate_rescore_request(
270 query: &[f32],
271 rows: &[f32],
272 dimension: usize,
273 pool: &RescorePool<'_>,
274 k: usize,
275) -> Result<(usize, usize), RescoreError> {
276 if dimension == 0 {
277 return Err(RescoreError::ZeroDimension);
278 }
279 if query.len() != dimension {
280 return Err(RescoreError::QueryDimension {
281 expected: dimension,
282 actual: query.len(),
283 });
284 }
285 if !rows.len().is_multiple_of(dimension) {
286 return Err(RescoreError::RowDataLength {
287 dimension,
288 actual: rows.len(),
289 });
290 }
291 let row_count = rows.len() / dimension;
292 match pool.row_indices {
293 None if pool.coarse_scores.len() != row_count => {
294 return Err(RescoreError::CoarseScoreCount {
295 expected: row_count,
296 actual: pool.coarse_scores.len(),
297 });
298 }
299 Some(row_indices) if row_indices.len() != pool.coarse_scores.len() => {
300 return Err(RescoreError::CandidateRowCount {
301 expected: pool.coarse_scores.len(),
302 actual: row_indices.len(),
303 });
304 }
305 None | Some(_) => {}
306 }
307 if k == 0 {
308 return Err(RescoreError::ZeroK);
309 }
310 let candidate_count = match pool.width {
311 PoolWidth::DenseOversample(0) => return Err(RescoreError::ZeroOversample),
312 PoolWidth::DenseOversample(oversample) => {
313 let requested = k
314 .checked_mul(oversample)
315 .ok_or(RescoreError::ArithmeticOverflow)?;
316 pool.coarse_scores.len().min(requested)
317 }
318 PoolWidth::Retained => pool.coarse_scores.len(),
319 };
320 if candidate_count < k {
321 return Err(RescoreError::InsufficientCandidates {
322 k,
323 candidates: candidate_count,
324 });
325 }
326 if let Some((index, _)) = pool
327 .coarse_scores
328 .iter()
329 .enumerate()
330 .find(|(_, score)| !score.is_finite())
331 {
332 return Err(RescoreError::NonFiniteCoarseScore { index });
333 }
334 Ok((row_count, candidate_count))
335}
336
337pub fn rescore_top_k(
354 query: &[f32],
355 rows: &[f32],
356 dimension: usize,
357 pool: RescorePool<'_>,
358 k: usize,
359) -> Result<RescoreResult, RescoreError> {
360 match rescore_top_k_with_check(query, rows, dimension, pool, k, |_, _| {
361 Ok::<(), Infallible>(())
362 }) {
363 Ok(result) => Ok(result),
364 Err(RescoreCheckError::Rescore(error)) => Err(error),
365 Err(RescoreCheckError::Check(error)) => match error {},
366 }
367}
368
369pub(crate) fn rescore_top_k_with_check<E>(
370 query: &[f32],
371 rows: &[f32],
372 dimension: usize,
373 pool: RescorePool<'_>,
374 k: usize,
375 check: impl FnMut(usize, bool) -> Result<(), E>,
376) -> Result<RescoreResult, RescoreCheckError<E>> {
377 rescore_top_k_reusing(query, rows, dimension, pool, k, check, &mut NoScoreReuse)
378}
379
380pub(crate) trait ExactScoreReuse<E> {
383 fn get(&mut self, row: usize) -> Result<Option<f64>, E>;
384 fn insert(&mut self, row: usize, score: f64) -> Result<(), E>;
385}
386
387struct NoScoreReuse;
388
389impl<E> ExactScoreReuse<E> for NoScoreReuse {
390 fn get(&mut self, _row: usize) -> Result<Option<f64>, E> {
391 Ok(None)
392 }
393
394 fn insert(&mut self, _row: usize, _score: f64) -> Result<(), E> {
395 Ok(())
396 }
397}
398
399#[allow(clippy::too_many_arguments)]
400pub(crate) fn rescore_top_k_reusing<E>(
401 query: &[f32],
402 rows: &[f32],
403 dimension: usize,
404 pool: RescorePool<'_>,
405 k: usize,
406 mut check: impl FnMut(usize, bool) -> Result<(), E>,
407 reuse: &mut impl ExactScoreReuse<E>,
408) -> Result<RescoreResult, RescoreCheckError<E>> {
409 let (row_count, candidate_count) = validate_rescore_request(query, rows, dimension, &pool, k)?;
410
411 let candidates = match pool.width {
412 PoolWidth::DenseOversample(_) => {
413 let mut coarse_top_k = BoundedTopK::new(candidate_count);
414 for (row_id, &score) in pool.coarse_scores.iter().enumerate() {
415 coarse_top_k.push(ScanCandidate { row_id, score });
416 }
417 Some(coarse_top_k.into_sorted())
420 }
421 PoolWidth::Retained => None,
422 };
423
424 let mut exact = Vec::with_capacity(candidate_count);
425 let mut batch: Vec<(usize, &[f32])> = Vec::with_capacity(ROW_BATCH);
426 let mut scores: Vec<f64> = Vec::with_capacity(ROW_BATCH);
427 let mut candidates_rescored = 0_usize;
428 let mut position = 0;
429 while position < candidate_count {
430 let batch_end = position.saturating_add(ROW_BATCH).min(candidate_count);
431 batch.clear();
432 let mut resolve_error = None;
436 for batch_position in position..batch_end {
437 match resolve_row(
438 candidates.as_deref(),
439 pool.row_indices,
440 batch_position,
441 row_count,
442 rows,
443 dimension,
444 ) {
445 Ok(resolved) => batch.push(resolved),
446 Err(error) => {
447 resolve_error = Some(error);
448 break;
449 }
450 }
451 }
452 if pool.prefetch {
453 let ahead_end = batch_end.saturating_add(ROW_BATCH).min(candidate_count);
454 for ahead_position in batch_end..ahead_end {
455 if let Ok(ahead) = selected_row_index(
456 candidates.as_deref(),
457 pool.row_indices,
458 ahead_position,
459 row_count,
460 ) {
461 prefetch_f32_row(rows, ahead, dimension);
462 }
463 }
464 }
465 let mut prior = [None; ROW_BATCH];
469 let mut missing = [(0, &[][..]); ROW_BATCH];
470 let mut missing_count = 0;
471 for (resolved, cached) in batch.iter().zip(&mut prior) {
472 *cached = reuse.get(resolved.0).map_err(RescoreCheckError::Check)?;
473 if cached.is_none() {
474 *missing
475 .get_mut(missing_count)
476 .ok_or(RescoreError::ArithmeticOverflow)? = *resolved;
477 missing_count += 1;
478 }
479 }
480 let missing = missing
481 .get(..missing_count)
482 .ok_or(RescoreError::ArithmeticOverflow)?;
483 score_batch(query, missing, pool.metric, &mut scores);
484 candidates_rescored = candidates_rescored
485 .checked_add(missing.len())
486 .ok_or(RescoreError::ArithmeticOverflow)?;
487 let mut new_scores = scores.iter();
488 for (offset, ((row_id, _), cached)) in batch.iter().zip(prior).enumerate() {
489 let row_id = *row_id;
490 let score = match cached {
491 Some(score) => score,
492 None => *new_scores.next().ok_or(RescoreError::ArithmeticOverflow)?,
493 };
494 let scored_position = position.saturating_add(offset);
495 check(row_id, scored_position.is_multiple_of(64)).map_err(RescoreCheckError::Check)?;
496 if !score.is_finite() {
497 return Err(RescoreError::NonFiniteExactScore { row_index: row_id }.into());
498 }
499 if cached.is_none() {
500 reuse
501 .insert(row_id, score)
502 .map_err(RescoreCheckError::Check)?;
503 }
504 exact.push(RescoreHit {
505 row_index: row_id,
506 score,
507 });
508 }
509 if let Some(error) = resolve_error {
510 return Err(error.into());
511 }
512 position = batch_end;
513 }
514 if exact.len() > k {
515 let _ = exact.select_nth_unstable_by(k, rescore_hit_best_first);
516 exact.truncate(k);
517 }
518 exact.sort_unstable_by(rescore_hit_best_first);
519
520 let coarse = pool
521 .coarse_rows_touched
522 .checked_mul(pool.coarse_bytes_per_row)
523 .ok_or(RescoreError::ArithmeticOverflow)?;
524 let rescore = candidates_rescored
525 .checked_mul(dimension)
526 .and_then(|values| values.checked_mul(std::mem::size_of::<f32>()))
527 .ok_or(RescoreError::ArithmeticOverflow)?;
528 let total = coarse
529 .checked_add(rescore)
530 .ok_or(RescoreError::ArithmeticOverflow)?;
531
532 Ok(RescoreResult {
533 hits: exact,
534 candidates_rescored,
535 bytes: SearchByteCounts {
536 coarse,
537 rescore,
538 total,
539 },
540 })
541}
542
543pub(crate) fn exact_squared_l2_with_sink<'a, E>(
546 query: &[f32],
547 mut rows: impl Iterator<Item = (usize, &'a [f32])>,
548 mut check: impl FnMut(usize, bool) -> Result<(), E>,
549 mut sink: impl FnMut(usize, f64) -> Result<(), E>,
550) -> Result<usize, RescoreCheckError<E>> {
551 let mut position = 0_usize;
552 let (mut current, mut len) = next_exact_batch(&mut rows);
553 loop {
554 if len == 0 {
555 return Ok(position);
556 }
557 let (next, next_len) = next_exact_batch(&mut rows);
560 for (_, row) in next.iter().take(next_len) {
561 prefetch_f32_row(row, 0, query.len());
562 }
563 let batch = current.get(..len).ok_or(RescoreError::ArithmeticOverflow)?;
564 let mut scores = [0.0_f64; ROW_BATCH];
565 if let [(_, first), (_, second), (_, third), (_, fourth)] = batch {
566 let (a, b, c, d) = squared_l2_f64_x4(query, first, second, third, fourth);
567 scores = [-a, -b, -c, -d];
568 } else {
569 for (score, (_, row)) in scores.iter_mut().zip(batch) {
570 *score = -squared_l2_f64(query, row);
571 }
572 }
573 for ((row_id, _), score) in batch.iter().zip(scores) {
574 check(*row_id, position.is_multiple_of(64)).map_err(RescoreCheckError::Check)?;
575 if !score.is_finite() {
576 return Err(RescoreError::NonFiniteExactScore { row_index: *row_id }.into());
577 }
578 sink(*row_id, score).map_err(RescoreCheckError::Check)?;
579 position += 1;
580 }
581 current = next;
582 len = next_len;
583 }
584}
585
586fn next_exact_batch<'a>(
587 rows: &mut impl Iterator<Item = (usize, &'a [f32])>,
588) -> ([(usize, &'a [f32]); ROW_BATCH], usize) {
589 let mut batch: [(usize, &[f32]); ROW_BATCH] = [(0, &[]); ROW_BATCH];
590 let mut len = 0;
591 for slot in &mut batch {
592 let Some(row) = rows.next() else {
593 break;
594 };
595 *slot = row;
596 len += 1;
597 }
598 (batch, len)
599}
600
601const ROW_BATCH: usize = 4;
608
609fn resolve_row<'a>(
610 selected: Option<&[ScanCandidate]>,
611 retained: Option<&[u32]>,
612 position: usize,
613 row_count: usize,
614 rows: &'a [f32],
615 dimension: usize,
616) -> Result<(usize, &'a [f32]), RescoreError> {
617 let row_id = selected_row_index(selected, retained, position, row_count)?;
618 let start = row_id
619 .checked_mul(dimension)
620 .ok_or(RescoreError::ArithmeticOverflow)?;
621 let end = start
622 .checked_add(dimension)
623 .ok_or(RescoreError::ArithmeticOverflow)?;
624 let row = rows.get(start..end).ok_or(RescoreError::RowDataLength {
625 dimension,
626 actual: rows.len(),
627 })?;
628 Ok((row_id, row))
629}
630
631fn score_batch(
632 query: &[f32],
633 batch: &[(usize, &[f32])],
634 metric: RescoreMetric,
635 scores: &mut Vec<f64>,
636) {
637 #[cfg(any(test, feature = "test-seams"))]
638 for (row, _) in batch {
639 crate::graph::search::observe_exact_score(*row);
640 }
641 scores.clear();
642 match metric {
643 RescoreMetric::InnerProduct => {
644 for (_, row) in batch {
645 scores.push(f64::from(dot_f32(query, row)));
646 }
647 }
648 RescoreMetric::SquaredL2 => {
649 let mut rest = batch;
650 while let [(_, first), (_, second), (_, third), (_, fourth), tail @ ..] = rest {
651 let (first, second, third, fourth) =
652 squared_l2_f64_x4(query, first, second, third, fourth);
653 scores.push(-first);
654 scores.push(-second);
655 scores.push(-third);
656 scores.push(-fourth);
657 rest = tail;
658 }
659 for (_, row) in rest {
660 scores.push(-squared_l2_f64(query, row));
661 }
662 }
663 }
664}
665
666fn selected_row_index(
667 selected: Option<&[ScanCandidate]>,
668 retained: Option<&[u32]>,
669 position: usize,
670 row_count: usize,
671) -> Result<usize, RescoreError> {
672 match selected {
673 Some(selected) => selected
674 .get(position)
675 .map(|candidate| candidate.row_id)
676 .ok_or(RescoreError::ArithmeticOverflow),
677 None => retained_row_index(retained, position, row_count),
678 }
679}
680
681fn rescore_hit_best_first(left: &RescoreHit, right: &RescoreHit) -> std::cmp::Ordering {
682 right
683 .score
684 .total_cmp(&left.score)
685 .then_with(|| left.row_index.cmp(&right.row_index))
686}
687
688fn retained_row_index(
689 row_indices: Option<&[u32]>,
690 position: usize,
691 row_count: usize,
692) -> Result<usize, RescoreError> {
693 let row_index = match row_indices {
694 None => position,
695 Some(row_indices) => usize::try_from(*row_indices.get(position).ok_or(
696 RescoreError::CandidateRowCount {
697 expected: position.saturating_add(1),
698 actual: row_indices.len(),
699 },
700 )?)
701 .map_err(|_| RescoreError::ArithmeticOverflow)?,
702 };
703 if row_index >= row_count {
704 return Err(RescoreError::CandidateRowOutOfRange {
705 position,
706 row_index,
707 row_count,
708 });
709 }
710 Ok(row_index)
711}
712
713pub(crate) fn squared_l2_f64(left: &[f32], right: &[f32]) -> f64 {
717 left.iter()
718 .zip(right)
719 .map(|(left, right)| {
720 let delta = f64::from(*left) - f64::from(*right);
721 delta * delta
722 })
723 .sum()
724}
725
726pub(crate) fn squared_l2_f64_x4(
736 query: &[f32],
737 first: &[f32],
738 second: &[f32],
739 third: &[f32],
740 fourth: &[f32],
741) -> (f64, f64, f64, f64) {
742 let mut first_sum = 0.0_f64;
743 let mut second_sum = 0.0_f64;
744 let mut third_sum = 0.0_f64;
745 let mut fourth_sum = 0.0_f64;
746 for ((((query, first), second), third), fourth) in
747 query.iter().zip(first).zip(second).zip(third).zip(fourth)
748 {
749 let query = f64::from(*query);
750 let first = query - f64::from(*first);
751 let second = query - f64::from(*second);
752 let third = query - f64::from(*third);
753 let fourth = query - f64::from(*fourth);
754 first_sum += first * first;
755 second_sum += second * second;
756 third_sum += third * third;
757 fourth_sum += fourth * fourth;
758 }
759 (first_sum, second_sum, third_sum, fourth_sum)
760}
761
762fn prefetch_f32_row(base: &[f32], row_id: usize, dimensions: usize) {
763 let Some(start) = row_id.checked_mul(dimensions) else {
764 return;
765 };
766 let Some(value) = base.get(start) else {
767 return;
768 };
769 let address = std::ptr::from_ref(value);
770 #[cfg(target_arch = "aarch64")]
771 {
772 const CACHE_LINE_BYTES: usize = 128;
773 let Some(row_bytes) = dimensions.checked_mul(std::mem::size_of::<f32>()) else {
774 return;
775 };
776 let cache_line_count = row_bytes.div_ceil(CACHE_LINE_BYTES);
777 for cache_line in 0..cache_line_count {
778 let Some(byte_offset) = cache_line.checked_mul(CACHE_LINE_BYTES) else {
779 return;
780 };
781 let line_address = address.cast::<u8>().wrapping_add(byte_offset);
782 unsafe {
784 std::arch::asm!(
785 "prfm pldl1keep, [{line_address}]",
786 line_address = in(reg) line_address,
787 options(readonly, nostack)
788 );
789 }
790 }
791 }
792 #[cfg(not(target_arch = "aarch64"))]
793 let _ = address;
794}
795
796#[cfg(test)]
797mod exact_sink_tests {
798 use super::*;
799
800 #[test]
801 fn astra_04_sink_checks_precede_nonfinite_and_count_eligible_rows() {
802 let mut values = [1.0_f32; 67];
803 if let Some(value) = values.get_mut(65) {
804 *value = f32::NAN;
805 }
806 for cancel in [true, false] {
807 let mut checks = Vec::new();
808 let mut emitted = Vec::new();
809 let result = exact_squared_l2_with_sink(
810 &[0.0],
811 values.chunks_exact(1).enumerate(),
812 |row, checkpoint| {
813 checks.push((row, checkpoint));
814 if cancel && row == 65 { Err(65) } else { Ok(()) }
815 },
816 |row, score| {
817 emitted.push((row, score.to_bits()));
818 Ok(())
819 },
820 );
821 if cancel {
822 assert!(matches!(result, Err(RescoreCheckError::Check(65))));
823 } else {
824 assert!(matches!(
825 result,
826 Err(RescoreCheckError::Rescore(
827 RescoreError::NonFiniteExactScore { row_index: 65 }
828 ))
829 ));
830 }
831 assert_eq!(
832 checks,
833 (0..66).map(|row| (row, row % 64 == 0)).collect::<Vec<_>>()
834 );
835 assert_eq!(
836 emitted,
837 (0..65)
838 .map(|row| (row, (-1.0_f64).to_bits()))
839 .collect::<Vec<_>>()
840 );
841 }
842 }
843}
844
845#[cfg(test)]
846#[allow(clippy::expect_used, clippy::indexing_slicing, clippy::panic)]
847mod reuse_tests {
848 use super::*;
849
850 #[derive(Default)]
851 struct Cache(std::collections::BTreeMap<usize, f64>);
852
853 impl ExactScoreReuse<usize> for Cache {
854 fn get(&mut self, row: usize) -> Result<Option<f64>, usize> {
855 Ok(self.0.get(&row).copied())
856 }
857 fn insert(&mut self, row: usize, score: f64) -> Result<(), usize> {
858 assert!(
859 self.0.insert(row, score).is_none(),
860 "only new rows are retained"
861 );
862 Ok(())
863 }
864 }
865
866 #[test]
867 fn astra_09_reused_rescores_preserve_f64_order_checks_and_new_row_work() {
868 let rows = [
869 1.0_f32,
870 2.0_f32.powi(-13),
871 1.0,
872 0.0,
873 2.0,
874 0.0,
875 3.0,
876 0.0,
877 4.0,
878 0.0,
879 5.0,
880 0.0,
881 ];
882 let query = [0.0_f32; 2];
883 let mut cache = Cache::default();
884 for (ids, expected_calls) in [
885 (&[0_u32, 1, 2, 3][..], 4),
886 (&[0, 1, 2, 3, 4, 5][..], 2),
887 (&[0, 1, 2, 3, 4, 5][..], 0),
888 ] {
889 let coarse = vec![0.0; ids.len()];
890 let pool = RescorePool::retained(ids, &coarse, RescoreMetric::SquaredL2, ids.len(), 13);
891 let mut checked = Vec::new();
892 let result = rescore_top_k_reusing(
893 &query,
894 &rows,
895 2,
896 pool,
897 2,
898 |row, _| {
899 checked.push(row);
900 Ok(())
901 },
902 &mut cache,
903 )
904 .expect("valid retained pool");
905 assert_eq!(
906 checked,
907 ids.iter().map(|row| *row as usize).collect::<Vec<_>>()
908 );
909 assert_eq!(result.candidates_rescored, expected_calls);
910 assert_eq!(result.bytes.rescore, expected_calls * 2 * 4);
911 assert_eq!(
912 result
913 .hits
914 .iter()
915 .map(|hit| hit.row_index)
916 .collect::<Vec<_>>(),
917 vec![1, 0]
918 );
919 assert_eq!(result.hits[0].score, -1.0);
920 assert_eq!(result.hits[1].score, -1.0 - 2.0_f64.powi(-26));
921 assert_eq!(
922 (result.hits[0].score as f32).to_bits(),
923 (result.hits[1].score as f32).to_bits(),
924 "a premature f32 cache would choose the wrong row on the next round"
925 );
926 let fresh = rescore_top_k(&query, &rows, 2, pool, 2).expect("fresh control");
927 assert_eq!(fresh.hits, result.hits);
928 }
929 let cancelled = rescore_top_k_reusing(
930 &query,
931 &rows,
932 2,
933 RescorePool::retained(&[0, 1], &[0.0, 0.0], RescoreMetric::SquaredL2, 2, 13),
934 1,
935 |row, _| if row == 1 { Err(row) } else { Ok(()) },
936 &mut cache,
937 );
938 assert!(
939 matches!(cancelled, Err(RescoreCheckError::Check(1))),
940 "cached rows still run checks"
941 );
942 let mut checked = Vec::new();
943 let malformed = rescore_top_k_reusing(
944 &query,
945 &rows,
946 2,
947 RescorePool::retained(&[0, 99], &[0.0, 0.0], RescoreMetric::SquaredL2, 2, 13),
948 1,
949 |row, _| {
950 checked.push(row);
951 Ok(())
952 },
953 &mut cache,
954 );
955 assert_eq!(checked, vec![0]);
956 assert!(matches!(
957 malformed,
958 Err(RescoreCheckError::Rescore(
959 RescoreError::CandidateRowOutOfRange {
960 position: 1,
961 row_index: 99,
962 row_count: 6
963 }
964 ))
965 ));
966 }
967}