1pub mod product;
7pub mod scalar;
8pub mod stores;
9
10pub use product::{
12 OptimizedProductQuantizer, PQCode, ProductQuantizer, ProductQuantizerConfig,
13 QuantizationBenchmark, QuantizationBenchmarker, QuantizationComparison,
14};
15pub use scalar::{QuantizedVector, ScalarQuantizer, ScalarQuantizerConfig};
16pub use stores::{BinaryVectorStore, QuantizedVectorStore};
17
18pub fn quantize_f32_to_i8(v: &[f32]) -> (Vec<i8>, f32, f32) {
32 if v.is_empty() {
33 return (Vec::new(), 1.0, 0.0);
34 }
35
36 let min_val = v.iter().cloned().fold(f32::INFINITY, f32::min);
37 let max_val = v.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
38
39 let zero_point = (min_val + max_val) * 0.5;
41 let half_range = (max_val - min_val) * 0.5;
42
43 let scale = if half_range < 1e-9 {
45 1.0_f32
46 } else {
47 half_range / 127.0
48 };
49
50 let quantized: Vec<i8> = v
51 .iter()
52 .map(|&x| {
53 let q = ((x - zero_point) / scale).round();
54 q.clamp(-128.0, 127.0) as i8
55 })
56 .collect();
57
58 (quantized, scale, zero_point)
59}
60
61pub fn dequantize_i8_to_f32(q: &[i8], scale: f32, zero_point: f32) -> Vec<f32> {
66 q.iter().map(|&qi| qi as f32 * scale + zero_point).collect()
67}
68
69#[cfg(test)]
70mod tests {
71 use super::*;
72
73 #[test]
74 fn test_scalar_quantizer_uint8() {
75 let mut quantizer = ScalarQuantizer::uint8(4);
76
77 let vectors = vec![
79 vec![0.0, 0.5, 1.0, -0.5],
80 vec![1.0, 0.0, 0.5, 0.5],
81 vec![0.5, 0.5, 0.0, 0.0],
82 ];
83
84 quantizer.train(&vectors).expect("train");
85 assert!(quantizer.is_trained());
86
87 let original = vec![0.5, 0.25, 0.75, 0.0];
89 let quantized = quantizer.quantize(&original).expect("quantize");
90 let restored = quantizer.dequantize(&quantized).expect("dequantize");
91
92 for (o, r) in original.iter().zip(restored.iter()) {
94 assert!((o - r).abs() < 0.05, "Expected {} ~= {}", o, r);
95 }
96
97 assert_eq!(quantizer.compression_ratio(), 4.0);
98 }
99
100 #[test]
101 fn test_scalar_quantizer_int8() {
102 let mut quantizer = ScalarQuantizer::int8(4);
103
104 let vectors = vec![vec![-1.0, 0.0, 1.0, -0.5], vec![1.0, -1.0, 0.5, 0.5]];
105
106 quantizer.train(&vectors).expect("train");
107
108 let original = vec![0.0, -0.5, 0.5, 0.25];
109 let quantized = quantizer.quantize(&original).expect("quantize");
110 let restored = quantizer.dequantize(&quantized).expect("dequantize");
111
112 for (o, r) in original.iter().zip(restored.iter()) {
113 assert!((o - r).abs() < 0.1, "Expected {} ~= {}", o, r);
114 }
115 }
116
117 #[test]
118 fn test_scalar_distance() {
119 let mut quantizer = ScalarQuantizer::uint8(4);
120
121 let vectors = vec![vec![0.0, 0.0, 0.0, 0.0], vec![1.0, 1.0, 1.0, 1.0]];
122
123 quantizer.train(&vectors).expect("train");
124
125 let a = quantizer.quantize(&[0.0, 0.0, 0.0, 0.0]).expect("qa");
126 let b = quantizer.quantize(&[1.0, 1.0, 1.0, 1.0]).expect("qb");
127
128 let dist = quantizer.distance_l2_quantized(&a, &b).expect("dist");
129 assert!(dist > 0.0, "Distance should be positive");
130 }
131
132 #[test]
133 fn test_product_quantizer() {
134 let mut pq = ProductQuantizer::new(8, 2, 4).expect("new pq"); let vectors: Vec<Vec<f32>> = (0..100)
138 .map(|i| (0..8).map(|j| i as f32 * 0.01 + j as f32 * 0.1).collect())
139 .collect();
140
141 pq.train(&vectors, 10).expect("train");
142 assert!(pq.is_trained());
143
144 let original = vec![0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8];
146 let code = pq.quantize(&original).expect("quantize");
147 let restored = pq.dequantize(&code).expect("dequantize");
148
149 assert_eq!(code.codes.len(), 2);
151 assert_eq!(restored.len(), 8);
152
153 assert_eq!(pq.compression_ratio(), 16.0);
155 }
156
157 #[test]
158 fn test_pq_distance_table() {
159 let mut pq = ProductQuantizer::new(8, 2, 4).expect("new pq");
160
161 let vectors: Vec<Vec<f32>> = (0..50)
162 .map(|i| (0..8).map(|j| i as f32 * 0.02 + j as f32 * 0.1).collect())
163 .collect();
164
165 pq.train(&vectors, 5).expect("train");
166
167 let query = vec![0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8];
168 let code = pq.quantize(&vectors[0]).expect("quantize");
169
170 let direct_dist = pq.asymmetric_distance(&query, &code).expect("dist");
172 let table = pq.compute_distance_table(&query).expect("table");
173 let table_dist = pq.distance_from_table(&table, &code);
174
175 assert!((direct_dist - table_dist).abs() < 1e-5);
177 }
178
179 #[test]
180 fn test_quantization_benchmarker() {
181 let dim = 8;
183 let n_vectors = 50;
184 let n_queries = 5;
185
186 let vectors: Vec<Vec<f32>> = (0..n_vectors)
187 .map(|i| (0..dim).map(|j| (i as f32 + j as f32) * 0.1).collect())
188 .collect();
189
190 let queries: Vec<Vec<f32>> = (0..n_queries)
191 .map(|i| {
192 (0..dim)
193 .map(|j| (i as f32 * 2.0 + j as f32) * 0.1)
194 .collect()
195 })
196 .collect();
197
198 let gt = QuantizationBenchmarker::compute_ground_truth(&vectors, &queries, 10);
200 assert_eq!(gt.len(), n_queries);
201 assert_eq!(gt[0].len(), 10);
202
203 let mut sq = ScalarQuantizer::uint8(dim);
205 sq.train(&vectors).expect("train");
206
207 let sq_benchmark =
208 QuantizationBenchmarker::benchmark_scalar(&sq, &vectors, &queries, >, &[1, 5, 10])
209 .expect("benchmark");
210
211 assert_eq!(sq_benchmark.recall_at_k.len(), 3);
212 assert!(sq_benchmark.compression_ratio > 1.0);
213 assert!(sq_benchmark.memory_savings > 0);
214 }
215
216 #[test]
217 fn test_quantization_comparison() {
218 let dim = 8;
219 let n_vectors = 100;
220 let n_queries = 10;
221
222 let vectors: Vec<Vec<f32>> = (0..n_vectors)
223 .map(|i| (0..dim).map(|j| (i as f32 + j as f32) * 0.05).collect())
224 .collect();
225
226 let queries: Vec<Vec<f32>> = (0..n_queries)
227 .map(|i| {
228 (0..dim)
229 .map(|j| (i as f32 * 3.0 + j as f32) * 0.05)
230 .collect()
231 })
232 .collect();
233
234 let comparison =
235 QuantizationBenchmarker::compare_methods(&vectors, &queries, &[1, 5, 10]).expect("cmp");
236
237 assert!(comparison.dataset_size == n_vectors);
238 assert!(comparison.dimension == dim);
239
240 let summary = comparison.summary();
241 assert!(!summary.is_empty());
242
243 let (method, _recall) = comparison.best_method_for_k(5);
244 assert!(!method.is_empty());
245 }
246
247 #[test]
248 fn test_opq_basic() {
249 let mut opq = OptimizedProductQuantizer::new(8, 2, 4).expect("new opq");
250
251 let vectors: Vec<Vec<f32>> = (0..100)
252 .map(|i| (0..8).map(|j| i as f32 * 0.01 + j as f32 * 0.1).collect())
253 .collect();
254
255 opq.train(&vectors, 10, 5).expect("train");
257 assert!(opq.is_trained());
258
259 let original = vec![0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8];
261 let code = opq.quantize(&original).expect("quantize");
262 let restored = opq.dequantize(&code).expect("dequantize");
263
264 assert_eq!(code.codes.len(), 2);
265 assert_eq!(restored.len(), 8);
266
267 assert_eq!(opq.compression_ratio(), 16.0);
269
270 let query = vec![0.15, 0.25, 0.35, 0.45, 0.55, 0.65, 0.75, 0.85];
272 let dist = opq.asymmetric_distance(&query, &code).expect("dist");
273 assert!(dist >= 0.0);
274 }
275
276 #[test]
277 fn test_opq_distance_table() {
278 let mut opq = OptimizedProductQuantizer::new(8, 2, 4).expect("new opq");
279
280 let vectors: Vec<Vec<f32>> = (0..50)
281 .map(|i| (0..8).map(|j| i as f32 * 0.02 + j as f32 * 0.1).collect())
282 .collect();
283
284 opq.train(&vectors, 5, 3).expect("train");
285
286 let query = vec![0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8];
287 let code = opq.quantize(&vectors[0]).expect("quantize");
288
289 let direct_dist = opq.asymmetric_distance(&query, &code).expect("dist");
291 let table = opq.compute_distance_table(&query).expect("table");
292 let table_dist = opq.distance_from_table(&table, &code);
293
294 assert!((direct_dist - table_dist).abs() < 1e-4);
296 }
297
298 #[test]
299 fn test_opq_serialization() {
300 let mut opq = OptimizedProductQuantizer::new(8, 2, 4).expect("new opq");
301
302 let vectors: Vec<Vec<f32>> = (0..50)
303 .map(|i| (0..8).map(|j| i as f32 * 0.02 + j as f32 * 0.1).collect())
304 .collect();
305
306 opq.train(&vectors, 5, 3).expect("train");
307
308 let serialized =
310 oxicode::serde::encode_to_vec(&opq, oxicode::config::standard()).expect("serialize");
311
312 let deserialized: OptimizedProductQuantizer =
314 oxicode::serde::decode_owned_from_slice(&serialized, oxicode::config::standard())
315 .map(|(v, _)| v)
316 .expect("deserialize");
317
318 assert!(deserialized.is_trained());
319 assert_eq!(deserialized.compression_ratio(), opq.compression_ratio());
320
321 let original = vec![0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8];
323 let code1 = opq.quantize(&original).expect("q1");
324 let code2 = deserialized.quantize(&original).expect("q2");
325
326 assert_eq!(code1.codes, code2.codes);
328 }
329
330 fn cosine_sim(a: &[f32], b: &[f32]) -> f32 {
336 let dot: f32 = a.iter().zip(b.iter()).map(|(x, y)| x * y).sum();
337 let na: f32 = a.iter().map(|x| x * x).sum::<f32>().sqrt();
338 let nb: f32 = b.iter().map(|x| x * x).sum::<f32>().sqrt();
339 if na < 1e-9 || nb < 1e-9 {
340 0.0
341 } else {
342 dot / (na * nb)
343 }
344 }
345
346 #[test]
347 fn test_quantize_dequantize_roundtrip() {
348 let v: Vec<f32> = (0..16).map(|i| i as f32 * 0.01).collect(); let (q, scale, zero_point) = quantize_f32_to_i8(&v);
351 let restored = dequantize_i8_to_f32(&q, scale, zero_point);
352
353 assert_eq!(restored.len(), v.len());
354 let max_err = v
355 .iter()
356 .zip(restored.iter())
357 .map(|(a, b)| (a - b).abs())
358 .fold(0.0_f32, f32::max);
359 assert!(max_err < 0.01, "max reconstruction error {max_err} >= 0.01");
360 }
361
362 #[test]
363 fn test_quantized_store_push_get() {
364 let dim = 32;
365 let mut store = QuantizedVectorStore::new(dim);
366
367 let originals: Vec<Vec<f32>> = (0..10usize)
369 .map(|i| {
370 let v: Vec<f32> = (0..dim)
371 .map(|d| ((i * dim + d) as f32) * 0.001 + 1.0)
372 .collect();
373 let norm: f32 = v.iter().map(|x| x * x).sum::<f32>().sqrt();
374 v.iter().map(|x| x / norm).collect()
375 })
376 .collect();
377 for (i, v_norm) in originals.iter().enumerate() {
378 let id = store.push(v_norm);
379 assert_eq!(id, i);
380 }
381
382 assert_eq!(store.len(), 10);
383 assert!(!store.is_empty());
384 assert_eq!(store.dim(), dim);
385
386 for (i, original) in originals.iter().enumerate() {
388 let recovered = store.get(i).expect("id must be valid");
389 let sim = cosine_sim(original, &recovered);
390 assert!(sim > 0.99, "cosine similarity {sim} <= 0.99 for vector {i}");
391 }
392
393 assert!(store.get(10).is_none());
395 }
396
397 #[test]
398 fn test_quantized_store_bytes_per_vector() {
399 let dim = 384;
400 let store = QuantizedVectorStore::new(dim);
401 assert_eq!(store.bytes_per_vector(), 384.0);
403 let f32_bytes = dim as f64 * 4.0;
405 assert!(store.bytes_per_vector() < f32_bytes / 3.0);
406 }
407
408 #[test]
409 fn test_binary_store_hamming() {
410 let dim = 128;
411 let mut store = BinaryVectorStore::new(dim);
412
413 let high: Vec<f32> = (0..dim)
416 .map(|i| if i % 2 == 0 { 2.0_f32 } else { -2.0_f32 })
417 .collect();
418 let id_high = store.push(&high);
419
420 let low: Vec<f32> = high.iter().map(|x| -x).collect();
423 let id_low = store.push(&low);
424
425 assert_eq!(store.hamming_distance(id_high, id_high), 0);
427 assert_eq!(store.hamming_distance(id_low, id_low), 0);
428
429 assert_eq!(
431 store.hamming_distance(id_high, id_low),
432 dim as u32,
433 "negated vectors should have max hamming distance"
434 );
435
436 assert_eq!(store.hamming_distance(0, 99), u32::MAX);
438 }
439
440 #[test]
441 fn test_binary_store_bytes_per_vector() {
442 let dim = 384;
443 let store = BinaryVectorStore::new(dim);
444 let expected_words = dim.div_ceil(64); let expected_bytes = (expected_words * 8) as f64; assert_eq!(store.bytes_per_vector(), expected_bytes);
448 assert_eq!(store.bytes_per_vector(), 48.0);
449 }
450}