1use std::collections::{HashMap, HashSet};
24use std::path::Path;
25
26use serde::{Deserialize, Serialize};
27
28use crate::errors::{Result, VecDbError};
29use crate::index::backend::IndexBackend;
30use crate::index::distance;
31use crate::types::{CollectionConfig, DistanceMetric, IndexType, Vector, VectorId};
32
33#[derive(Debug, Clone, Serialize, Deserialize, Default)]
39pub struct ScalarQuantizer {
40 pub dmin: Vec<f32>,
42 pub dscale: Vec<f32>,
44}
45
46impl ScalarQuantizer {
47 pub fn fit(dimension: usize, sample: &[Vector]) -> Self {
49 let mut dmin = vec![f32::INFINITY; dimension];
50 let mut dmax = vec![f32::NEG_INFINITY; dimension];
51 for v in sample {
52 for (i, &x) in v.iter().enumerate().take(dimension) {
53 if x < dmin[i] {
54 dmin[i] = x;
55 }
56 if x > dmax[i] {
57 dmax[i] = x;
58 }
59 }
60 }
61 let mut dscale = vec![1.0f32; dimension];
62 for i in 0..dimension {
63 if !dmin[i].is_finite() {
64 dmin[i] = 0.0;
65 }
66 let range = dmax[i] - dmin[i];
67 dscale[i] = if range > 0.0 { range / 255.0 } else { 1.0 };
68 }
69 Self { dmin, dscale }
70 }
71
72 #[inline]
73 pub fn dimension(&self) -> usize {
74 self.dmin.len()
75 }
76
77 pub fn quantize(&self, v: &[f32]) -> Vec<i8> {
79 let d = self.dimension();
80 let mut out = vec![0i8; d];
81 for i in 0..d {
82 let u = ((v[i] - self.dmin[i]) / self.dscale[i]).round();
83 let code = (u.clamp(0.0, 255.0) as i32) - 128;
84 out[i] = code as i8;
85 }
86 out
87 }
88
89 #[inline]
91 pub fn reconstruct_at(&self, code: i8, i: usize) -> f32 {
92 self.dmin[i] + ((code as i32 + 128) as f32) * self.dscale[i]
93 }
94
95 pub fn dequantize(&self, codes: &[i8]) -> Vec<f32> {
97 (0..self.dimension())
98 .map(|i| self.reconstruct_at(codes[i], i))
99 .collect()
100 }
101
102 fn offsets(&self) -> Vec<f32> {
105 (0..self.dimension())
106 .map(|i| self.dmin[i] + 128.0 * self.dscale[i])
107 .collect()
108 }
109}
110
111pub struct ScalarQuantizedIndex {
116 dimension: usize,
117 metric: DistanceMetric,
118 quantizer: ScalarQuantizer,
119 trained: bool,
120 codes: Vec<i8>,
122 dnorm: Vec<f32>,
124 id_to_pos: HashMap<VectorId, usize>,
125 pos_to_id: Vec<VectorId>,
126 deleted: HashSet<VectorId>,
127 pending: Vec<(VectorId, Vector)>,
130}
131
132const TRAIN_THRESHOLD: usize = 256;
135
136impl ScalarQuantizedIndex {
137 pub fn new(config: &CollectionConfig) -> Self {
138 Self {
139 dimension: config.dimension,
140 metric: config.metric.clone(),
141 quantizer: ScalarQuantizer::default(),
142 trained: false,
143 codes: Vec::new(),
144 dnorm: Vec::new(),
145 id_to_pos: HashMap::new(),
146 pos_to_id: Vec::new(),
147 deleted: HashSet::new(),
148 pending: Vec::new(),
149 }
150 }
151
152 pub fn code_bytes(&self) -> usize {
157 self.codes.len() * std::mem::size_of::<i8>()
158 + self.dnorm.len() * std::mem::size_of::<f32>()
159 + self.quantizer.dmin.len() * std::mem::size_of::<f32>() * 2
160 }
161
162 fn maybe_normalize(&self, v: Vec<f32>) -> Vec<f32> {
163 if matches!(self.metric, DistanceMetric::Cosine) {
164 distance::normalize(&v)
165 } else {
166 v
167 }
168 }
169
170 fn to_score(&self, dist: f32) -> f32 {
171 match self.metric {
172 DistanceMetric::Cosine => 1.0 - dist,
173 DistanceMetric::Euclidean => 1.0 / (1.0 + dist),
174 DistanceMetric::DotProduct => 1.0 - dist,
175 }
176 }
177
178 fn fit_and_encode(&mut self, vectors: Vec<(VectorId, Vector)>) -> Result<()> {
181 self.id_to_pos.clear();
182 self.pos_to_id.clear();
183 self.codes.clear();
184 self.dnorm.clear();
185 self.deleted.clear();
186 self.pending.clear();
187
188 if vectors.is_empty() {
189 self.trained = false;
190 return Ok(());
191 }
192
193 let normalized: Vec<(VectorId, Vector)> = vectors
194 .into_iter()
195 .map(|(id, v)| {
196 if v.len() != self.dimension {
197 return Err(VecDbError::DimensionMismatch {
198 expected: self.dimension,
199 got: v.len(),
200 });
201 }
202 Ok((id, self.maybe_normalize(v)))
203 })
204 .collect::<Result<_>>()?;
205
206 let sample: Vec<Vector> = normalized.iter().map(|(_, v)| v.clone()).collect();
207 self.quantizer = ScalarQuantizer::fit(self.dimension, &sample);
208
209 self.codes.reserve(normalized.len() * self.dimension);
210 self.dnorm.reserve(normalized.len());
211 for (id, v) in normalized {
212 let pos = self.pos_to_id.len();
213 let code = self.quantizer.quantize(&v);
214 self.push_code(pos, &code);
215 self.id_to_pos.insert(id.clone(), pos);
216 self.pos_to_id.push(id);
217 }
218 self.trained = true;
219 Ok(())
220 }
221
222 fn push_code(&mut self, _pos: usize, code: &[i8]) {
224 self.codes.extend_from_slice(code);
225 self.dnorm.push(self.code_norm_sq(code));
226 }
227
228 fn code_norm_sq(&self, code: &[i8]) -> f32 {
230 let mut s = 0.0f32;
231 for (i, &c) in code.iter().enumerate() {
232 let x = self.quantizer.reconstruct_at(c, i);
233 s += x * x;
234 }
235 s
236 }
237
238 fn ensure_trained(&mut self) -> Result<()> {
239 if !self.pending.is_empty() {
240 let batch = std::mem::take(&mut self.pending);
241 let mut all: Vec<(VectorId, Vector)> = self.live_vectors_reconstructed();
242 all.extend(batch);
243 self.fit_and_encode(all)?;
244 }
245 Ok(())
246 }
247
248 fn live_vectors_reconstructed(&self) -> Vec<(VectorId, Vector)> {
249 self.pos_to_id
250 .iter()
251 .enumerate()
252 .filter(|(_, id)| !self.deleted.contains(*id))
253 .map(|(pos, id)| {
254 let start = pos * self.dimension;
255 let codes = &self.codes[start..start + self.dimension];
256 (id.clone(), self.quantizer.dequantize(codes))
257 })
258 .collect()
259 }
260
261 fn scan(&self, query: &[f32], k: usize) -> Vec<(VectorId, f32)> {
265 let d = self.dimension;
266 let o = self.quantizer.offsets();
267 let s = &self.quantizer.dscale;
268
269 let qnorm: f32 = query.iter().map(|v| v * v).sum();
271 let qo: f32 = query.iter().zip(o.iter()).map(|(q, o)| q * o).sum();
272 let b: Vec<f32> = query.iter().zip(s.iter()).map(|(q, s)| q * s).collect();
273
274 let mut scored: Vec<(VectorId, f32)> = Vec::with_capacity(self.pos_to_id.len());
275 for (pos, id) in self.pos_to_id.iter().enumerate() {
276 if self.deleted.contains(id) {
277 continue;
278 }
279 let start = pos * d;
280 let codes = &self.codes[start..start + d];
281 let mut acc = 0.0f32;
283 for i in 0..d {
284 acc += b[i] * codes[i] as f32;
285 }
286 let dot_qx = qo + acc;
287 let xnorm = self.dnorm[pos];
288 let score = match self.metric {
289 DistanceMetric::Euclidean => {
290 let dist2 = (qnorm - 2.0 * dot_qx + xnorm).max(0.0);
291 self.to_score(dist2.sqrt())
292 }
293 DistanceMetric::Cosine => {
294 let denom = qnorm.sqrt() * xnorm.sqrt();
295 if denom == 0.0 {
296 0.0
297 } else {
298 dot_qx / denom
299 }
300 }
301 DistanceMetric::DotProduct => dot_qx,
302 };
303 scored.push((id.clone(), score));
304 }
305 scored.sort_unstable_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
306 scored.truncate(k);
307 scored
308 }
309}
310
311impl IndexBackend for ScalarQuantizedIndex {
312 fn build(&mut self, vectors: Vec<(VectorId, Vector)>) -> Result<()> {
313 self.fit_and_encode(vectors)
314 }
315
316 fn insert(&mut self, id: VectorId, vector: Vector) -> Result<()> {
317 if vector.len() != self.dimension {
318 return Err(VecDbError::DimensionMismatch {
319 expected: self.dimension,
320 got: vector.len(),
321 });
322 }
323
324 if self.trained {
325 let v = self.maybe_normalize(vector);
326 let code = self.quantizer.quantize(&v);
327 let norm = self.code_norm_sq(&code);
328 if let Some(&pos) = self.id_to_pos.get(&id) {
329 self.deleted.remove(&id);
330 let start = pos * self.dimension;
331 self.codes[start..start + self.dimension].copy_from_slice(&code);
332 self.dnorm[pos] = norm;
333 } else {
334 let pos = self.pos_to_id.len();
335 self.push_code(pos, &code);
336 self.id_to_pos.insert(id.clone(), pos);
337 self.pos_to_id.push(id);
338 }
339 Ok(())
340 } else {
341 self.pending.push((id, vector));
342 if self.pending.len() >= TRAIN_THRESHOLD {
343 self.ensure_trained()?;
344 }
345 Ok(())
346 }
347 }
348
349 fn search(&self, query: &Vector, k: usize) -> Result<Vec<(VectorId, f32)>> {
350 if query.len() != self.dimension {
351 return Err(VecDbError::DimensionMismatch {
352 expected: self.dimension,
353 got: query.len(),
354 });
355 }
356
357 if !self.trained {
360 let q = self.maybe_normalize(query.clone());
361 let mut scored: Vec<(VectorId, f32)> = self
362 .pending
363 .iter()
364 .filter(|(id, _)| !self.deleted.contains(id))
365 .map(|(id, v)| {
366 let vn = self.maybe_normalize(v.clone());
367 (
368 id.clone(),
369 self.to_score(distance::compute_distance(&q, &vn, &self.metric)),
370 )
371 })
372 .collect();
373 scored.sort_unstable_by(|a, b| {
374 b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal)
375 });
376 scored.truncate(k);
377 return Ok(scored);
378 }
379
380 let q = self.maybe_normalize(query.clone());
381 Ok(self.scan(&q, k))
382 }
383
384 fn delete(&mut self, id: &VectorId) -> Result<()> {
385 if self.id_to_pos.contains_key(id) || self.pending.iter().any(|(pid, _)| pid == id) {
386 self.deleted.insert(id.clone());
387 Ok(())
388 } else {
389 Err(VecDbError::NotFound { id: id.clone() })
390 }
391 }
392
393 fn save(&self, path: &Path) -> Result<()> {
394 let saved = SerializedScalarIndex {
395 dimension: self.dimension,
396 metric: self.metric.clone(),
397 quantizer: self.quantizer.clone(),
398 trained: self.trained,
399 codes: self.codes.clone(),
400 dnorm: self.dnorm.clone(),
401 pos_to_id: self.pos_to_id.clone(),
402 deleted: self.deleted.iter().cloned().collect(),
403 pending: self.pending.clone(),
404 version: 1,
405 };
406 let json = serde_json::to_string(&saved)
407 .map_err(|e| VecDbError::SerializationError(e.to_string()))?;
408 std::fs::write(path, json)
409 .map_err(|e| VecDbError::StorageError(format!("scalar index save failed: {e}")))?;
410 Ok(())
411 }
412
413 fn load_from(path: &Path, _config: &CollectionConfig) -> Result<Box<dyn IndexBackend>>
414 where
415 Self: Sized,
416 {
417 Ok(Box::new(Self::load_file(path)?))
418 }
419
420 fn len(&self) -> usize {
421 let total = self.pos_to_id.len() + self.pending.len();
422 total.saturating_sub(self.deleted.len())
423 }
424
425 fn index_type(&self) -> IndexType {
426 IndexType::HNSW
429 }
430}
431
432impl ScalarQuantizedIndex {
433 pub fn load_file(path: &Path) -> Result<Self> {
434 let json = std::fs::read_to_string(path)
435 .map_err(|e| VecDbError::StorageError(format!("scalar index load failed: {e}")))?;
436 let saved: SerializedScalarIndex = serde_json::from_str(&json)
437 .map_err(|e| VecDbError::SerializationError(e.to_string()))?;
438 let mut id_to_pos = HashMap::with_capacity(saved.pos_to_id.len());
439 for (pos, id) in saved.pos_to_id.iter().enumerate() {
440 id_to_pos.insert(id.clone(), pos);
441 }
442 Ok(Self {
443 dimension: saved.dimension,
444 metric: saved.metric,
445 quantizer: saved.quantizer,
446 trained: saved.trained,
447 codes: saved.codes,
448 dnorm: saved.dnorm,
449 id_to_pos,
450 pos_to_id: saved.pos_to_id,
451 deleted: saved.deleted.into_iter().collect(),
452 pending: saved.pending,
453 })
454 }
455}
456
457#[derive(Serialize, Deserialize)]
458struct SerializedScalarIndex {
459 dimension: usize,
460 metric: DistanceMetric,
461 quantizer: ScalarQuantizer,
462 trained: bool,
463 codes: Vec<i8>,
464 dnorm: Vec<f32>,
465 pos_to_id: Vec<VectorId>,
466 deleted: Vec<VectorId>,
467 pending: Vec<(VectorId, Vector)>,
468 version: u32,
469}
470
471#[cfg(test)]
472mod tests {
473 use super::*;
474
475 fn cfg(dim: usize, metric: DistanceMetric) -> CollectionConfig {
476 let mut c = CollectionConfig::new("q", dim);
477 c.metric = metric;
478 c
479 }
480
481 #[test]
482 fn quantizer_roundtrip_is_close() {
483 let sample = vec![
484 vec![0.0, 10.0, -5.0],
485 vec![1.0, 8.0, -1.0],
486 vec![0.5, 9.0, -3.0],
487 ];
488 let q = ScalarQuantizer::fit(3, &sample);
489 for v in &sample {
490 let deq = q.dequantize(&q.quantize(v));
491 for (a, b) in v.iter().zip(deq.iter()) {
492 assert!((a - b).abs() < 0.1, "roundtrip {a} vs {b}");
493 }
494 }
495 }
496
497 #[test]
498 fn bulk_build_search_euclidean() {
499 let mut idx = ScalarQuantizedIndex::new(&cfg(4, DistanceMetric::Euclidean));
500 let mut data = Vec::new();
501 for i in 0..500u32 {
502 let f = i as f32;
503 data.push((format!("v{i}"), vec![f, f + 1.0, f + 2.0, f + 3.0]));
504 }
505 idx.build(data).unwrap();
506 assert_eq!(idx.len(), 500);
507 let hits = idx.search(&vec![100.0, 101.0, 102.0, 103.0], 1).unwrap();
511 let top: u32 = hits[0].0.trim_start_matches('v').parse().unwrap();
512 assert!((99..=101).contains(&top), "top hit v{top} not near v100");
513 assert!(idx.code_bytes() < 500 * 4 * 4, "codes must be << f32 size");
514 }
515
516 #[test]
517 fn kernel_matches_reconstruction() {
518 let mut idx = ScalarQuantizedIndex::new(&cfg(8, DistanceMetric::Euclidean));
520 let data: Vec<_> = (0..300u32)
521 .map(|i| {
522 let f = i as f32;
523 (
524 format!("v{i}"),
525 (0..8).map(|j| f + j as f32 * 0.3).collect(),
526 )
527 })
528 .collect();
529 idx.build(data).unwrap();
530 let q: Vec<f32> = (0..8).map(|j| 50.0 + j as f32).collect();
531 let hits = idx.search(&q, 5).unwrap();
532 let (id, score) = &hits[0];
534 let pos = idx.id_to_pos[id];
535 let codes = &idx.codes[pos * 8..pos * 8 + 8];
536 let x = idx.quantizer.dequantize(codes);
537 let dist = distance::compute_distance(&q, &x, &DistanceMetric::Euclidean);
538 let expect = idx.to_score(dist);
539 assert!((score - expect).abs() < 1e-3, "{score} vs {expect}");
540 }
541
542 #[test]
543 fn streaming_insert_trains_then_queries() {
544 let mut idx = ScalarQuantizedIndex::new(&cfg(3, DistanceMetric::Euclidean));
545 idx.insert("a".into(), vec![1.0, 0.0, 0.0]).unwrap();
546 idx.insert("b".into(), vec![0.0, 1.0, 0.0]).unwrap();
547 assert_eq!(idx.search(&vec![1.0, 0.0, 0.0], 1).unwrap()[0].0, "a");
548 for i in 0..TRAIN_THRESHOLD {
549 idx.insert(format!("x{i}"), vec![i as f32, i as f32, i as f32])
550 .unwrap();
551 }
552 assert!(idx.trained);
553 assert_eq!(idx.search(&vec![1.0, 0.0, 0.0], 1).unwrap()[0].0, "a");
554 }
555
556 #[test]
557 fn delete_excludes_from_results() {
558 let mut idx = ScalarQuantizedIndex::new(&cfg(2, DistanceMetric::Euclidean));
559 let data: Vec<_> = (0..300u32)
560 .map(|i| (format!("v{i}"), vec![i as f32, 0.0]))
561 .collect();
562 idx.build(data).unwrap();
563 idx.delete(&"v5".to_string()).unwrap();
564 let hits = idx.search(&vec![5.0, 0.0], 5).unwrap();
565 assert!(hits.iter().all(|(id, _)| id != "v5"));
566 }
567
568 #[test]
569 fn save_load_roundtrip() {
570 let dir = tempfile::tempdir().unwrap();
571 let path = dir.path().join("q.sq.json");
572 let mut idx = ScalarQuantizedIndex::new(&cfg(4, DistanceMetric::Euclidean));
573 let data: Vec<_> = (0..300u32)
574 .map(|i| (format!("v{i}"), vec![i as f32, 1.0, 2.0, 3.0]))
575 .collect();
576 idx.build(data).unwrap();
577 let before = idx.search(&vec![50.0, 1.0, 2.0, 3.0], 3).unwrap();
578 idx.save(&path).unwrap();
579 let loaded = ScalarQuantizedIndex::load_file(&path).unwrap();
580 let after = loaded.search(&vec![50.0, 1.0, 2.0, 3.0], 3).unwrap();
581 assert_eq!(before[0].0, after[0].0);
582 assert_eq!(before.len(), after.len());
583 }
584}