1use std::collections::{HashMap, HashSet};
14
15use crate::distance::scalar::scalar_distance;
16use crate::error::{VectorError, check_dim};
17use nodedb_types::vector_distance::DistanceMetric;
18
19use super::storage::MultiVectorStore;
20
21fn assign(vectors: &[Vec<f32>], centroids: &[Vec<f32>]) -> Vec<usize> {
27 vectors
28 .iter()
29 .map(|v| {
30 centroids
31 .iter()
32 .enumerate()
33 .map(|(i, c)| (i, scalar_distance(v, c, DistanceMetric::L2)))
34 .min_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal))
35 .map(|(i, _)| i)
36 .unwrap_or(0)
37 })
38 .collect()
39}
40
41fn lcg_next(s: &mut u64) -> u64 {
43 *s = s
44 .wrapping_mul(6364136223846793005)
45 .wrapping_add(1442695040888963407);
46 *s
47}
48
49fn min_dist_to_centroids(v: &[f32], centroids: &[Vec<f32>]) -> f32 {
51 centroids
52 .iter()
53 .map(|c| scalar_distance(v, c, DistanceMetric::L2))
54 .min_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
55 .unwrap_or(f32::INFINITY)
56}
57
58fn recompute(
63 vectors: &[Vec<f32>],
64 assignments: &[usize],
65 num_centroids: usize,
66 dim: usize,
67 prev_centroids: &[Vec<f32>],
68) -> Vec<Vec<f32>> {
69 let mut sums = vec![vec![0.0f32; dim]; num_centroids];
70 let mut counts = vec![0usize; num_centroids];
71
72 for (v, &c) in vectors.iter().zip(assignments.iter()) {
73 for (s, x) in sums[c].iter_mut().zip(v.iter()) {
74 *s += x;
75 }
76 counts[c] += 1;
77 }
78
79 for (s, &n) in sums.iter_mut().zip(counts.iter()) {
81 if n > 0 {
82 s.iter_mut().for_each(|x| *x /= n as f32);
83 }
84 }
85
86 for c_idx in 0..num_centroids {
91 if counts[c_idx] != 0 {
92 continue;
93 }
94 let live: Vec<Vec<f32>> = counts
95 .iter()
96 .enumerate()
97 .filter(|(i, cnt)| *i != c_idx && **cnt > 0)
98 .map(|(i, _)| sums[i].clone())
99 .collect();
100 let seed_pool: &[Vec<f32>] = if live.is_empty() {
101 prev_centroids
102 } else {
103 &live
104 };
105 let farthest = vectors
106 .iter()
107 .map(|v| (v, min_dist_to_centroids(v, seed_pool)))
108 .max_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal))
109 .map(|(v, _)| v.clone());
110 if let Some(v) = farthest {
111 sums[c_idx] = v;
112 counts[c_idx] = 1; } else if c_idx < prev_centroids.len() {
114 sums[c_idx] = prev_centroids[c_idx].clone();
115 counts[c_idx] = 1;
116 }
117 }
118
119 sums
120}
121
122fn kmeans_plus_plus_init(vectors: &[Vec<f32>], k: usize, seed: u64) -> Vec<Vec<f32>> {
126 let mut state = seed.wrapping_add(1);
127 let first = (lcg_next(&mut state) as usize) % vectors.len();
128 let mut centroids: Vec<Vec<f32>> = vec![vectors[first].clone()];
129
130 while centroids.len() < k {
131 let dists: Vec<f32> = vectors
132 .iter()
133 .map(|v| {
134 let d = min_dist_to_centroids(v, ¢roids);
135 d * d
136 })
137 .collect();
138 let total: f64 = dists.iter().map(|&d| d as f64).sum();
139 if total <= 0.0 {
140 let idx = (lcg_next(&mut state) as usize) % vectors.len();
143 centroids.push(vectors[idx].clone());
144 continue;
145 }
146 let r = (lcg_next(&mut state) as f64) / (u64::MAX as f64) * total;
148 let mut acc = 0.0f64;
149 let mut pick = vectors.len() - 1;
150 for (i, &d) in dists.iter().enumerate() {
151 acc += d as f64;
152 if acc >= r {
153 pick = i;
154 break;
155 }
156 }
157 centroids.push(vectors[pick].clone());
158 }
159
160 centroids
161}
162
163fn kmeans(
167 vectors: &[Vec<f32>],
168 num_centroids: usize,
169 iters: usize,
170 seed: u64,
171 dim: usize,
172) -> Vec<Vec<f32>> {
173 if vectors.is_empty() || num_centroids == 0 {
174 return Vec::new();
175 }
176
177 let k = num_centroids.min(vectors.len());
178 let mut centroids = kmeans_plus_plus_init(vectors, k, seed);
179
180 for _ in 0..iters {
181 let assignments = assign(vectors, ¢roids);
182 let new_centroids = recompute(vectors, &assignments, k, dim, ¢roids);
183 centroids = new_centroids;
184 }
185
186 centroids
187}
188
189pub struct PlaidPruner {
198 pub centroids: Vec<Vec<f32>>,
199 doc_centroids: HashMap<u32, Vec<u16>>,
201}
202
203impl PlaidPruner {
204 pub fn train(
210 store: &MultiVectorStore,
211 num_centroids: u16,
212 kmeans_iters: usize,
213 seed: u64,
214 ) -> Self {
215 let dim = store.dim;
216 let nc = num_centroids as usize;
217
218 let all_vectors: Vec<Vec<f32>> = store
220 .iter()
221 .flat_map(|doc| doc.vectors.iter().cloned())
222 .collect();
223
224 let centroids = kmeans(&all_vectors, nc, kmeans_iters, seed, dim);
225
226 let doc_centroids: HashMap<u32, Vec<u16>> = store
228 .iter()
229 .map(|doc| {
230 let mut ids: Vec<u16> = doc
231 .vectors
232 .iter()
233 .map(|v| {
234 centroids
235 .iter()
236 .enumerate()
237 .map(|(i, c)| (i as u16, scalar_distance(v, c, DistanceMetric::L2)))
238 .min_by(|a, b| {
239 a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal)
240 })
241 .map(|(i, _)| i)
242 .unwrap_or(0)
243 })
244 .collect();
245 ids.sort_unstable();
246 ids.dedup();
247 (doc.doc_id, ids)
248 })
249 .collect();
250
251 Self {
252 centroids,
253 doc_centroids,
254 }
255 }
256
257 pub fn candidates(&self, query: &[Vec<f32>]) -> Result<Vec<u32>, VectorError> {
266 let Some(first) = self.centroids.first() else {
267 return Ok(Vec::new());
268 };
269 for v in query {
270 check_dim(first.len(), v.len())?;
271 }
272 if query.is_empty() {
273 return Ok(Vec::new());
274 }
275
276 let query_bag: HashSet<u16> = query
278 .iter()
279 .filter_map(|v| {
280 self.centroids
281 .iter()
282 .enumerate()
283 .map(|(i, c)| (i as u16, scalar_distance(v, c, DistanceMetric::L2)))
284 .min_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal))
285 .map(|(id, _)| id)
286 })
287 .collect();
288
289 Ok(self
291 .doc_centroids
292 .iter()
293 .filter(|(_, doc_ids)| doc_ids.iter().any(|id| query_bag.contains(id)))
294 .map(|(&doc_id, _)| doc_id)
295 .collect())
296 }
297}
298
299#[cfg(test)]
304mod tests {
305 use super::*;
306 use crate::multivec::storage::{MultiVecMode, MultiVectorDoc, MultiVectorStore};
307
308 fn build_store() -> MultiVectorStore {
309 let mut store = MultiVectorStore::new(2, MultiVecMode::PerToken);
310
311 for i in 0u32..3 {
316 store
317 .insert(MultiVectorDoc {
318 doc_id: i,
319 vectors: vec![vec![i as f32 * 0.1, i as f32 * 0.1]],
320 })
321 .unwrap();
322 }
323 for i in 3u32..6 {
324 store
325 .insert(MultiVectorDoc {
326 doc_id: i,
327 vectors: vec![vec![10.0 + i as f32 * 0.1, 0.0]],
328 })
329 .unwrap();
330 }
331 for i in 6u32..9 {
332 store
333 .insert(MultiVectorDoc {
334 doc_id: i,
335 vectors: vec![vec![0.0, 10.0 + i as f32 * 0.1]],
336 })
337 .unwrap();
338 }
339 store
340 }
341
342 #[test]
343 fn train_produces_correct_centroid_count() {
344 let store = build_store();
345 let pruner = PlaidPruner::train(&store, 3, 10, 42);
346 assert_eq!(pruner.centroids.len(), 3);
347 }
348
349 #[test]
350 fn centroids_have_correct_dim() {
351 let store = build_store();
352 let pruner = PlaidPruner::train(&store, 3, 10, 42);
353 for c in &pruner.centroids {
354 assert_eq!(c.len(), 2);
355 }
356 }
357
358 #[test]
359 fn candidates_non_empty_for_matching_query() {
360 let store = build_store();
361 let pruner = PlaidPruner::train(&store, 3, 10, 42);
362
363 let query = vec![vec![0.0f32, 0.0f32]];
365 let cands = pruner.candidates(&query).unwrap();
366 assert!(!cands.is_empty(), "expected at least one candidate");
367 }
368
369 #[test]
370 fn candidates_empty_when_no_centroids() {
371 let store = MultiVectorStore::new(2, MultiVecMode::PerToken);
373 let pruner = PlaidPruner::train(&store, 3, 5, 1);
374 let query = vec![vec![0.0f32, 0.0f32]];
375 assert!(pruner.candidates(&query).unwrap().is_empty());
376 }
377
378 #[test]
379 fn candidates_cover_input_range() {
380 let store = build_store();
383 let pruner = PlaidPruner::train(&store, 3, 15, 7);
384 let query = vec![
385 vec![0.0f32, 0.0f32],
386 vec![10.0f32, 0.0f32],
387 vec![0.0f32, 10.0f32],
388 ];
389 let mut cands = pruner.candidates(&query).unwrap();
390 cands.sort_unstable();
391 cands.dedup();
392 assert_eq!(cands.len(), 9, "all docs should be candidates: {:?}", cands);
393 }
394}