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, parent_probe_count,
11 routing_parent_count, select_best, select_best_candidates,
12};
13use super::soar::{MultiAssignment, SoarConfig};
14use crate::dsl::IvfRoutingMode;
15
16#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct CoarseConfig {
19 pub num_clusters: usize,
21 pub dim: usize,
23 pub max_iters: usize,
25 pub seed: u64,
27 pub soar: Option<SoarConfig>,
29 pub routing: IvfRoutingMode,
31}
32
33impl CoarseConfig {
34 pub fn new(dim: usize, num_clusters: usize) -> Self {
35 Self {
36 num_clusters,
37 dim,
38 max_iters: 25,
39 seed: 42,
40 soar: None,
41 routing: IvfRoutingMode::Auto,
42 }
43 }
44
45 pub fn with_soar(mut self, config: SoarConfig) -> Self {
46 self.soar = Some(config);
47 self
48 }
49
50 pub fn with_seed(mut self, seed: u64) -> Self {
51 self.seed = seed;
52 self
53 }
54
55 pub fn with_max_iters(mut self, iters: usize) -> Self {
56 self.max_iters = iters;
57 self
58 }
59
60 pub fn with_routing(mut self, routing: IvfRoutingMode) -> Self {
61 self.routing = routing;
62 self
63 }
64}
65
66#[derive(Debug, Clone, Serialize, Deserialize)]
68pub struct CoarseCentroids {
69 pub num_clusters: u32,
71 pub dim: usize,
73 pub centroids: Vec<f32>,
75 pub version: u64,
77 pub soar_config: Option<SoarConfig>,
79 pub(crate) routing_index: Option<FloatCentroidRouter>,
81}
82
83#[derive(Debug, Clone, Serialize, Deserialize)]
84pub(crate) enum FloatCentroidRouter {
85 TwoLevel {
86 parent_centroids: Vec<f32>,
87 topology: IvfRoutingTopology,
88 },
89 Hnsw(HnswRoutingGraph),
90}
91
92impl CoarseCentroids {
93 pub fn train(config: &CoarseConfig, vectors: &[Vec<f32>]) -> Self {
97 assert!(!vectors.is_empty(), "Cannot train on empty vector set");
98 assert!(config.num_clusters > 0, "Need at least 1 cluster");
99 assert!(vectors.iter().all(|vector| vector.len() == config.dim));
100
101 let actual_clusters = config.num_clusters.min(vectors.len());
102 let (centroids, routing_index) =
103 match effective_routing_mode(config.routing, actual_clusters) {
104 IvfRoutingMode::TwoLevel => {
105 let (leaves, router) =
106 Self::train_hierarchical(config, vectors, actual_clusters);
107 (leaves, Some(router))
108 }
109 IvfRoutingMode::Hnsw => {
110 let leaves = if actual_clusters >= HNSW_AUTO_THRESHOLD {
111 Self::train_hierarchical(config, vectors, actual_clusters).0
112 } else {
113 Self::train_flat(config, vectors, actual_clusters).0
114 };
115 let graph = HnswRoutingGraph::build(
116 actual_clusters,
117 |left, right| {
118 let left = left as usize * config.dim;
119 let right = right as usize * config.dim;
120 squared_l2(
121 &leaves[left..left + config.dim],
122 &leaves[right..right + config.dim],
123 )
124 },
125 config.seed,
126 );
127 (leaves, Some(FloatCentroidRouter::Hnsw(graph)))
128 }
129 IvfRoutingMode::Flat | IvfRoutingMode::Auto => {
130 (Self::train_flat(config, vectors, actual_clusters).0, None)
131 }
132 };
133
134 let version = std::time::SystemTime::now()
135 .duration_since(std::time::UNIX_EPOCH)
136 .unwrap_or_default()
137 .as_nanos() as u64;
138
139 Self {
140 num_clusters: actual_clusters as u32,
141 dim: config.dim,
142 centroids,
143 version,
144 soar_config: config.soar.clone(),
145 routing_index,
146 }
147 }
148
149 fn train_flat(
150 config: &CoarseConfig,
151 vectors: &[Vec<f32>],
152 clusters: usize,
153 ) -> (Vec<f32>, Vec<Vec<usize>>) {
154 let dim = config.dim;
155 let flat: Vec<f32> = vectors.iter().flat_map(|v| v.iter().copied()).collect();
156 let trained = crate::structures::vector::kmeans::train_euclidean_kmeans(
157 &flat,
158 vectors.len(),
159 dim,
160 clusters,
161 config.max_iters,
162 config.seed,
163 );
164 let mut groups = vec![Vec::new(); clusters];
165 for (point, cluster) in trained.assignments.into_iter().enumerate() {
166 groups[cluster].push(point);
167 }
168 (trained.centroids, groups)
169 }
170
171 fn train_hierarchical(
172 config: &CoarseConfig,
173 vectors: &[Vec<f32>],
174 leaf_count: usize,
175 ) -> (Vec<f32>, FloatCentroidRouter) {
176 let parent_count = routing_parent_count(leaf_count).min(vectors.len());
177 let mut parent_config = config.clone();
178 parent_config.routing = IvfRoutingMode::Flat;
179 parent_config.num_clusters = parent_count;
180 let (parent_centroids, groups) = Self::train_flat(&parent_config, vectors, parent_count);
181 let group_sizes: Vec<usize> = groups.iter().map(Vec::len).collect();
182 let child_counts = allocate_child_clusters(&group_sizes, leaf_count);
183 let mut leaves = Vec::with_capacity(leaf_count.saturating_mul(config.dim));
184 let mut children = vec![Vec::new(); parent_count];
185
186 for (parent, (indices, &child_count)) in groups.iter().zip(&child_counts).enumerate() {
187 if child_count == 0 {
188 continue;
189 }
190 let group_vectors: Vec<Vec<f32>> = indices
191 .iter()
192 .map(|&index| vectors[index].clone())
193 .collect();
194 let mut child_config = config.clone();
195 child_config.routing = IvfRoutingMode::Flat;
196 child_config.num_clusters = child_count;
197 child_config.seed = config.seed ^ (parent as u64).wrapping_mul(0x9e37_79b9_7f4a_7c15);
198 let first_leaf = leaves.len() / config.dim;
199 leaves
200 .extend_from_slice(&Self::train_flat(&child_config, &group_vectors, child_count).0);
201 children[parent].extend((first_leaf..first_leaf + child_count).map(|leaf| leaf as u32));
202 }
203 debug_assert_eq!(leaves.len(), leaf_count * config.dim);
204
205 (
206 leaves,
207 FloatCentroidRouter::TwoLevel {
208 parent_centroids,
209 topology: IvfRoutingTopology::from_children(&children),
210 },
211 )
212 }
213
214 fn find_nearest_idx_static(vector: &[f32], centroids: &[f32], dim: usize) -> usize {
216 let num_clusters = centroids.len() / dim;
217 let mut best_idx = 0;
218 let mut best_dist = f32::MAX;
219
220 for c in 0..num_clusters {
221 let offset = c * dim;
222 let dist: f32 = vector
223 .iter()
224 .zip(¢roids[offset..offset + dim])
225 .map(|(&a, &b)| (a - b) * (a - b))
226 .sum();
227
228 if dist < best_dist {
229 best_dist = dist;
230 best_idx = c;
231 }
232 }
233
234 best_idx
235 }
236
237 pub fn find_nearest(&self, vector: &[f32]) -> u32 {
239 Self::find_nearest_idx_static(vector, &self.centroids, self.dim) as u32
240 }
241
242 pub fn find_k_nearest(&self, vector: &[f32], k: usize) -> Vec<u32> {
244 let mut distances: Vec<(u32, f32)> = (0..self.num_clusters)
245 .map(|c| {
246 let offset = c as usize * self.dim;
247 let dist: f32 = vector
248 .iter()
249 .zip(&self.centroids[offset..offset + self.dim])
250 .map(|(&a, &b)| (a - b) * (a - b))
251 .sum();
252 (c, dist)
253 })
254 .collect();
255
256 if distances.len() > k {
258 distances.select_nth_unstable_by(k, |a, b| a.1.total_cmp(&b.1));
259 distances.truncate(k);
260 }
261 distances.sort_unstable_by(|a, b| a.1.total_cmp(&b.1));
262 distances.into_iter().map(|(c, _)| c).collect()
263 }
264
265 pub fn probe(&self, vector: &[f32], k: usize, mode: IvfRoutingMode) -> IvfProbePlan {
270 let take = k.clamp(1, self.num_clusters as usize);
271 let clusters = match effective_routing_mode(mode, self.num_clusters as usize) {
272 IvfRoutingMode::Flat | IvfRoutingMode::Auto => self.find_k_nearest(vector, take),
273 IvfRoutingMode::TwoLevel => self.find_k_nearest_two_level(vector, take),
274 IvfRoutingMode::Hnsw => self.find_k_nearest_hnsw(vector, take),
275 };
276 IvfProbePlan::new(
277 self.version,
278 float_probe_fingerprint(vector, take, mode),
279 clusters,
280 )
281 }
282
283 pub fn validate_routing(&self, mode: IvfRoutingMode) -> Result<(), String> {
284 match effective_routing_mode(mode, self.num_clusters as usize) {
285 IvfRoutingMode::Flat | IvfRoutingMode::Auto => Ok(()),
286 IvfRoutingMode::TwoLevel => {
287 let Some(FloatCentroidRouter::TwoLevel {
288 parent_centroids,
289 topology,
290 }) = self.routing_index.as_ref()
291 else {
292 return Err(
293 "two-level IVF routing was requested but the global codebook has no matching router"
294 .to_string(),
295 );
296 };
297 let parent_count = topology.parent_count();
298 if parent_count == 0
299 || parent_centroids.len() != parent_count.saturating_mul(self.dim)
300 || !topology.validate(self.num_clusters as usize)
301 || parent_centroids.iter().any(|value| !value.is_finite())
302 {
303 return Err("invalid float two-level IVF routing index".to_string());
304 }
305 Ok(())
306 }
307 IvfRoutingMode::Hnsw => {
308 let Some(FloatCentroidRouter::Hnsw(graph)) = self.routing_index.as_ref() else {
309 return Err(
310 "HNSW IVF routing was requested but the global codebook has no HNSW graph"
311 .to_string(),
312 );
313 };
314 if !graph.validate(self.num_clusters as usize) {
315 return Err("invalid float HNSW routing graph".to_string());
316 }
317 Ok(())
318 }
319 }
320 }
321
322 fn find_k_nearest_two_level(&self, vector: &[f32], k: usize) -> Vec<u32> {
323 let Some(FloatCentroidRouter::TwoLevel {
324 parent_centroids,
325 topology,
326 }) = self.routing_index.as_ref()
327 else {
328 return self.find_k_nearest(vector, k);
329 };
330 if topology.parent_count() <= 1 {
331 return self.find_k_nearest(vector, k);
332 }
333
334 let mut parent_scores = vec![0.0; topology.parent_count()];
335 for (parent_id, score) in parent_scores.iter_mut().enumerate() {
336 let offset = parent_id * self.dim;
337 *score = squared_l2(vector, &parent_centroids[offset..offset + self.dim]);
338 }
339 let parent_take =
340 parent_probe_count(k, self.num_clusters as usize, topology.parent_count());
341 let parents = select_best::<false>(&parent_scores, parent_take);
342 let candidate_capacity = parents
343 .iter()
344 .map(|&parent| topology.children(parent as usize).len())
345 .sum();
346 let mut candidates = Vec::with_capacity(candidate_capacity);
347 for parent in parents {
348 for &leaf in topology.children(parent as usize) {
349 candidates.push((leaf, squared_l2(vector, self.get_centroid(leaf))));
350 }
351 }
352 select_best_candidates::<false>(&mut candidates, k)
353 }
354
355 fn find_k_nearest_hnsw(&self, vector: &[f32], k: usize) -> Vec<u32> {
356 let Some(FloatCentroidRouter::Hnsw(graph)) = self.routing_index.as_ref() else {
357 return self.find_k_nearest(vector, k);
358 };
359 graph.search(|leaf| squared_l2(vector, self.get_centroid(leaf)), k)
360 }
361
362 fn find_nearest_hnsw(&self, vector: &[f32]) -> u32 {
363 let Some(FloatCentroidRouter::Hnsw(graph)) = self.routing_index.as_ref() else {
364 return self.find_nearest(vector);
365 };
366 graph.search_one(|leaf| squared_l2(vector, self.get_centroid(leaf)))
367 }
368
369 pub fn find_k_nearest_with_distances(&self, vector: &[f32], k: usize) -> Vec<(u32, f32)> {
371 let mut distances: Vec<(u32, f32)> = (0..self.num_clusters)
372 .map(|c| {
373 let offset = c as usize * self.dim;
374 let dist: f32 = vector
375 .iter()
376 .zip(&self.centroids[offset..offset + self.dim])
377 .map(|(&a, &b)| (a - b) * (a - b))
378 .sum();
379 (c, dist)
380 })
381 .collect();
382
383 if distances.len() > k {
385 distances.select_nth_unstable_by(k, |a, b| a.1.total_cmp(&b.1));
386 distances.truncate(k);
387 }
388 distances.sort_unstable_by(|a, b| a.1.total_cmp(&b.1));
389 distances
390 }
391
392 pub fn assign(&self, vector: &[f32]) -> MultiAssignment {
394 self.assign_with_routing(vector, IvfRoutingMode::Flat)
395 }
396
397 pub fn assign_with_routing(&self, vector: &[f32], routing: IvfRoutingMode) -> MultiAssignment {
401 if let Some(ref soar_config) = self.soar_config {
402 self.assign_with_soar_and_routing(vector, soar_config, routing)
403 } else {
404 let primary_cluster = match effective_routing_mode(routing, self.num_clusters as usize)
405 {
406 IvfRoutingMode::Hnsw => self.find_nearest_hnsw(vector),
407 IvfRoutingMode::TwoLevel => self.find_k_nearest_two_level(vector, 1)[0],
408 IvfRoutingMode::Flat | IvfRoutingMode::Auto => self.find_nearest(vector),
409 };
410 MultiAssignment {
411 primary_cluster,
412 secondary_clusters: Vec::new(),
413 }
414 }
415 }
416
417 pub fn assign_with_soar(&self, vector: &[f32], config: &SoarConfig) -> MultiAssignment {
419 self.assign_with_soar_and_routing(vector, config, IvfRoutingMode::Flat)
420 }
421
422 fn assign_with_soar_and_routing(
423 &self,
424 vector: &[f32],
425 config: &SoarConfig,
426 routing: IvfRoutingMode,
427 ) -> MultiAssignment {
428 let leaf_ids: Vec<u32> = match effective_routing_mode(routing, self.num_clusters as usize) {
429 IvfRoutingMode::TwoLevel => {
430 self.two_level_candidate_leaves(vector, config.num_secondary + 1)
431 }
432 IvfRoutingMode::Hnsw => self.find_k_nearest_hnsw(
433 vector,
434 (config.num_secondary + 1)
435 .saturating_mul(16)
436 .max(32)
437 .min(self.num_clusters as usize),
438 ),
439 IvfRoutingMode::Flat | IvfRoutingMode::Auto => (0..self.num_clusters).collect(),
440 };
441 let primary = leaf_ids
442 .iter()
443 .copied()
444 .min_by(|&left, &right| {
445 squared_l2(vector, self.get_centroid(left))
446 .total_cmp(&squared_l2(vector, self.get_centroid(right)))
447 .then_with(|| left.cmp(&right))
448 })
449 .unwrap_or(0);
450 let primary_centroid = self.get_centroid(primary);
451
452 let residual: Vec<f32> = vector
454 .iter()
455 .zip(primary_centroid)
456 .map(|(v, c)| v - c)
457 .collect();
458
459 let residual_norm_sq: f32 = residual.iter().map(|x| x * x).sum();
460
461 if config.selective && residual_norm_sq < config.spill_threshold * config.spill_threshold {
463 return MultiAssignment {
464 primary_cluster: primary,
465 secondary_clusters: Vec::new(),
466 };
467 }
468
469 let mut candidates: Vec<(u32, f32)> = leaf_ids
471 .into_iter()
472 .filter(|&c| c != primary)
473 .map(|c| {
474 let centroid = self.get_centroid(c);
475 let dot: f32 = vector
478 .iter()
479 .zip(centroid)
480 .zip(&residual)
481 .map(|((v, c), r)| (v - c) * r)
482 .sum();
483 (c, dot.abs())
484 })
485 .collect();
486
487 let take = config.num_secondary.min(candidates.len());
489 if candidates.len() > take {
490 candidates.select_nth_unstable_by(take, |a, b| a.1.total_cmp(&b.1));
491 candidates.truncate(take);
492 }
493
494 MultiAssignment {
495 primary_cluster: primary,
496 secondary_clusters: candidates
497 .iter()
498 .take(config.num_secondary)
499 .map(|(c, _)| *c)
500 .collect(),
501 }
502 }
503
504 fn two_level_candidate_leaves(&self, vector: &[f32], k: usize) -> Vec<u32> {
505 let Some(FloatCentroidRouter::TwoLevel {
506 parent_centroids,
507 topology,
508 }) = self.routing_index.as_ref()
509 else {
510 return (0..self.num_clusters).collect();
511 };
512 let mut parent_scores = vec![0.0; topology.parent_count()];
513 for (parent_id, score) in parent_scores.iter_mut().enumerate() {
514 let offset = parent_id * self.dim;
515 *score = squared_l2(vector, &parent_centroids[offset..offset + self.dim]);
516 }
517 let parents = select_best::<false>(
518 &parent_scores,
519 parent_probe_count(k, self.num_clusters as usize, topology.parent_count()),
520 );
521 let capacity = parents
522 .iter()
523 .map(|&parent| topology.children(parent as usize).len())
524 .sum();
525 let mut leaves = Vec::with_capacity(capacity);
526 for parent in parents {
527 leaves.extend_from_slice(topology.children(parent as usize));
528 }
529 leaves
530 }
531
532 pub fn get_centroid(&self, cluster_id: u32) -> &[f32] {
534 let offset = cluster_id as usize * self.dim;
535 &self.centroids[offset..offset + self.dim]
536 }
537
538 pub fn compute_residual(&self, vector: &[f32], cluster_id: u32) -> Vec<f32> {
540 let centroid = self.get_centroid(cluster_id);
541 vector.iter().zip(centroid).map(|(&v, &c)| v - c).collect()
542 }
543
544 pub fn size_bytes(&self) -> usize {
546 let routing_bytes = self
547 .routing_index
548 .as_ref()
549 .map_or(0, |router| match router {
550 FloatCentroidRouter::TwoLevel {
551 parent_centroids,
552 topology,
553 } => {
554 parent_centroids.len() * size_of::<f32>()
555 + topology.parent_count() * size_of::<u32>()
556 + self.num_clusters as usize * size_of::<u32>()
557 }
558 FloatCentroidRouter::Hnsw(graph) => graph.size_bytes(),
559 });
560 self.centroids.len() * size_of::<f32>() + routing_bytes + 64
561 }
562
563 #[cfg(feature = "native")]
566 pub(crate) fn visit_routing_regions(&self, visit: &mut dyn FnMut(&'static str, &[u8])) {
567 if let Some(router) = &self.routing_index {
568 match router {
569 FloatCentroidRouter::TwoLevel {
570 parent_centroids,
571 topology,
572 } => {
573 topology.visit_resident_regions(visit);
574 visit(
575 "float parent centroids",
576 super::routing::bytes_of_slice(parent_centroids),
577 );
578 }
579 FloatCentroidRouter::Hnsw(graph) => graph.visit_resident_regions(visit),
580 }
581 }
582 }
583
584 #[cfg(feature = "native")]
585 pub(crate) fn visit_leaf_centroid_region(&self, visit: &mut dyn FnMut(&'static str, &[u8])) {
586 visit(
587 "float leaf centroids",
588 super::routing::bytes_of_slice(&self.centroids),
589 );
590 }
591
592 pub fn to_bytes(&self) -> std::io::Result<Vec<u8>> {
594 bincode::serde::encode_to_vec(self, bincode::config::standard())
595 .map_err(|error| std::io::Error::new(std::io::ErrorKind::InvalidData, error))
596 }
597}
598
599#[inline]
600fn squared_l2(left: &[f32], right: &[f32]) -> f32 {
601 left.iter()
602 .zip(right)
603 .map(|(&a, &b)| {
604 let delta = a - b;
605 delta * delta
606 })
607 .sum()
608}
609
610#[cfg(test)]
611mod tests {
612 use super::*;
613 use rand::prelude::*;
614
615 #[test]
616 fn test_coarse_centroids_basic() {
617 let dim = 64;
618 let n = 1000;
619 let num_clusters = 16;
620
621 let mut rng = rand::rngs::StdRng::seed_from_u64(42);
622 let vectors: Vec<Vec<f32>> = (0..n)
623 .map(|_| (0..dim).map(|_| rng.random::<f32>() - 0.5).collect())
624 .collect();
625
626 let config = CoarseConfig::new(dim, num_clusters);
627 let centroids = CoarseCentroids::train(&config, &vectors);
628
629 assert_eq!(centroids.num_clusters, num_clusters as u32);
630 assert_eq!(centroids.dim, dim);
631 }
632
633 #[test]
634 fn test_find_nearest() {
635 let dim = 32;
636 let n = 500;
637 let num_clusters = 8;
638
639 let mut rng = rand::rngs::StdRng::seed_from_u64(123);
640 let vectors: Vec<Vec<f32>> = (0..n)
641 .map(|_| (0..dim).map(|_| rng.random::<f32>()).collect())
642 .collect();
643
644 let config = CoarseConfig::new(dim, num_clusters);
645 let centroids = CoarseCentroids::train(&config, &vectors);
646
647 for v in &vectors {
649 let cluster = centroids.find_nearest(v);
650 assert!(cluster < centroids.num_clusters);
651 }
652 }
653
654 #[test]
655 fn test_soar_assignment() {
656 let dim = 32;
657 let n = 100;
658 let num_clusters = 8;
659
660 let mut rng = rand::rngs::StdRng::seed_from_u64(456);
661 let vectors: Vec<Vec<f32>> = (0..n)
662 .map(|_| (0..dim).map(|_| rng.random::<f32>()).collect())
663 .collect();
664
665 let soar_config = SoarConfig {
666 num_secondary: 2,
667 selective: false,
668 spill_threshold: 0.0,
669 };
670 let config = CoarseConfig::new(dim, num_clusters).with_soar(soar_config);
671 let centroids = CoarseCentroids::train(&config, &vectors);
672
673 let assignment = centroids.assign(&vectors[0]);
675 assert!(assignment.primary_cluster < centroids.num_clusters);
676 assert_eq!(assignment.secondary_clusters.len(), 2);
677
678 for &sec in &assignment.secondary_clusters {
680 assert_ne!(sec, assignment.primary_cluster);
681 }
682 }
683
684 #[test]
685 fn test_serialization() {
686 let dim = 16;
687 let n = 50;
688 let num_clusters = 4;
689
690 let mut rng = rand::rngs::StdRng::seed_from_u64(789);
691 let vectors: Vec<Vec<f32>> = (0..n)
692 .map(|_| (0..dim).map(|_| rng.random::<f32>()).collect())
693 .collect();
694
695 let config = CoarseConfig::new(dim, num_clusters);
696 let centroids = CoarseCentroids::train(&config, &vectors);
697
698 let bytes = bincode::serde::encode_to_vec(¢roids, bincode::config::standard()).unwrap();
700 let (loaded, consumed): (CoarseCentroids, usize) =
701 bincode::serde::decode_from_slice(&bytes, bincode::config::standard()).unwrap();
702 assert_eq!(consumed, bytes.len());
703
704 assert_eq!(loaded.num_clusters, centroids.num_clusters);
705 assert_eq!(loaded.dim, centroids.dim);
706 assert_eq!(loaded.centroids.len(), centroids.centroids.len());
707 }
708
709 #[test]
710 fn persisted_hnsw_and_two_level_routers_are_valid() {
711 let dim = 4;
712 let mut rng = rand::rngs::StdRng::seed_from_u64(991);
713 let vectors: Vec<Vec<f32>> = (0..256)
714 .map(|_| (0..dim).map(|_| rng.random::<f32>()).collect())
715 .collect();
716
717 for routing in [IvfRoutingMode::Hnsw, IvfRoutingMode::TwoLevel] {
718 let trained =
719 CoarseCentroids::train(&CoarseConfig::new(dim, 16).with_routing(routing), &vectors);
720 trained.validate_routing(routing).unwrap();
721 let plan = trained.probe(&vectors[0], 8, routing);
722 assert_eq!(plan.cluster_ids.len(), 8);
723 assert!(
724 plan.cluster_ids
725 .iter()
726 .all(|&cluster| cluster < trained.num_clusters)
727 );
728
729 let bytes =
730 bincode::serde::encode_to_vec(&trained, bincode::config::standard()).unwrap();
731 let (loaded, consumed): (CoarseCentroids, usize) =
732 bincode::serde::decode_from_slice(&bytes, bincode::config::standard()).unwrap();
733 assert_eq!(consumed, bytes.len());
734 loaded.validate_routing(routing).unwrap();
735 }
736 }
737}