1use serde::{Deserialize, Serialize};
7
8use super::routing::{
9 HNSW_AUTO_THRESHOLD, HnswRoutingGraph, IvfProbePlan, IvfRoutingTopology,
10 allocate_child_clusters, effective_routing_mode, float_probe_fingerprint, routing_parent_count,
11 select_best_candidates, select_parent_beam, select_parent_beam_for_build,
12};
13use super::soar::{MultiAssignment, SoarConfig};
14use crate::dsl::IvfRoutingMode;
15
16const SOAR_LAMBDA: f32 = 1.0;
20const MAX_SELECTIVE_SECONDARY_TO_PRIMARY_DISTANCE_RATIO_SQ: f32 = 4.0;
28const BUILD_ASSIGNMENT_CANDIDATES: usize = 128;
32
33#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct CoarseConfig {
36 pub num_clusters: usize,
38 pub dim: usize,
40 pub max_iters: usize,
42 pub seed: u64,
44 pub soar: Option<SoarConfig>,
46 pub routing: IvfRoutingMode,
48}
49
50impl CoarseConfig {
51 pub fn new(dim: usize, num_clusters: usize) -> Self {
52 Self {
53 num_clusters,
54 dim,
55 max_iters: 25,
56 seed: 42,
57 soar: None,
58 routing: IvfRoutingMode::Auto,
59 }
60 }
61
62 pub fn with_soar(mut self, config: SoarConfig) -> Self {
63 self.soar = Some(config);
64 self
65 }
66
67 pub fn with_seed(mut self, seed: u64) -> Self {
68 self.seed = seed;
69 self
70 }
71
72 pub fn with_max_iters(mut self, iters: usize) -> Self {
73 self.max_iters = iters;
74 self
75 }
76
77 pub fn with_routing(mut self, routing: IvfRoutingMode) -> Self {
78 self.routing = routing;
79 self
80 }
81}
82
83#[derive(Debug, Clone, Serialize, Deserialize)]
85pub struct CoarseCentroids {
86 pub num_clusters: u32,
88 pub dim: usize,
90 pub centroids: Vec<f32>,
92 pub version: u64,
94 pub soar_config: Option<SoarConfig>,
96 pub(crate) routing_index: Option<FloatCentroidRouter>,
98}
99
100#[derive(Debug, Clone, Serialize, Deserialize)]
101pub(crate) enum FloatCentroidRouter {
102 TwoLevel {
103 parent_centroids: Vec<f32>,
104 topology: IvfRoutingTopology,
105 },
106 Hnsw(HnswRoutingGraph),
107}
108
109struct FlatClusterMemberships {
110 offsets: Vec<usize>,
111 members: Vec<usize>,
112}
113
114impl FlatClusterMemberships {
115 fn cluster(&self, cluster: usize) -> &[usize] {
116 &self.members[self.offsets[cluster]..self.offsets[cluster + 1]]
117 }
118}
119
120impl CoarseCentroids {
121 pub fn train(config: &CoarseConfig, vectors: &[Vec<f32>]) -> Self {
125 assert!(!vectors.is_empty(), "Cannot train on empty vector set");
126 assert!(config.num_clusters > 0, "Need at least 1 cluster");
127 assert!(vectors.iter().all(|vector| vector.len() == config.dim));
128
129 let flat = vectors
133 .iter()
134 .flat_map(|vector| vector.iter().copied())
135 .collect::<Vec<_>>();
136 Self::train_contiguous(config, &flat, vectors.len())
137 }
138
139 pub(crate) fn train_contiguous(
141 config: &CoarseConfig,
142 vectors: &[f32],
143 vector_count: usize,
144 ) -> Self {
145 assert!(vector_count > 0, "Cannot train on empty vector set");
146 assert!(config.num_clusters > 0, "Need at least 1 cluster");
147 assert_eq!(vectors.len(), vector_count.saturating_mul(config.dim));
148
149 let actual_clusters = config.num_clusters.min(vector_count);
150 let (centroids, routing_index) =
151 match effective_routing_mode(config.routing, actual_clusters) {
152 IvfRoutingMode::TwoLevel => {
153 let (leaves, router) =
154 Self::train_hierarchical(config, vectors, vector_count, actual_clusters);
155 (leaves, Some(router))
156 }
157 IvfRoutingMode::Hnsw => {
158 let leaves = if actual_clusters >= HNSW_AUTO_THRESHOLD {
159 Self::train_hierarchical(config, vectors, vector_count, actual_clusters).0
160 } else {
161 Self::train_flat(config, vectors, vector_count, actual_clusters)
162 };
163 let graph = HnswRoutingGraph::build(
164 actual_clusters,
165 |left, right| {
166 let left = left as usize * config.dim;
167 let right = right as usize * config.dim;
168 squared_l2(
169 &leaves[left..left + config.dim],
170 &leaves[right..right + config.dim],
171 )
172 },
173 config.seed,
174 );
175 (leaves, Some(FloatCentroidRouter::Hnsw(graph)))
176 }
177 IvfRoutingMode::Flat | IvfRoutingMode::Auto => (
178 Self::train_flat(config, vectors, vector_count, actual_clusters),
179 None,
180 ),
181 };
182
183 let version = std::time::SystemTime::now()
184 .duration_since(std::time::UNIX_EPOCH)
185 .unwrap_or_default()
186 .as_nanos() as u64;
187
188 let mut soar_config = config.soar.clone();
189 if let Some(soar) = &mut soar_config
190 && soar.num_secondary > 1
191 {
192 log::warn!(
193 "SOAR currently implements the published primary + one-secondary objective; \
194 clamping {} requested secondary assignments to one",
195 soar.num_secondary,
196 );
197 soar.num_secondary = 1;
198 }
199 let calibration_target = soar_config
200 .as_ref()
201 .and_then(SoarConfig::calibration_target);
202 let mut trained = Self {
203 num_clusters: actual_clusters as u32,
204 dim: config.dim,
205 centroids,
206 version,
207 soar_config: None,
210 routing_index,
211 };
212 if let Some(target) = calibration_target {
213 let threshold = trained.calibrate_selective_spill_threshold(
214 vectors,
215 vector_count,
216 config.routing,
217 target,
218 );
219 if let Some(soar) = &mut soar_config {
220 soar.spill_threshold = threshold;
221 }
222 log::info!(
223 "Calibrated SOAR selective spilling to at most {:.1}% of the training sample \
224 (residual threshold {:.6})",
225 target * 100.0,
226 threshold,
227 );
228 }
229 trained.soar_config = soar_config;
230 trained
231 }
232
233 fn calibrate_selective_spill_threshold(
234 &self,
235 vectors: &[f32],
236 vector_count: usize,
237 routing: IvfRoutingMode,
238 target_fraction: f32,
239 ) -> f32 {
240 let routing = effective_routing_mode(routing, self.num_clusters as usize);
244 let sample_limit = match routing {
245 IvfRoutingMode::Flat | IvfRoutingMode::Auto => {
246 let work_per_vector = (self.num_clusters as usize).saturating_mul(self.dim).max(1);
247 (64_000_000usize / work_per_vector).clamp(32, 4_096)
248 }
249 IvfRoutingMode::TwoLevel | IvfRoutingMode::Hnsw => 8_192,
250 }
251 .min(vector_count);
252 let mut residual_norms = Vec::with_capacity(sample_limit);
253 for sample in 0..sample_limit {
254 let vector_index = sample.saturating_mul(vector_count) / sample_limit;
255 let offset = vector_index * self.dim;
256 let vector = &vectors[offset..offset + self.dim];
257 let primary = self.assign_with_routing(vector, routing).primary_cluster;
258 residual_norms.push(squared_l2(vector, self.get_centroid(primary)).sqrt());
259 }
260 residual_norms.sort_unstable_by(f32::total_cmp);
261 if residual_norms.is_empty() {
262 return 0.0;
263 }
264 let spill_count = ((residual_norms.len() as f32 * target_fraction.clamp(0.0, 1.0)).round()
265 as usize)
266 .min(residual_norms.len());
267 if spill_count == 0 {
268 let largest = residual_norms.last().copied().unwrap_or(0.0);
269 return threshold_strictly_above(largest);
270 }
271 let boundary = residual_norms[residual_norms.len() - spill_count];
272 let at_or_above =
273 residual_norms.len() - residual_norms.partition_point(|&norm| norm < boundary);
274 if at_or_above > spill_count {
275 threshold_strictly_above(boundary)
280 } else {
281 boundary
282 }
283 }
284
285 fn train_flat(
286 config: &CoarseConfig,
287 vectors: &[f32],
288 vector_count: usize,
289 clusters: usize,
290 ) -> Vec<f32> {
291 Self::run_kmeans(config, vectors, vector_count, clusters).centroids
292 }
293
294 fn train_flat_with_memberships(
295 config: &CoarseConfig,
296 vectors: &[f32],
297 vector_count: usize,
298 clusters: usize,
299 ) -> (Vec<f32>, FlatClusterMemberships) {
300 let trained = Self::run_kmeans(config, vectors, vector_count, clusters);
301 (
302 trained.centroids,
303 FlatClusterMemberships {
304 offsets: trained.member_offsets,
305 members: trained.members,
306 },
307 )
308 }
309
310 fn run_kmeans(
311 config: &CoarseConfig,
312 vectors: &[f32],
313 vector_count: usize,
314 clusters: usize,
315 ) -> crate::structures::vector::kmeans::EuclideanKMeans {
316 crate::structures::vector::kmeans::train_euclidean_kmeans(
317 vectors,
318 vector_count,
319 config.dim,
320 clusters,
321 config.max_iters,
322 config.seed,
323 )
324 }
325
326 fn train_hierarchical(
327 config: &CoarseConfig,
328 vectors: &[f32],
329 vector_count: usize,
330 leaf_count: usize,
331 ) -> (Vec<f32>, FloatCentroidRouter) {
332 let parent_count = routing_parent_count(leaf_count).min(vector_count);
333 let mut parent_config = config.clone();
334 parent_config.routing = IvfRoutingMode::Flat;
335 parent_config.num_clusters = parent_count;
336 let (parent_centroids, groups) =
337 Self::train_flat_with_memberships(&parent_config, vectors, vector_count, parent_count);
338 let group_sizes: Vec<usize> = (0..parent_count)
339 .map(|parent| groups.cluster(parent).len())
340 .collect();
341 let child_counts = allocate_child_clusters(&group_sizes, leaf_count);
342 let mut leaves = Vec::with_capacity(leaf_count.saturating_mul(config.dim));
343 let mut children = vec![Vec::new(); parent_count];
344 let mut group_vectors = Vec::new();
345
346 for (parent, &child_count) in child_counts.iter().enumerate() {
347 if child_count == 0 {
348 continue;
349 }
350 let indices = groups.cluster(parent);
351 group_vectors.clear();
352 group_vectors.reserve(indices.len().saturating_mul(config.dim));
353 for &index in indices {
354 let offset = index * config.dim;
355 group_vectors.extend_from_slice(&vectors[offset..offset + config.dim]);
356 }
357 let mut child_config = config.clone();
358 child_config.routing = IvfRoutingMode::Flat;
359 child_config.num_clusters = child_count;
360 child_config.seed = config.seed ^ (parent as u64).wrapping_mul(0x9e37_79b9_7f4a_7c15);
361 let first_leaf = leaves.len() / config.dim;
362 leaves.extend_from_slice(&Self::train_flat(
363 &child_config,
364 &group_vectors,
365 indices.len(),
366 child_count,
367 ));
368 children[parent].extend((first_leaf..first_leaf + child_count).map(|leaf| leaf as u32));
369 }
370 debug_assert_eq!(leaves.len(), leaf_count * config.dim);
371
372 (
373 leaves,
374 FloatCentroidRouter::TwoLevel {
375 parent_centroids,
376 topology: IvfRoutingTopology::from_children(&children),
377 },
378 )
379 }
380
381 fn find_nearest_idx_static(vector: &[f32], centroids: &[f32], dim: usize) -> usize {
383 let num_clusters = centroids.len() / dim;
384 let mut best_idx = 0;
385 let mut best_dist = f32::MAX;
386
387 for c in 0..num_clusters {
388 let offset = c * dim;
389 let dist: f32 = vector
390 .iter()
391 .zip(¢roids[offset..offset + dim])
392 .map(|(&a, &b)| (a - b) * (a - b))
393 .sum();
394
395 if dist < best_dist {
396 best_dist = dist;
397 best_idx = c;
398 }
399 }
400
401 best_idx
402 }
403
404 pub fn find_nearest(&self, vector: &[f32]) -> u32 {
406 Self::find_nearest_idx_static(vector, &self.centroids, self.dim) as u32
407 }
408
409 pub fn find_k_nearest(&self, vector: &[f32], k: usize) -> Vec<u32> {
411 let mut distances: Vec<(u32, f32)> = (0..self.num_clusters)
412 .map(|c| {
413 let offset = c as usize * self.dim;
414 let dist: f32 = vector
415 .iter()
416 .zip(&self.centroids[offset..offset + self.dim])
417 .map(|(&a, &b)| (a - b) * (a - b))
418 .sum();
419 (c, dist)
420 })
421 .collect();
422
423 if distances.len() > k {
425 distances.select_nth_unstable_by(k, |a, b| a.1.total_cmp(&b.1));
426 distances.truncate(k);
427 }
428 distances.sort_unstable_by(|a, b| a.1.total_cmp(&b.1));
429 distances.into_iter().map(|(c, _)| c).collect()
430 }
431
432 pub fn probe(&self, vector: &[f32], k: usize, mode: IvfRoutingMode) -> IvfProbePlan {
437 let take = k.clamp(1, self.num_clusters as usize);
438 let clusters = match effective_routing_mode(mode, self.num_clusters as usize) {
439 IvfRoutingMode::Flat | IvfRoutingMode::Auto => self.find_k_nearest(vector, take),
440 IvfRoutingMode::TwoLevel => self.find_k_nearest_two_level(vector, take),
441 IvfRoutingMode::Hnsw => self.find_k_nearest_hnsw(vector, take),
442 };
443 IvfProbePlan::new(
444 self.version,
445 float_probe_fingerprint(vector, take, mode),
446 clusters,
447 )
448 }
449
450 pub fn validate_routing(&self, mode: IvfRoutingMode) -> Result<(), String> {
451 match effective_routing_mode(mode, self.num_clusters as usize) {
452 IvfRoutingMode::Flat | IvfRoutingMode::Auto => Ok(()),
453 IvfRoutingMode::TwoLevel => {
454 let Some(FloatCentroidRouter::TwoLevel {
455 parent_centroids,
456 topology,
457 }) = self.routing_index.as_ref()
458 else {
459 return Err(
460 "two-level IVF routing was requested but the global codebook has no matching router"
461 .to_string(),
462 );
463 };
464 let parent_count = topology.parent_count();
465 if parent_count == 0
466 || parent_centroids.len() != parent_count.saturating_mul(self.dim)
467 || !topology.validate(self.num_clusters as usize)
468 || parent_centroids.iter().any(|value| !value.is_finite())
469 {
470 return Err("invalid float two-level IVF routing index".to_string());
471 }
472 Ok(())
473 }
474 IvfRoutingMode::Hnsw => {
475 let Some(FloatCentroidRouter::Hnsw(graph)) = self.routing_index.as_ref() else {
476 return Err(
477 "HNSW IVF routing was requested but the global codebook has no HNSW graph"
478 .to_string(),
479 );
480 };
481 if !graph.validate(self.num_clusters as usize) {
482 return Err("invalid float HNSW routing graph".to_string());
483 }
484 Ok(())
485 }
486 }
487 }
488
489 fn find_k_nearest_two_level(&self, vector: &[f32], k: usize) -> Vec<u32> {
490 self.find_k_nearest_two_level_impl::<false>(vector, k)
491 }
492
493 fn find_k_nearest_two_level_for_build(&self, vector: &[f32], k: usize) -> Vec<u32> {
494 self.find_k_nearest_two_level_impl::<true>(vector, k)
495 }
496
497 fn find_k_nearest_two_level_impl<const FOR_BUILD: bool>(
498 &self,
499 vector: &[f32],
500 k: usize,
501 ) -> Vec<u32> {
502 let Some(FloatCentroidRouter::TwoLevel {
503 parent_centroids,
504 topology,
505 }) = self.routing_index.as_ref()
506 else {
507 return self.find_k_nearest(vector, k);
508 };
509 if topology.parent_count() <= 1 {
510 return self.find_k_nearest(vector, k);
511 }
512
513 let mut parent_scores = vec![0.0; topology.parent_count()];
514 for (parent_id, score) in parent_scores.iter_mut().enumerate() {
515 let offset = parent_id * self.dim;
516 *score = squared_l2(vector, &parent_centroids[offset..offset + self.dim]);
517 }
518 let parents = if FOR_BUILD {
519 select_parent_beam_for_build::<false>(&parent_scores, topology, k)
520 } else {
521 select_parent_beam::<false>(&parent_scores, topology, k)
522 };
523 let candidate_capacity = parents
524 .iter()
525 .map(|&parent| topology.children(parent as usize).len())
526 .sum();
527 let mut candidates = Vec::with_capacity(candidate_capacity);
528 for parent in parents {
529 for &leaf in topology.children(parent as usize) {
530 candidates.push((leaf, squared_l2(vector, self.get_centroid(leaf))));
531 }
532 }
533 select_best_candidates::<false>(&mut candidates, k)
534 }
535
536 fn find_k_nearest_hnsw(&self, vector: &[f32], k: usize) -> Vec<u32> {
537 let Some(FloatCentroidRouter::Hnsw(graph)) = self.routing_index.as_ref() else {
538 return self.find_k_nearest(vector, k);
539 };
540 graph.search(|leaf| squared_l2(vector, self.get_centroid(leaf)), k)
541 }
542
543 pub fn find_k_nearest_with_distances(&self, vector: &[f32], k: usize) -> Vec<(u32, f32)> {
545 let mut distances: Vec<(u32, f32)> = (0..self.num_clusters)
546 .map(|c| {
547 let offset = c as usize * self.dim;
548 let dist: f32 = vector
549 .iter()
550 .zip(&self.centroids[offset..offset + self.dim])
551 .map(|(&a, &b)| (a - b) * (a - b))
552 .sum();
553 (c, dist)
554 })
555 .collect();
556
557 if distances.len() > k {
559 distances.select_nth_unstable_by(k, |a, b| a.1.total_cmp(&b.1));
560 distances.truncate(k);
561 }
562 distances.sort_unstable_by(|a, b| a.1.total_cmp(&b.1));
563 distances
564 }
565
566 pub fn assign(&self, vector: &[f32]) -> MultiAssignment {
568 self.assign_with_routing(vector, IvfRoutingMode::Flat)
569 }
570
571 pub fn assign_with_routing(&self, vector: &[f32], routing: IvfRoutingMode) -> MultiAssignment {
575 if let Some(ref soar_config) = self.soar_config {
576 self.assign_with_soar_and_routing(vector, soar_config, routing)
577 } else {
578 let primary_cluster = match effective_routing_mode(routing, self.num_clusters as usize)
579 {
580 IvfRoutingMode::Hnsw => self.find_k_nearest_hnsw_for_build(vector, 1)[0],
581 IvfRoutingMode::TwoLevel => self
582 .find_k_nearest_two_level_for_build(
583 vector,
584 BUILD_ASSIGNMENT_CANDIDATES.min(self.num_clusters as usize),
585 )
586 .first()
587 .copied()
588 .unwrap_or(0),
589 IvfRoutingMode::Flat | IvfRoutingMode::Auto => self.find_nearest(vector),
590 };
591 MultiAssignment {
592 primary_cluster,
593 secondary_clusters: Vec::new(),
594 }
595 }
596 }
597
598 pub fn assign_with_soar(&self, vector: &[f32], config: &SoarConfig) -> MultiAssignment {
600 self.assign_with_soar_and_routing(vector, config, IvfRoutingMode::Flat)
601 }
602
603 fn assign_with_soar_and_routing(
604 &self,
605 vector: &[f32],
606 config: &SoarConfig,
607 routing: IvfRoutingMode,
608 ) -> MultiAssignment {
609 let num_secondary = config.num_secondary.min(1);
614 let candidate_budget =
618 soar_build_candidate_budget(num_secondary, self.num_clusters as usize);
619 let leaf_ids: Vec<u32> = match effective_routing_mode(routing, self.num_clusters as usize) {
620 IvfRoutingMode::TwoLevel => {
621 self.two_level_candidate_leaves_for_build(vector, candidate_budget)
622 }
623 IvfRoutingMode::Hnsw => self.find_k_nearest_hnsw_for_build(vector, candidate_budget),
624 IvfRoutingMode::Flat | IvfRoutingMode::Auto => (0..self.num_clusters).collect(),
625 };
626 let leaf_distances: Vec<(u32, f32)> = leaf_ids
629 .into_iter()
630 .map(|cluster| (cluster, squared_l2(vector, self.get_centroid(cluster))))
631 .collect();
632 let primary = leaf_distances
633 .iter()
634 .min_by(|left, right| scored_cluster_order(left, right))
635 .map(|&(cluster, _)| cluster)
636 .unwrap_or(0);
637 let primary_centroid = self.get_centroid(primary);
638
639 let residual: Vec<f32> = vector
641 .iter()
642 .zip(primary_centroid)
643 .map(|(v, c)| v - c)
644 .collect();
645
646 let residual_norm_sq: f32 = residual.iter().map(|x| x * x).sum();
647
648 if config.selective && residual_norm_sq < config.spill_threshold * config.spill_threshold {
650 return MultiAssignment {
651 primary_cluster: primary,
652 secondary_clusters: Vec::new(),
653 };
654 }
655
656 let mut candidates: Vec<(u32, f32)> = leaf_distances
664 .into_iter()
665 .filter(|&(cluster, secondary_residual_norm_sq)| {
666 cluster != primary
667 && (!config.selective
668 || secondary_residual_norm_sq
669 <= MAX_SELECTIVE_SECONDARY_TO_PRIMARY_DISTANCE_RATIO_SQ
670 * residual_norm_sq)
671 })
672 .map(|(cluster, secondary_residual_norm_sq)| {
673 (
674 cluster,
675 soar_secondary_loss(
676 vector,
677 self.get_centroid(cluster),
678 &residual,
679 residual_norm_sq,
680 secondary_residual_norm_sq,
681 ),
682 )
683 })
684 .collect();
685
686 let take = num_secondary.min(candidates.len());
689 if candidates.len() > take {
690 candidates.select_nth_unstable_by(take, scored_cluster_order);
691 candidates.truncate(take);
692 }
693 candidates.sort_unstable_by(scored_cluster_order);
694
695 MultiAssignment {
696 primary_cluster: primary,
697 secondary_clusters: candidates
698 .iter()
699 .take(num_secondary)
700 .map(|(c, _)| *c)
701 .collect(),
702 }
703 }
704
705 fn two_level_candidate_leaves_for_build(&self, vector: &[f32], k: usize) -> Vec<u32> {
706 let Some(FloatCentroidRouter::TwoLevel {
707 parent_centroids,
708 topology,
709 }) = self.routing_index.as_ref()
710 else {
711 return (0..self.num_clusters).collect();
712 };
713 let mut parent_scores = vec![0.0; topology.parent_count()];
714 for (parent_id, score) in parent_scores.iter_mut().enumerate() {
715 let offset = parent_id * self.dim;
716 *score = squared_l2(vector, &parent_centroids[offset..offset + self.dim]);
717 }
718 let parents = select_parent_beam_for_build::<false>(&parent_scores, topology, k);
719 let capacity = parents
720 .iter()
721 .map(|&parent| topology.children(parent as usize).len())
722 .sum();
723 let mut leaves = Vec::with_capacity(capacity);
724 for parent in parents {
725 leaves.extend_from_slice(topology.children(parent as usize));
726 }
727 leaves
728 }
729
730 fn find_k_nearest_hnsw_for_build(&self, vector: &[f32], k: usize) -> Vec<u32> {
731 let Some(FloatCentroidRouter::Hnsw(graph)) = self.routing_index.as_ref() else {
732 return self.find_k_nearest(vector, k);
733 };
734 graph.search_for_build(|leaf| squared_l2(vector, self.get_centroid(leaf)), k)
735 }
736
737 pub fn get_centroid(&self, cluster_id: u32) -> &[f32] {
739 let offset = cluster_id as usize * self.dim;
740 &self.centroids[offset..offset + self.dim]
741 }
742
743 pub fn compute_residual(&self, vector: &[f32], cluster_id: u32) -> Vec<f32> {
745 let centroid = self.get_centroid(cluster_id);
746 vector.iter().zip(centroid).map(|(&v, &c)| v - c).collect()
747 }
748
749 pub fn size_bytes(&self) -> usize {
751 let routing_bytes = self
752 .routing_index
753 .as_ref()
754 .map_or(0, |router| match router {
755 FloatCentroidRouter::TwoLevel {
756 parent_centroids,
757 topology,
758 } => {
759 parent_centroids.len() * size_of::<f32>()
760 + topology.parent_count() * size_of::<u32>()
761 + self.num_clusters as usize * size_of::<u32>()
762 }
763 FloatCentroidRouter::Hnsw(graph) => graph.size_bytes(),
764 });
765 self.centroids.len() * size_of::<f32>() + routing_bytes + 64
766 }
767
768 #[cfg(feature = "native")]
771 pub(crate) fn visit_routing_regions(&self, visit: &mut dyn FnMut(&'static str, &[u8])) {
772 if let Some(router) = &self.routing_index {
773 match router {
774 FloatCentroidRouter::TwoLevel {
775 parent_centroids,
776 topology,
777 } => {
778 topology.visit_resident_regions(visit);
779 visit(
780 "float parent centroids",
781 super::routing::bytes_of_slice(parent_centroids),
782 );
783 }
784 FloatCentroidRouter::Hnsw(graph) => graph.visit_resident_regions(visit),
785 }
786 }
787 }
788
789 #[cfg(feature = "native")]
790 pub(crate) fn visit_leaf_centroid_region(&self, visit: &mut dyn FnMut(&'static str, &[u8])) {
791 visit(
792 "float leaf centroids",
793 super::routing::bytes_of_slice(&self.centroids),
794 );
795 }
796
797 pub fn to_bytes(&self) -> std::io::Result<Vec<u8>> {
799 bincode::serde::encode_to_vec(self, bincode::config::standard())
800 .map_err(|error| std::io::Error::new(std::io::ErrorKind::InvalidData, error))
801 }
802}
803
804#[inline]
805fn squared_l2(left: &[f32], right: &[f32]) -> f32 {
806 left.iter()
807 .zip(right)
808 .map(|(&a, &b)| {
809 let delta = a - b;
810 delta * delta
811 })
812 .sum()
813}
814
815#[inline]
816fn threshold_strictly_above(value: f32) -> f32 {
817 if !value.is_finite() {
818 return f32::INFINITY;
819 }
820 let next = value + value.abs().max(1.0) * (4.0 * f32::EPSILON);
825 if next > value { next } else { f32::INFINITY }
826}
827
828#[inline]
829fn soar_build_candidate_budget(num_secondary: usize, num_clusters: usize) -> usize {
830 num_secondary
831 .saturating_add(1)
832 .saturating_mul(64)
833 .max(BUILD_ASSIGNMENT_CANDIDATES)
834 .min(num_clusters)
835}
836
837#[inline]
838fn soar_secondary_loss(
839 vector: &[f32],
840 secondary_centroid: &[f32],
841 primary_residual: &[f32],
842 primary_residual_norm_sq: f32,
843 secondary_residual_norm_sq: f32,
844) -> f32 {
845 let residual_dot = vector
846 .iter()
847 .zip(secondary_centroid)
848 .zip(primary_residual)
849 .map(|((&value, ¢roid), &primary)| primary * (value - centroid))
850 .sum::<f32>();
851
852 let projection_norm_sq = if primary_residual_norm_sq > 0.0 {
855 residual_dot * residual_dot / primary_residual_norm_sq
856 } else {
857 0.0
858 };
859 secondary_residual_norm_sq + SOAR_LAMBDA * projection_norm_sq
860}
861
862#[inline]
863fn scored_cluster_order(left: &(u32, f32), right: &(u32, f32)) -> std::cmp::Ordering {
864 left.1
865 .total_cmp(&right.1)
866 .then_with(|| left.0.cmp(&right.0))
867}
868
869#[cfg(test)]
870mod tests {
871 use super::*;
872 use rand::prelude::*;
873
874 #[test]
875 fn test_coarse_centroids_basic() {
876 let dim = 64;
877 let n = 1000;
878 let num_clusters = 16;
879
880 let mut rng = rand::rngs::StdRng::seed_from_u64(42);
881 let vectors: Vec<Vec<f32>> = (0..n)
882 .map(|_| (0..dim).map(|_| rng.random::<f32>() - 0.5).collect())
883 .collect();
884
885 let config = CoarseConfig::new(dim, num_clusters);
886 let centroids = CoarseCentroids::train(&config, &vectors);
887
888 assert_eq!(centroids.num_clusters, num_clusters as u32);
889 assert_eq!(centroids.dim, dim);
890 }
891
892 #[test]
893 fn contiguous_training_matches_row_wrapper() {
894 let dim = 8;
895 let vectors: Vec<Vec<f32>> = (0..128)
896 .map(|row| {
897 (0..dim)
898 .map(|column| ((row * 31 + column * 17) % 101) as f32 / 101.0)
899 .collect()
900 })
901 .collect();
902 let flat = vectors.iter().flatten().copied().collect::<Vec<_>>();
903 let config = CoarseConfig::new(dim, 12)
904 .with_seed(91)
905 .with_routing(IvfRoutingMode::TwoLevel);
906
907 let rows = CoarseCentroids::train(&config, &vectors);
908 let contiguous = CoarseCentroids::train_contiguous(&config, &flat, vectors.len());
909
910 assert_eq!(rows.centroids, contiguous.centroids);
911 assert_eq!(
912 bincode::serde::encode_to_vec(&rows.routing_index, bincode::config::standard())
913 .unwrap(),
914 bincode::serde::encode_to_vec(&contiguous.routing_index, bincode::config::standard())
915 .unwrap(),
916 );
917 }
918
919 #[test]
920 fn test_find_nearest() {
921 let dim = 32;
922 let n = 500;
923 let num_clusters = 8;
924
925 let mut rng = rand::rngs::StdRng::seed_from_u64(123);
926 let vectors: Vec<Vec<f32>> = (0..n)
927 .map(|_| (0..dim).map(|_| rng.random::<f32>()).collect())
928 .collect();
929
930 let config = CoarseConfig::new(dim, num_clusters);
931 let centroids = CoarseCentroids::train(&config, &vectors);
932
933 for v in &vectors {
935 let cluster = centroids.find_nearest(v);
936 assert!(cluster < centroids.num_clusters);
937 }
938 }
939
940 #[test]
941 fn scaled_l2_probes_keep_distinct_cache_identities() {
942 let centroids = CoarseCentroids {
943 num_clusters: 2,
944 dim: 2,
945 centroids: vec![1.0, 0.0, 10.0, 0.0],
946 version: 7,
947 soar_config: None,
948 routing_index: None,
949 };
950
951 let near = centroids.probe(&[1.0, 0.0], 1, IvfRoutingMode::Flat);
952 let scaled = centroids.probe(&[100.0, 0.0], 1, IvfRoutingMode::Flat);
953
954 assert_eq!(&*near.cluster_ids, &[0]);
955 assert_eq!(&*scaled.cluster_ids, &[1]);
956 assert_ne!(near.request_fingerprint, scaled.request_fingerprint);
957 }
958
959 #[test]
960 fn test_soar_assignment() {
961 let dim = 32;
962 let n = 100;
963 let num_clusters = 8;
964
965 let mut rng = rand::rngs::StdRng::seed_from_u64(456);
966 let vectors: Vec<Vec<f32>> = (0..n)
967 .map(|_| (0..dim).map(|_| rng.random::<f32>()).collect())
968 .collect();
969
970 let soar_config = SoarConfig {
971 num_secondary: 2,
972 selective: false,
973 spill_threshold: 0.0,
974 };
975 let config = CoarseConfig::new(dim, num_clusters).with_soar(soar_config);
976 let centroids = CoarseCentroids::train(&config, &vectors);
977
978 let assignment = centroids.assign(&vectors[0]);
980 assert!(assignment.primary_cluster < centroids.num_clusters);
981 assert_eq!(centroids.soar_config.as_ref().unwrap().num_secondary, 1);
982 assert_eq!(assignment.secondary_clusters.len(), 1);
983
984 for &sec in &assignment.secondary_clusters {
986 assert_ne!(sec, assignment.primary_cluster);
987 }
988 }
989
990 #[test]
991 fn soar_loss_includes_distortion_and_normalized_projection() {
992 let vector = [0.0, 0.0];
993 let primary_residual = [2.0, 0.0];
994 let secondary_centroid = [-3.0, -4.0];
995
996 let loss = soar_secondary_loss(&vector, &secondary_centroid, &primary_residual, 4.0, 25.0);
999 assert!((loss - 34.0).abs() <= f32::EPSILON);
1000 }
1001
1002 #[test]
1003 fn soar_routing_keeps_an_oversampled_secondary_candidate_pool() {
1004 assert_eq!(soar_build_candidate_budget(1, 1_000), 128);
1005 assert_eq!(soar_build_candidate_budget(2, 1_000), 192);
1006 assert_eq!(soar_build_candidate_budget(8, 64), 64);
1007 }
1008
1009 #[test]
1010 fn two_level_build_assignment_checks_four_parents_for_primary_and_soar() {
1011 let leaves_per_parent = 512;
1012 let children: Vec<Vec<u32>> = (0..4)
1013 .map(|parent| {
1014 let first = parent * leaves_per_parent;
1015 (first..first + leaves_per_parent)
1016 .map(|leaf| leaf as u32)
1017 .collect()
1018 })
1019 .collect();
1020 let mut leaf_centroids = vec![1.0; 4 * leaves_per_parent];
1021 let best_leaf = 3 * leaves_per_parent;
1022 leaf_centroids[best_leaf] = 0.0;
1023 let centroids = CoarseCentroids {
1024 num_clusters: leaf_centroids.len() as u32,
1025 dim: 1,
1026 centroids: leaf_centroids,
1027 version: 1,
1028 soar_config: None,
1029 routing_index: Some(FloatCentroidRouter::TwoLevel {
1030 parent_centroids: vec![0.0, 10.0, 20.0, 30.0],
1031 topology: IvfRoutingTopology::from_children(&children),
1032 }),
1033 };
1034
1035 let query = [0.0];
1036 assert_eq!(
1037 &*centroids
1038 .probe(&query, 1, IvfRoutingMode::TwoLevel)
1039 .cluster_ids,
1040 &[0]
1041 );
1042 assert_eq!(
1043 centroids
1044 .assign_with_routing(&query, IvfRoutingMode::TwoLevel)
1045 .primary_cluster,
1046 best_leaf as u32
1047 );
1048 assert_eq!(
1049 centroids
1050 .assign_with_soar_and_routing(
1051 &query,
1052 &SoarConfig::full(),
1053 IvfRoutingMode::TwoLevel,
1054 )
1055 .primary_cluster,
1056 best_leaf as u32
1057 );
1058 }
1059
1060 #[test]
1061 fn selective_soar_calibrates_to_a_storage_budget() {
1062 let centroids = CoarseCentroids {
1063 num_clusters: 2,
1064 dim: 2,
1065 centroids: vec![0.0, 0.0, 10.0, 0.0],
1066 version: 1,
1067 soar_config: None,
1068 routing_index: None,
1069 };
1070 let vectors: Vec<Vec<f32>> = (0..100)
1071 .map(|index| vec![index as f32 / 100.0, 0.0])
1072 .collect();
1073 let flat = vectors.iter().flatten().copied().collect::<Vec<_>>();
1074 let threshold = centroids.calibrate_selective_spill_threshold(
1075 &flat,
1076 vectors.len(),
1077 IvfRoutingMode::Flat,
1078 0.30,
1079 );
1080 let spilled = vectors
1081 .iter()
1082 .filter(|vector| squared_l2(vector, centroids.get_centroid(0)).sqrt() >= threshold)
1083 .count();
1084 assert!((29..=31).contains(&spilled), "{spilled}");
1085 }
1086
1087 #[test]
1088 fn selective_soar_never_exceeds_budget_when_residuals_tie() {
1089 let centroids = CoarseCentroids {
1090 num_clusters: 2,
1091 dim: 2,
1092 centroids: vec![0.0, 0.0, 10.0, 0.0],
1093 version: 1,
1094 soar_config: None,
1095 routing_index: None,
1096 };
1097 let vectors = [1.0f32, 0.0].repeat(100);
1098 let threshold = centroids.calibrate_selective_spill_threshold(
1099 &vectors,
1100 100,
1101 IvfRoutingMode::Flat,
1102 0.30,
1103 );
1104 let config = SoarConfig::new().threshold(threshold);
1105 let spilled = vectors
1106 .chunks_exact(2)
1107 .filter(|vector| centroids.assign_with_soar(vector, &config).is_spilled())
1108 .count();
1109
1110 assert!(threshold > 1.0);
1111 assert!(spilled <= 30, "{spilled}");
1112 }
1113
1114 #[test]
1115 fn selective_soar_preserves_boundary_query_candidate_recall_with_bounded_postings() {
1116 const DIM: usize = 16;
1117 const CLUSTERS: usize = 16;
1118 const MEMBERS_PER_CLUSTER: usize = 128;
1119 const TOP_K: usize = 20;
1120 const TARGET_SPILL: f32 = 0.30;
1121
1122 fn normalize(values: &mut [f32]) {
1123 let norm = values.iter().map(|value| value * value).sum::<f32>().sqrt();
1124 for value in values {
1125 *value /= norm;
1126 }
1127 }
1128
1129 let mut rng = rand::rngs::StdRng::seed_from_u64(0x50a4_5eed);
1130 let source_centers: Vec<Vec<f32>> = (0..CLUSTERS)
1131 .map(|_| {
1132 let mut center: Vec<f32> = (0..DIM).map(|_| rng.random::<f32>() - 0.5).collect();
1133 normalize(&mut center);
1134 center
1135 })
1136 .collect();
1137 let corpus: Vec<Vec<f32>> = source_centers
1138 .iter()
1139 .flat_map(|center| {
1140 (0..MEMBERS_PER_CLUSTER)
1141 .map(|_| {
1142 let mut noise: Vec<f32> =
1143 (0..DIM).map(|_| rng.random::<f32>() - 0.5).collect();
1144 normalize(&mut noise);
1145 let mut vector: Vec<f32> = center
1146 .iter()
1147 .zip(noise)
1148 .map(|(&value, noise)| value + 0.75 * noise)
1149 .collect();
1150 normalize(&mut vector);
1151 vector
1152 })
1153 .collect::<Vec<_>>()
1154 })
1155 .collect();
1156
1157 let selective = CoarseCentroids::train(
1158 &CoarseConfig::new(DIM, CLUSTERS)
1159 .with_seed(0x1f4)
1160 .with_routing(IvfRoutingMode::Flat)
1161 .with_soar(SoarConfig::new().target_spill_fraction(TARGET_SPILL)),
1162 &corpus,
1163 );
1164 let mut primary_only = selective.clone();
1167 primary_only.soar_config = None;
1168
1169 let primary_assignments: Vec<MultiAssignment> = corpus
1170 .iter()
1171 .map(|vector| primary_only.assign(vector))
1172 .collect();
1173 let selective_assignments: Vec<MultiAssignment> = corpus
1174 .iter()
1175 .map(|vector| selective.assign(vector))
1176 .collect();
1177 for (primary, spilled) in primary_assignments.iter().zip(&selective_assignments) {
1178 assert_eq!(
1179 primary.primary_cluster, spilled.primary_cluster,
1180 "SOAR policy must not change the primary posting"
1181 );
1182 }
1183
1184 let spilled = selective_assignments
1185 .iter()
1186 .filter(|assignment| assignment.is_spilled())
1187 .count();
1188 let posting_factor = selective_assignments
1189 .iter()
1190 .map(MultiAssignment::num_assignments)
1191 .sum::<usize>() as f32
1192 / corpus.len() as f32;
1193 let spill_budget = (corpus.len() as f32 * TARGET_SPILL).round() as usize;
1194 assert!(
1195 spilled > 0,
1196 "the smoke corpus must exercise selective spilling"
1197 );
1198 assert!(
1199 spilled <= spill_budget,
1200 "{spilled} spilled vectors exceeded the calibrated budget of {spill_budget}"
1201 );
1202 assert!(
1203 posting_factor <= 1.0 + TARGET_SPILL + f32::EPSILON,
1204 "posting amplification {posting_factor:.4} exceeded the 1.30 calibration target"
1205 );
1206
1207 let queries: Vec<Vec<f32>> = (0..selective.num_clusters)
1211 .map(|left| {
1212 let left_centroid = selective.get_centroid(left);
1213 let right = (0..selective.num_clusters)
1214 .filter(|&candidate| candidate != left)
1215 .min_by(|&a, &b| {
1216 squared_l2(left_centroid, selective.get_centroid(a))
1217 .total_cmp(&squared_l2(left_centroid, selective.get_centroid(b)))
1218 .then_with(|| a.cmp(&b))
1219 })
1220 .unwrap();
1221 let mut query: Vec<f32> = left_centroid
1222 .iter()
1223 .zip(selective.get_centroid(right))
1224 .map(|(&a, &b)| 0.51 * a + 0.49 * b)
1225 .collect();
1226 normalize(&mut query);
1227 query
1228 })
1229 .collect();
1230
1231 let mut gained_queries = 0usize;
1232 for nprobe in [1, 2] {
1233 let mut primary_hits = 0usize;
1234 let mut selective_hits = 0usize;
1235 for query in &queries {
1236 let primary_plan = primary_only.probe(query, nprobe, IvfRoutingMode::Flat);
1237 let selective_plan = selective.probe(query, nprobe, IvfRoutingMode::Flat);
1238 assert_eq!(
1239 primary_plan.cluster_ids, selective_plan.cluster_ids,
1240 "SOAR must not alter query routing"
1241 );
1242
1243 let mut truth: Vec<(usize, f32)> = corpus
1244 .iter()
1245 .enumerate()
1246 .map(|(document, vector)| (document, squared_l2(query, vector)))
1247 .collect();
1248 truth.select_nth_unstable_by(TOP_K, |left, right| {
1249 left.1
1250 .total_cmp(&right.1)
1251 .then_with(|| left.0.cmp(&right.0))
1252 });
1253 truth.truncate(TOP_K);
1254
1255 let query_primary_hits = truth
1256 .iter()
1257 .filter(|&&(document, _)| {
1258 primary_assignments[document]
1259 .all_clusters()
1260 .any(|cluster| primary_plan.cluster_ids.contains(&cluster))
1261 })
1262 .count();
1263 let query_selective_hits = truth
1264 .iter()
1265 .filter(|&&(document, _)| {
1266 selective_assignments[document]
1267 .all_clusters()
1268 .any(|cluster| selective_plan.cluster_ids.contains(&cluster))
1269 })
1270 .count();
1271 primary_hits += query_primary_hits;
1272 selective_hits += query_selective_hits;
1273 gained_queries += usize::from(query_selective_hits > query_primary_hits);
1274 }
1275
1276 let denominator = (queries.len() * TOP_K) as f32;
1277 let primary_recall = primary_hits as f32 / denominator;
1278 let selective_recall = selective_hits as f32 / denominator;
1279 assert!(
1280 selective_recall + 0.005 >= primary_recall,
1281 "selective SOAR candidate recall regressed at nprobe={nprobe}: \
1282 {selective_recall:.4} vs {primary_recall:.4}"
1283 );
1284 if nprobe == 1 {
1285 assert!(
1286 selective_recall >= primary_recall + 0.01,
1287 "selective SOAR must recover boundary candidates at nprobe=1: \
1288 {selective_recall:.4} vs {primary_recall:.4}"
1289 );
1290 }
1291 }
1292 assert!(
1293 gained_queries > 0,
1294 "boundary-query smoke did not exercise a selective SOAR recall gain"
1295 );
1296 }
1297
1298 #[test]
1299 fn soar_does_not_choose_an_arbitrarily_distant_orthogonal_centroid() {
1300 let centroids = CoarseCentroids {
1301 num_clusters: 3,
1302 dim: 2,
1303 centroids: vec![-1.0, 0.0, 0.0, -100.0, 1.1, 0.0],
1308 version: 1,
1309 soar_config: None,
1310 routing_index: None,
1311 };
1312
1313 let assignment = centroids.assign_with_soar(&[0.0, 0.0], &SoarConfig::full());
1314 assert_eq!(assignment.primary_cluster, 0);
1315 assert_eq!(assignment.secondary_clusters, vec![2]);
1316 }
1317
1318 #[test]
1319 fn selective_soar_rejects_a_far_secondary_residual() {
1320 let centroids = CoarseCentroids {
1321 num_clusters: 2,
1322 dim: 2,
1323 centroids: vec![0.0, 0.0, 10.0, 0.0],
1324 version: 1,
1325 soar_config: None,
1326 routing_index: None,
1327 };
1328 let config = SoarConfig::new().threshold(0.0);
1329
1330 let assignment = centroids.assign_with_soar(&[1.0, 0.0], &config);
1333 assert_eq!(assignment.primary_cluster, 0);
1334 assert!(assignment.secondary_clusters.is_empty());
1335 }
1336
1337 #[test]
1338 fn selective_soar_keeps_a_comparable_boundary_secondary() {
1339 let centroids = CoarseCentroids {
1340 num_clusters: 2,
1341 dim: 2,
1342 centroids: vec![0.0, 0.0, 2.0, 0.0],
1343 version: 1,
1344 soar_config: None,
1345 routing_index: None,
1346 };
1347 let config = SoarConfig::new().threshold(0.0);
1348
1349 let assignment = centroids.assign_with_soar(&[0.9, 0.1], &config);
1352 assert_eq!(assignment.primary_cluster, 0);
1353 assert_eq!(assignment.secondary_clusters, vec![1]);
1354 }
1355
1356 #[test]
1357 fn soar_secondary_ties_are_ordered_by_cluster_id_and_capped_to_one() {
1358 let centroids = CoarseCentroids {
1359 num_clusters: 3,
1360 dim: 2,
1361 centroids: vec![0.0, 0.0, 1.0, 0.0, -1.0, 0.0],
1362 version: 1,
1363 soar_config: None,
1364 routing_index: None,
1365 };
1366 let config = SoarConfig {
1367 num_secondary: 2,
1368 selective: false,
1369 spill_threshold: 0.0,
1370 };
1371
1372 let assignment = centroids.assign_with_soar(&[0.0, 0.0], &config);
1373 assert_eq!(assignment.primary_cluster, 0);
1374 assert_eq!(assignment.secondary_clusters, vec![1]);
1375 }
1376
1377 #[test]
1378 fn test_serialization() {
1379 let dim = 16;
1380 let n = 50;
1381 let num_clusters = 4;
1382
1383 let mut rng = rand::rngs::StdRng::seed_from_u64(789);
1384 let vectors: Vec<Vec<f32>> = (0..n)
1385 .map(|_| (0..dim).map(|_| rng.random::<f32>()).collect())
1386 .collect();
1387
1388 let config = CoarseConfig::new(dim, num_clusters);
1389 let centroids = CoarseCentroids::train(&config, &vectors);
1390
1391 let bytes = bincode::serde::encode_to_vec(¢roids, bincode::config::standard()).unwrap();
1393 let (loaded, consumed): (CoarseCentroids, usize) =
1394 bincode::serde::decode_from_slice(&bytes, bincode::config::standard()).unwrap();
1395 assert_eq!(consumed, bytes.len());
1396
1397 assert_eq!(loaded.num_clusters, centroids.num_clusters);
1398 assert_eq!(loaded.dim, centroids.dim);
1399 assert_eq!(loaded.centroids.len(), centroids.centroids.len());
1400 }
1401
1402 #[test]
1403 fn persisted_hnsw_and_two_level_routers_are_valid() {
1404 let dim = 4;
1405 let mut rng = rand::rngs::StdRng::seed_from_u64(991);
1406 let vectors: Vec<Vec<f32>> = (0..256)
1407 .map(|_| (0..dim).map(|_| rng.random::<f32>()).collect())
1408 .collect();
1409
1410 for routing in [IvfRoutingMode::Hnsw, IvfRoutingMode::TwoLevel] {
1411 let trained =
1412 CoarseCentroids::train(&CoarseConfig::new(dim, 16).with_routing(routing), &vectors);
1413 trained.validate_routing(routing).unwrap();
1414 let plan = trained.probe(&vectors[0], 8, routing);
1415 assert_eq!(plan.cluster_ids.len(), 8);
1416 assert!(
1417 plan.cluster_ids
1418 .iter()
1419 .all(|&cluster| cluster < trained.num_clusters)
1420 );
1421
1422 let bytes =
1423 bincode::serde::encode_to_vec(&trained, bincode::config::standard()).unwrap();
1424 let (loaded, consumed): (CoarseCentroids, usize) =
1425 bincode::serde::decode_from_slice(&bytes, bincode::config::standard()).unwrap();
1426 assert_eq!(consumed, bytes.len());
1427 loaded.validate_routing(routing).unwrap();
1428 }
1429 }
1430}