1use super::binary::BinaryVector;
17use super::int4::Int4Vector;
18use super::quantized::{QuantizedVector, cosine_similarity_i8_trusted, dot_product_i8_trusted};
19use super::{cosine_similarity, dot_product};
20use crate::error::{EmbedError, Result};
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum NormalizationHint {
28 Unknown,
30 Unit,
32}
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
38pub enum QuantizationTier {
39 Full,
41 Int8,
43 Int4,
45 Binary,
47}
48
49impl QuantizationTier {
50 pub fn bytes_per_dim(&self) -> f32 {
52 match self {
53 Self::Full => 4.0,
54 Self::Int8 => 1.0,
55 Self::Int4 => 0.5,
56 Self::Binary => 0.125,
57 }
58 }
59
60 pub fn compression_ratio(&self) -> f32 {
62 4.0 / self.bytes_per_dim()
63 }
64
65 pub fn storage_bytes(&self, dims: usize) -> usize {
67 match self {
68 Self::Full => dims * 4,
69 Self::Int8 => dims,
70 Self::Int4 => dims.div_ceil(2),
71 Self::Binary => dims.div_ceil(8),
72 }
73 }
74
75 pub fn from_age_seconds(age_secs: u64) -> Self {
82 const HOUR: u64 = 3600;
83 const DAY: u64 = 86400;
84 const WEEK: u64 = 604800;
85
86 if age_secs < HOUR {
87 Self::Full
88 } else if age_secs < DAY {
89 Self::Int8
90 } else if age_secs < WEEK {
91 Self::Int4
92 } else {
93 Self::Binary
94 }
95 }
96}
97
98#[derive(Debug, Clone)]
103pub enum QuantizedData {
104 Full(Vec<f32>),
106 Int8(QuantizedVector),
108 Int4(Int4Vector),
110 Binary(BinaryVector),
112}
113
114impl QuantizedData {
115 pub fn tier(&self) -> QuantizationTier {
117 match self {
118 Self::Full(_) => QuantizationTier::Full,
119 Self::Int8(_) => QuantizationTier::Int8,
120 Self::Int4(_) => QuantizationTier::Int4,
121 Self::Binary(_) => QuantizationTier::Binary,
122 }
123 }
124
125 pub fn dims(&self) -> usize {
127 match self {
128 Self::Full(v) => v.len(),
129 Self::Int8(q) => q.len(),
130 Self::Int4(q) => q.dims,
131 Self::Binary(q) => q.dims,
132 }
133 }
134
135 pub fn storage_bytes(&self) -> usize {
137 match self {
138 Self::Full(v) => v.len() * 4,
139 Self::Int8(q) => q.len(),
140 Self::Int4(q) => q.data.len(),
141 Self::Binary(q) => q.data.len(),
142 }
143 }
144
145 pub fn from_f32(vector: &[f32], tier: QuantizationTier) -> Self {
147 match tier {
148 QuantizationTier::Full => Self::Full(vector.to_vec()),
149 QuantizationTier::Int8 => Self::Int8(QuantizedVector::from_f32(vector)),
150 QuantizationTier::Int4 => Self::Int4(Int4Vector::from_f32(vector)),
151 QuantizationTier::Binary => Self::Binary(BinaryVector::from_f32(vector)),
152 }
153 }
154
155 pub fn to_f32(&self) -> Vec<f32> {
157 match self {
158 Self::Full(v) => v.clone(),
159 Self::Int8(q) => q.to_f32(),
160 Self::Int4(q) => q.to_f32(),
161 Self::Binary(q) => q.to_f32(),
162 }
163 }
164
165 pub fn promote(&self, target: QuantizationTier) -> Self {
172 let f32_data = self.to_f32();
173 Self::from_f32(&f32_data, target)
174 }
175
176 pub fn demote(&self, target: QuantizationTier) -> Self {
178 self.promote(target) }
180}
181
182#[derive(Debug, Clone)]
187pub enum PreparedQuery {
188 Full(Vec<f32>),
190 Int8(QuantizedVector),
192 Int4(Int4Vector),
194 Binary(BinaryVector),
196}
197
198impl PreparedQuery {
199 #[inline]
201 pub fn from_f32(query_f32: &[f32], tier: QuantizationTier) -> Self {
202 match tier {
203 QuantizationTier::Full => Self::Full(query_f32.to_vec()),
204 QuantizationTier::Int8 => Self::Int8(QuantizedVector::from_f32(query_f32)),
205 QuantizationTier::Int4 => Self::Int4(Int4Vector::from_f32(query_f32)),
206 QuantizationTier::Binary => Self::Binary(BinaryVector::from_f32(query_f32)),
207 }
208 }
209
210 #[inline]
212 pub fn tier(&self) -> QuantizationTier {
213 match self {
214 Self::Full(_) => QuantizationTier::Full,
215 Self::Int8(_) => QuantizationTier::Int8,
216 Self::Int4(_) => QuantizationTier::Int4,
217 Self::Binary(_) => QuantizationTier::Binary,
218 }
219 }
220
221 #[inline]
223 pub fn dims(&self) -> usize {
224 match self {
225 Self::Full(v) => v.len(),
226 Self::Int8(q) => q.len(),
227 Self::Int4(q) => q.dims,
228 Self::Binary(q) => q.dims,
229 }
230 }
231}
232
233#[inline]
235pub fn prepare_query(query_f32: &[f32], tier: QuantizationTier) -> PreparedQuery {
236 PreparedQuery::from_f32(query_f32, tier)
237}
238
239#[derive(Debug, Clone)]
245pub struct PreparedQueryWithMeta {
246 pub query: PreparedQuery,
248 pub norm: NormalizationHint,
250}
251
252impl PreparedQueryWithMeta {
253 #[inline]
255 pub fn from_f32(query_f32: &[f32], tier: QuantizationTier, norm: NormalizationHint) -> Self {
256 Self {
257 query: PreparedQuery::from_f32(query_f32, tier),
258 norm,
259 }
260 }
261
262 #[inline]
264 pub fn tier(&self) -> QuantizationTier {
265 self.query.tier()
266 }
267
268 #[inline]
270 pub fn dims(&self) -> usize {
271 self.query.dims()
272 }
273}
274
275#[inline]
277pub fn is_unit_norm(v: &[f32]) -> bool {
278 let sq: f32 = v.iter().map(|x| x * x).sum();
279 (sq - 1.0).abs() < 1e-4
280}
281
282#[inline]
284pub fn prepare_query_with_norm(
285 query_f32: &[f32],
286 tier: QuantizationTier,
287 norm: NormalizationHint,
288) -> PreparedQueryWithMeta {
289 PreparedQueryWithMeta::from_f32(query_f32, tier, norm)
290}
291
292#[inline]
302pub fn approximate_cosine_distance_prepared(query: &PreparedQuery, stored: &QuantizedData) -> f32 {
303 match (query, stored) {
304 (PreparedQuery::Full(q), QuantizedData::Full(s)) => 1.0 - cosine_similarity(q, s),
305 (PreparedQuery::Int8(q), QuantizedData::Int8(s)) => {
306 1.0 - cosine_similarity_i8_trusted(s, q)
307 }
308 (PreparedQuery::Int4(q), QuantizedData::Int4(s)) => s.cosine_distance(q),
309 (PreparedQuery::Binary(q), QuantizedData::Binary(s)) => s.cosine_distance_approx(q),
310 _ => panic!("PreparedQuery tier must match QuantizedData tier"),
311 }
312}
313
314#[inline]
320pub fn try_approximate_cosine_distance_prepared(
321 query: &PreparedQuery,
322 stored: &QuantizedData,
323) -> Result<f32> {
324 match (query, stored) {
325 (PreparedQuery::Full(q), QuantizedData::Full(s)) => Ok(1.0 - cosine_similarity(q, s)),
326 (PreparedQuery::Int8(q), QuantizedData::Int8(s)) => {
327 Ok(1.0 - cosine_similarity_i8_trusted(s, q))
328 }
329 (PreparedQuery::Int4(q), QuantizedData::Int4(s)) => Ok(s.cosine_distance(q)),
330 (PreparedQuery::Binary(q), QuantizedData::Binary(s)) => Ok(s.cosine_distance_approx(q)),
331 _ => Err(EmbedError::Internal(
332 "PreparedQuery tier must match QuantizedData tier for cosine distance".into(),
333 )),
334 }
335}
336
337#[inline]
342pub fn try_approximate_dot_product_prepared(
343 query: &PreparedQuery,
344 stored: &QuantizedData,
345) -> Result<f32> {
346 match (query, stored) {
347 (PreparedQuery::Full(q), QuantizedData::Full(s)) => Ok(dot_product(q, s)),
348 (PreparedQuery::Int8(q), QuantizedData::Int8(s)) => Ok(dot_product_i8_trusted(q, s)),
349 (PreparedQuery::Int4(q), QuantizedData::Int4(s)) => Ok(s.dot_product(q)),
350 (PreparedQuery::Binary(_), QuantizedData::Binary(_)) => Err(EmbedError::Internal(
351 "Binary has no prepared dot product; use try_approximate_cosine_distance_prepared"
352 .into(),
353 )),
354 _ => Err(EmbedError::Internal(
355 "PreparedQuery tier must match QuantizedData tier for dot product".into(),
356 )),
357 }
358}
359
360#[inline]
377pub fn approximate_cosine_distance_prepared_with_meta(
378 meta: &PreparedQueryWithMeta,
379 stored: &QuantizedData,
380 stored_norm: NormalizationHint,
381) -> f32 {
382 if meta.norm == NormalizationHint::Unit && stored_norm == NormalizationHint::Unit {
383 if let (PreparedQuery::Full(q), QuantizedData::Full(s)) = (&meta.query, stored) {
384 let dot = dot_product(q, s);
385 return 1.0 - dot.clamp(-1.0, 1.0);
386 }
387 }
388 approximate_cosine_distance_prepared(&meta.query, stored)
389}
390
391#[inline]
400pub fn approximate_dot_product_prepared(query: &PreparedQuery, stored: &QuantizedData) -> f32 {
401 match (query, stored) {
402 (PreparedQuery::Full(q), QuantizedData::Full(s)) => dot_product(q, s),
403 (PreparedQuery::Int8(q), QuantizedData::Int8(s)) => dot_product_i8_trusted(q, s),
404 (PreparedQuery::Int4(q), QuantizedData::Int4(s)) => s.dot_product(q),
405 (PreparedQuery::Binary(_), QuantizedData::Binary(_)) => {
406 panic!("Binary has no prepared dot product; use approximate_cosine_distance_prepared")
407 }
408 _ => panic!("PreparedQuery tier must match QuantizedData tier"),
409 }
410}
411
412#[inline]
420pub fn batch_approximate_cosine_distance_prepared(
421 query: &PreparedQuery,
422 stored: &[QuantizedData],
423) -> Vec<f32> {
424 stored
425 .iter()
426 .map(|item| approximate_cosine_distance_prepared(query, item))
427 .collect()
428}
429
430#[inline]
439pub fn batch_approximate_cosine_distance_prepared_into(
440 query: &PreparedQuery,
441 stored: &[QuantizedData],
442 out: &mut Vec<f32>,
443) {
444 out.clear();
445 out.reserve(stored.len());
446 out.extend(
447 stored
448 .iter()
449 .map(|item| approximate_cosine_distance_prepared(query, item)),
450 );
451}
452
453#[inline]
461pub fn approximate_int8_batch_prepared(
462 query: &PreparedQuery,
463 candidates: &[QuantizedVector],
464) -> Vec<f32> {
465 let PreparedQuery::Int8(q) = query else {
466 panic!("PreparedQuery tier must be Int8");
467 };
468 candidates
469 .iter()
470 .map(|candidate| 1.0 - cosine_similarity_i8_trusted(candidate, q))
471 .collect()
472}
473
474#[inline]
480pub fn approximate_int8_batch_prepared_into(
481 query: &PreparedQuery,
482 candidates: &[QuantizedVector],
483 out: &mut Vec<f32>,
484) {
485 let PreparedQuery::Int8(q) = query else {
486 panic!("PreparedQuery tier must be Int8");
487 };
488 out.clear();
489 out.reserve(candidates.len());
490 out.extend(
491 candidates
492 .iter()
493 .map(|candidate| 1.0 - cosine_similarity_i8_trusted(candidate, q)),
494 );
495}
496
497#[inline]
505pub fn approximate_int4_batch_prepared(
506 query: &PreparedQuery,
507 candidates: &[Int4Vector],
508) -> Vec<f32> {
509 let PreparedQuery::Int4(q) = query else {
510 panic!("PreparedQuery tier must be Int4");
511 };
512 candidates
513 .iter()
514 .map(|candidate| candidate.cosine_distance(q))
515 .collect()
516}
517
518#[inline]
524pub fn approximate_int4_batch_prepared_into(
525 query: &PreparedQuery,
526 candidates: &[Int4Vector],
527 out: &mut Vec<f32>,
528) {
529 let PreparedQuery::Int4(q) = query else {
530 panic!("PreparedQuery tier must be Int4");
531 };
532 out.clear();
533 out.reserve(candidates.len());
534 out.extend(
535 candidates
536 .iter()
537 .map(|candidate| candidate.cosine_distance(q)),
538 );
539}
540
541pub fn approximate_cosine_distance(query_f32: &[f32], stored: &QuantizedData) -> f32 {
553 debug_assert_eq!(
554 query_f32.len(),
555 stored.dims(),
556 "approximate_cosine_distance: query length {} != stored dims {}",
557 query_f32.len(),
558 stored.dims(),
559 );
560 match stored {
561 QuantizedData::Full(v) => {
562 1.0 - cosine_similarity(query_f32, v)
564 }
565 QuantizedData::Int8(q) => {
566 let query_q = QuantizedVector::from_f32(query_f32);
568 1.0 - q.cosine_similarity(&query_q)
569 }
570 QuantizedData::Int4(q) => {
571 let query_q = Int4Vector::from_f32(query_f32);
573 q.cosine_distance(&query_q)
574 }
575 QuantizedData::Binary(q) => {
576 let query_q = BinaryVector::from_f32(query_f32);
578 q.cosine_distance_approx(&query_q)
579 }
580 }
581}
582
583pub fn approximate_dot_product(query_f32: &[f32], stored: &QuantizedData) -> f32 {
585 match stored {
586 QuantizedData::Full(v) => dot_product(query_f32, v),
587 QuantizedData::Int8(q) => {
588 let query_q = QuantizedVector::from_f32(query_f32);
589 q.dot_product(&query_q)
590 }
591 QuantizedData::Int4(q) => {
592 let query_q = Int4Vector::from_f32(query_f32);
593 q.dot_product(&query_q)
594 }
595 QuantizedData::Binary(_q) => {
596 let stored_f32 = _q.to_f32();
598 dot_product(query_f32, &stored_f32)
599 }
600 }
601}
602
603#[cfg(test)]
604mod tests {
605 use super::*;
606
607 fn generate_vector(dim: usize, seed: u64) -> Vec<f32> {
608 let mut state = seed ^ ((dim as u64).wrapping_mul(0x9E37_79B9_7F4A_7C15));
609 (0..dim)
610 .map(|i| {
611 state = state
612 .wrapping_mul(6364136223846793005)
613 .wrapping_add(1442695040888963407)
614 .wrapping_add(i as u64);
615 let unit = ((state >> 32) as u32) as f32 / u32::MAX as f32;
616 unit * 2.0 - 1.0
617 })
618 .collect()
619 }
620
621 #[test]
622 fn test_tier_bytes_per_dim() {
623 assert_eq!(QuantizationTier::Full.bytes_per_dim(), 4.0);
624 assert_eq!(QuantizationTier::Int8.bytes_per_dim(), 1.0);
625 assert_eq!(QuantizationTier::Int4.bytes_per_dim(), 0.5);
626 assert_eq!(QuantizationTier::Binary.bytes_per_dim(), 0.125);
627 }
628
629 #[test]
630 fn test_tier_compression_ratios() {
631 assert_eq!(QuantizationTier::Full.compression_ratio(), 1.0);
632 assert_eq!(QuantizationTier::Int8.compression_ratio(), 4.0);
633 assert_eq!(QuantizationTier::Int4.compression_ratio(), 8.0);
634 assert_eq!(QuantizationTier::Binary.compression_ratio(), 32.0);
635 }
636
637 #[test]
638 fn test_tier_storage_bytes() {
639 assert_eq!(QuantizationTier::Full.storage_bytes(384), 1536);
640 assert_eq!(QuantizationTier::Int8.storage_bytes(384), 384);
641 assert_eq!(QuantizationTier::Int4.storage_bytes(384), 192);
642 assert_eq!(QuantizationTier::Binary.storage_bytes(384), 48);
643 }
644
645 #[test]
646 fn test_tier_from_age() {
647 assert_eq!(
648 QuantizationTier::from_age_seconds(0),
649 QuantizationTier::Full
650 );
651 assert_eq!(
652 QuantizationTier::from_age_seconds(1800),
653 QuantizationTier::Full
654 ); assert_eq!(
656 QuantizationTier::from_age_seconds(7200),
657 QuantizationTier::Int8
658 ); assert_eq!(
660 QuantizationTier::from_age_seconds(172800),
661 QuantizationTier::Int4
662 ); assert_eq!(
664 QuantizationTier::from_age_seconds(1_000_000),
665 QuantizationTier::Binary
666 ); }
668
669 #[test]
670 fn test_quantized_data_from_f32_all_tiers() {
671 let v = generate_vector(384, 42);
672
673 for tier in [
674 QuantizationTier::Full,
675 QuantizationTier::Int8,
676 QuantizationTier::Int4,
677 QuantizationTier::Binary,
678 ] {
679 let data = QuantizedData::from_f32(&v, tier);
680 assert_eq!(data.tier(), tier, "tier mismatch for {tier:?}");
681 assert_eq!(data.dims(), 384, "dims mismatch for {tier:?}");
682
683 let expected_bytes = tier.storage_bytes(384);
685 assert_eq!(
686 data.storage_bytes(),
687 expected_bytes,
688 "storage bytes mismatch for {tier:?}"
689 );
690 }
691 }
692
693 #[test]
694 fn test_approximate_cosine_distance_ordering() {
695 let a = generate_vector(384, 1);
697 let b: Vec<f32> = a
699 .iter()
700 .enumerate()
701 .map(|(i, &x)| x + 0.05 * (i as f32 * 0.3).sin())
702 .collect();
703 let c = generate_vector(384, 999);
705
706 for tier in [
707 QuantizationTier::Full,
708 QuantizationTier::Int8,
709 QuantizationTier::Int4,
710 QuantizationTier::Binary,
711 ] {
712 let stored_b = QuantizedData::from_f32(&b, tier);
713 let stored_c = QuantizedData::from_f32(&c, tier);
714
715 let dist_ab = approximate_cosine_distance(&a, &stored_b);
716 let dist_ac = approximate_cosine_distance(&a, &stored_c);
717
718 assert!(
720 dist_ab < dist_ac,
721 "{tier:?}: dist(a,b)={dist_ab} should be < dist(a,c)={dist_ac}"
722 );
723 }
724 }
725
726 #[test]
727 fn test_promote_demote_roundtrip() {
728 let v = generate_vector(384, 42);
729 let binary = QuantizedData::from_f32(&v, QuantizationTier::Binary);
730
731 let int4 = binary.promote(QuantizationTier::Int4);
733 assert_eq!(int4.tier(), QuantizationTier::Int4);
734
735 let int8 = int4.promote(QuantizationTier::Int8);
736 assert_eq!(int8.tier(), QuantizationTier::Int8);
737
738 let full = int8.promote(QuantizationTier::Full);
739 assert_eq!(full.tier(), QuantizationTier::Full);
740 assert_eq!(full.dims(), 384);
741 }
742
743 #[test]
744 fn test_int8_batch_prepared_matches_per_item_prepared() {
745 let query = generate_vector(384, 42);
746 let prepared = PreparedQuery::from_f32(&query, QuantizationTier::Int8);
747 let candidates: Vec<QuantizedVector> = (0..32)
748 .map(|i| QuantizedVector::from_f32(&generate_vector(384, i + 1)))
749 .collect();
750 let wrapped: Vec<QuantizedData> = candidates
751 .iter()
752 .cloned()
753 .map(QuantizedData::Int8)
754 .collect();
755
756 let got = approximate_int8_batch_prepared(&prepared, &candidates);
757 for (i, item) in wrapped.iter().enumerate() {
758 let expected = approximate_cosine_distance_prepared(&prepared, item);
759 assert!(
760 (got[i] - expected).abs() < 1e-6,
761 "int8 batch prepared mismatch at candidate {i}: got={}, expected={}",
762 got[i],
763 expected
764 );
765 }
766 }
767
768 #[test]
769 fn test_int4_batch_prepared_matches_per_item_prepared() {
770 let query = generate_vector(384, 42);
771 let prepared = PreparedQuery::from_f32(&query, QuantizationTier::Int4);
772 let candidates: Vec<Int4Vector> = (0..32)
773 .map(|i| Int4Vector::from_f32(&generate_vector(384, i + 1)))
774 .collect();
775 let wrapped: Vec<QuantizedData> = candidates
776 .iter()
777 .cloned()
778 .map(QuantizedData::Int4)
779 .collect();
780
781 let got = approximate_int4_batch_prepared(&prepared, &candidates);
782 for (i, item) in wrapped.iter().enumerate() {
783 let expected = approximate_cosine_distance_prepared(&prepared, item);
784 assert!(
785 (got[i] - expected).abs() < 1e-5,
786 "int4 batch prepared mismatch at candidate {i}: got={}, expected={}",
787 got[i],
788 expected
789 );
790 }
791 }
792
793 #[test]
794 fn test_int4_batch_prepared_api_dispatch_parity() {
795 for dim in [1usize, 3, 31, 127, 383, 384] {
800 let query = generate_vector(dim, 700 + dim as u64);
801 let candidate = generate_vector(dim, 800 + dim as u64);
802 let prepared = PreparedQuery::from_f32(&query, QuantizationTier::Int4);
803 let q_cand = Int4Vector::from_f32(&candidate);
804 let wrapped = QuantizedData::Int4(q_cand.clone());
805
806 let batch_result = approximate_int4_batch_prepared(&prepared, &[q_cand]);
807 let per_item_result = approximate_cosine_distance_prepared(&prepared, &wrapped);
808
809 assert!(
810 (batch_result[0] - per_item_result).abs() < 1e-5,
811 "int4 batch prepared dispatch mismatch at dim={dim}: batch={}, per_item={}",
812 batch_result[0],
813 per_item_result
814 );
815 }
816 }
817
818 #[test]
819 fn test_quantized_data_to_f32_roundtrip() {
820 let v = generate_vector(384, 55);
821
822 let full_data = QuantizedData::from_f32(&v, QuantizationTier::Full);
824 let full_rt = full_data.to_f32();
825 for (a, b) in v.iter().zip(full_rt.iter()) {
826 assert!((a - b).abs() < 1e-10, "Full tier should be lossless");
827 }
828 }
829}