1use std::collections::{HashMap, HashSet};
15use std::path::Path;
16
17use serde::{Deserialize, Serialize};
18
19use crate::errors::{Result, VecDbError};
20use crate::index::backend::IndexBackend;
21use crate::index::distance;
22use crate::types::{CollectionConfig, DistanceMetric, IndexType, Vector, VectorId};
23
24#[derive(Debug, Clone, Serialize, Deserialize, Default)]
26pub struct BinaryQuantizer {
27 pub thresholds: Vec<f32>,
29}
30
31impl BinaryQuantizer {
32 pub fn fit(dimension: usize, sample: &[Vector]) -> Self {
33 let mut sums = vec![0.0f64; dimension];
34 for v in sample {
35 for (i, &x) in v.iter().enumerate().take(dimension) {
36 sums[i] += x as f64;
37 }
38 }
39 let n = sample.len().max(1) as f64;
40 let thresholds = sums.iter().map(|s| (s / n) as f32).collect();
41 Self { thresholds }
42 }
43
44 #[inline]
45 pub fn dimension(&self) -> usize {
46 self.thresholds.len()
47 }
48
49 #[inline]
51 pub fn words(&self) -> usize {
52 self.dimension().div_ceil(64)
53 }
54
55 pub fn quantize(&self, v: &[f32]) -> Vec<u64> {
57 let mut out = vec![0u64; self.words()];
58 for i in 0..self.dimension() {
59 if v[i] > self.thresholds[i] {
60 out[i / 64] |= 1u64 << (i % 64);
61 }
62 }
63 out
64 }
65}
66
67pub struct BinaryQuantizedIndex {
68 dimension: usize,
69 metric: DistanceMetric,
70 quantizer: BinaryQuantizer,
71 trained: bool,
72 words: usize,
73 codes: Vec<u64>,
75 id_to_pos: HashMap<VectorId, usize>,
76 pos_to_id: Vec<VectorId>,
77 deleted: HashSet<VectorId>,
78 pending: Vec<(VectorId, Vector)>,
79}
80
81const TRAIN_THRESHOLD: usize = 256;
82
83impl BinaryQuantizedIndex {
84 pub fn new(config: &CollectionConfig) -> Self {
85 Self {
86 dimension: config.dimension,
87 metric: config.metric.clone(),
88 quantizer: BinaryQuantizer::default(),
89 trained: false,
90 words: config.dimension.div_ceil(64),
91 codes: Vec::new(),
92 id_to_pos: HashMap::new(),
93 pos_to_id: Vec::new(),
94 deleted: HashSet::new(),
95 pending: Vec::new(),
96 }
97 }
98
99 pub fn code_bytes(&self) -> usize {
102 self.codes.len() * std::mem::size_of::<u64>()
103 + self.quantizer.thresholds.len() * std::mem::size_of::<f32>()
104 }
105
106 fn fit_and_encode(&mut self, vectors: Vec<(VectorId, Vector)>) -> Result<()> {
107 self.id_to_pos.clear();
108 self.pos_to_id.clear();
109 self.codes.clear();
110 self.deleted.clear();
111 self.pending.clear();
112
113 if vectors.is_empty() {
114 self.trained = false;
115 return Ok(());
116 }
117 for (_, v) in &vectors {
118 if v.len() != self.dimension {
119 return Err(VecDbError::DimensionMismatch {
120 expected: self.dimension,
121 got: v.len(),
122 });
123 }
124 }
125
126 let sample: Vec<Vector> = vectors.iter().map(|(_, v)| v.clone()).collect();
127 self.quantizer = BinaryQuantizer::fit(self.dimension, &sample);
128 self.words = self.quantizer.words();
129
130 self.codes.reserve(vectors.len() * self.words);
131 for (id, v) in vectors {
132 let pos = self.pos_to_id.len();
133 self.codes.extend_from_slice(&self.quantizer.quantize(&v));
134 self.id_to_pos.insert(id.clone(), pos);
135 self.pos_to_id.push(id);
136 }
137 self.trained = true;
138 Ok(())
139 }
140
141 fn ensure_trained(&mut self) -> Result<()> {
142 if !self.pending.is_empty() {
143 let batch = std::mem::take(&mut self.pending);
144 self.fit_and_encode(batch)?;
145 }
146 Ok(())
147 }
148
149 #[inline]
152 fn similarity(&self, qcode: &[u64], pos: usize) -> f32 {
153 let start = pos * self.words;
154 let row = &self.codes[start..start + self.words];
155 let mut ham = 0u32;
156 for w in 0..self.words {
157 ham += (qcode[w] ^ row[w]).count_ones();
158 }
159 1.0 - (ham as f32 / self.dimension as f32)
160 }
161}
162
163impl IndexBackend for BinaryQuantizedIndex {
164 fn build(&mut self, vectors: Vec<(VectorId, Vector)>) -> Result<()> {
165 self.fit_and_encode(vectors)
166 }
167
168 fn insert(&mut self, id: VectorId, vector: Vector) -> Result<()> {
169 if vector.len() != self.dimension {
170 return Err(VecDbError::DimensionMismatch {
171 expected: self.dimension,
172 got: vector.len(),
173 });
174 }
175 if self.trained {
176 let code = self.quantizer.quantize(&vector);
177 if let Some(&pos) = self.id_to_pos.get(&id) {
178 self.deleted.remove(&id);
179 let start = pos * self.words;
180 self.codes[start..start + self.words].copy_from_slice(&code);
181 } else {
182 let pos = self.pos_to_id.len();
183 self.codes.extend_from_slice(&code);
184 self.id_to_pos.insert(id.clone(), pos);
185 self.pos_to_id.push(id);
186 }
187 Ok(())
188 } else {
189 self.pending.push((id, vector));
190 if self.pending.len() >= TRAIN_THRESHOLD {
191 self.ensure_trained()?;
192 }
193 Ok(())
194 }
195 }
196
197 fn search(&self, query: &Vector, k: usize) -> Result<Vec<(VectorId, f32)>> {
198 if query.len() != self.dimension {
199 return Err(VecDbError::DimensionMismatch {
200 expected: self.dimension,
201 got: query.len(),
202 });
203 }
204
205 if !self.trained {
206 let mut scored: Vec<(VectorId, f32)> = self
207 .pending
208 .iter()
209 .filter(|(id, _)| !self.deleted.contains(id))
210 .map(|(id, v)| {
211 let dist = distance::compute_distance(query, v, &self.metric);
212 let score = match self.metric {
213 DistanceMetric::Euclidean => 1.0 / (1.0 + dist),
214 _ => 1.0 - dist,
215 };
216 (id.clone(), score)
217 })
218 .collect();
219 scored.sort_unstable_by(|a, b| {
220 b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal)
221 });
222 scored.truncate(k);
223 return Ok(scored);
224 }
225
226 let qcode = self.quantizer.quantize(query);
227 let mut scored: Vec<(VectorId, f32)> = self
228 .pos_to_id
229 .iter()
230 .enumerate()
231 .filter(|(_, id)| !self.deleted.contains(*id))
232 .map(|(pos, id)| (id.clone(), self.similarity(&qcode, pos)))
233 .collect();
234 scored.sort_unstable_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
235 scored.truncate(k);
236 Ok(scored)
237 }
238
239 fn delete(&mut self, id: &VectorId) -> Result<()> {
240 if self.id_to_pos.contains_key(id) || self.pending.iter().any(|(pid, _)| pid == id) {
241 self.deleted.insert(id.clone());
242 Ok(())
243 } else {
244 Err(VecDbError::NotFound { id: id.clone() })
245 }
246 }
247
248 fn save(&self, path: &Path) -> Result<()> {
249 let saved = SerializedBinaryIndex {
250 dimension: self.dimension,
251 metric: self.metric.clone(),
252 quantizer: self.quantizer.clone(),
253 trained: self.trained,
254 words: self.words,
255 codes: self.codes.clone(),
256 pos_to_id: self.pos_to_id.clone(),
257 deleted: self.deleted.iter().cloned().collect(),
258 pending: self.pending.clone(),
259 version: 1,
260 };
261 let json = serde_json::to_string(&saved)
262 .map_err(|e| VecDbError::SerializationError(e.to_string()))?;
263 std::fs::write(path, json)
264 .map_err(|e| VecDbError::StorageError(format!("binary index save failed: {e}")))?;
265 Ok(())
266 }
267
268 fn load_from(path: &Path, _config: &CollectionConfig) -> Result<Box<dyn IndexBackend>>
269 where
270 Self: Sized,
271 {
272 Ok(Box::new(Self::load_file(path)?))
273 }
274
275 fn len(&self) -> usize {
276 let total = self.pos_to_id.len() + self.pending.len();
277 total.saturating_sub(self.deleted.len())
278 }
279
280 fn index_type(&self) -> IndexType {
281 IndexType::HNSW
282 }
283}
284
285impl BinaryQuantizedIndex {
286 pub fn load_file(path: &Path) -> Result<Self> {
287 let json = std::fs::read_to_string(path)
288 .map_err(|e| VecDbError::StorageError(format!("binary index load failed: {e}")))?;
289 let saved: SerializedBinaryIndex = serde_json::from_str(&json)
290 .map_err(|e| VecDbError::SerializationError(e.to_string()))?;
291 let mut id_to_pos = HashMap::with_capacity(saved.pos_to_id.len());
292 for (pos, id) in saved.pos_to_id.iter().enumerate() {
293 id_to_pos.insert(id.clone(), pos);
294 }
295 Ok(Self {
296 dimension: saved.dimension,
297 metric: saved.metric,
298 quantizer: saved.quantizer,
299 trained: saved.trained,
300 words: saved.words,
301 codes: saved.codes,
302 id_to_pos,
303 pos_to_id: saved.pos_to_id,
304 deleted: saved.deleted.into_iter().collect(),
305 pending: saved.pending,
306 })
307 }
308}
309
310#[derive(Serialize, Deserialize)]
311struct SerializedBinaryIndex {
312 dimension: usize,
313 metric: DistanceMetric,
314 quantizer: BinaryQuantizer,
315 trained: bool,
316 words: usize,
317 codes: Vec<u64>,
318 pos_to_id: Vec<VectorId>,
319 deleted: Vec<VectorId>,
320 pending: Vec<(VectorId, Vector)>,
321 version: u32,
322}
323
324#[cfg(test)]
325mod tests {
326 use super::*;
327
328 fn cfg(dim: usize) -> CollectionConfig {
329 let mut c = CollectionConfig::new("b", dim);
330 c.metric = DistanceMetric::Euclidean;
331 c
332 }
333
334 #[test]
335 fn packs_and_matches_identical() {
336 let q = BinaryQuantizer::fit(4, &[vec![0.0, 10.0, 0.0, 10.0]]);
337 let a = q.quantize(&[1.0, 20.0, -1.0, 20.0]);
340 let b = q.quantize(&[1.0, 20.0, -1.0, 20.0]);
341 assert_eq!(a, b);
342 }
343
344 #[test]
345 fn separates_two_clusters() {
346 let mut idx = BinaryQuantizedIndex::new(&cfg(8));
349 let mut data = Vec::new();
350 for i in 0..150u32 {
351 data.push((
352 format!("a{i}"),
353 vec![10.0, 10.0, 10.0, 10.0, 0.0, 0.0, 0.0, 0.0],
354 ));
355 }
356 for i in 0..150u32 {
357 data.push((
358 format!("b{i}"),
359 vec![0.0, 0.0, 0.0, 0.0, 10.0, 10.0, 10.0, 10.0],
360 ));
361 }
362 idx.build(data).unwrap();
363 let hits = idx
364 .search(&vec![9.0, 9.0, 9.0, 9.0, 1.0, 1.0, 1.0, 1.0], 10)
365 .unwrap();
366 assert!(
367 hits.iter().all(|(id, _)| id.starts_with('a')),
368 "top hits must be cluster A"
369 );
370 assert!(idx.code_bytes() <= 300 * 8 + 64);
372 }
373
374 #[test]
375 fn save_load_roundtrip() {
376 let dir = tempfile::tempdir().unwrap();
377 let path = dir.path().join("b.bq.json");
378 let mut idx = BinaryQuantizedIndex::new(&cfg(16));
379 let data: Vec<_> = (0..300u32)
380 .map(|i| {
381 let f = (i % 7) as f32;
382 (format!("v{i}"), (0..16).map(|j| f + j as f32).collect())
383 })
384 .collect();
385 idx.build(data).unwrap();
386 let q: Vec<f32> = (0..16).map(|j| 3.0 + j as f32).collect();
387 let before = idx.search(&q, 5).unwrap();
388 idx.save(&path).unwrap();
389 let loaded = BinaryQuantizedIndex::load_file(&path).unwrap();
390 let after = loaded.search(&q, 5).unwrap();
391 assert_eq!(before[0].0, after[0].0);
392 }
393}