1use super::binary::BinaryVector;
9use super::int4::Int4Vector;
10use super::quantized::{QuantizedVector, cosine_similarity_i8_trusted, dot_product_i8_trusted};
11use super::{cosine_similarity, dot_product};
12use crate::error::{EmbedError, Result};
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub enum NormalizationHint {
20 Unknown,
22 Unit,
24}
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
30pub enum QuantizationTier {
31 Full,
33 Int8,
35 Int4,
37 Binary,
39}
40
41impl QuantizationTier {
42 pub fn bytes_per_dim(&self) -> f32 {
44 match self {
45 Self::Full => 4.0,
46 Self::Int8 => 1.0,
47 Self::Int4 => 0.5,
48 Self::Binary => 0.125,
49 }
50 }
51
52 pub fn compression_ratio(&self) -> f32 {
54 4.0 / self.bytes_per_dim()
55 }
56
57 pub fn storage_bytes(&self, dims: usize) -> usize {
59 match self {
60 Self::Full => dims * 4,
61 Self::Int8 => dims,
62 Self::Int4 => dims.div_ceil(2),
63 Self::Binary => dims.div_ceil(8),
64 }
65 }
66
67 pub fn from_age_seconds(age_secs: u64) -> Self {
72 const HOUR: u64 = 3600;
73 const DAY: u64 = 86400;
74 const WEEK: u64 = 604800;
75
76 if age_secs < HOUR {
77 Self::Full
78 } else if age_secs < DAY {
79 Self::Int8
80 } else if age_secs < WEEK {
81 Self::Int4
82 } else {
83 Self::Binary
84 }
85 }
86}
87
88#[derive(Debug, Clone)]
93pub enum QuantizedData {
94 Full(Vec<f32>),
96 Int8(QuantizedVector),
98 Int4(Int4Vector),
100 Binary(BinaryVector),
102}
103
104impl QuantizedData {
105 pub fn tier(&self) -> QuantizationTier {
107 match self {
108 Self::Full(_) => QuantizationTier::Full,
109 Self::Int8(_) => QuantizationTier::Int8,
110 Self::Int4(_) => QuantizationTier::Int4,
111 Self::Binary(_) => QuantizationTier::Binary,
112 }
113 }
114
115 pub fn dims(&self) -> usize {
117 match self {
118 Self::Full(v) => v.len(),
119 Self::Int8(q) => q.len(),
120 Self::Int4(q) => q.dims,
121 Self::Binary(q) => q.dims,
122 }
123 }
124
125 pub fn storage_bytes(&self) -> usize {
127 match self {
128 Self::Full(v) => v.len() * 4,
129 Self::Int8(q) => q.len(),
130 Self::Int4(q) => q.data.len(),
131 Self::Binary(q) => q.data.len(),
132 }
133 }
134
135 pub fn from_f32(vector: &[f32], tier: QuantizationTier) -> Self {
137 match tier {
138 QuantizationTier::Full => Self::Full(vector.to_vec()),
139 QuantizationTier::Int8 => Self::Int8(QuantizedVector::from_f32(vector)),
140 QuantizationTier::Int4 => Self::Int4(Int4Vector::from_f32(vector)),
141 QuantizationTier::Binary => Self::Binary(BinaryVector::from_f32(vector)),
142 }
143 }
144
145 pub fn to_f32(&self) -> Vec<f32> {
147 match self {
148 Self::Full(v) => v.clone(),
149 Self::Int8(q) => q.to_f32(),
150 Self::Int4(q) => q.to_f32(),
151 Self::Binary(q) => q.to_f32(),
152 }
153 }
154
155 pub fn promote(&self, target: QuantizationTier) -> Self {
157 let f32_data = self.to_f32();
158 Self::from_f32(&f32_data, target)
159 }
160
161 pub fn demote(&self, target: QuantizationTier) -> Self {
163 self.promote(target) }
165}
166
167#[derive(Debug, Clone)]
169pub enum PreparedQuery {
170 Full(Vec<f32>),
172 Int8(QuantizedVector),
174 Int4(Int4Vector),
176 Binary(BinaryVector),
178}
179
180impl PreparedQuery {
181 #[inline]
183 pub fn from_f32(query_f32: &[f32], tier: QuantizationTier) -> Self {
184 match tier {
185 QuantizationTier::Full => Self::Full(query_f32.to_vec()),
186 QuantizationTier::Int8 => Self::Int8(QuantizedVector::from_f32(query_f32)),
187 QuantizationTier::Int4 => Self::Int4(Int4Vector::from_f32(query_f32)),
188 QuantizationTier::Binary => Self::Binary(BinaryVector::from_f32(query_f32)),
189 }
190 }
191
192 #[inline]
194 pub fn tier(&self) -> QuantizationTier {
195 match self {
196 Self::Full(_) => QuantizationTier::Full,
197 Self::Int8(_) => QuantizationTier::Int8,
198 Self::Int4(_) => QuantizationTier::Int4,
199 Self::Binary(_) => QuantizationTier::Binary,
200 }
201 }
202
203 #[inline]
205 pub fn dims(&self) -> usize {
206 match self {
207 Self::Full(v) => v.len(),
208 Self::Int8(q) => q.len(),
209 Self::Int4(q) => q.dims,
210 Self::Binary(q) => q.dims,
211 }
212 }
213}
214
215#[inline]
217pub fn prepare_query(query_f32: &[f32], tier: QuantizationTier) -> PreparedQuery {
218 PreparedQuery::from_f32(query_f32, tier)
219}
220
221#[derive(Debug, Clone)]
223pub struct PreparedQueryWithMeta {
224 pub query: PreparedQuery,
226 pub norm: NormalizationHint,
228}
229
230impl PreparedQueryWithMeta {
231 #[inline]
233 pub fn from_f32(query_f32: &[f32], tier: QuantizationTier, norm: NormalizationHint) -> Self {
234 Self {
235 query: PreparedQuery::from_f32(query_f32, tier),
236 norm,
237 }
238 }
239
240 #[inline]
242 pub fn tier(&self) -> QuantizationTier {
243 self.query.tier()
244 }
245
246 #[inline]
248 pub fn dims(&self) -> usize {
249 self.query.dims()
250 }
251}
252
253#[inline]
261pub fn is_unit_norm(v: &[f32]) -> bool {
262 let sq = dot_product(v, v);
263 (sq - 1.0).abs() < 1e-4
264}
265
266#[inline]
268pub fn prepare_query_with_norm(
269 query_f32: &[f32],
270 tier: QuantizationTier,
271 norm: NormalizationHint,
272) -> PreparedQueryWithMeta {
273 PreparedQueryWithMeta::from_f32(query_f32, tier, norm)
274}
275
276#[inline]
281pub fn approximate_cosine_distance_prepared(
282 query: &PreparedQuery,
283 stored: &QuantizedData,
284) -> Result<f32> {
285 match (query, stored) {
286 (PreparedQuery::Full(q), QuantizedData::Full(s)) => Ok(1.0 - cosine_similarity(q, s)),
287 (PreparedQuery::Int8(q), QuantizedData::Int8(s)) => {
288 Ok(1.0 - cosine_similarity_i8_trusted(s, q))
289 }
290 (PreparedQuery::Int4(q), QuantizedData::Int4(s)) => Ok(s.cosine_distance(q)),
291 (PreparedQuery::Binary(q), QuantizedData::Binary(s)) => Ok(s.cosine_distance_approx(q)),
292 _ => Err(EmbedError::TierMismatch {
293 op: "approximate_cosine_distance_prepared",
294 expected: stored.tier(),
295 actual: query.tier(),
296 }),
297 }
298}
299
300#[inline]
302pub fn try_approximate_cosine_distance_prepared(
303 query: &PreparedQuery,
304 stored: &QuantizedData,
305) -> Result<f32> {
306 approximate_cosine_distance_prepared(query, stored)
307}
308
309#[inline]
311pub fn try_approximate_dot_product_prepared(
312 query: &PreparedQuery,
313 stored: &QuantizedData,
314) -> Result<f32> {
315 approximate_dot_product_prepared(query, stored)
316}
317
318#[inline]
331pub fn approximate_cosine_distance_prepared_with_meta(
332 meta: &PreparedQueryWithMeta,
333 stored: &QuantizedData,
334 _stored_norm: NormalizationHint,
335) -> Result<f32> {
336 approximate_cosine_distance_prepared(&meta.query, stored)
337}
338
339#[inline]
344pub fn approximate_dot_product_prepared(
345 query: &PreparedQuery,
346 stored: &QuantizedData,
347) -> Result<f32> {
348 match (query, stored) {
349 (PreparedQuery::Full(q), QuantizedData::Full(s)) => Ok(dot_product(q, s)),
350 (PreparedQuery::Int8(q), QuantizedData::Int8(s)) => Ok(dot_product_i8_trusted(q, s)),
351 (PreparedQuery::Int4(q), QuantizedData::Int4(s)) => Ok(s.dot_product(q)),
352 (PreparedQuery::Binary(_), QuantizedData::Binary(_)) => Err(EmbedError::Internal(
353 "Binary has no prepared dot product; use approximate_cosine_distance_prepared".into(),
354 )),
355 _ => Err(EmbedError::TierMismatch {
356 op: "approximate_dot_product_prepared",
357 expected: stored.tier(),
358 actual: query.tier(),
359 }),
360 }
361}
362
363#[inline]
367pub fn batch_approximate_cosine_distance_prepared(
368 query: &PreparedQuery,
369 stored: &[QuantizedData],
370) -> Result<Vec<f32>> {
371 stored
372 .iter()
373 .map(|item| approximate_cosine_distance_prepared(query, item))
374 .collect()
375}
376
377#[inline]
382pub fn batch_approximate_cosine_distance_prepared_into(
383 query: &PreparedQuery,
384 stored: &[QuantizedData],
385 out: &mut Vec<f32>,
386) -> Result<()> {
387 out.clear();
388 out.reserve(stored.len());
389 for item in stored {
390 match approximate_cosine_distance_prepared(query, item) {
391 Ok(distance) => out.push(distance),
392 Err(e) => {
393 out.clear();
394 return Err(e);
395 }
396 }
397 }
398 Ok(())
399}
400
401#[inline]
405pub fn approximate_int8_batch_prepared(
406 query: &PreparedQuery,
407 candidates: &[QuantizedVector],
408) -> Result<Vec<f32>> {
409 let PreparedQuery::Int8(q) = query else {
410 return Err(EmbedError::TierMismatch {
411 op: "approximate_int8_batch_prepared",
412 expected: QuantizationTier::Int8,
413 actual: query.tier(),
414 });
415 };
416 Ok(candidates
417 .iter()
418 .map(|candidate| 1.0 - cosine_similarity_i8_trusted(candidate, q))
419 .collect())
420}
421
422#[inline]
426pub fn approximate_int8_batch_prepared_into(
427 query: &PreparedQuery,
428 candidates: &[QuantizedVector],
429 out: &mut Vec<f32>,
430) -> Result<()> {
431 out.clear();
432 let PreparedQuery::Int8(q) = query else {
433 return Err(EmbedError::TierMismatch {
434 op: "approximate_int8_batch_prepared_into",
435 expected: QuantizationTier::Int8,
436 actual: query.tier(),
437 });
438 };
439 out.reserve(candidates.len());
440 out.extend(
441 candidates
442 .iter()
443 .map(|candidate| 1.0 - cosine_similarity_i8_trusted(candidate, q)),
444 );
445 Ok(())
446}
447
448#[inline]
452pub fn approximate_int4_batch_prepared(
453 query: &PreparedQuery,
454 candidates: &[Int4Vector],
455) -> Result<Vec<f32>> {
456 let PreparedQuery::Int4(q) = query else {
457 return Err(EmbedError::TierMismatch {
458 op: "approximate_int4_batch_prepared",
459 expected: QuantizationTier::Int4,
460 actual: query.tier(),
461 });
462 };
463 Ok(candidates
464 .iter()
465 .map(|candidate| candidate.cosine_distance(q))
466 .collect())
467}
468
469#[inline]
473pub fn approximate_int4_batch_prepared_into(
474 query: &PreparedQuery,
475 candidates: &[Int4Vector],
476 out: &mut Vec<f32>,
477) -> Result<()> {
478 out.clear();
479 let PreparedQuery::Int4(q) = query else {
480 return Err(EmbedError::TierMismatch {
481 op: "approximate_int4_batch_prepared_into",
482 expected: QuantizationTier::Int4,
483 actual: query.tier(),
484 });
485 };
486 out.reserve(candidates.len());
487 out.extend(
488 candidates
489 .iter()
490 .map(|candidate| candidate.cosine_distance(q)),
491 );
492 Ok(())
493}
494
495pub fn approximate_cosine_distance(query_f32: &[f32], stored: &QuantizedData) -> f32 {
500 debug_assert_eq!(
501 query_f32.len(),
502 stored.dims(),
503 "approximate_cosine_distance: query length {} != stored dims {}",
504 query_f32.len(),
505 stored.dims(),
506 );
507 match stored {
508 QuantizedData::Full(v) => {
509 1.0 - cosine_similarity(query_f32, v)
511 }
512 QuantizedData::Int8(q) => {
513 let query_q = QuantizedVector::from_f32(query_f32);
514 1.0 - q.cosine_similarity(&query_q)
515 }
516 QuantizedData::Int4(q) => {
517 let query_q = Int4Vector::from_f32(query_f32);
518 q.cosine_distance(&query_q)
519 }
520 QuantizedData::Binary(q) => {
521 let query_q = BinaryVector::from_f32(query_f32);
522 q.cosine_distance_approx(&query_q)
523 }
524 }
525}
526
527pub fn approximate_dot_product(query_f32: &[f32], stored: &QuantizedData) -> f32 {
529 match stored {
530 QuantizedData::Full(v) => dot_product(query_f32, v),
531 QuantizedData::Int8(q) => {
532 let query_q = QuantizedVector::from_f32(query_f32);
533 q.dot_product(&query_q)
534 }
535 QuantizedData::Int4(q) => {
536 let query_q = Int4Vector::from_f32(query_f32);
537 q.dot_product(&query_q)
538 }
539 QuantizedData::Binary(_q) => {
540 let stored_f32 = _q.to_f32();
542 dot_product(query_f32, &stored_f32)
543 }
544 }
545}
546
547#[cfg(test)]
548mod tests {
549 use super::*;
550
551 fn generate_vector(dim: usize, seed: u64) -> Vec<f32> {
552 let mut state = seed ^ ((dim as u64).wrapping_mul(0x9E37_79B9_7F4A_7C15));
553 (0..dim)
554 .map(|i| {
555 state = state
556 .wrapping_mul(6364136223846793005)
557 .wrapping_add(1442695040888963407)
558 .wrapping_add(i as u64);
559 let unit = ((state >> 32) as u32) as f32 / u32::MAX as f32;
560 unit * 2.0 - 1.0
561 })
562 .collect()
563 }
564
565 fn scalar_cosine_f64(a: &[f32], b: &[f32]) -> f64 {
566 assert_eq!(a.len(), b.len());
567 let mut dot = 0.0f64;
568 let mut norm_a = 0.0f64;
569 let mut norm_b = 0.0f64;
570 for (&a, &b) in a.iter().zip(b) {
571 let a = f64::from(a);
572 let b = f64::from(b);
573 dot += a * b;
574 norm_a += a * a;
575 norm_b += b * b;
576 }
577 let denom = norm_a.sqrt() * norm_b.sqrt();
578 if denom == 0.0 { 0.0 } else { dot / denom }
579 }
580
581 fn reference_ranking(query: &[f32], corpus: &[Vec<f32>]) -> Vec<usize> {
582 let mut ranked: Vec<_> = corpus
583 .iter()
584 .enumerate()
585 .map(|(index, candidate)| (index, scalar_cosine_f64(query, candidate)))
586 .collect();
587 ranked.sort_unstable_by(|a, b| b.1.total_cmp(&a.1).then_with(|| a.0.cmp(&b.0)));
588 ranked.into_iter().map(|(index, _)| index).collect()
589 }
590
591 fn tier_ranking(query: &[f32], stored: &[QuantizedData], tier: QuantizationTier) -> Vec<usize> {
592 let prepared = PreparedQuery::from_f32(query, tier);
593 assert_eq!(prepared.tier(), tier);
594 let mut ranked: Vec<_> = stored
595 .iter()
596 .enumerate()
597 .map(|(index, candidate)| {
598 (
599 index,
600 approximate_cosine_distance_prepared(&prepared, candidate).unwrap(),
601 )
602 })
603 .collect();
604 ranked.sort_unstable_by(|a, b| a.1.total_cmp(&b.1).then_with(|| a.0.cmp(&b.0)));
605 ranked.into_iter().map(|(index, _)| index).collect()
606 }
607
608 fn recall_hits_at(reference: &[usize], actual: &[usize], k: usize) -> usize {
609 actual[..k]
610 .iter()
611 .filter(|candidate| reference[..k].contains(candidate))
612 .count()
613 }
614
615 fn recall_at(reference: &[usize], actual: &[usize], k: usize) -> f64 {
616 recall_hits_at(reference, actual, k) as f64 / k as f64
617 }
618
619 fn pairwise_ranking_agreements(reference: &[usize], actual: &[usize]) -> usize {
620 assert_eq!(reference.len(), actual.len());
621 let mut actual_position = vec![0usize; actual.len()];
622 for (position, &candidate) in actual.iter().enumerate() {
623 actual_position[candidate] = position;
624 }
625 let mut agreements = 0usize;
626 for (position, &left) in reference.iter().enumerate() {
627 for &right in &reference[position + 1..] {
628 agreements += usize::from(actual_position[left] < actual_position[right]);
629 }
630 }
631 agreements
632 }
633
634 fn pairwise_ranking_agreement(reference: &[usize], actual: &[usize]) -> f64 {
635 let pairs = reference.len() * (reference.len() - 1) / 2;
636 pairwise_ranking_agreements(reference, actual) as f64 / pairs as f64
637 }
638
639 fn retrieval_quality_counts(
640 reference: &[usize],
641 actual: &[usize],
642 top_k: usize,
643 ) -> (usize, usize) {
644 (
645 recall_hits_at(reference, actual, top_k),
646 pairwise_ranking_agreements(reference, actual),
647 )
648 }
649
650 fn index_order_surrogate_quality(
651 corpus: &[Vec<f32>],
652 queries: &[Vec<f32>],
653 top_k: usize,
654 ) -> (f64, f64) {
655 let index_order: Vec<_> = (0..corpus.len()).collect();
656 let mut recall = 0.0;
657 let mut agreement = 0.0;
658 for query in queries {
659 let reference = reference_ranking(query, corpus);
660 recall += recall_at(&reference, &index_order, top_k);
661 agreement += pairwise_ranking_agreement(&reference, &index_order);
662 }
663 (
664 recall / queries.len() as f64,
665 agreement / queries.len() as f64,
666 )
667 }
668
669 fn retrieval_quality_floor(tier: QuantizationTier) -> (f64, f64) {
670 match tier {
671 QuantizationTier::Full => (1.0, 0.999),
672 QuantizationTier::Int8 => (0.98, 0.995),
673 QuantizationTier::Int4 => (0.85, 0.95),
674 QuantizationTier::Binary => (0.30, 0.70),
675 }
676 }
677
678 fn meets_retrieval_quality_floor(value: f64, minimum: f64) -> bool {
680 value.is_finite() && value + f64::EPSILON >= minimum
681 }
682
683 fn retrieval_quality_per_query_floor(tier: QuantizationTier) -> (usize, usize) {
700 match tier {
701 QuantizationTier::Full => (9, 32_640),
702 QuantizationTier::Int8 => (9, 32_530),
703 QuantizationTier::Int4 => (7, 31_289),
704 QuantizationTier::Binary => (2, 23_141),
705 }
706 }
707
708 const HEALTHY_FULL_QUERY_QUALITY: [(usize, usize); 16] = [(10, 32_640); 16];
719 const HEALTHY_INT8_QUERY_QUALITY: [(usize, usize); 16] = [
720 (10, 32_588),
721 (10, 32_575),
722 (10, 32_597),
723 (10, 32_590),
724 (10, 32_595),
725 (10, 32_592),
726 (10, 32_580),
727 (10, 32_604),
728 (10, 32_587),
729 (10, 32_573),
730 (10, 32_580),
731 (10, 32_579),
732 (10, 32_567),
733 (10, 32_578),
734 (10, 32_576),
735 (10, 32_588),
736 ];
737 const HEALTHY_INT4_QUERY_QUALITY: [(usize, usize); 16] = [
738 (8, 31_582),
739 (9, 31_531),
740 (9, 31_642),
741 (8, 31_656),
742 (10, 31_773),
743 (8, 31_638),
744 (10, 31_744),
745 (9, 31_758),
746 (10, 31_661),
747 (10, 31_651),
748 (10, 31_662),
749 (8, 31_727),
750 (10, 31_531),
751 (10, 31_625),
752 (9, 31_655),
753 (9, 31_653),
754 ];
755 const HEALTHY_BINARY_QUERY_QUALITY: [(usize, usize); 16] = [
756 (7, 25_430),
757 (3, 25_031),
758 (6, 25_033),
759 (5, 25_339),
760 (4, 25_534),
761 (3, 25_166),
762 (3, 25_156),
763 (6, 25_705),
764 (5, 25_677),
765 (4, 25_419),
766 (4, 25_040),
767 (3, 25_150),
768 (4, 24_965),
769 (5, 25_190),
770 (5, 24_423),
771 (4, 25_355),
772 ];
773
774 fn healthy_query_quality(tier: QuantizationTier) -> &'static [(usize, usize); 16] {
775 match tier {
776 QuantizationTier::Full => &HEALTHY_FULL_QUERY_QUALITY,
777 QuantizationTier::Int8 => &HEALTHY_INT8_QUERY_QUALITY,
778 QuantizationTier::Int4 => &HEALTHY_INT4_QUERY_QUALITY,
779 QuantizationTier::Binary => &HEALTHY_BINARY_QUERY_QUALITY,
780 }
781 }
782
783 fn retrieval_quality_movement_budget(healthy: &[(usize, usize)]) -> (usize, usize) {
784 let minimum_recall_hits = healthy.iter().map(|quality| quality.0).min().unwrap();
785 let maximum_recall_hits = healthy.iter().map(|quality| quality.0).max().unwrap();
786 let minimum_agreements = healthy.iter().map(|quality| quality.1).min().unwrap();
787 let maximum_agreements = healthy.iter().map(|quality| quality.1).max().unwrap();
788 (
789 maximum_recall_hits - minimum_recall_hits,
790 maximum_agreements - minimum_agreements,
791 )
792 }
793
794 fn retrieval_quality_concentration_budget(healthy: &[(usize, usize)]) -> (usize, usize) {
801 let movement_budget = retrieval_quality_movement_budget(healthy);
802 (
803 movement_budget.0.div_ceil(healthy.len()),
804 movement_budget.1.div_ceil(healthy.len()),
805 )
806 }
807
808 fn validate_tier_retrieval_quality(
809 tier: QuantizationTier,
810 query_quality: &[(usize, usize)],
811 top_k: usize,
812 ) -> std::result::Result<(f64, f64), String> {
813 if query_quality.is_empty() {
814 return Err(format!("{tier:?} retrieval quality has zero queries"));
815 }
816
817 let healthy = healthy_query_quality(tier);
818 if query_quality.len() != healthy.len() {
819 return Err(format!(
820 "{tier:?} retrieval quality has {} queries, expected {}",
821 query_quality.len(),
822 healthy.len()
823 ));
824 }
825
826 let (minimum_query_recall, minimum_query_agreement) =
827 retrieval_quality_per_query_floor(tier);
828 for (query_index, &(recall_hits, agreements)) in query_quality.iter().enumerate() {
829 if recall_hits < minimum_query_recall || agreements < minimum_query_agreement {
830 return Err(format!(
831 "{tier:?} query {query_index} fails the per-query floor: Recall@{top_k}=\
832 {:.6} (minimum {:.6}), pairwise ranking agreement=\
833 {:.6} (minimum {:.6})",
834 recall_hits as f64 / top_k as f64,
835 minimum_query_recall as f64 / top_k as f64,
836 agreements as f64 / 32_640.0,
837 minimum_query_agreement as f64 / 32_640.0,
838 ));
839 }
840 }
841
842 let recall_hits = query_quality.iter().map(|quality| quality.0).sum::<usize>();
843 let agreements = query_quality.iter().map(|quality| quality.1).sum::<usize>();
844 let recall = recall_hits as f64 / (query_quality.len() * top_k) as f64;
845 let agreement = agreements as f64 / (query_quality.len() * 32_640) as f64;
846 let (minimum_recall, minimum_agreement) = retrieval_quality_floor(tier);
847 if !meets_retrieval_quality_floor(recall, minimum_recall) {
848 return Err(format!(
849 "{tier:?} Recall@{top_k} {recall:.6} is below the measured-data floor \
850 {minimum_recall:.3}"
851 ));
852 }
853 if !meets_retrieval_quality_floor(agreement, minimum_agreement) {
854 return Err(format!(
855 "{tier:?} pairwise ranking agreement {agreement:.6} is below the measured-data \
856 floor {minimum_agreement:.3}"
857 ));
858 }
859
860 let (recall_movement, agreement_movement) = query_quality.iter().zip(healthy).fold(
861 (0usize, 0usize),
862 |(recall_movement, agreement_movement), (actual, expected)| {
863 (
864 recall_movement + actual.0.abs_diff(expected.0),
865 agreement_movement + actual.1.abs_diff(expected.1),
866 )
867 },
868 );
869 let (recall_budget, agreement_budget) = retrieval_quality_movement_budget(healthy);
870 if recall_movement > recall_budget || agreement_movement > agreement_budget {
871 return Err(format!(
872 "{tier:?} retrieval quality exceeds the fixture-relative movement budget: \
873 total absolute Recall@{top_k} movement={recall_movement} hit(s) \
874 (maximum {recall_budget}), total absolute pairwise-agreement movement=\
875 {agreement_movement} pair(s) (maximum {agreement_budget})"
876 ));
877 }
878
879 let (maximum_query_recall_movement, maximum_query_agreement_movement) =
880 retrieval_quality_concentration_budget(healthy);
881 for (query_index, (&(recall_hits, agreements), &(healthy_hits, healthy_agreements))) in
882 query_quality.iter().zip(healthy).enumerate()
883 {
884 let recall_movement = recall_hits.abs_diff(healthy_hits);
885 let agreement_movement = agreements.abs_diff(healthy_agreements);
886 if recall_movement > maximum_query_recall_movement
887 || agreement_movement > maximum_query_agreement_movement
888 {
889 return Err(format!(
890 "{tier:?} query {query_index} exceeds the fixture-relative concentration \
891 bound: Recall@{top_k} movement={recall_movement} hit(s) \
892 (maximum {maximum_query_recall_movement}), pairwise-agreement \
893 movement={agreement_movement} pair(s) \
894 (maximum {maximum_query_agreement_movement})"
895 ));
896 }
897 }
898 Ok((recall, agreement))
899 }
900
901 fn validate_retrieval_fixture(
909 corpus: &[Vec<f32>],
910 queries: &[Vec<f32>],
911 top_k: usize,
912 ) -> std::result::Result<(), String> {
913 if top_k == 0 {
914 return Err("retrieval fixture has top_k=0".to_string());
915 }
916 if queries.is_empty() {
917 return Err("retrieval fixture has zero queries".to_string());
918 }
919 if corpus.len() < top_k {
920 return Err(format!(
921 "retrieval fixture corpus size {} is smaller than top_k={top_k}",
922 corpus.len()
923 ));
924 }
925 for (query_index, query) in queries.iter().enumerate() {
926 let mut ranked_scores = Vec::with_capacity(corpus.len());
927 for (candidate_index, candidate) in corpus.iter().enumerate() {
928 let score = scalar_cosine_f64(query, candidate);
929 if !score.is_finite() {
930 return Err(format!(
931 "retrieval fixture query {query_index} candidate {candidate_index} has \
932 non-finite reference score {score}"
933 ));
934 }
935 ranked_scores.push((candidate_index, score));
936 }
937 ranked_scores.sort_unstable_by(|a, b| b.1.total_cmp(&a.1).then_with(|| a.0.cmp(&b.0)));
938
939 let mut distinct_scores: Vec<_> =
940 ranked_scores.iter().map(|(_, score)| *score).collect();
941 distinct_scores.sort_unstable_by(f64::total_cmp);
942 distinct_scores.dedup();
943 if distinct_scores.len() <= top_k {
944 return Err(format!(
945 "retrieval fixture query {query_index} is non-discriminating: only \
946 {} distinct finite reference score(s) across {} candidates, need \
947 more than top_k={top_k}",
948 distinct_scores.len(),
949 corpus.len()
950 ));
951 }
952
953 for (rank, boundary) in ranked_scores.windows(2).enumerate() {
954 let higher_score = boundary[0].1;
955 let lower_score = boundary[1].1;
956 if higher_score as f32 <= lower_score as f32 {
957 return Err(format!(
958 "retrieval fixture query {query_index} has a near-tied ranking boundary \
959 at ranks {rank} and {}: reference scores {higher_score} and \
960 {lower_score} do not remain ordered at f32 precision",
961 rank + 1
962 ));
963 }
964 }
965 }
966
967 let (surrogate_recall, surrogate_agreement) =
968 index_order_surrogate_quality(corpus, queries, top_k);
969 for tier in [
970 QuantizationTier::Full,
971 QuantizationTier::Int8,
972 QuantizationTier::Int4,
973 QuantizationTier::Binary,
974 ] {
975 let (minimum_recall, minimum_agreement) = retrieval_quality_floor(tier);
976 if meets_retrieval_quality_floor(surrogate_recall, minimum_recall)
977 && meets_retrieval_quality_floor(surrogate_agreement, minimum_agreement)
978 {
979 return Err(format!(
980 "retrieval fixture is non-discriminating: an all-tied index-order surrogate \
981 passes the {tier:?} floor with Recall@{top_k}={surrogate_recall:.6} and \
982 pairwise ranking agreement={surrogate_agreement:.6}"
983 ));
984 }
985 }
986 Ok(())
987 }
988
989 fn fixed_retrieval_fixture() -> (Vec<Vec<f32>>, Vec<Vec<f32>>) {
990 const DIMS: usize = 384;
991 const CORPUS_SIZE: usize = 256;
992 const QUERY_COUNT: usize = 16;
993 let corpus = (0..CORPUS_SIZE)
994 .map(|index| generate_vector(DIMS, 0xC0A5_0000 + index as u64))
995 .collect();
996 let queries = (0..QUERY_COUNT)
997 .map(|index| generate_vector(DIMS, 0x0A11_0000 + index as u64))
998 .collect();
999 (corpus, queries)
1000 }
1001
1002 #[test]
1003 fn test_tier_retrieval_quality_against_independent_f64_ranking() {
1004 const TOP_K: usize = 10;
1005 let (corpus, queries) = fixed_retrieval_fixture();
1006 validate_retrieval_fixture(&corpus, &queries, TOP_K)
1007 .expect("retrieval fixture must be discriminating before scoring tiers against it");
1008
1009 for tier in [
1010 QuantizationTier::Full,
1011 QuantizationTier::Int8,
1012 QuantizationTier::Int4,
1013 QuantizationTier::Binary,
1014 ] {
1015 let stored: Vec<_> = corpus
1016 .iter()
1017 .map(|candidate| QuantizedData::from_f32(candidate, tier))
1018 .collect();
1019 assert!(
1020 stored.iter().all(|candidate| candidate.tier() == tier),
1021 "{tier:?} conversion was bypassed or routed to another tier"
1022 );
1023 assert!(
1024 stored
1025 .iter()
1026 .all(|candidate| candidate.storage_bytes()
1027 == tier.storage_bytes(candidate.dims())),
1028 "{tier:?} conversion produced the wrong representation size"
1029 );
1030
1031 if tier != QuantizationTier::Full {
1032 assert!(
1033 stored.iter().zip(&corpus).any(|(quantized, original)| {
1034 quantized
1035 .to_f32()
1036 .iter()
1037 .zip(original)
1038 .any(|(actual, expected)| (actual - expected).abs() > 1e-4)
1039 }),
1040 "{tier:?} conversion did not exercise a lossy representation"
1041 );
1042 }
1043
1044 let mut query_quality = Vec::with_capacity(queries.len());
1045 let mut quantized_distance_witness = false;
1046 for query in &queries {
1047 let reference = reference_ranking(query, &corpus);
1048 let actual = tier_ranking(query, &stored, tier);
1049 query_quality.push(retrieval_quality_counts(&reference, &actual, TOP_K));
1050
1051 if tier != QuantizationTier::Full {
1052 let prepared = PreparedQuery::from_f32(query, tier);
1053 quantized_distance_witness |=
1054 stored.iter().zip(&corpus).any(|(quantized, original)| {
1055 let actual =
1056 approximate_cosine_distance_prepared(&prepared, quantized).unwrap();
1057 let reference = 1.0 - scalar_cosine_f64(query, original) as f32;
1058 (actual - reference).abs() > 1e-4
1059 });
1060 }
1061 }
1062 let (recall, agreement) = validate_tier_retrieval_quality(tier, &query_quality, TOP_K)
1063 .unwrap_or_else(|error| panic!("{error}"));
1064 eprintln!(
1065 "{tier:?}: Recall@{TOP_K}={recall:.6}, pairwise ranking agreement={agreement:.6}"
1066 );
1067
1068 if tier != QuantizationTier::Full {
1069 assert!(
1070 quantized_distance_witness,
1071 "{tier:?} distance path did not differ from the independent f32 reference"
1072 );
1073 }
1074 }
1075 }
1076
1077 #[test]
1078 fn test_tier_retrieval_quality_rejects_concentrated_binary_query_collapse() {
1079 const TOP_K: usize = 10;
1080 const COLLAPSED_QUERY_COUNT: usize = 10;
1081 let (corpus, queries) = fixed_retrieval_fixture();
1082 let index_order: Vec<_> = (0..corpus.len()).collect();
1083 let query_quality: Vec<_> = queries
1084 .iter()
1085 .enumerate()
1086 .map(|(query_index, query)| {
1087 let reference = reference_ranking(query, &corpus);
1088 let actual = if query_index < COLLAPSED_QUERY_COUNT {
1089 index_order.clone()
1090 } else {
1091 reference.clone()
1092 };
1093 retrieval_quality_counts(&reference, &actual, TOP_K)
1094 })
1095 .collect();
1096
1097 let mean_recall = query_quality.iter().map(|quality| quality.0).sum::<usize>() as f64
1098 / (query_quality.len() * TOP_K) as f64;
1099 let mean_agreement = query_quality.iter().map(|quality| quality.1).sum::<usize>() as f64
1100 / (query_quality.len() * 32_640) as f64;
1101 assert_eq!(format!("{mean_recall:.6}"), "0.381250");
1102 assert_eq!(format!("{mean_agreement:.6}"), "0.705356");
1103
1104 let error = validate_tier_retrieval_quality(
1105 QuantizationTier::Binary,
1106 &query_quality,
1107 TOP_K,
1108 )
1109 .expect_err(
1110 "collapsing fixed fixture queries 0 through 9 must fail despite passing both means",
1111 );
1112 assert!(error.contains("query 0"), "unexpected error: {error}");
1113 }
1114
1115 #[test]
1116 fn test_tier_retrieval_quality_rejects_single_binary_query_concentration() {
1117 const TOP_K: usize = 10;
1118 const SUFFIX_INVERSIONS: usize = 7_161;
1119 let (corpus, queries) = fixed_retrieval_fixture();
1120 let reference = reference_ranking(&queries[0], &corpus);
1121 let mut remaining = reference[3..10]
1122 .iter()
1123 .chain(&reference[17..])
1124 .copied()
1125 .collect::<Vec<_>>();
1126 let mut suffix = Vec::with_capacity(remaining.len());
1127 let mut inversions = SUFFIX_INVERSIONS;
1128 while !remaining.is_empty() {
1129 let index = inversions.min(remaining.len() - 1);
1130 inversions -= index;
1131 suffix.push(remaining.remove(index));
1132 }
1133 assert_eq!(inversions, 0);
1134
1135 let mut actual = Vec::with_capacity(reference.len());
1136 actual.extend_from_slice(&reference[..3]);
1137 actual.extend_from_slice(&reference[10..17]);
1138 actual.extend(suffix);
1139 assert_eq!(actual.len(), reference.len());
1140
1141 let collapsed_quality = retrieval_quality_counts(&reference, &actual, TOP_K);
1142 assert_eq!(collapsed_quality, (3, 25_430));
1143
1144 let mut query_quality = healthy_query_quality(QuantizationTier::Binary).to_vec();
1145 query_quality[0] = collapsed_quality;
1146
1147 let error =
1148 validate_tier_retrieval_quality(QuantizationTier::Binary, &query_quality, TOP_K)
1149 .expect_err("one Binary query must not spend the complete fixture movement budget");
1150 assert!(
1151 error.contains("query 0") && error.contains("concentration"),
1152 "unexpected error: {error}"
1153 );
1154 }
1155
1156 #[test]
1157 fn test_tier_retrieval_quality_rejects_single_binary_query_agreement_concentration() {
1158 const TOP_K: usize = 10;
1159 let mut query_quality = healthy_query_quality(QuantizationTier::Binary).to_vec();
1160 query_quality[0].1 -= 1_281;
1161
1162 let error =
1163 validate_tier_retrieval_quality(QuantizationTier::Binary, &query_quality, TOP_K)
1164 .expect_err("one Binary query must not spend nearly the complete pair budget");
1165 assert!(
1166 error.contains("query 0") && error.contains("concentration"),
1167 "unexpected error: {error}"
1168 );
1169 }
1170
1171 #[test]
1172 fn test_tier_retrieval_quality_rejects_distributed_binary_query_collapse() {
1173 const TOP_K: usize = 10;
1174 let (corpus, queries) = fixed_retrieval_fixture();
1175 let query_quality: Vec<_> = queries
1176 .iter()
1177 .map(|query| {
1178 let reference = reference_ranking(query, &corpus);
1179 let mut actual = Vec::with_capacity(reference.len());
1180 actual.extend_from_slice(&reference[..3]);
1181 actual.extend_from_slice(&reference[10..17]);
1182 actual.extend_from_slice(&reference[3..10]);
1183 actual.extend(reference[17..154].iter().rev().copied());
1184 actual.extend_from_slice(&reference[154..]);
1185 assert_eq!(actual.len(), reference.len());
1186 retrieval_quality_counts(&reference, &actual, TOP_K)
1187 })
1188 .collect();
1189
1190 assert!(
1191 query_quality
1192 .iter()
1193 .all(|&(recall, agreement)| { recall == 3 && agreement == 32_640 - 9_365 })
1194 );
1195
1196 let error =
1197 validate_tier_retrieval_quality(QuantizationTier::Binary, &query_quality, TOP_K)
1198 .expect_err("a distributed 70% Recall@10 loss across every query must fail");
1199 assert!(
1200 error.contains("movement budget"),
1201 "unexpected error: {error}"
1202 );
1203 }
1204
1205 #[test]
1206 fn test_tier_retrieval_quality_rejects_shallow_all_query_movement() {
1207 const TOP_K: usize = 10;
1208 let (corpus, queries) = fixed_retrieval_fixture();
1209 let query_quality: Vec<_> = queries
1210 .iter()
1211 .map(|query| {
1212 let reference = reference_ranking(query, &corpus);
1213 let mut actual = reference.clone();
1214 let last = actual.len() - 1;
1215 actual.swap(TOP_K - 1, last);
1216 retrieval_quality_counts(&reference, &actual, TOP_K)
1217 })
1218 .collect();
1219
1220 assert!(
1221 query_quality
1222 .iter()
1223 .all(|&(recall, agreement)| { recall == 9 && agreement == 32_640 - 491 })
1224 );
1225
1226 for (tier, result) in [
1227 (
1228 QuantizationTier::Int4,
1229 validate_tier_retrieval_quality(QuantizationTier::Int4, &query_quality, TOP_K),
1230 ),
1231 (
1232 QuantizationTier::Binary,
1233 validate_tier_retrieval_quality(QuantizationTier::Binary, &query_quality, TOP_K),
1234 ),
1235 ] {
1236 let error = result.unwrap_err();
1237 assert!(
1238 error.contains("movement budget"),
1239 "unexpected {tier:?} error: {error}"
1240 );
1241 }
1242 }
1243
1244 #[test]
1245 fn test_tier_retrieval_quality_bounds_stated_binary_uniform_movement() {
1246 const TOP_K: usize = 10;
1247 let query_quality_with_pair_loss = |pair_loss: u16| {
1248 healthy_query_quality(QuantizationTier::Binary)
1249 .iter()
1250 .map(|&(recall_hits, agreements)| {
1251 (recall_hits, agreements - usize::from(pair_loss))
1252 })
1253 .collect::<Vec<_>>()
1254 };
1255
1256 validate_tier_retrieval_quality(
1257 QuantizationTier::Binary,
1258 &query_quality_with_pair_loss(80),
1259 TOP_K,
1260 )
1261 .expect("1,280 total agreement-pair changes are within the 1,282-pair budget");
1262
1263 let error = validate_tier_retrieval_quality(
1264 QuantizationTier::Binary,
1265 &query_quality_with_pair_loss(81),
1266 TOP_K,
1267 )
1268 .expect_err("1,296 total agreement-pair changes must exceed the 1,282-pair budget");
1269 assert!(
1270 error.contains("movement budget"),
1271 "unexpected error: {error}"
1272 );
1273 }
1274
1275 #[test]
1276 fn test_tier_retrieval_quality_accepts_non_uniform_exact_binary_movement_boundary() {
1277 const TOP_K: usize = 10;
1278 let query_quality = healthy_query_quality(QuantizationTier::Binary)
1279 .iter()
1280 .enumerate()
1281 .map(|(query_index, &(recall_hits, agreements))| {
1282 let pair_loss = if query_index < 14 { 81 } else { 74 };
1283 (recall_hits, agreements - pair_loss)
1284 })
1285 .collect::<Vec<_>>();
1286
1287 validate_tier_retrieval_quality(QuantizationTier::Binary, &query_quality, TOP_K)
1288 .expect("the exact 1,282-pair non-uniform movement boundary must be accepted");
1289 }
1290
1291 #[test]
1292 fn test_validate_retrieval_fixture_rejects_non_discriminating_corpus() {
1293 const TOP_K: usize = 10;
1294 const DIMS: usize = 384;
1295 const CORPUS_SIZE: usize = 256;
1296 const QUERY_COUNT: usize = 16;
1297
1298 let repeated_vector = generate_vector(DIMS, 0xC0A5_0000);
1303 let corpus: Vec<Vec<f32>> = std::iter::repeat_n(repeated_vector, CORPUS_SIZE).collect();
1304 let queries: Vec<Vec<f32>> = (0..QUERY_COUNT)
1305 .map(|index| generate_vector(DIMS, 0x0A11_0000 + index as u64))
1306 .collect();
1307
1308 let err = validate_retrieval_fixture(&corpus, &queries, TOP_K)
1309 .expect_err("a corpus of identical vectors ties every reference score; the guard must refuse rather than let it be scored");
1310 eprintln!("guard refused as expected: {err}");
1311 }
1312
1313 #[test]
1314 fn test_validate_retrieval_fixture_rejects_zero_queries() {
1315 let (corpus, _queries) = fixed_retrieval_fixture();
1316 let err = validate_retrieval_fixture(&corpus, &[], 10)
1317 .expect_err("zero queries must be refused rather than panic or silently pass");
1318 eprintln!("guard refused as expected: {err}");
1319 }
1320
1321 #[test]
1322 fn test_validate_retrieval_fixture_rejects_corpus_smaller_than_top_k() {
1323 let (corpus, queries) = fixed_retrieval_fixture();
1324 let small_corpus = corpus[..5].to_vec();
1325 let err = validate_retrieval_fixture(&small_corpus, &queries, 10).expect_err(
1326 "a corpus smaller than top_k must be refused rather than panic or return NaN",
1327 );
1328 eprintln!("guard refused as expected: {err}");
1329 }
1330
1331 #[test]
1332 fn test_validate_retrieval_fixture_accepts_the_real_fixture() {
1333 let (corpus, queries) = fixed_retrieval_fixture();
1334 validate_retrieval_fixture(&corpus, &queries, 10)
1335 .expect("the real fixture is discriminating and must not be rejected");
1336
1337 let (recall, agreement) = index_order_surrogate_quality(&corpus, &queries, 10);
1338 assert!((recall - 0.025).abs() < f64::EPSILON);
1339 assert!((agreement - 0.526_646_752_450_980_4).abs() < 1e-15);
1340 for (tier, minimum_recall, minimum_agreement) in [
1341 ("Full", 1.0, 0.999),
1342 ("Int8", 0.98, 0.995),
1343 ("Int4", 0.85, 0.95),
1344 ("Binary", 0.30, 0.70),
1345 ] {
1346 assert!(
1347 !meets_retrieval_quality_floor(recall, minimum_recall)
1348 || !meets_retrieval_quality_floor(agreement, minimum_agreement),
1349 "index-order surrogate must fail the pinned {tier} floor"
1350 );
1351 }
1352 }
1353
1354 #[test]
1355 fn test_validate_retrieval_fixture_rejects_binary_index_aligned_collapse() {
1356 const TOP_K: usize = 10;
1357 let query = vec![1.0, 0.0];
1358 let corpus: Vec<_> = (0..21)
1359 .map(|index| vec![21.0 - index as f32, 1.0])
1360 .collect();
1361 let queries = vec![query.clone()];
1362
1363 let mut scores: Vec<_> = corpus
1364 .iter()
1365 .map(|candidate| scalar_cosine_f64(&query, candidate))
1366 .collect();
1367 scores.sort_unstable_by(f64::total_cmp);
1368 scores.dedup();
1369 assert_eq!(scores.len(), 21);
1370
1371 let reference = reference_ranking(&query, &corpus);
1372 let binary: Vec<_> = corpus
1373 .iter()
1374 .map(|candidate| QuantizedData::from_f32(candidate, QuantizationTier::Binary))
1375 .collect();
1376 let prepared = PreparedQuery::from_f32(&query, QuantizationTier::Binary);
1377 let first_code = match &binary[0] {
1378 QuantizedData::Binary(value) => &value.data,
1379 _ => unreachable!("binary conversion must produce the Binary variant"),
1380 };
1381 assert!(binary.iter().all(|candidate| {
1382 let QuantizedData::Binary(value) = candidate else {
1383 return false;
1384 };
1385 value.data == *first_code
1386 && approximate_cosine_distance_prepared(&prepared, candidate).unwrap() == 0.0
1387 }));
1388 let actual = tier_ranking(&query, &binary, QuantizationTier::Binary);
1389 assert_eq!(reference, (0..21).collect::<Vec<_>>());
1390 assert_eq!(actual, reference);
1391 assert_eq!(recall_at(&reference, &actual, TOP_K), 1.0);
1392 assert_eq!(pairwise_ranking_agreement(&reference, &actual), 1.0);
1393
1394 let err = validate_retrieval_fixture(&corpus, &queries, TOP_K).expect_err(
1395 "an index-aligned reference must not let a totally collapsed tier pass its floors",
1396 );
1397 assert!(
1398 err.contains("index-order surrogate"),
1399 "unexpected error: {err}"
1400 );
1401 }
1402
1403 #[test]
1404 fn test_validate_retrieval_fixture_rejects_mixed_non_finite_scores() {
1405 for non_finite in [f32::NAN, f32::INFINITY] {
1406 let (mut corpus, queries) = fixed_retrieval_fixture();
1407 corpus[0][0] = non_finite;
1408 let err = validate_retrieval_fixture(&corpus, &queries, 10)
1409 .expect_err("the first non-finite reference score must invalidate the oracle");
1410 assert!(
1411 err.contains("query 0 candidate 0"),
1412 "unexpected error: {err}"
1413 );
1414 assert!(err.contains("non-finite"), "unexpected error: {err}");
1415 }
1416 }
1417
1418 #[test]
1419 fn test_validate_retrieval_fixture_rejects_zero_top_k() {
1420 let (corpus, queries) = fixed_retrieval_fixture();
1421 let err = validate_retrieval_fixture(&corpus, &queries, 0)
1422 .expect_err("top_k=0 must be refused before recall divides by zero");
1423 assert!(err.contains("top_k=0"), "unexpected error: {err}");
1424 }
1425
1426 #[test]
1427 fn test_validate_retrieval_fixture_rejects_near_tied_top_k_boundary() {
1428 let query = vec![1.0, 0.0];
1429 let corpus = vec![
1430 vec![0.0, 1.0],
1431 vec![1.0, 0.0],
1432 vec![1.0, 0.0001],
1433 vec![-1.0, 0.0],
1434 ];
1435 let best = scalar_cosine_f64(&query, &corpus[1]);
1436 let runner_up = scalar_cosine_f64(&query, &corpus[2]);
1437 let boundary_gap = best - runner_up;
1438 assert!(boundary_gap > 0.0 && boundary_gap < 1e-6);
1439
1440 let err = validate_retrieval_fixture(&corpus, &[query], 1)
1441 .expect_err("an f64-only near-tie at the evaluated cutoff must be refused");
1442 assert!(err.contains("near-tied"), "unexpected error: {err}");
1443 }
1444
1445 #[test]
1446 fn test_validate_retrieval_fixture_rejects_near_tied_pairwise_boundary() {
1447 let query = vec![1.0, 0.0];
1448 let corpus = vec![
1449 vec![-1.0, 0.0],
1450 vec![1.0, 0.0],
1451 vec![100.0, 1.0],
1452 vec![100.0, 1.0001],
1453 ];
1454 let second = scalar_cosine_f64(&query, &corpus[2]);
1455 let third = scalar_cosine_f64(&query, &corpus[3]);
1456 assert!(second > third);
1457 assert_eq!(second as f32, third as f32);
1458
1459 let err = validate_retrieval_fixture(&corpus, &[query], 1)
1460 .expect_err("an f64-only near-tie evaluated by pairwise agreement must be refused");
1461 assert!(err.contains("near-tied"), "unexpected error: {err}");
1462 }
1463
1464 #[test]
1465 fn test_tier_bytes_per_dim() {
1466 assert_eq!(QuantizationTier::Full.bytes_per_dim(), 4.0);
1467 assert_eq!(QuantizationTier::Int8.bytes_per_dim(), 1.0);
1468 assert_eq!(QuantizationTier::Int4.bytes_per_dim(), 0.5);
1469 assert_eq!(QuantizationTier::Binary.bytes_per_dim(), 0.125);
1470 }
1471
1472 #[test]
1473 fn test_tier_compression_ratios() {
1474 assert_eq!(QuantizationTier::Full.compression_ratio(), 1.0);
1475 assert_eq!(QuantizationTier::Int8.compression_ratio(), 4.0);
1476 assert_eq!(QuantizationTier::Int4.compression_ratio(), 8.0);
1477 assert_eq!(QuantizationTier::Binary.compression_ratio(), 32.0);
1478 }
1479
1480 #[test]
1481 fn test_tier_storage_bytes() {
1482 assert_eq!(QuantizationTier::Full.storage_bytes(384), 1536);
1483 assert_eq!(QuantizationTier::Int8.storage_bytes(384), 384);
1484 assert_eq!(QuantizationTier::Int4.storage_bytes(384), 192);
1485 assert_eq!(QuantizationTier::Binary.storage_bytes(384), 48);
1486 }
1487
1488 #[test]
1489 fn test_tier_from_age() {
1490 assert_eq!(
1491 QuantizationTier::from_age_seconds(0),
1492 QuantizationTier::Full
1493 );
1494 assert_eq!(
1495 QuantizationTier::from_age_seconds(1800),
1496 QuantizationTier::Full
1497 ); assert_eq!(
1499 QuantizationTier::from_age_seconds(7200),
1500 QuantizationTier::Int8
1501 ); assert_eq!(
1503 QuantizationTier::from_age_seconds(172800),
1504 QuantizationTier::Int4
1505 ); assert_eq!(
1507 QuantizationTier::from_age_seconds(1_000_000),
1508 QuantizationTier::Binary
1509 ); }
1511
1512 #[test]
1513 fn test_quantized_data_from_f32_all_tiers() {
1514 let v = generate_vector(384, 42);
1515
1516 for tier in [
1517 QuantizationTier::Full,
1518 QuantizationTier::Int8,
1519 QuantizationTier::Int4,
1520 QuantizationTier::Binary,
1521 ] {
1522 let data = QuantizedData::from_f32(&v, tier);
1523 assert_eq!(data.tier(), tier, "tier mismatch for {tier:?}");
1524 assert_eq!(data.dims(), 384, "dims mismatch for {tier:?}");
1525
1526 let expected_bytes = tier.storage_bytes(384);
1528 assert_eq!(
1529 data.storage_bytes(),
1530 expected_bytes,
1531 "storage bytes mismatch for {tier:?}"
1532 );
1533 }
1534 }
1535
1536 #[test]
1537 fn test_approximate_cosine_distance_ordering() {
1538 let a = generate_vector(384, 1);
1540 let b: Vec<f32> = a
1542 .iter()
1543 .enumerate()
1544 .map(|(i, &x)| x + 0.05 * (i as f32 * 0.3).sin())
1545 .collect();
1546 let c = generate_vector(384, 999);
1548
1549 for tier in [
1550 QuantizationTier::Full,
1551 QuantizationTier::Int8,
1552 QuantizationTier::Int4,
1553 QuantizationTier::Binary,
1554 ] {
1555 let stored_b = QuantizedData::from_f32(&b, tier);
1556 let stored_c = QuantizedData::from_f32(&c, tier);
1557
1558 let dist_ab = approximate_cosine_distance(&a, &stored_b);
1559 let dist_ac = approximate_cosine_distance(&a, &stored_c);
1560
1561 assert!(
1563 dist_ab < dist_ac,
1564 "{tier:?}: dist(a,b)={dist_ab} should be < dist(a,c)={dist_ac}"
1565 );
1566 }
1567 }
1568
1569 #[test]
1570 fn test_promote_demote_roundtrip() {
1571 let v = generate_vector(384, 42);
1572 let binary = QuantizedData::from_f32(&v, QuantizationTier::Binary);
1573
1574 let int4 = binary.promote(QuantizationTier::Int4);
1576 assert_eq!(int4.tier(), QuantizationTier::Int4);
1577
1578 let int8 = int4.promote(QuantizationTier::Int8);
1579 assert_eq!(int8.tier(), QuantizationTier::Int8);
1580
1581 let full = int8.promote(QuantizationTier::Full);
1582 assert_eq!(full.tier(), QuantizationTier::Full);
1583 assert_eq!(full.dims(), 384);
1584 }
1585
1586 #[test]
1587 fn test_int8_batch_prepared_matches_per_item_prepared() {
1588 let query = generate_vector(384, 42);
1589 let prepared = PreparedQuery::from_f32(&query, QuantizationTier::Int8);
1590 let candidates: Vec<QuantizedVector> = (0..32)
1591 .map(|i| QuantizedVector::from_f32(&generate_vector(384, i + 1)))
1592 .collect();
1593 let wrapped: Vec<QuantizedData> = candidates
1594 .iter()
1595 .cloned()
1596 .map(QuantizedData::Int8)
1597 .collect();
1598
1599 let got = approximate_int8_batch_prepared(&prepared, &candidates).unwrap();
1600 for (i, item) in wrapped.iter().enumerate() {
1601 let expected = approximate_cosine_distance_prepared(&prepared, item).unwrap();
1602 assert!(
1603 (got[i] - expected).abs() < 1e-6,
1604 "int8 batch prepared mismatch at candidate {i}: got={}, expected={}",
1605 got[i],
1606 expected
1607 );
1608 }
1609 }
1610
1611 #[test]
1612 fn test_int4_batch_prepared_matches_per_item_prepared() {
1613 let query = generate_vector(384, 42);
1614 let prepared = PreparedQuery::from_f32(&query, QuantizationTier::Int4);
1615 let candidates: Vec<Int4Vector> = (0..32)
1616 .map(|i| Int4Vector::from_f32(&generate_vector(384, i + 1)))
1617 .collect();
1618 let wrapped: Vec<QuantizedData> = candidates
1619 .iter()
1620 .cloned()
1621 .map(QuantizedData::Int4)
1622 .collect();
1623
1624 let got = approximate_int4_batch_prepared(&prepared, &candidates).unwrap();
1625 for (i, item) in wrapped.iter().enumerate() {
1626 let expected = approximate_cosine_distance_prepared(&prepared, item).unwrap();
1627 assert!(
1628 (got[i] - expected).abs() < 1e-5,
1629 "int4 batch prepared mismatch at candidate {i}: got={}, expected={}",
1630 got[i],
1631 expected
1632 );
1633 }
1634 }
1635
1636 #[test]
1637 fn test_int4_batch_prepared_api_dispatch_parity() {
1638 for dim in [1usize, 3, 31, 127, 383, 384] {
1643 let query = generate_vector(dim, 700 + dim as u64);
1644 let candidate = generate_vector(dim, 800 + dim as u64);
1645 let prepared = PreparedQuery::from_f32(&query, QuantizationTier::Int4);
1646 let q_cand = Int4Vector::from_f32(&candidate);
1647 let wrapped = QuantizedData::Int4(q_cand.clone());
1648
1649 let batch_result = approximate_int4_batch_prepared(&prepared, &[q_cand]).unwrap();
1650 let per_item_result =
1651 approximate_cosine_distance_prepared(&prepared, &wrapped).unwrap();
1652
1653 assert!(
1654 (batch_result[0] - per_item_result).abs() < 1e-5,
1655 "int4 batch prepared dispatch mismatch at dim={dim}: batch={}, per_item={}",
1656 batch_result[0],
1657 per_item_result
1658 );
1659 }
1660 }
1661
1662 #[test]
1663 fn test_quantized_data_to_f32_roundtrip() {
1664 let v = generate_vector(384, 55);
1665
1666 let full_data = QuantizedData::from_f32(&v, QuantizationTier::Full);
1668 let full_rt = full_data.to_f32();
1669 for (a, b) in v.iter().zip(full_rt.iter()) {
1670 assert!((a - b).abs() < 1e-10, "Full tier should be lossless");
1671 }
1672 }
1673
1674 #[test]
1680 fn test_cosine_distance_prepared_tier_mismatch_returns_typed_error() {
1681 let v = generate_vector(64, 1);
1682 let query = PreparedQuery::from_f32(&v, QuantizationTier::Int8);
1683 let stored = QuantizedData::from_f32(&v, QuantizationTier::Int4);
1684
1685 let err = approximate_cosine_distance_prepared(&query, &stored).unwrap_err();
1686 match err {
1687 EmbedError::TierMismatch {
1688 op,
1689 expected,
1690 actual,
1691 } => {
1692 assert_eq!(op, "approximate_cosine_distance_prepared");
1693 assert_eq!(expected, QuantizationTier::Int4);
1694 assert_eq!(actual, QuantizationTier::Int8);
1695 }
1696 other => panic!("expected TierMismatch, got {other:?}"),
1697 }
1698
1699 assert!(try_approximate_cosine_distance_prepared(&query, &stored).is_err());
1701 }
1702
1703 #[test]
1704 fn test_dot_product_prepared_tier_mismatch_returns_typed_error() {
1705 let v = generate_vector(64, 2);
1706 let query = PreparedQuery::from_f32(&v, QuantizationTier::Full);
1707 let stored = QuantizedData::from_f32(&v, QuantizationTier::Int8);
1708
1709 let err = approximate_dot_product_prepared(&query, &stored).unwrap_err();
1710 assert!(
1711 matches!(
1712 err,
1713 EmbedError::TierMismatch {
1714 op: "approximate_dot_product_prepared",
1715 ..
1716 }
1717 ),
1718 "unexpected error variant: {err:?}"
1719 );
1720
1721 assert!(try_approximate_dot_product_prepared(&query, &stored).is_err());
1722 }
1723
1724 #[test]
1725 fn test_dot_product_prepared_binary_returns_typed_error_not_panic() {
1726 let v = generate_vector(64, 3);
1727 let query = PreparedQuery::from_f32(&v, QuantizationTier::Binary);
1728 let stored = QuantizedData::from_f32(&v, QuantizationTier::Binary);
1729
1730 let err = approximate_dot_product_prepared(&query, &stored).unwrap_err();
1731 assert!(
1732 matches!(err, EmbedError::Internal(_)),
1733 "unexpected error variant: {err:?}"
1734 );
1735 }
1736
1737 #[test]
1738 fn test_cosine_distance_prepared_with_meta_tier_mismatch_returns_typed_error() {
1739 let v = generate_vector(64, 4);
1740 let meta =
1741 PreparedQueryWithMeta::from_f32(&v, QuantizationTier::Full, NormalizationHint::Unknown);
1742 let stored = QuantizedData::from_f32(&v, QuantizationTier::Int8);
1743
1744 let err = approximate_cosine_distance_prepared_with_meta(
1745 &meta,
1746 &stored,
1747 NormalizationHint::Unknown,
1748 )
1749 .unwrap_err();
1750 assert!(matches!(err, EmbedError::TierMismatch { .. }));
1751 }
1752
1753 #[test]
1754 fn test_cosine_distance_prepared_with_meta_validates_stored_unit_norm() {
1755 let query = vec![std::f32::consts::FRAC_1_SQRT_2; 2];
1756 let meta = PreparedQueryWithMeta::from_f32(
1757 &query,
1758 QuantizationTier::Full,
1759 NormalizationHint::Unit,
1760 );
1761 let stored = QuantizedData::Full(vec![2.0, 0.0]);
1762
1763 let got =
1764 approximate_cosine_distance_prepared_with_meta(&meta, &stored, NormalizationHint::Unit)
1765 .unwrap();
1766 let expected = approximate_cosine_distance_prepared(&meta.query, &stored).unwrap();
1767
1768 assert!(
1769 (got - expected).abs() < 1e-6,
1770 "got={got}, expected={expected}"
1771 );
1772 }
1773
1774 #[test]
1775 fn test_batch_cosine_distance_prepared_tier_mismatch_returns_typed_error() {
1776 let v = generate_vector(64, 5);
1777 let query = PreparedQuery::from_f32(&v, QuantizationTier::Int8);
1778 let stored = vec![
1779 QuantizedData::from_f32(&v, QuantizationTier::Int8),
1780 QuantizedData::from_f32(&v, QuantizationTier::Int4), ];
1782
1783 let err = batch_approximate_cosine_distance_prepared(&query, &stored).unwrap_err();
1784 assert!(matches!(err, EmbedError::TierMismatch { .. }));
1785
1786 let mut out = vec![9.0, 9.0, 9.0]; let err =
1788 batch_approximate_cosine_distance_prepared_into(&query, &stored, &mut out).unwrap_err();
1789 assert!(matches!(err, EmbedError::TierMismatch { .. }));
1790 assert!(
1791 out.is_empty(),
1792 "buffer must be cleared, not left with stale data"
1793 );
1794 }
1795
1796 #[test]
1797 fn test_int8_batch_prepared_wrong_tier_returns_typed_error() {
1798 let v = generate_vector(64, 6);
1799 let query = PreparedQuery::from_f32(&v, QuantizationTier::Int4); let candidates = vec![QuantizedVector::from_f32(&v)];
1801
1802 let err = approximate_int8_batch_prepared(&query, &candidates).unwrap_err();
1803 match err {
1804 EmbedError::TierMismatch {
1805 op,
1806 expected,
1807 actual,
1808 } => {
1809 assert_eq!(op, "approximate_int8_batch_prepared");
1810 assert_eq!(expected, QuantizationTier::Int8);
1811 assert_eq!(actual, QuantizationTier::Int4);
1812 }
1813 other => panic!("expected TierMismatch, got {other:?}"),
1814 }
1815
1816 let mut out = vec![9.0];
1817 let err = approximate_int8_batch_prepared_into(&query, &candidates, &mut out).unwrap_err();
1818 assert!(matches!(err, EmbedError::TierMismatch { .. }));
1819 assert!(
1820 out.is_empty(),
1821 "buffer must be cleared, not left with stale data"
1822 );
1823 }
1824
1825 #[test]
1826 fn test_int4_batch_prepared_wrong_tier_returns_typed_error() {
1827 let v = generate_vector(64, 7);
1828 let query = PreparedQuery::from_f32(&v, QuantizationTier::Int8); let candidates = vec![Int4Vector::from_f32(&v)];
1830
1831 let err = approximate_int4_batch_prepared(&query, &candidates).unwrap_err();
1832 match err {
1833 EmbedError::TierMismatch {
1834 op,
1835 expected,
1836 actual,
1837 } => {
1838 assert_eq!(op, "approximate_int4_batch_prepared");
1839 assert_eq!(expected, QuantizationTier::Int4);
1840 assert_eq!(actual, QuantizationTier::Int8);
1841 }
1842 other => panic!("expected TierMismatch, got {other:?}"),
1843 }
1844
1845 let mut out = vec![9.0];
1846 let err = approximate_int4_batch_prepared_into(&query, &candidates, &mut out).unwrap_err();
1847 assert!(matches!(err, EmbedError::TierMismatch { .. }));
1848 assert!(
1849 out.is_empty(),
1850 "buffer must be cleared, not left with stale data"
1851 );
1852 }
1853}