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