1use std::collections::HashMap;
24
25use rand::rngs::StdRng;
26use rand::seq::SliceRandom;
27use rand::{Rng, SeedableRng};
28use serde::{Deserialize, Serialize};
29
30use crate::error::{MnemoError, Result};
31
32const DEFAULT_SEED: u64 = 0x4d_4e_45_4d_4f_49_44_58; const TRAIN_CAP: usize = 50_000;
36const PQ_CODEWORDS: usize = 256;
38
39#[derive(Clone, Copy, Debug)]
41pub struct IndexConfig {
42 pub n_partitions: usize,
44 pub pq_subspaces: usize,
46 pub n_probe: usize,
48 pub n_rerank: usize,
50 pub kmeans_iters: usize,
52 pub seed: u64,
54}
55
56impl Default for IndexConfig {
57 fn default() -> Self {
58 Self {
59 n_partitions: 0,
60 pq_subspaces: 0,
61 n_probe: 8,
62 n_rerank: 64,
63 kmeans_iters: 25,
64 seed: DEFAULT_SEED,
65 }
66 }
67}
68
69#[derive(Clone, Copy, Debug)]
71pub struct IndexInfo {
72 pub vectors: usize,
74 pub partitions: usize,
76 pub subspaces: usize,
78 pub n_probe: usize,
80 pub n_rerank: usize,
82}
83
84fn squared_l2(a: &[f32], b: &[f32]) -> f32 {
86 let mut s = 0.0f32;
87 for i in 0..a.len() {
88 let d = a[i] - b[i];
89 s += d * d;
90 }
91 s
92}
93
94fn nearest(centroids: &[f32], stride: usize, v: &[f32]) -> usize {
96 let mut best = 0usize;
97 let mut best_d = f32::INFINITY;
98 let count = centroids.len() / stride;
99 for i in 0..count {
100 let d = squared_l2(¢roids[i * stride..(i + 1) * stride], v);
101 if d < best_d {
102 best_d = d;
103 best = i;
104 }
105 }
106 best
107}
108
109fn kmeans(points: &[&[f32]], k: usize, iters: usize, rng: &mut StdRng) -> Vec<f32> {
112 let n = points.len();
113 let dim = points[0].len();
114 let k = k.clamp(1, n);
115
116 let mut centroids: Vec<f32> = Vec::with_capacity(k * dim);
118 let first = rng.gen_range(0..n);
119 centroids.extend_from_slice(points[first]);
120 let mut d2: Vec<f32> = points
121 .iter()
122 .map(|p| squared_l2(p, ¢roids[0..dim]))
123 .collect();
124
125 while centroids.len() / dim < k {
126 let sum: f32 = d2.iter().sum();
127 let pick = if sum <= 0.0 {
128 rng.gen_range(0..n)
129 } else {
130 let mut t = rng.gen::<f32>() * sum;
131 let mut chosen = n - 1;
132 for (i, &w) in d2.iter().enumerate() {
133 t -= w;
134 if t <= 0.0 {
135 chosen = i;
136 break;
137 }
138 }
139 chosen
140 };
141 let base = centroids.len();
142 centroids.extend_from_slice(points[pick]);
143 let new = ¢roids[base..base + dim];
144 for (i, p) in points.iter().enumerate() {
145 let nd = squared_l2(p, new);
146 if nd < d2[i] {
147 d2[i] = nd;
148 }
149 }
150 }
151
152 let kk = centroids.len() / dim;
154 for _ in 0..iters {
155 let mut sums = vec![0.0f32; kk * dim];
156 let mut counts = vec![0usize; kk];
157 for p in points {
158 let a = nearest(¢roids, dim, p);
159 counts[a] += 1;
160 for d in 0..dim {
161 sums[a * dim + d] += p[d];
162 }
163 }
164 for c in 0..kk {
165 if counts[c] == 0 {
166 let r = rng.gen_range(0..n);
168 centroids[c * dim..(c + 1) * dim].copy_from_slice(points[r]);
169 } else {
170 let inv = 1.0 / counts[c] as f32;
171 for d in 0..dim {
172 centroids[c * dim + d] = sums[c * dim + d] * inv;
173 }
174 }
175 }
176 }
177 centroids
178}
179
180#[derive(Serialize, Deserialize, Clone, Debug)]
182struct PqCodebook {
183 sub_offsets: Vec<usize>,
185 centroids: Vec<Vec<f32>>,
187 ks: Vec<usize>,
189}
190
191impl PqCodebook {
192 fn m(&self) -> usize {
193 self.sub_offsets.len() - 1
194 }
195
196 fn train(sub_offsets: &[usize], train: &[&[f32]], iters: usize, rng: &mut StdRng) -> PqCodebook {
198 let m = sub_offsets.len() - 1;
199 let mut centroids = Vec::with_capacity(m);
200 let mut ks = Vec::with_capacity(m);
201 for s in 0..m {
202 let (lo, hi) = (sub_offsets[s], sub_offsets[s + 1]);
203 let subs: Vec<Vec<f32>> = train.iter().map(|v| v[lo..hi].to_vec()).collect();
204 let refs: Vec<&[f32]> = subs.iter().map(|x| x.as_slice()).collect();
205 let k = PQ_CODEWORDS.min(refs.len()).max(1);
206 let cs = kmeans(&refs, k, iters, rng);
207 ks.push(cs.len() / (hi - lo));
208 centroids.push(cs);
209 }
210 PqCodebook { sub_offsets: sub_offsets.to_vec(), centroids, ks }
211 }
212
213 fn encode(&self, v: &[f32]) -> Vec<u8> {
215 let m = self.m();
216 let mut code = vec![0u8; m];
217 for (s, slot) in code.iter_mut().enumerate().take(m) {
218 let (lo, hi) = (self.sub_offsets[s], self.sub_offsets[s + 1]);
219 *slot = nearest(&self.centroids[s], hi - lo, &v[lo..hi]) as u8;
220 }
221 code
222 }
223
224 fn distance_table(&self, q: &[f32]) -> Vec<Vec<f32>> {
227 let m = self.m();
228 let mut table = Vec::with_capacity(m);
229 for s in 0..m {
230 let (lo, hi) = (self.sub_offsets[s], self.sub_offsets[s + 1]);
231 let sd = hi - lo;
232 let qs = &q[lo..hi];
233 let cs = &self.centroids[s];
234 let mut row = vec![0.0f32; self.ks[s]];
235 for (c, slot) in row.iter_mut().enumerate() {
236 *slot = squared_l2(qs, &cs[c * sd..(c + 1) * sd]);
237 }
238 table.push(row);
239 }
240 table
241 }
242}
243
244#[derive(Serialize, Deserialize, Clone, Debug, Default)]
246struct Posting {
247 ids: Vec<u128>,
248 codes: Vec<u8>,
250}
251
252#[derive(Serialize, Deserialize, Clone, Debug)]
254pub struct IvfPqIndex {
255 dims: usize,
256 n_partitions: usize,
257 n_probe: usize,
258 n_rerank: usize,
259 centroids: Vec<f32>,
261 pq: PqCodebook,
262 partitions: Vec<Posting>,
263 #[serde(skip)]
266 assignment: HashMap<u128, usize>,
267}
268
269impl IvfPqIndex {
270 pub fn build(dims: usize, items: &[(u128, &[f32])], cfg: IndexConfig) -> Result<IvfPqIndex> {
272 if items.is_empty() {
273 return Err(MnemoError::Invalid(
274 "cannot build an index over an empty database".into(),
275 ));
276 }
277 let n = items.len();
278 let mut rng = StdRng::seed_from_u64(cfg.seed);
279
280 let mut order: Vec<usize> = (0..n).collect();
282 let train_n = TRAIN_CAP.min(n);
283 order.partial_shuffle(&mut rng, train_n);
284 let train: Vec<&[f32]> = order[..train_n].iter().map(|&i| items[i].1).collect();
285
286 let n_partitions = if cfg.n_partitions > 0 {
288 cfg.n_partitions
289 } else {
290 (n as f64).sqrt().ceil() as usize
291 }
292 .clamp(1, n);
293 let centroids = kmeans(&train, n_partitions, cfg.kmeans_iters, &mut rng);
294 let n_partitions = centroids.len() / dims;
295
296 let m = if cfg.pq_subspaces > 0 {
298 cfg.pq_subspaces
299 } else {
300 (dims / 8).max(1)
301 }
302 .clamp(1, dims);
303 let mut sub_offsets = Vec::with_capacity(m + 1);
304 for s in 0..=m {
305 sub_offsets.push(s * dims / m);
306 }
307 let pq = PqCodebook::train(&sub_offsets, &train, cfg.kmeans_iters, &mut rng);
308
309 let mut index = IvfPqIndex {
310 dims,
311 n_partitions,
312 n_probe: cfg.n_probe.max(1),
313 n_rerank: cfg.n_rerank.max(1),
314 centroids,
315 pq,
316 partitions: vec![Posting::default(); n_partitions],
317 assignment: HashMap::with_capacity(n),
318 };
319 for &(id, v) in items {
320 index.add(id, v);
321 }
322 Ok(index)
323 }
324
325 pub fn dims(&self) -> usize {
327 self.dims
328 }
329
330 pub fn len(&self) -> usize {
332 self.partitions.iter().map(|p| p.ids.len()).sum()
333 }
334
335 pub fn n_probe(&self) -> usize {
337 self.n_probe
338 }
339
340 pub fn n_rerank(&self) -> usize {
342 self.n_rerank
343 }
344
345 pub fn info(&self) -> IndexInfo {
347 IndexInfo {
348 vectors: self.len(),
349 partitions: self.n_partitions,
350 subspaces: self.pq.m(),
351 n_probe: self.n_probe,
352 n_rerank: self.n_rerank,
353 }
354 }
355
356 pub fn rebuild_assignment(&mut self) {
359 self.assignment.clear();
360 for (pi, p) in self.partitions.iter().enumerate() {
361 for &id in &p.ids {
362 self.assignment.insert(id, pi);
363 }
364 }
365 }
366
367 fn nearest_centroid(&self, v: &[f32]) -> usize {
368 nearest(&self.centroids, self.dims, v)
369 }
370
371 pub fn add(&mut self, id: u128, vector: &[f32]) {
376 self.remove(id);
377 let part = self.nearest_centroid(vector);
378 let code = self.pq.encode(vector);
379 let p = &mut self.partitions[part];
380 p.ids.push(id);
381 p.codes.extend_from_slice(&code);
382 self.assignment.insert(id, part);
383 }
384
385 pub fn remove(&mut self, id: u128) {
387 if let Some(part) = self.assignment.remove(&id) {
388 let m = self.pq.m();
389 let p = &mut self.partitions[part];
390 if let Some(pos) = p.ids.iter().position(|&x| x == id) {
391 p.ids.remove(pos);
392 p.codes.drain(pos * m..(pos + 1) * m);
393 }
394 }
395 }
396
397 pub fn query(
401 &self,
402 q: &[f32],
403 n_probe: Option<usize>,
404 n_rerank: Option<usize>,
405 ) -> Vec<u128> {
406 let n_probe = n_probe.unwrap_or(self.n_probe).clamp(1, self.n_partitions);
407 let n_rerank = n_rerank.unwrap_or(self.n_rerank).max(1);
408
409 let mut parts: Vec<(usize, f32)> = (0..self.n_partitions)
411 .map(|i| {
412 let c = &self.centroids[i * self.dims..(i + 1) * self.dims];
413 (i, squared_l2(q, c))
414 })
415 .collect();
416 parts.sort_by(|a, b| a.1.total_cmp(&b.1));
417 parts.truncate(n_probe);
418
419 let table = self.pq.distance_table(q);
421 let m = self.pq.m();
422 let mut cands: Vec<(u128, f32)> = Vec::new();
423 for (pi, _) in parts {
424 let p = &self.partitions[pi];
425 for (j, &id) in p.ids.iter().enumerate() {
426 let code = &p.codes[j * m..(j + 1) * m];
427 let mut d = 0.0f32;
428 for (s, row) in table.iter().enumerate() {
429 d += row[code[s] as usize];
430 }
431 cands.push((id, d));
432 }
433 }
434
435 cands.sort_by(|a, b| a.1.total_cmp(&b.1));
437 cands.truncate(n_rerank);
438 cands.into_iter().map(|(id, _)| id).collect()
439 }
440}