ipfrs_semantic/quantization/stores.rs
1//! Memory-efficient vector stores: INT8 (`QuantizedVectorStore`) and binary (`BinaryVectorStore`).
2
3use super::{dequantize_i8_to_f32, quantize_f32_to_i8};
4
5/// Stores f32 vectors quantized to INT8, reducing memory ~4× vs raw f32.
6///
7/// Each vector is independently scaled with per-vector `scale` and `zero_point`
8/// so that dequantization error is bounded to roughly `scale / 2` per element.
9///
10/// # Memory layout
11/// Internally uses a single flat `Vec<i8>` of length `count * dim`.
12pub struct QuantizedVectorStore {
13 /// Flattened quantized data: `[v0_d0, v0_d1, …, v1_d0, …]`
14 data: Vec<i8>,
15 /// Vector dimension
16 dim: usize,
17 /// Number of stored vectors
18 count: usize,
19 /// Per-vector scale factors (index → scale)
20 scales: Vec<f32>,
21 /// Per-vector zero-points (index → zero_point)
22 zero_points: Vec<f32>,
23}
24
25impl QuantizedVectorStore {
26 /// Create an empty store for vectors of `dim` dimensions.
27 pub fn new(dim: usize) -> Self {
28 Self {
29 data: Vec::new(),
30 dim,
31 count: 0,
32 scales: Vec::new(),
33 zero_points: Vec::new(),
34 }
35 }
36
37 /// Quantize `vector` to INT8 and append it to the store.
38 ///
39 /// Returns the assigned integer ID (0-based).
40 pub fn push(&mut self, vector: &[f32]) -> usize {
41 debug_assert_eq!(
42 vector.len(),
43 self.dim,
44 "push: vector length {} != dim {}",
45 vector.len(),
46 self.dim
47 );
48
49 let (q, scale, zero_point) = quantize_f32_to_i8(vector);
50 self.data.extend_from_slice(&q);
51 self.scales.push(scale);
52 self.zero_points.push(zero_point);
53
54 let id = self.count;
55 self.count += 1;
56 id
57 }
58
59 /// Dequantize the vector stored at `id` back to f32.
60 ///
61 /// Returns `None` if `id >= self.len()`.
62 pub fn get(&self, id: usize) -> Option<Vec<f32>> {
63 if id >= self.count {
64 return None;
65 }
66 let start = id * self.dim;
67 let end = start + self.dim;
68 let q = &self.data[start..end];
69 Some(dequantize_i8_to_f32(
70 q,
71 self.scales[id],
72 self.zero_points[id],
73 ))
74 }
75
76 /// Compute approximate cosine similarity between two stored vectors.
77 ///
78 /// Dequantizes both vectors then computes exact cosine similarity in f32.
79 /// Returns 0.0 if either id is out of range or a vector has zero norm.
80 pub fn cosine_similarity_q(&self, a_id: usize, b_id: usize) -> f32 {
81 let a = match self.get(a_id) {
82 Some(v) => v,
83 None => return 0.0,
84 };
85 let b = match self.get(b_id) {
86 Some(v) => v,
87 None => return 0.0,
88 };
89
90 let dot: f32 = a.iter().zip(b.iter()).map(|(x, y)| x * y).sum();
91 let norm_a: f32 = a.iter().map(|x| x * x).sum::<f32>().sqrt();
92 let norm_b: f32 = b.iter().map(|x| x * x).sum::<f32>().sqrt();
93
94 if norm_a < 1e-9 || norm_b < 1e-9 {
95 return 0.0;
96 }
97 dot / (norm_a * norm_b)
98 }
99
100 /// Approximate memory usage per stored vector (bytes).
101 ///
102 /// Counts the `i8` storage only (not scale/zero_point metadata):
103 /// `dim * 1 byte` per vector.
104 pub fn bytes_per_vector(&self) -> f64 {
105 self.dim as f64
106 }
107
108 /// Number of stored vectors.
109 pub fn len(&self) -> usize {
110 self.count
111 }
112
113 /// Returns `true` if no vectors have been stored.
114 pub fn is_empty(&self) -> bool {
115 self.count == 0
116 }
117
118 /// Vector dimension.
119 pub fn dim(&self) -> usize {
120 self.dim
121 }
122}
123
124/// Stores f32 vectors binarized to 1 bit per dimension.
125///
126/// Each dimension is thresholded at the per-vector mean:
127/// `bit = 1 if v[i] >= mean(v) else 0`.
128///
129/// Bits are packed into `u64` words (64 dims per word), so a 384-dim vector
130/// occupies `ceil(384/64) = 6` words = 48 bytes, vs 1536 bytes for f32.
131///
132/// Similarity is measured with Hamming distance (popcount of XOR).
133pub struct BinaryVectorStore {
134 /// Packed bit data: `dim_words` u64 words per vector.
135 data: Vec<u64>,
136 /// Original vector dimension (number of bits per vector).
137 dim: usize,
138 /// Number of stored vectors.
139 count: usize,
140 /// Number of u64 words per vector: `(dim + 63) / 64`.
141 dim_words: usize,
142}
143
144impl BinaryVectorStore {
145 /// Create an empty store for vectors of `dim` dimensions.
146 pub fn new(dim: usize) -> Self {
147 let dim_words = dim.div_ceil(64);
148 Self {
149 data: Vec::new(),
150 dim,
151 count: 0,
152 dim_words,
153 }
154 }
155
156 /// Binarize `vector` (threshold at mean) and store it.
157 ///
158 /// Returns the assigned ID.
159 pub fn push(&mut self, vector: &[f32]) -> usize {
160 debug_assert_eq!(
161 vector.len(),
162 self.dim,
163 "push: vector length {} != dim {}",
164 vector.len(),
165 self.dim
166 );
167
168 // Compute per-vector mean for threshold
169 let mean = if vector.is_empty() {
170 0.0_f32
171 } else {
172 vector.iter().sum::<f32>() / vector.len() as f32
173 };
174
175 // Pack bits into u64 words
176 let mut words = vec![0u64; self.dim_words];
177 for (i, &val) in vector.iter().enumerate() {
178 if val >= mean {
179 let word_idx = i / 64;
180 let bit_idx = i % 64;
181 words[word_idx] |= 1u64 << bit_idx;
182 }
183 }
184
185 self.data.extend_from_slice(&words);
186 let id = self.count;
187 self.count += 1;
188 id
189 }
190
191 /// Compute the Hamming distance between two stored vectors.
192 ///
193 /// Hamming distance = number of bit positions where the two vectors differ.
194 /// Returns `u32::MAX` if either id is out of range.
195 pub fn hamming_distance(&self, a_id: usize, b_id: usize) -> u32 {
196 if a_id >= self.count || b_id >= self.count {
197 return u32::MAX;
198 }
199
200 let a_start = a_id * self.dim_words;
201 let b_start = b_id * self.dim_words;
202 let a_words = &self.data[a_start..a_start + self.dim_words];
203 let b_words = &self.data[b_start..b_start + self.dim_words];
204
205 a_words
206 .iter()
207 .zip(b_words.iter())
208 .map(|(a, b)| (a ^ b).count_ones())
209 .sum()
210 }
211
212 /// Approximate memory per stored vector (bytes).
213 ///
214 /// `dim_words * 8` bytes per vector, e.g. dim=384 → 6 × 8 = 48 bytes.
215 pub fn bytes_per_vector(&self) -> f64 {
216 (self.dim_words * 8) as f64
217 }
218
219 /// Number of stored vectors.
220 pub fn len(&self) -> usize {
221 self.count
222 }
223
224 /// Returns `true` if no vectors have been stored.
225 pub fn is_empty(&self) -> bool {
226 self.count == 0
227 }
228
229 /// Vector dimension.
230 pub fn dim(&self) -> usize {
231 self.dim
232 }
233}