1use half::f16;
22
23use crate::{
24 bitpack::unpack_indices, codec::FibCodeV1, profile::FibQuantProfileV1, FibQuantError,
25 FibQuantizer, Result,
26};
27
28pub struct GramTable {
38 values: Vec<f32>,
40 n: usize,
41}
42
43impl GramTable {
44 pub fn build(codewords: &[f32], n: usize, k: usize) -> Result<Self> {
49 if codewords.len() != n * k {
50 return Err(FibQuantError::CorruptPayload(format!(
51 "codewords has {} values, expected {} (n={} k={})",
52 codewords.len(),
53 n * k,
54 n,
55 k
56 )));
57 }
58 let mut values = vec![0.0f32; n * n];
59 for i in 0..n {
60 let mut dot_ii = 0.0f32;
62 for d in 0..k {
63 let vi = codewords[i * k + d];
64 dot_ii += vi * vi;
65 }
66 values[i * n + i] = dot_ii;
67 for j in (i + 1)..n {
69 let mut dot = 0.0f32;
70 for d in 0..k {
71 dot += codewords[i * k + d] * codewords[j * k + d];
72 }
73 values[i * n + j] = dot;
74 values[j * n + i] = dot;
75 }
76 }
77 Ok(Self { values, n })
78 }
79
80 #[inline]
82 pub fn get(&self, i: usize, j: usize) -> f32 {
83 debug_assert!(i < self.n && j < self.n);
84 self.values[i * self.n + j]
85 }
86
87 pub fn n(&self) -> usize {
89 self.n
90 }
91
92 pub fn values(&self) -> &[f32] {
94 &self.values
95 }
96}
97
98pub struct FibScorer {
107 quantizer: FibQuantizer,
108 gram: GramTable,
109}
110
111#[derive(Debug, Clone)]
113pub struct ScoredItem {
114 pub idx: usize,
116 pub score: f32,
118}
119
120#[derive(Debug, Clone)]
130pub struct FibPreparedQuery {
131 pub rotated_query: Vec<f32>,
133 pub query_norm: f64,
135 pub query_indices: Vec<u32>,
137}
138
139impl FibScorer {
140 pub fn new(quantizer: FibQuantizer) -> Result<Self> {
142 let n = quantizer.profile().codebook_size as usize;
143 let k = quantizer.profile().block_dim as usize;
144 let gram = GramTable::build(&quantizer.codebook().codewords, n, k)?;
145 Ok(Self { quantizer, gram })
146 }
147
148 pub fn quantizer(&self) -> &FibQuantizer {
150 &self.quantizer
151 }
152
153 pub fn gram_table(&self) -> &GramTable {
155 &self.gram
156 }
157
158 pub fn inner_product_estimate(&self, query: &[f32], code: &FibCodeV1) -> Result<f32> {
169 let d = self.quantizer.profile().ambient_dim as usize;
170 let k = self.quantizer.profile().block_dim as usize;
171 if query.len() != d {
172 return Err(FibQuantError::CorruptPayload(format!(
173 "query dimension {}, expected {}",
174 query.len(),
175 d
176 )));
177 }
178 if query.iter().any(|v| !v.is_finite()) {
180 return Err(FibQuantError::NonFiniteInput(0));
181 }
182 let query_norm: f64 = query
183 .iter()
184 .map(|v| (*v as f64) * (*v as f64))
185 .sum::<f64>()
186 .sqrt();
187 if query_norm == 0.0 {
188 return Ok(0.0);
189 }
190 let normalized: Vec<f64> = query.iter().map(|v| f64::from(*v) / query_norm).collect();
192 let rotated = self.quantizer.profile();
193 let _ = rotated; let rotated_query = self.quantizer_codebook_rotation_apply(&normalized)?;
195
196 let stored_norm = decode_stored_norm(code, self.quantizer.profile())?;
198
199 let block_count = self.quantizer.profile().block_count() as usize;
201 let stored_indices = unpack_indices(
202 &code.indices,
203 block_count,
204 self.quantizer.profile().wire_index_bits,
205 )?;
206
207 let rotated_query_f32: Vec<f32> = rotated_query.iter().map(|&v| v as f32).collect();
209 let codewords = &self.quantizer.codebook().codewords;
210 let n = self.quantizer.profile().codebook_size as usize;
211
212 let mut total = 0.0f32;
213 for (block_idx, stored_idx) in stored_indices.iter().enumerate() {
214 let stored_idx = *stored_idx as usize;
215 if stored_idx >= n {
216 return Err(FibQuantError::IndexOutOfRange {
217 index: stored_idx as u32,
218 codebook_size: n as u32,
219 });
220 }
221 let query_block = &rotated_query_f32[block_idx * k..(block_idx + 1) * k];
222 let query_idx =
223 crate::ffi::c_encode_vector_block(query_block, codewords, n, k)[0] as usize;
224 total += self.gram.get(query_idx, stored_idx);
226 }
227
228 Ok(total * (query_norm as f32) * (stored_norm as f32))
230 }
231
232 pub fn score_batch(&self, query: &[f32], codes: &[FibCodeV1]) -> Result<Vec<ScoredItem>> {
236 let mut results = Vec::with_capacity(codes.len());
237 for (idx, code) in codes.iter().enumerate() {
238 let score = self.inner_product_estimate(query, code)?;
239 results.push(ScoredItem { idx, score });
240 }
241 results.sort_by(|a, b| {
242 b.score
243 .partial_cmp(&a.score)
244 .unwrap_or(std::cmp::Ordering::Equal)
245 });
246 Ok(results)
247 }
248
249 pub fn search(
255 &self,
256 query: &[f32],
257 codes: &[FibCodeV1],
258 top_k: usize,
259 oversample: usize,
260 ) -> Result<Vec<ScoredItem>> {
261 let limit = top_k.saturating_mul(oversample.max(1)).min(codes.len());
262 let scored = self.score_batch(query, codes)?;
263 Ok(scored.into_iter().take(limit).collect())
264 }
265
266 pub fn prepare_query(&self, query: &[f32]) -> Result<FibPreparedQuery> {
279 let d = self.quantizer.profile().ambient_dim as usize;
280 let k = self.quantizer.profile().block_dim as usize;
281 if query.len() != d {
282 return Err(FibQuantError::CorruptPayload(format!(
283 "query dimension {}, expected {}",
284 query.len(),
285 d
286 )));
287 }
288 if query.iter().any(|v| !v.is_finite()) {
289 return Err(FibQuantError::NonFiniteInput(0));
290 }
291
292 let query_norm: f64 = query
293 .iter()
294 .map(|v| (*v as f64) * (*v as f64))
295 .sum::<f64>()
296 .sqrt();
297 if query_norm == 0.0 {
298 let block_count = self.quantizer.profile().block_count() as usize;
300 return Ok(FibPreparedQuery {
301 rotated_query: vec![0.0f32; d],
302 query_norm: 0.0,
303 query_indices: vec![0u32; block_count],
304 });
305 }
306
307 let normalized: Vec<f64> = query.iter().map(|v| f64::from(*v) / query_norm).collect();
309 let rotated_query = self.quantizer_codebook_rotation_apply(&normalized)?;
310 let rotated_query_f32: Vec<f32> = rotated_query.iter().map(|&v| v as f32).collect();
311
312 let _block_count = self.quantizer.profile().block_count() as usize;
314 let codewords = &self.quantizer.codebook().codewords;
315 let n = self.quantizer.profile().codebook_size as usize;
316 let c_indices = crate::ffi::c_encode_vector_block(&rotated_query_f32, codewords, n, k);
317 let query_indices: Vec<u32> = c_indices.iter().map(|&i| i as u32).collect();
318
319 Ok(FibPreparedQuery {
320 rotated_query: rotated_query_f32,
321 query_norm,
322 query_indices,
323 })
324 }
325
326 pub fn score_prepared(&self, prepared: &FibPreparedQuery, code: &FibCodeV1) -> Result<f32> {
334 if prepared.query_norm == 0.0 {
335 return Ok(0.0);
336 }
337
338 let block_count = self.quantizer.profile().block_count() as usize;
339 let stored_indices = unpack_indices(
340 &code.indices,
341 block_count,
342 self.quantizer.profile().wire_index_bits,
343 )?;
344
345 let stored_norm = decode_stored_norm(code, self.quantizer.profile())?;
346 let n = self.quantizer.profile().codebook_size as usize;
347
348 let mut total = 0.0f32;
349 for (block_idx, stored_idx) in stored_indices.iter().enumerate() {
350 let stored_idx = *stored_idx as usize;
351 if stored_idx >= n {
352 return Err(FibQuantError::IndexOutOfRange {
353 index: stored_idx as u32,
354 codebook_size: n as u32,
355 });
356 }
357 let query_idx = prepared.query_indices[block_idx] as usize;
358 total += self.gram.get(query_idx, stored_idx);
359 }
360
361 Ok(total * (prepared.query_norm as f32) * (stored_norm as f32))
362 }
363
364 pub fn score_batch_prepared(
370 &self,
371 prepared: &FibPreparedQuery,
372 codes: &[FibCodeV1],
373 ) -> Result<Vec<ScoredItem>> {
374 let mut results = Vec::with_capacity(codes.len());
375 for (idx, code) in codes.iter().enumerate() {
376 let score = self.score_prepared(prepared, code)?;
377 results.push(ScoredItem { idx, score });
378 }
379 results.sort_by(|a, b| {
380 b.score
381 .partial_cmp(&a.score)
382 .unwrap_or(std::cmp::Ordering::Equal)
383 });
384 Ok(results)
385 }
386
387 pub fn search_prepared(
391 &self,
392 prepared: &FibPreparedQuery,
393 codes: &[FibCodeV1],
394 top_k: usize,
395 oversample: usize,
396 ) -> Result<Vec<ScoredItem>> {
397 let limit = top_k.saturating_mul(oversample.max(1)).min(codes.len());
398 let scored = self.score_batch_prepared(prepared, codes)?;
399 Ok(scored.into_iter().take(limit).collect())
400 }
401
402 fn quantizer_codebook_rotation_apply(&self, x: &[f64]) -> Result<Vec<f64>> {
404 self.quantizer.rotation().apply(x)
406 }
407
408 pub fn l2_distance_sq_estimate(&self, query: &[f32], code: &FibCodeV1) -> Result<f32> {
415 let ip = self.inner_product_estimate(query, code)?;
416 let q_norm_sq: f32 = query.iter().map(|v| v * v).sum();
417 let stored_norm = decode_stored_norm(code, self.quantizer.profile())? as f32;
418 let v_norm_sq = stored_norm * stored_norm;
419 Ok((q_norm_sq + v_norm_sq - 2.0 * ip).max(0.0))
420 }
421
422 pub fn cosine_estimate(&self, query: &[f32], code: &FibCodeV1) -> Result<f32> {
425 let ip = self.inner_product_estimate(query, code)?;
426 let q_norm: f32 = query.iter().map(|v| v * v).sum::<f32>().sqrt();
427 let stored_norm = decode_stored_norm(code, self.quantizer.profile())? as f32;
428 if q_norm == 0.0 || stored_norm == 0.0 {
429 return Ok(0.0);
430 }
431 Ok(ip / (q_norm * stored_norm))
432 }
433
434 pub fn l2_distance_sq_prepared(
436 &self,
437 prepared: &FibPreparedQuery,
438 code: &FibCodeV1,
439 ) -> Result<f32> {
440 let ip = self.score_prepared(prepared, code)?;
441 let q_norm_sq = (prepared.query_norm * prepared.query_norm) as f32;
442 let stored_norm = decode_stored_norm(code, self.quantizer.profile())? as f32;
443 let v_norm_sq = stored_norm * stored_norm;
444 Ok((q_norm_sq + v_norm_sq - 2.0 * ip).max(0.0))
445 }
446
447 pub fn cosine_prepared(&self, prepared: &FibPreparedQuery, code: &FibCodeV1) -> Result<f32> {
449 let ip = self.score_prepared(prepared, code)?;
450 let q_norm = prepared.query_norm as f32;
451 let stored_norm = decode_stored_norm(code, self.quantizer.profile())? as f32;
452 if q_norm == 0.0 || stored_norm == 0.0 {
453 return Ok(0.0);
454 }
455 Ok(ip / (q_norm * stored_norm))
456 }
457}
458
459pub fn decode_stored_norm(code: &FibCodeV1, _profile: &FibQuantProfileV1) -> Result<f64> {
460 use crate::profile::NormFormat;
464 match code.norm_format {
465 NormFormat::Fp16Paper => {
466 let bytes: [u8; 2] =
467 code.norm_payload.as_slice().try_into().map_err(|_| {
468 FibQuantError::CorruptPayload("fp16 norm payload length".into())
469 })?;
470 let value = f16::from_le_bytes(bytes).to_f32() as f64;
471 if value.is_finite() && value > 0.0 {
472 Ok(value)
473 } else {
474 Err(FibQuantError::CorruptPayload("invalid fp16 norm".into()))
475 }
476 }
477 NormFormat::F32Reference => {
478 let bytes: [u8; 4] = code
479 .norm_payload
480 .as_slice()
481 .try_into()
482 .map_err(|_| FibQuantError::CorruptPayload("f32 norm payload length".into()))?;
483 let value = f32::from_le_bytes(bytes) as f64;
484 if value.is_finite() && value > 0.0 {
485 Ok(value)
486 } else {
487 Err(FibQuantError::CorruptPayload("invalid f32 norm".into()))
488 }
489 }
490 }
491}
492
493#[cfg(test)]
494mod tests {
495 use super::*;
496
497 fn build_test_scorer() -> Result<FibScorer> {
498 let mut profile = FibQuantProfileV1::paper_default(8, 2, 8, 7)?;
499 profile.training_samples = 128;
500 profile.lloyd_restarts = 1;
501 profile.lloyd_iterations = 2;
502 let quantizer = FibQuantizer::new(profile)?;
503 FibScorer::new(quantizer)
504 }
505
506 #[test]
507 fn gram_table_diagonal_matches_codeword_norms() -> Result<()> {
508 let scorer = build_test_scorer()?;
509 let k = scorer.quantizer.profile().block_dim as usize;
510 let n = scorer.quantizer.profile().codebook_size as usize;
511 let codewords = &scorer.quantizer.codebook().codewords;
512 for i in 0..n {
513 let mut norm_sq = 0.0f32;
514 for d in 0..k {
515 let v = codewords[i * k + d];
516 norm_sq += v * v;
517 }
518 let gram_diag = scorer.gram.get(i, i);
519 assert!(
520 (norm_sq - gram_diag).abs() < 1e-5,
521 "gram diagonal mismatch at {}: ||cw||^2 = {}, gram = {}",
522 i,
523 norm_sq,
524 gram_diag
525 );
526 }
527 Ok(())
528 }
529
530 #[test]
531 fn gram_table_symmetric() -> Result<()> {
532 let scorer = build_test_scorer()?;
533 let n = scorer.gram.n();
534 for i in 0..n {
535 for j in 0..n {
536 assert!(
537 (scorer.gram.get(i, j) - scorer.gram.get(j, i)).abs() < 1e-6,
538 "gram not symmetric at ({}, {})",
539 i,
540 j
541 );
542 }
543 }
544 Ok(())
545 }
546
547 #[test]
548 fn inner_product_estimate_positive_for_self() -> Result<()> {
549 let scorer = build_test_scorer()?;
550 let d = scorer.quantizer.profile().ambient_dim as usize;
551 let input: Vec<f32> = vec![0.25, -0.5, 0.75, 1.0, -1.25, 0.5, 0.125, -0.875];
552 assert_eq!(input.len(), d);
553 let code = scorer.quantizer.encode(&input)?;
554 let est = scorer.inner_product_estimate(&input, &code)?;
555 assert!(
557 est > 0.0,
558 "inner product estimate of self should be positive, got {}",
559 est
560 );
561 let true_ip: f32 = input.iter().map(|v| v * v).sum();
563 let ratio = est / true_ip;
564 assert!(
565 ratio > 0.5 && ratio < 2.0,
566 "estimate {} vs true {} — ratio {} out of [0.5, 2.0]",
567 est,
568 true_ip,
569 ratio
570 );
571 Ok(())
572 }
573
574 #[test]
575 fn search_returns_sorted_descending() -> Result<()> {
576 let scorer = build_test_scorer()?;
577 let d = scorer.quantizer.profile().ambient_dim as usize;
578 let query: Vec<f32> = vec![0.5, -0.3, 0.8, -0.1, 0.2, -0.4, 0.7, -0.6];
579 assert_eq!(query.len(), d);
580
581 let vectors: Vec<Vec<f32>> = (0..16)
583 .map(|seed| {
584 (0..d)
585 .map(|i| (seed as f32 * 0.1 + i as f32 * 0.05 - 0.3).sin())
586 .collect()
587 })
588 .collect();
589 let codes: Vec<FibCodeV1> = vectors
590 .iter()
591 .map(|v| scorer.quantizer.encode(v).unwrap())
592 .collect();
593
594 let results = scorer.search(&query, &codes, 5, 2)?;
595 assert_eq!(results.len(), 10);
597 for w in results.windows(2) {
598 assert!(
599 w[0].score >= w[1].score,
600 "results not sorted: {} before {}",
601 w[0].score,
602 w[1].score
603 );
604 }
605 Ok(())
606 }
607
608 #[test]
609 fn score_batch_handles_empty() -> Result<()> {
610 let scorer = build_test_scorer()?;
611 let d = scorer.quantizer.profile().ambient_dim as usize;
612 let query = vec![0.0f32; d];
613 let results = scorer.score_batch(&query, &[])?;
614 assert!(results.is_empty());
615 Ok(())
616 }
617
618 #[test]
619 fn prepared_query_matches_inner_product_estimate() -> Result<()> {
620 let scorer = build_test_scorer()?;
621 let d = scorer.quantizer.profile().ambient_dim as usize;
622 let query: Vec<f32> = vec![0.5, -0.3, 0.8, -0.1, 0.2, -0.4, 0.7, -0.6];
623 assert_eq!(query.len(), d);
624
625 let vectors: Vec<Vec<f32>> = (0..16)
627 .map(|seed| {
628 (0..d)
629 .map(|i| (seed as f32 * 0.1 + i as f32 * 0.05 - 0.3).sin())
630 .collect()
631 })
632 .collect();
633 let codes: Vec<FibCodeV1> = vectors
634 .iter()
635 .map(|v| scorer.quantizer.encode(v).unwrap())
636 .collect();
637
638 let prepared = scorer.prepare_query(&query)?;
639 for (i, code) in codes.iter().enumerate() {
640 let direct = scorer.inner_product_estimate(&query, code)?;
641 let prepared_score = scorer.score_prepared(&prepared, code)?;
642 assert!(
643 (direct - prepared_score).abs() < 1e-4,
644 "mismatch at code {}: direct={}, prepared={}",
645 i,
646 direct,
647 prepared_score
648 );
649 }
650 Ok(())
651 }
652
653 #[test]
654 fn prepared_batch_matches_score_batch() -> Result<()> {
655 let scorer = build_test_scorer()?;
656 let d = scorer.quantizer.profile().ambient_dim as usize;
657 let query: Vec<f32> = vec![0.3, 0.7, -0.2, 0.9, -0.5, 0.1, -0.8, 0.4];
658 assert_eq!(query.len(), d);
659
660 let vectors: Vec<Vec<f32>> = (0..24)
661 .map(|seed| {
662 (0..d)
663 .map(|i| ((seed as f32 + i as f32) * 0.13).cos())
664 .collect()
665 })
666 .collect();
667 let codes: Vec<FibCodeV1> = vectors
668 .iter()
669 .map(|v| scorer.quantizer.encode(v).unwrap())
670 .collect();
671
672 let batch = scorer.score_batch(&query, &codes)?;
673 let prepared = scorer.prepare_query(&query)?;
674 let batch_prepared = scorer.score_batch_prepared(&prepared, &codes)?;
675
676 assert_eq!(batch.len(), batch_prepared.len());
677 for (a, b) in batch.iter().zip(batch_prepared.iter()) {
678 assert_eq!(a.idx, b.idx);
679 assert!(
680 (a.score - b.score).abs() < 1e-4,
681 "score mismatch at idx {}: batch={}, prepared={}",
682 a.idx,
683 a.score,
684 b.score
685 );
686 }
687 Ok(())
688 }
689
690 #[test]
691 fn prepared_search_matches_search() -> Result<()> {
692 let scorer = build_test_scorer()?;
693 let d = scorer.quantizer.profile().ambient_dim as usize;
694 let query: Vec<f32> = vec![0.6, -0.1, 0.3, -0.7, 0.8, 0.2, -0.4, 0.5];
695 assert_eq!(query.len(), d);
696
697 let vectors: Vec<Vec<f32>> = (0..32)
698 .map(|seed| {
699 (0..d)
700 .map(|i| (seed as f32 * 0.17 + i as f32 * 0.03).sin())
701 .collect()
702 })
703 .collect();
704 let codes: Vec<FibCodeV1> = vectors
705 .iter()
706 .map(|v| scorer.quantizer.encode(v).unwrap())
707 .collect();
708
709 let direct = scorer.search(&query, &codes, 5, 2)?;
710 let prepared = scorer.prepare_query(&query)?;
711 let prepared_results = scorer.search_prepared(&prepared, &codes, 5, 2)?;
712
713 assert_eq!(direct.len(), prepared_results.len());
714 for (a, b) in direct.iter().zip(prepared_results.iter()) {
715 assert_eq!(a.idx, b.idx);
716 assert!(
717 (a.score - b.score).abs() < 1e-4,
718 "search mismatch at idx {}: direct={}, prepared={}",
719 a.idx,
720 a.score,
721 b.score
722 );
723 }
724 Ok(())
725 }
726
727 #[test]
728 fn prepared_query_zero_norm() -> Result<()> {
729 let scorer = build_test_scorer()?;
730 let d = scorer.quantizer.profile().ambient_dim as usize;
731 let query = vec![0.0f32; d];
732 let prepared = scorer.prepare_query(&query)?;
733 assert_eq!(prepared.query_norm, 0.0);
734
735 let input: Vec<f32> = vec![0.25, -0.5, 0.75, 1.0, -1.25, 0.5, 0.125, -0.875];
736 let code = scorer.quantizer.encode(&input)?;
737 let score = scorer.score_prepared(&prepared, &code)?;
738 assert!(score.abs() < 1e-6, "zero query should give zero score");
739 Ok(())
740 }
741
742 #[test]
743 fn l2_distance_is_non_negative() -> Result<()> {
744 let scorer = build_test_scorer()?;
745 let input: Vec<f32> = vec![0.25, -0.5, 0.75, 1.0, -1.25, 0.5, 0.125, -0.875];
746 let code = scorer.quantizer.encode(&input)?;
747 let dist = scorer.l2_distance_sq_estimate(&input, &code)?;
748 assert!(
749 dist >= 0.0,
750 "L2 distance squared should be non-negative, got {}",
751 dist
752 );
753 Ok(())
754 }
755
756 #[test]
757 fn cosine_estimate_in_valid_range() -> Result<()> {
758 let scorer = build_test_scorer()?;
759 let input: Vec<f32> = vec![0.25, -0.5, 0.75, 1.0, -1.25, 0.5, 0.125, -0.875];
760 let code = scorer.quantizer.encode(&input)?;
761 let cos = scorer.cosine_estimate(&input, &code)?;
762 assert!(
763 (-1.5..=1.5).contains(&cos),
764 "cosine should be in [-1.5, 1.5], got {}",
765 cos
766 );
767 Ok(())
768 }
769
770 #[test]
771 fn cosine_prepared_matches_cosine_estimate() -> Result<()> {
772 let scorer = build_test_scorer()?;
773 let query: Vec<f32> = vec![0.5, -0.3, 0.8, -0.1, 0.2, -0.4, 0.7, -0.6];
774 let input: Vec<f32> = vec![0.25, -0.5, 0.75, 1.0, -1.25, 0.5, 0.125, -0.875];
775 let code = scorer.quantizer.encode(&input)?;
776 let cos_direct = scorer.cosine_estimate(&query, &code)?;
777 let prepared = scorer.prepare_query(&query)?;
778 let cos_prepared = scorer.cosine_prepared(&prepared, &code)?;
779 assert!(
780 (cos_direct - cos_prepared).abs() < 1e-5,
781 "prepared cosine {} should match direct {}",
782 cos_prepared,
783 cos_direct
784 );
785 Ok(())
786 }
787
788 #[test]
789 fn l2_prepared_matches_l2_estimate() -> Result<()> {
790 let scorer = build_test_scorer()?;
791 let query: Vec<f32> = vec![0.5, -0.3, 0.8, -0.1, 0.2, -0.4, 0.7, -0.6];
792 let input: Vec<f32> = vec![0.25, -0.5, 0.75, 1.0, -1.25, 0.5, 0.125, -0.875];
793 let code = scorer.quantizer.encode(&input)?;
794 let dist_direct = scorer.l2_distance_sq_estimate(&query, &code)?;
795 let prepared = scorer.prepare_query(&query)?;
796 let dist_prepared = scorer.l2_distance_sq_prepared(&prepared, &code)?;
797 assert!(
798 (dist_direct - dist_prepared).abs() < 1e-5,
799 "prepared L2 {} should match direct {}",
800 dist_prepared,
801 dist_direct
802 );
803 Ok(())
804 }
805}