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, Serialize, Deserialize)]
76pub struct IndexInfo {
77 pub vectors: usize,
79 pub partitions: usize,
81 pub subspaces: usize,
83 pub n_probe: usize,
85 pub n_rerank: usize,
87}
88
89fn squared_l2(a: &[f32], b: &[f32]) -> f32 {
91 let mut s = 0.0f32;
92 for i in 0..a.len() {
93 let d = a[i] - b[i];
94 s += d * d;
95 }
96 s
97}
98
99fn nearest(centroids: &[f32], stride: usize, v: &[f32]) -> usize {
101 let mut best = 0usize;
102 let mut best_d = f32::INFINITY;
103 let count = centroids.len() / stride;
104 for i in 0..count {
105 let d = squared_l2(¢roids[i * stride..(i + 1) * stride], v);
106 if d < best_d {
107 best_d = d;
108 best = i;
109 }
110 }
111 best
112}
113
114fn kmeans(points: &[&[f32]], k: usize, iters: usize, rng: &mut StdRng) -> Vec<f32> {
117 let n = points.len();
118 let dim = points[0].len();
119 let k = k.clamp(1, n);
120
121 let mut centroids: Vec<f32> = Vec::with_capacity(k * dim);
123 let first = rng.gen_range(0..n);
124 centroids.extend_from_slice(points[first]);
125 let mut d2: Vec<f32> = points
126 .iter()
127 .map(|p| squared_l2(p, ¢roids[0..dim]))
128 .collect();
129
130 while centroids.len() / dim < k {
131 let sum: f32 = d2.iter().sum();
132 let pick = if sum <= 0.0 {
133 rng.gen_range(0..n)
134 } else {
135 let mut t = rng.gen::<f32>() * sum;
136 let mut chosen = n - 1;
137 for (i, &w) in d2.iter().enumerate() {
138 t -= w;
139 if t <= 0.0 {
140 chosen = i;
141 break;
142 }
143 }
144 chosen
145 };
146 let base = centroids.len();
147 centroids.extend_from_slice(points[pick]);
148 let new = ¢roids[base..base + dim];
149 for (i, p) in points.iter().enumerate() {
150 let nd = squared_l2(p, new);
151 if nd < d2[i] {
152 d2[i] = nd;
153 }
154 }
155 }
156
157 let kk = centroids.len() / dim;
159 for _ in 0..iters {
160 let mut sums = vec![0.0f32; kk * dim];
161 let mut counts = vec![0usize; kk];
162 for p in points {
163 let a = nearest(¢roids, dim, p);
164 counts[a] += 1;
165 for d in 0..dim {
166 sums[a * dim + d] += p[d];
167 }
168 }
169 for c in 0..kk {
170 if counts[c] == 0 {
171 let r = rng.gen_range(0..n);
173 centroids[c * dim..(c + 1) * dim].copy_from_slice(points[r]);
174 } else {
175 let inv = 1.0 / counts[c] as f32;
176 for d in 0..dim {
177 centroids[c * dim + d] = sums[c * dim + d] * inv;
178 }
179 }
180 }
181 }
182 centroids
183}
184
185#[derive(Serialize, Deserialize, Clone, Debug)]
187struct PqCodebook {
188 sub_offsets: Vec<usize>,
190 centroids: Vec<Vec<f32>>,
192 ks: Vec<usize>,
194}
195
196impl PqCodebook {
197 fn m(&self) -> usize {
198 self.sub_offsets.len() - 1
199 }
200
201 fn train(
203 sub_offsets: &[usize],
204 train: &[&[f32]],
205 iters: usize,
206 rng: &mut StdRng,
207 ) -> PqCodebook {
208 let m = sub_offsets.len() - 1;
209 let mut centroids = Vec::with_capacity(m);
210 let mut ks = Vec::with_capacity(m);
211 for s in 0..m {
212 let (lo, hi) = (sub_offsets[s], sub_offsets[s + 1]);
213 let subs: Vec<Vec<f32>> = train.iter().map(|v| v[lo..hi].to_vec()).collect();
214 let refs: Vec<&[f32]> = subs.iter().map(|x| x.as_slice()).collect();
215 let k = PQ_CODEWORDS.min(refs.len()).max(1);
216 let cs = kmeans(&refs, k, iters, rng);
217 ks.push(cs.len() / (hi - lo));
218 centroids.push(cs);
219 }
220 PqCodebook {
221 sub_offsets: sub_offsets.to_vec(),
222 centroids,
223 ks,
224 }
225 }
226
227 fn encode(&self, v: &[f32]) -> Vec<u8> {
229 let m = self.m();
230 let mut code = vec![0u8; m];
231 for (s, slot) in code.iter_mut().enumerate().take(m) {
232 let (lo, hi) = (self.sub_offsets[s], self.sub_offsets[s + 1]);
233 *slot = nearest(&self.centroids[s], hi - lo, &v[lo..hi]) as u8;
234 }
235 code
236 }
237
238 fn distance_table(&self, q: &[f32]) -> Vec<Vec<f32>> {
241 let m = self.m();
242 let mut table = Vec::with_capacity(m);
243 for s in 0..m {
244 let (lo, hi) = (self.sub_offsets[s], self.sub_offsets[s + 1]);
245 let sd = hi - lo;
246 let qs = &q[lo..hi];
247 let cs = &self.centroids[s];
248 let mut row = vec![0.0f32; self.ks[s]];
249 for (c, slot) in row.iter_mut().enumerate() {
250 *slot = squared_l2(qs, &cs[c * sd..(c + 1) * sd]);
251 }
252 table.push(row);
253 }
254 table
255 }
256}
257
258#[derive(Serialize, Deserialize, Clone, Debug, Default)]
260struct Posting {
261 ids: Vec<u128>,
262 codes: Vec<u8>,
264}
265
266#[derive(Serialize, Deserialize, Clone, Debug)]
268pub struct IvfPqIndex {
269 dims: usize,
270 n_partitions: usize,
271 n_probe: usize,
272 n_rerank: usize,
273 centroids: Vec<f32>,
275 pq: PqCodebook,
276 partitions: Vec<Posting>,
277 #[serde(skip)]
280 assignment: HashMap<u128, usize>,
281}
282
283impl IvfPqIndex {
284 pub fn build(dims: usize, items: &[(u128, &[f32])], cfg: IndexConfig) -> Result<IvfPqIndex> {
286 if items.is_empty() {
287 return Err(MnemoError::Invalid(
288 "cannot build an index over an empty database".into(),
289 ));
290 }
291 let n = items.len();
292 let mut rng = StdRng::seed_from_u64(cfg.seed);
293
294 let mut order: Vec<usize> = (0..n).collect();
296 let train_n = TRAIN_CAP.min(n);
297 order.partial_shuffle(&mut rng, train_n);
298 let train: Vec<&[f32]> = order[..train_n].iter().map(|&i| items[i].1).collect();
299
300 let n_partitions = if cfg.n_partitions > 0 {
302 cfg.n_partitions
303 } else {
304 (n as f64).sqrt().ceil() as usize
305 }
306 .clamp(1, n);
307 let centroids = kmeans(&train, n_partitions, cfg.kmeans_iters, &mut rng);
308 let n_partitions = centroids.len() / dims;
309
310 let m = if cfg.pq_subspaces > 0 {
312 cfg.pq_subspaces
313 } else {
314 (dims / 8).max(1)
315 }
316 .clamp(1, dims);
317 let mut sub_offsets = Vec::with_capacity(m + 1);
318 for s in 0..=m {
319 sub_offsets.push(s * dims / m);
320 }
321 let pq = PqCodebook::train(&sub_offsets, &train, cfg.kmeans_iters, &mut rng);
322
323 let mut index = IvfPqIndex {
324 dims,
325 n_partitions,
326 n_probe: cfg.n_probe.max(1),
327 n_rerank: cfg.n_rerank.max(1),
328 centroids,
329 pq,
330 partitions: vec![Posting::default(); n_partitions],
331 assignment: HashMap::with_capacity(n),
332 };
333 for &(id, v) in items {
334 index.add(id, v);
335 }
336 Ok(index)
337 }
338
339 pub fn dims(&self) -> usize {
341 self.dims
342 }
343
344 pub fn len(&self) -> usize {
346 self.partitions.iter().map(|p| p.ids.len()).sum()
347 }
348
349 pub fn n_probe(&self) -> usize {
351 self.n_probe
352 }
353
354 pub fn n_rerank(&self) -> usize {
356 self.n_rerank
357 }
358
359 pub fn info(&self) -> IndexInfo {
361 IndexInfo {
362 vectors: self.len(),
363 partitions: self.n_partitions,
364 subspaces: self.pq.m(),
365 n_probe: self.n_probe,
366 n_rerank: self.n_rerank,
367 }
368 }
369
370 pub fn rebuild_assignment(&mut self) {
373 self.assignment.clear();
374 for (pi, p) in self.partitions.iter().enumerate() {
375 for &id in &p.ids {
376 self.assignment.insert(id, pi);
377 }
378 }
379 }
380
381 fn nearest_centroid(&self, v: &[f32]) -> usize {
382 nearest(&self.centroids, self.dims, v)
383 }
384
385 pub fn add(&mut self, id: u128, vector: &[f32]) {
390 self.remove(id);
391 let part = self.nearest_centroid(vector);
392 let code = self.pq.encode(vector);
393 let p = &mut self.partitions[part];
394 p.ids.push(id);
395 p.codes.extend_from_slice(&code);
396 self.assignment.insert(id, part);
397 }
398
399 pub fn remove(&mut self, id: u128) {
401 if let Some(part) = self.assignment.remove(&id) {
402 let m = self.pq.m();
403 let p = &mut self.partitions[part];
404 if let Some(pos) = p.ids.iter().position(|&x| x == id) {
405 p.ids.remove(pos);
406 p.codes.drain(pos * m..(pos + 1) * m);
407 }
408 }
409 }
410
411 pub fn query(&self, q: &[f32], n_probe: Option<usize>, n_rerank: Option<usize>) -> Vec<u128> {
415 let n_probe = n_probe.unwrap_or(self.n_probe).clamp(1, self.n_partitions);
416 let n_rerank = n_rerank.unwrap_or(self.n_rerank).max(1);
417
418 let mut parts: Vec<(usize, f32)> = (0..self.n_partitions)
420 .map(|i| {
421 let c = &self.centroids[i * self.dims..(i + 1) * self.dims];
422 (i, squared_l2(q, c))
423 })
424 .collect();
425 parts.sort_by(|a, b| a.1.total_cmp(&b.1));
426 parts.truncate(n_probe);
427
428 let table = self.pq.distance_table(q);
430 let m = self.pq.m();
431 let mut cands: Vec<(u128, f32)> = Vec::new();
432 for (pi, _) in parts {
433 let p = &self.partitions[pi];
434 for (j, &id) in p.ids.iter().enumerate() {
435 let code = &p.codes[j * m..(j + 1) * m];
436 let mut d = 0.0f32;
437 for (s, row) in table.iter().enumerate() {
438 d += row[code[s] as usize];
439 }
440 cands.push((id, d));
441 }
442 }
443
444 cands.sort_by(|a, b| a.1.total_cmp(&b.1));
446 cands.truncate(n_rerank);
447 cands.into_iter().map(|(id, _)| id).collect()
448 }
449}