1use std::cmp::Reverse;
10use std::collections::BinaryHeap;
11use std::ops::Range;
12
13use rand::SeedableRng;
14
15use super::{
16 MAX_SCANN_TREE_LEVELS, MIN_PARTITION_TRAINING_POINTS_PER_LEAF, MIN_POINTS_FOR_PARTITIONING,
17 ScannConfig, ScannEncoding, ScannFormatError, ScannGeometry, ScannLeafRun, ScannResult,
18 ScannRoutingLevel, ScannSegmentPayload, ScannTrainedArtifact, ScannTrainedArtifactView,
19 ScannTrainingState, desired_training_sample,
20};
21use crate::dsl::IvfRoutingMode;
22use crate::structures::simd::HammingKernel;
23use crate::structures::vector::index::{BinaryIvfConfig, train_binary_k_majority_codebook};
24use crate::structures::vector::ivf::SoarConfig;
25use crate::structures::vector::ivf::routing::allocate_child_clusters;
26
27const HAMMING_SCAN_BLOCK: usize = 1_024;
28const MAX_LOCAL_K_MAJORITY_BRANCHES: usize = 64;
29const BINARY_SPILL_ASSIGNMENT_CANDIDATES: usize = 8;
30
31#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
32pub struct BinaryScannTrainingStats {
33 pub splits: usize,
34 pub max_split_clusters: usize,
35 pub max_depth: usize,
36 pub retained_groups: usize,
37 pub max_materialized_training_bytes: usize,
40}
41
42#[derive(Clone, Debug)]
47pub struct BinaryScannTraining {
48 pub dim_bits: u32,
49 pub geometry: ScannGeometry,
50 pub train_iters: usize,
51 pub seed: u64,
52}
53
54impl BinaryScannTraining {
55 pub fn validate(&self) -> ScannResult<()> {
56 if self.dim_bits == 0 || !self.dim_bits.is_multiple_of(8) {
57 return Err(ScannFormatError::new(
58 "binary ScaNN dimension must be a positive multiple of eight bits",
59 ));
60 }
61 let levels = usize::from(self.geometry.centroid_levels);
62 if levels == 0
63 || self.geometry.centroid_levels > MAX_SCANN_TREE_LEVELS
64 || self.geometry.level_counts.len() != levels
65 || self.geometry.level_counts.last().copied() != Some(self.geometry.num_leaves)
66 || self.geometry.level_counts.contains(&0)
67 || self
68 .geometry
69 .level_counts
70 .windows(2)
71 .any(|counts| counts[0] > counts[1])
72 {
73 return Err(ScannFormatError::new(
74 "binary ScaNN needs a valid one-to-three-level cumulative geometry",
75 ));
76 }
77 if self.train_iters == 0 {
78 return Err(ScannFormatError::new(
79 "binary ScaNN k-majority iterations must be positive",
80 ));
81 }
82 Ok(())
83 }
84
85 pub fn training_state(&self, observed: u64) -> ScannResult<ScannTrainingState> {
89 self.validate()?;
90 let geometry_required = u64::from(self.geometry.num_leaves)
91 .checked_mul(MIN_PARTITION_TRAINING_POINTS_PER_LEAF)
92 .ok_or_else(|| {
93 ScannFormatError::new("binary ScaNN minimum training sample overflows u64")
94 })?;
95 let required = MIN_POINTS_FOR_PARTITIONING.max(geometry_required);
96 Ok(if observed < required {
97 ScannTrainingState::AwaitingData { observed, required }
98 } else {
99 ScannTrainingState::Ready { observed, required }
100 })
101 }
102
103 pub fn desired_training_vectors(&self, observed: u64) -> ScannResult<u64> {
104 self.validate()?;
105 Ok(desired_training_sample(observed, self.geometry.num_leaves))
106 }
107}
108
109#[derive(Clone, Debug)]
110struct BinaryRoutingLevel {
111 centroids: Vec<u8>,
113 parent_offsets: Vec<u32>,
115}
116
117#[derive(Clone, Debug)]
119pub struct BinaryScannModel {
120 dim_bits: u32,
121 num_leaves: u32,
122 levels: Vec<BinaryRoutingLevel>,
123 fingerprint: u64,
124}
125
126#[derive(Clone, Debug, PartialEq, Eq)]
127struct QuantizedBinaryRoutingLevel {
128 centroid_count: usize,
129 centroid_codes: Range<usize>,
130 parent_offsets: Vec<u32>,
131}
132
133#[derive(Clone, Debug, PartialEq, Eq)]
136pub struct QuantizedBinaryScannModel {
137 dim_bits: u32,
138 num_leaves: u32,
139 artifact_id: u64,
140 artifact_len: usize,
141 levels: Vec<QuantizedBinaryRoutingLevel>,
142 fingerprint: u64,
143}
144
145#[derive(Clone, Copy, Debug)]
146pub struct QuantizedBinaryScannModelView<'a> {
147 model: &'a QuantizedBinaryScannModel,
148 artifact_bytes: &'a [u8],
149}
150
151impl BinaryScannModel {
152 pub fn to_artifact(
153 &self,
154 generation: u64,
155 trained_vectors: u64,
156 ) -> ScannResult<ScannTrainedArtifact> {
157 self.validate()?;
158 let levels = self
159 .levels
160 .iter()
161 .enumerate()
162 .map(|(index, level)| ScannRoutingLevel {
163 centroid_count: (level.centroids.len() / self.byte_len()) as u32,
164 centroid_codes: level.centroids.clone(),
165 minimums: Vec::new(),
166 steps: Vec::new(),
167 child_offsets: self
168 .levels
169 .get(index + 1)
170 .map_or_else(Vec::new, |next| next.parent_offsets.clone()),
171 })
172 .collect();
173 ScannTrainedArtifact::new(
174 generation,
175 trained_vectors,
176 ScannConfig {
177 dimension: self.dim_bits,
178 tree_levels: self.levels.len() as u8,
179 num_leaves: self.num_leaves,
180 encoding: ScannEncoding::BinaryHamming,
181 },
182 levels,
183 None,
184 )
185 }
186
187 pub fn from_artifact(artifact: &ScannTrainedArtifact) -> ScannResult<Self> {
188 artifact.validate()?;
189 if artifact.config.encoding != ScannEncoding::BinaryHamming {
190 return Err(ScannFormatError::new(
191 "float ScaNN artifact cannot be opened as a binary model",
192 ));
193 }
194 let byte_len = artifact.config.dimension as usize / 8;
195 let mut levels = Vec::with_capacity(artifact.levels.len());
196 for (index, level) in artifact.levels.iter().enumerate() {
197 let parent_offsets = if index == 0 {
198 vec![0, level.centroid_count]
199 } else {
200 artifact.levels[index - 1].child_offsets.clone()
201 };
202 if level.centroid_codes.len() != level.centroid_count as usize * byte_len {
203 return Err(ScannFormatError::new(
204 "binary ScaNN artifact centroid plane is inconsistent",
205 ));
206 }
207 levels.push(BinaryRoutingLevel {
208 centroids: level.centroid_codes.clone(),
209 parent_offsets,
210 });
211 }
212 let mut model = Self {
213 dim_bits: artifact.config.dimension,
214 num_leaves: artifact.config.num_leaves,
215 levels,
216 fingerprint: 0,
217 };
218 model.fingerprint = model.compute_fingerprint();
219 model.validate()?;
220 Ok(model)
221 }
222
223 pub fn train(
224 training: &BinaryScannTraining,
225 codes: &[u8],
226 num_vectors: usize,
227 index_label: &str,
228 ) -> ScannResult<Self> {
229 Self::train_with_stats(training, codes, num_vectors, index_label).map(|(model, _)| model)
230 }
231
232 pub fn train_with_stats(
233 training: &BinaryScannTraining,
234 codes: &[u8],
235 num_vectors: usize,
236 index_label: &str,
237 ) -> ScannResult<(Self, BinaryScannTrainingStats)> {
238 training.validate()?;
239 match training.training_state(num_vectors as u64)? {
240 ScannTrainingState::AwaitingData { observed, required } => {
241 return Err(ScannFormatError::new(format!(
242 "binary ScaNN training deferred: geometry requires {required} vectors, observed {observed}"
243 )));
244 }
245 ScannTrainingState::Ready { .. } => {}
246 }
247 let byte_len = usize::try_from(training.dim_bits / 8)
248 .map_err(|_| ScannFormatError::new("binary ScaNN row size exceeds usize"))?;
249 let expected = num_vectors
250 .checked_mul(byte_len)
251 .ok_or_else(|| ScannFormatError::new("binary ScaNN training matrix overflows"))?;
252 if codes.len() != expected {
253 return Err(ScannFormatError::new(format!(
254 "binary ScaNN training matrix is truncated: expected {expected} bytes, got {}",
255 codes.len()
256 )));
257 }
258
259 let sample_count = usize::try_from(training.desired_training_vectors(num_vectors as u64)?)
260 .map_err(|_| ScannFormatError::new("binary ScaNN sample count exceeds usize"))?;
261 let mut groups = vec![deterministic_sample_rows(
262 num_vectors,
263 sample_count,
264 training.seed,
265 )];
266 let mut levels = Vec::with_capacity(training.geometry.level_counts.len());
267 let mut stats = BinaryScannTrainingStats::default();
268
269 for (level_index, &level_count) in training.geometry.level_counts.iter().enumerate() {
270 let group_sizes: Vec<usize> = groups.iter().map(BinaryTrainingRows::len).collect();
271 let child_counts = allocate_child_clusters(&group_sizes, level_count as usize);
272 if child_counts.iter().sum::<usize>() != level_count as usize {
273 return Err(ScannFormatError::new(format!(
274 "binary ScaNN geometry level {level_index} cannot allocate {level_count} centroids from {sample_count} samples"
275 )));
276 }
277
278 let mut parent_offsets = Vec::with_capacity(groups.len() + 1);
279 let centroid_bytes = (level_count as usize)
280 .checked_mul(byte_len)
281 .ok_or_else(|| ScannFormatError::new("binary ScaNN centroid matrix overflows"))?;
282 let mut centroids = Vec::with_capacity(centroid_bytes);
283 let mut next_groups = Vec::with_capacity(level_count as usize);
284 parent_offsets.push(0);
285 let current_groups = std::mem::take(&mut groups);
286 for (parent, (group, &children)) in
287 current_groups.into_iter().zip(&child_counts).enumerate()
288 {
289 if children > 0 {
290 let partition = train_binary_partition(
291 codes,
292 &group,
293 byte_len,
294 training.dim_bits,
295 children,
296 training.train_iters,
297 derived_seed(training.seed, level_index, parent),
298 0,
299 level_index + 1 < training.geometry.level_counts.len(),
300 index_label,
301 &mut stats,
302 )?;
303 centroids.extend_from_slice(&partition.centroids);
304 if level_index + 1 < training.geometry.level_counts.len() {
305 next_groups.extend(partition.groups);
306 }
307 }
308 parent_offsets.push(u32::try_from(centroids.len() / byte_len).map_err(|_| {
309 ScannFormatError::new("binary ScaNN centroid identifier exceeds u32")
310 })?);
311 }
312 debug_assert_eq!(centroids.len(), centroid_bytes);
313
314 let is_leaf_level = level_index + 1 == training.geometry.level_counts.len();
315 if !is_leaf_level {
316 groups = next_groups;
317 }
318 levels.push(BinaryRoutingLevel {
319 centroids,
320 parent_offsets,
321 });
322 }
323
324 let mut model = Self {
325 dim_bits: training.dim_bits,
326 num_leaves: training.geometry.num_leaves,
327 levels,
328 fingerprint: 0,
329 };
330 model.fingerprint = model.compute_fingerprint();
331 model.validate()?;
332 Ok((model, stats))
333 }
334
335 pub fn dim_bits(&self) -> u32 {
336 self.dim_bits
337 }
338
339 pub fn num_leaves(&self) -> u32 {
340 self.num_leaves
341 }
342
343 pub fn fingerprint(&self) -> u64 {
344 self.fingerprint
345 }
346
347 pub fn validate(&self) -> ScannResult<()> {
348 if self.dim_bits == 0
349 || !self.dim_bits.is_multiple_of(8)
350 || self.levels.is_empty()
351 || self.levels.len() > usize::from(MAX_SCANN_TREE_LEVELS)
352 {
353 return Err(ScannFormatError::new("invalid binary ScaNN model header"));
354 }
355 let byte_len = self.byte_len();
356 let mut parents = 1usize;
357 for level in &self.levels {
358 if level.parent_offsets.len() != parents + 1
359 || level.parent_offsets.first() != Some(&0)
360 || level
361 .parent_offsets
362 .windows(2)
363 .any(|pair| pair[0] > pair[1])
364 {
365 return Err(ScannFormatError::new(
366 "invalid binary ScaNN parent directory",
367 ));
368 }
369 let children = level.centroids.len() / byte_len;
370 if level.centroids.len() % byte_len != 0
371 || level.parent_offsets.last().copied() != Some(children as u32)
372 {
373 return Err(ScannFormatError::new(
374 "invalid binary ScaNN centroid matrix",
375 ));
376 }
377 parents = children;
378 }
379 if parents != self.num_leaves as usize || self.compute_fingerprint() != self.fingerprint {
380 return Err(ScannFormatError::new(
381 "binary ScaNN leaf count or fingerprint is inconsistent",
382 ));
383 }
384 Ok(())
385 }
386
387 pub fn probe(
390 &self,
391 query: &[u8],
392 nprobe: usize,
393 beam_width: usize,
394 scratch: &mut BinaryScannSearchScratch,
395 ) -> ScannResult<BinaryScannProbePlan> {
396 if query.len() != self.byte_len() {
397 return Err(ScannFormatError::new(
398 "binary ScaNN query dimension does not match the model",
399 ));
400 }
401 if nprobe == 0 || beam_width == 0 {
402 return Err(ScannFormatError::new(
403 "binary ScaNN nprobe and beam width must be positive",
404 ));
405 }
406 let kernel = HammingKernel::resolve();
407 scratch.frontier.clear();
408 scratch.frontier.push(0);
409
410 for (level_index, level) in self.levels.iter().enumerate() {
411 scratch.candidates.clear();
412 for &parent in &scratch.frontier {
413 let parent = parent as usize;
414 let start = level.parent_offsets[parent] as usize;
415 let end = level.parent_offsets[parent + 1] as usize;
416 let rows = end - start;
417 scratch.distances.clear();
418 scratch.distances.resize(rows, 0);
419 kernel.distances(
420 query,
421 &level.centroids[start * self.byte_len()..end * self.byte_len()],
422 self.byte_len(),
423 &mut scratch.distances,
424 );
425 scratch
426 .candidates
427 .extend(
428 scratch
429 .distances
430 .iter()
431 .enumerate()
432 .map(|(local, &distance)| RouteCandidate {
433 node: (start + local) as u32,
434 distance,
435 }),
436 );
437 }
438 scratch.candidates.sort_unstable();
439 let width = if level_index + 1 == self.levels.len() {
440 nprobe.min(self.num_leaves as usize)
441 } else {
442 super::routing_prefix_for_child_coverage(
443 &scratch.candidates,
444 &self.levels[level_index + 1].parent_offsets,
445 beam_width,
446 nprobe,
447 |candidate| candidate.node as usize,
448 )
449 };
450 scratch.frontier.clear();
451 scratch.frontier.extend(
452 scratch
453 .candidates
454 .iter()
455 .take(width)
456 .map(|candidate| candidate.node),
457 );
458 if scratch.frontier.is_empty() {
459 return Err(ScannFormatError::new(
460 "binary ScaNN routing reached an empty branch",
461 ));
462 }
463 }
464 Ok(BinaryScannProbePlan {
465 model_fingerprint: self.fingerprint,
466 leaf_ids: scratch.frontier.clone(),
467 })
468 }
469
470 pub fn assign(&self, code: &[u8], scratch: &mut BinaryScannSearchScratch) -> ScannResult<u32> {
471 self.probe(code, 1, 1, scratch)?
472 .leaf_ids
473 .first()
474 .copied()
475 .ok_or_else(|| ScannFormatError::new("binary ScaNN assignment returned no leaf"))
476 }
477
478 pub fn spill_assignment(
483 &self,
484 code: &[u8],
485 scratch: &mut BinaryScannSearchScratch,
486 ) -> ScannResult<BinaryScannSpillAssignment> {
487 let primary_leaf = self.assign(code, scratch)?;
488 let candidate_count = BINARY_SPILL_ASSIGNMENT_CANDIDATES
489 .min(self.num_leaves as usize)
490 .max(1);
491 let plan = self.probe(code, candidate_count, candidate_count, scratch)?;
492 let kernel = HammingKernel::resolve();
493 let leaf_centroids = &self
494 .levels
495 .last()
496 .expect("validated binary ScaNN model has a terminal level")
497 .centroids;
498 let centroid = |leaf_id: u32| {
499 let start = leaf_id as usize * self.byte_len();
500 &leaf_centroids[start..start + self.byte_len()]
501 };
502 let primary_distance = kernel.distance(code, centroid(primary_leaf));
503 let secondary_leaf = plan
504 .leaf_ids
505 .into_iter()
506 .filter(|&leaf_id| leaf_id != primary_leaf)
507 .map(|leaf_id| (kernel.distance(code, centroid(leaf_id)), leaf_id))
508 .min()
509 .map(|(_, leaf_id)| leaf_id);
510 Ok(BinaryScannSpillAssignment {
511 primary_leaf,
512 secondary_leaf,
513 primary_distance,
514 })
515 }
516
517 pub fn search_segments(
520 &self,
521 query: &[u8],
522 k: usize,
523 nprobe: usize,
524 beam_width: usize,
525 segments: &[(&BinaryScannSegment, u32)],
526 scratch: &mut BinaryScannSearchScratch,
527 ) -> ScannResult<Vec<BinaryScannHit>> {
528 let plan = self.probe(query, nprobe, beam_width, scratch)?;
529 scratch.best_hit_keys.clear();
530 let mut best = BinaryHeap::with_capacity(k.min(8_192));
531 for &(segment, doc_base) in segments {
532 segment.validate_for(self)?;
533 segment.scan(query, &plan, doc_base, k, &mut best, scratch)?;
534 }
535 let mut hits = best.into_vec();
536 hits.sort_unstable();
537 Ok(hits)
538 }
539
540 fn byte_len(&self) -> usize {
541 self.dim_bits as usize / 8
542 }
543
544 fn compute_fingerprint(&self) -> u64 {
545 let mut hash = Fingerprint::new();
546 hash.write(&self.dim_bits.to_le_bytes());
547 hash.write(&self.num_leaves.to_le_bytes());
548 hash.write(&(self.levels.len() as u32).to_le_bytes());
549 for level in &self.levels {
550 for offset in &level.parent_offsets {
551 hash.write(&offset.to_le_bytes());
552 }
553 hash.write(&level.centroids);
554 }
555 hash.finish()
556 }
557}
558
559impl QuantizedBinaryScannModel {
560 pub fn from_artifact_view(artifact: &ScannTrainedArtifactView<'_>) -> ScannResult<Self> {
561 if artifact.config.encoding != ScannEncoding::BinaryHamming {
562 return Err(ScannFormatError::new(
563 "float ScaNN artifact cannot be opened as a binary mmap model",
564 ));
565 }
566 let byte_len = artifact.config.dimension as usize / 8;
567 let mut levels = Vec::with_capacity(artifact.level_count());
568 for index in 0..artifact.level_count() {
569 let level = artifact.level(index).ok_or_else(|| {
570 ScannFormatError::new("binary ScaNN artifact routing level disappeared")
571 })?;
572 let centroid_codes = artifact.level_centroid_codes_range(index).ok_or_else(|| {
573 ScannFormatError::new("binary ScaNN artifact centroid range disappeared")
574 })?;
575 if centroid_codes.len() != level.centroid_count as usize * byte_len {
576 return Err(ScannFormatError::new(
577 "binary ScaNN artifact centroid plane is inconsistent",
578 ));
579 }
580 let parent_offsets = if index == 0 {
581 vec![0, level.centroid_count]
582 } else {
583 artifact
584 .level(index - 1)
585 .expect("previous validated routing level exists")
586 .child_offsets()
587 .collect()
588 };
589 levels.push(QuantizedBinaryRoutingLevel {
590 centroid_count: level.centroid_count as usize,
591 centroid_codes,
592 parent_offsets,
593 });
594 }
595 let mut model = Self {
596 dim_bits: artifact.config.dimension,
597 num_leaves: artifact.config.num_leaves,
598 artifact_id: artifact.artifact_id,
599 artifact_len: artifact.bytes().len(),
600 levels,
601 fingerprint: 0,
602 };
603 model.fingerprint = model.compute_fingerprint(artifact.bytes());
604 model.validate_metadata()?;
605 Ok(model)
606 }
607
608 pub fn view<'a>(
609 &'a self,
610 artifact_bytes: &'a [u8],
611 ) -> ScannResult<QuantizedBinaryScannModelView<'a>> {
612 let stored_id = artifact_bytes
613 .get(12..20)
614 .and_then(|bytes| <[u8; 8]>::try_from(bytes).ok())
615 .map(u64::from_le_bytes);
616 if artifact_bytes.len() != self.artifact_len || stored_id != Some(self.artifact_id) {
617 return Err(ScannFormatError::new(
618 "quantized binary ScaNN model was paired with a different artifact mapping",
619 ));
620 }
621 Ok(QuantizedBinaryScannModelView {
622 model: self,
623 artifact_bytes,
624 })
625 }
626
627 pub fn dim_bits(&self) -> u32 {
628 self.dim_bits
629 }
630
631 pub fn num_leaves(&self) -> u32 {
632 self.num_leaves
633 }
634
635 pub fn fingerprint(&self) -> u64 {
636 self.fingerprint
637 }
638
639 pub fn estimated_memory_bytes(&self) -> usize {
640 self.levels.iter().fold(0usize, |total, level| {
641 total.saturating_add(level.parent_offsets.len() * std::mem::size_of::<u32>())
642 })
643 }
644
645 fn byte_len(&self) -> usize {
646 self.dim_bits as usize / 8
647 }
648
649 fn validate_metadata(&self) -> ScannResult<()> {
650 if self.dim_bits == 0
651 || !self.dim_bits.is_multiple_of(8)
652 || self.levels.is_empty()
653 || self.levels.len() > usize::from(MAX_SCANN_TREE_LEVELS)
654 || self.levels.last().map(|level| level.centroid_count)
655 != Some(self.num_leaves as usize)
656 {
657 return Err(ScannFormatError::new(
658 "invalid quantized binary ScaNN model header",
659 ));
660 }
661 let mut parents = 1usize;
662 for level in &self.levels {
663 if level.centroid_codes.end > self.artifact_len
664 || level.centroid_codes.len()
665 != level.centroid_count.saturating_mul(self.byte_len())
666 || level.parent_offsets.len() != parents + 1
667 || level.parent_offsets.first() != Some(&0)
668 || level.parent_offsets.last().copied() != Some(level.centroid_count as u32)
669 || level
670 .parent_offsets
671 .windows(2)
672 .any(|pair| pair[0] > pair[1])
673 {
674 return Err(ScannFormatError::new(
675 "invalid quantized binary ScaNN routing level",
676 ));
677 }
678 parents = level.centroid_count;
679 }
680 Ok(())
681 }
682
683 fn compute_fingerprint(&self, artifact_bytes: &[u8]) -> u64 {
684 let mut hash = Fingerprint::new();
685 hash.write(&self.dim_bits.to_le_bytes());
686 hash.write(&self.num_leaves.to_le_bytes());
687 hash.write(&(self.levels.len() as u32).to_le_bytes());
688 for level in &self.levels {
689 for offset in &level.parent_offsets {
690 hash.write(&offset.to_le_bytes());
691 }
692 hash.write(&artifact_bytes[level.centroid_codes.clone()]);
693 }
694 hash.finish()
695 }
696}
697
698impl QuantizedBinaryScannModelView<'_> {
699 pub fn dim_bits(&self) -> u32 {
700 self.model.dim_bits
701 }
702
703 pub fn num_leaves(&self) -> u32 {
704 self.model.num_leaves
705 }
706
707 pub fn fingerprint(&self) -> u64 {
708 self.model.fingerprint
709 }
710
711 pub fn probe(
712 &self,
713 query: &[u8],
714 nprobe: usize,
715 beam_width: usize,
716 scratch: &mut BinaryScannSearchScratch,
717 ) -> ScannResult<BinaryScannProbePlan> {
718 if query.len() != self.model.byte_len() {
719 return Err(ScannFormatError::new(
720 "binary ScaNN query dimension does not match the model",
721 ));
722 }
723 if nprobe == 0 || beam_width == 0 {
724 return Err(ScannFormatError::new(
725 "binary ScaNN nprobe and beam width must be positive",
726 ));
727 }
728 let kernel = HammingKernel::resolve();
729 scratch.frontier.clear();
730 scratch.frontier.push(0);
731 for (level_index, level) in self.model.levels.iter().enumerate() {
732 scratch.candidates.clear();
733 let centroids = &self.artifact_bytes[level.centroid_codes.clone()];
734 for &parent in &scratch.frontier {
735 let parent = parent as usize;
736 let start = level.parent_offsets[parent] as usize;
737 let end = level.parent_offsets[parent + 1] as usize;
738 let rows = end - start;
739 scratch.distances.clear();
740 scratch.distances.resize(rows, 0);
741 kernel.distances(
742 query,
743 ¢roids[start * self.model.byte_len()..end * self.model.byte_len()],
744 self.model.byte_len(),
745 &mut scratch.distances,
746 );
747 scratch
748 .candidates
749 .extend(
750 scratch
751 .distances
752 .iter()
753 .enumerate()
754 .map(|(local, &distance)| RouteCandidate {
755 node: (start + local) as u32,
756 distance,
757 }),
758 );
759 }
760 scratch.candidates.sort_unstable();
761 let width = if level_index + 1 == self.model.levels.len() {
762 nprobe.min(self.model.num_leaves as usize)
763 } else {
764 super::routing_prefix_for_child_coverage(
765 &scratch.candidates,
766 &self.model.levels[level_index + 1].parent_offsets,
767 beam_width,
768 nprobe,
769 |candidate| candidate.node as usize,
770 )
771 };
772 scratch.frontier.clear();
773 scratch.frontier.extend(
774 scratch
775 .candidates
776 .iter()
777 .take(width)
778 .map(|candidate| candidate.node),
779 );
780 if scratch.frontier.is_empty() {
781 return Err(ScannFormatError::new(
782 "binary ScaNN routing reached an empty branch",
783 ));
784 }
785 }
786 Ok(BinaryScannProbePlan {
787 model_fingerprint: self.model.fingerprint,
788 leaf_ids: scratch.frontier.clone(),
789 })
790 }
791
792 pub fn assign(&self, code: &[u8], scratch: &mut BinaryScannSearchScratch) -> ScannResult<u32> {
793 self.probe(code, 1, 1, scratch)?
794 .leaf_ids
795 .first()
796 .copied()
797 .ok_or_else(|| ScannFormatError::new("binary ScaNN assignment returned no leaf"))
798 }
799
800 pub fn spill_assignment(
803 &self,
804 code: &[u8],
805 scratch: &mut BinaryScannSearchScratch,
806 ) -> ScannResult<BinaryScannSpillAssignment> {
807 let primary_leaf = self.assign(code, scratch)?;
808 let candidate_count = BINARY_SPILL_ASSIGNMENT_CANDIDATES
809 .min(self.model.num_leaves as usize)
810 .max(1);
811 let plan = self.probe(code, candidate_count, candidate_count, scratch)?;
812 let kernel = HammingKernel::resolve();
813 let terminal = self
814 .model
815 .levels
816 .last()
817 .expect("validated binary ScaNN model has a terminal level");
818 let leaf_centroids = &self.artifact_bytes[terminal.centroid_codes.clone()];
819 let centroid = |leaf_id: u32| {
820 let start = leaf_id as usize * self.model.byte_len();
821 &leaf_centroids[start..start + self.model.byte_len()]
822 };
823 let primary_distance = kernel.distance(code, centroid(primary_leaf));
824 let secondary_leaf = plan
825 .leaf_ids
826 .into_iter()
827 .filter(|&leaf_id| leaf_id != primary_leaf)
828 .map(|leaf_id| (kernel.distance(code, centroid(leaf_id)), leaf_id))
829 .min()
830 .map(|(_, leaf_id)| leaf_id);
831 Ok(BinaryScannSpillAssignment {
832 primary_leaf,
833 secondary_leaf,
834 primary_distance,
835 })
836 }
837}
838
839#[derive(Clone, Debug, Eq, PartialEq)]
840pub struct BinaryScannProbePlan {
841 pub model_fingerprint: u64,
842 pub leaf_ids: Vec<u32>,
843}
844
845#[derive(Clone, Copy, Debug, Eq, PartialEq)]
846struct RouteCandidate {
847 node: u32,
848 distance: u32,
849}
850
851impl Ord for RouteCandidate {
852 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
853 self.distance
854 .cmp(&other.distance)
855 .then_with(|| self.node.cmp(&other.node))
856 }
857}
858
859impl PartialOrd for RouteCandidate {
860 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
861 Some(self.cmp(other))
862 }
863}
864
865#[derive(Default, Debug)]
867pub struct BinaryScannSearchScratch {
868 frontier: Vec<u32>,
869 candidates: Vec<RouteCandidate>,
870 distances: Vec<u32>,
871 best_hit_keys: rustc_hash::FxHashSet<(u32, u16)>,
875}
876
877#[derive(Clone, Copy, Debug, Eq, PartialEq)]
880pub struct BinaryScannSpillAssignment {
881 pub primary_leaf: u32,
882 pub secondary_leaf: Option<u32>,
883 pub primary_distance: u32,
884}
885
886#[derive(Clone, Debug)]
887struct BinaryScannLeaf {
888 leaf_id: u32,
889 doc_ids: Vec<u32>,
890 ordinals: Vec<u16>,
891 codes: Vec<u8>,
892}
893
894fn push_binary_posting(
895 grouped: &mut rustc_hash::FxHashMap<u32, BinaryScannLeaf>,
896 leaf_id: u32,
897 doc_id: u32,
898 ordinal: u16,
899 code: &[u8],
900) {
901 let leaf = grouped.entry(leaf_id).or_insert_with(|| BinaryScannLeaf {
902 leaf_id,
903 doc_ids: Vec::new(),
904 ordinals: Vec::new(),
905 codes: Vec::new(),
906 });
907 leaf.doc_ids.push(doc_id);
908 leaf.ordinals.push(ordinal);
909 leaf.codes.extend_from_slice(code);
910}
911
912#[derive(Clone, Debug)]
914pub struct BinaryScannSegment {
915 dim_bits: u32,
916 model_fingerprint: u64,
917 num_leaves: u32,
918 leaves: Vec<BinaryScannLeaf>,
919 len: usize,
921 stored_len: usize,
923}
924
925impl BinaryScannSegment {
926 pub fn build(
927 model: &BinaryScannModel,
928 codes: &[u8],
929 doc_id_ordinals: &[(u32, u16)],
930 scratch: &mut BinaryScannSearchScratch,
931 ) -> ScannResult<Self> {
932 Self::build_internal(model, codes, doc_id_ordinals, None, scratch)
933 }
934
935 pub fn build_with_soar(
943 model: &BinaryScannModel,
944 codes: &[u8],
945 doc_id_ordinals: &[(u32, u16)],
946 soar: &SoarConfig,
947 scratch: &mut BinaryScannSearchScratch,
948 ) -> ScannResult<Self> {
949 Self::build_internal(model, codes, doc_id_ordinals, Some(soar), scratch)
950 }
951
952 fn build_internal(
953 model: &BinaryScannModel,
954 codes: &[u8],
955 doc_id_ordinals: &[(u32, u16)],
956 soar: Option<&SoarConfig>,
957 scratch: &mut BinaryScannSearchScratch,
958 ) -> ScannResult<Self> {
959 let expected = doc_id_ordinals
960 .len()
961 .checked_mul(model.byte_len())
962 .ok_or_else(|| ScannFormatError::new("binary ScaNN segment size overflows"))?;
963 if codes.len() != expected {
964 return Err(ScannFormatError::new(
965 "binary ScaNN segment code and label columns are inconsistent",
966 ));
967 }
968 if soar.is_some_and(|config| !config.spill_threshold.is_finite()) {
969 return Err(ScannFormatError::new(
970 "binary ScaNN spill threshold must be finite",
971 ));
972 }
973
974 let spill_enabled =
975 soar.is_some_and(|config| config.num_secondary > 0) && model.num_leaves > 1;
976 if !spill_enabled {
977 let mut grouped = rustc_hash::FxHashMap::<u32, BinaryScannLeaf>::default();
981 for (&(doc_id, ordinal), code) in doc_id_ordinals
982 .iter()
983 .zip(codes.chunks_exact(model.byte_len()))
984 {
985 let primary_leaf = model.assign(code, scratch)?;
986 push_binary_posting(&mut grouped, primary_leaf, doc_id, ordinal, code);
987 }
988 let mut leaves: Vec<_> = grouped.into_values().collect();
989 leaves.sort_unstable_by_key(|leaf| leaf.leaf_id);
990 let segment = Self {
991 dim_bits: model.dim_bits,
992 model_fingerprint: model.fingerprint,
993 num_leaves: model.num_leaves,
994 leaves,
995 len: doc_id_ordinals.len(),
996 stored_len: doc_id_ordinals.len(),
997 };
998 segment.validate_for(model)?;
999 return Ok(segment);
1000 }
1001
1002 let mut assignments = Vec::with_capacity(doc_id_ordinals.len());
1003 for code in codes.chunks_exact(model.byte_len()) {
1004 assignments.push(model.spill_assignment(code, scratch)?);
1005 }
1006
1007 if let Some(config) = soar {
1008 if let Some(target_fraction) = config.calibration_target() {
1009 let spill_budget = ((assignments.len() as f64 * f64::from(target_fraction)).floor()
1012 as usize)
1013 .min(assignments.len());
1014 let mut ranked: Vec<usize> = assignments
1015 .iter()
1016 .enumerate()
1017 .filter_map(|(row, assignment)| assignment.secondary_leaf.map(|_| row))
1018 .collect();
1019 ranked.sort_unstable_by(|&left, &right| {
1020 assignments[right]
1021 .primary_distance
1022 .cmp(&assignments[left].primary_distance)
1023 .then_with(|| left.cmp(&right))
1024 });
1025 for &row in ranked.iter().skip(spill_budget) {
1026 assignments[row].secondary_leaf = None;
1027 }
1028 } else if config.selective {
1029 let threshold_sq = f64::from(config.spill_threshold).powi(2);
1030 for assignment in &mut assignments {
1031 if f64::from(assignment.primary_distance) < threshold_sq {
1032 assignment.secondary_leaf = None;
1033 }
1034 }
1035 }
1036 }
1037
1038 let mut grouped = rustc_hash::FxHashMap::<u32, BinaryScannLeaf>::default();
1039 for ((&(doc_id, ordinal), code), assignment) in doc_id_ordinals
1040 .iter()
1041 .zip(codes.chunks_exact(model.byte_len()))
1042 .zip(assignments)
1043 {
1044 push_binary_posting(&mut grouped, assignment.primary_leaf, doc_id, ordinal, code);
1045 if let Some(secondary_leaf) = assignment.secondary_leaf {
1046 push_binary_posting(&mut grouped, secondary_leaf, doc_id, ordinal, code);
1047 }
1048 }
1049 let mut leaves: Vec<_> = grouped.into_values().collect();
1050 leaves.sort_unstable_by_key(|leaf| leaf.leaf_id);
1051 let stored_len = leaves.iter().map(|leaf| leaf.doc_ids.len()).sum();
1052 let segment = Self {
1053 dim_bits: model.dim_bits,
1054 model_fingerprint: model.fingerprint,
1055 num_leaves: model.num_leaves,
1056 leaves,
1057 len: doc_id_ordinals.len(),
1058 stored_len,
1059 };
1060 segment.validate_for(model)?;
1061 Ok(segment)
1062 }
1063
1064 pub fn merge_compatible(
1067 model: &BinaryScannModel,
1068 segments: &[(&Self, u32)],
1069 ) -> ScannResult<Self> {
1070 for &(segment, _) in segments {
1071 segment.validate_for(model)?;
1072 }
1073 let mut cursors = vec![0usize; segments.len()];
1074 let mut queue = BinaryHeap::new();
1075 for (segment_index, (segment, _)) in segments.iter().enumerate() {
1076 if let Some(first) = segment.leaves.first() {
1077 queue.push(Reverse((first.leaf_id, segment_index)));
1078 }
1079 }
1080 let mut leaves: Vec<BinaryScannLeaf> = Vec::new();
1081 let len = segments.iter().try_fold(0usize, |total, (segment, _)| {
1082 total
1083 .checked_add(segment.len)
1084 .ok_or_else(|| ScannFormatError::new("binary ScaNN merge count overflows"))
1085 })?;
1086 let mut stored_len = 0usize;
1087 while let Some(Reverse((leaf_id, segment_index))) = queue.pop() {
1088 let (segment, doc_base) = segments[segment_index];
1089 let source = &segment.leaves[cursors[segment_index]];
1090 if leaves.last().is_none_or(|leaf| leaf.leaf_id != leaf_id) {
1091 leaves.push(BinaryScannLeaf {
1092 leaf_id,
1093 doc_ids: Vec::new(),
1094 ordinals: Vec::new(),
1095 codes: Vec::new(),
1096 });
1097 }
1098 let target = leaves.last_mut().expect("leaf was just inserted");
1099 target.doc_ids.reserve(source.doc_ids.len());
1100 for &doc_id in &source.doc_ids {
1101 target
1102 .doc_ids
1103 .push(doc_id.checked_add(doc_base).ok_or_else(|| {
1104 ScannFormatError::new("binary ScaNN merge document ID overflows u32")
1105 })?);
1106 }
1107 target.ordinals.extend_from_slice(&source.ordinals);
1108 target.codes.extend_from_slice(&source.codes);
1109 stored_len = stored_len
1110 .checked_add(source.doc_ids.len())
1111 .ok_or_else(|| ScannFormatError::new("binary ScaNN merge count overflows"))?;
1112 cursors[segment_index] += 1;
1113 if let Some(next) = segment.leaves.get(cursors[segment_index]) {
1114 queue.push(Reverse((next.leaf_id, segment_index)));
1115 }
1116 }
1117 let merged = Self {
1118 dim_bits: model.dim_bits,
1119 model_fingerprint: model.fingerprint,
1120 num_leaves: model.num_leaves,
1121 leaves,
1122 len,
1123 stored_len,
1124 };
1125 merged.validate_for(model)?;
1126 Ok(merged)
1127 }
1128
1129 pub fn len(&self) -> usize {
1130 self.len
1131 }
1132
1133 pub fn is_empty(&self) -> bool {
1134 self.len == 0
1135 }
1136
1137 pub fn stored_len(&self) -> usize {
1139 self.stored_len
1140 }
1141
1142 pub fn to_payload(
1143 &self,
1144 model: &BinaryScannModel,
1145 artifact: &ScannTrainedArtifact,
1146 doc_count: u32,
1147 ) -> ScannResult<ScannSegmentPayload> {
1148 self.validate_for(model)?;
1149 let reopened = BinaryScannModel::from_artifact(artifact)?;
1150 if reopened.fingerprint != model.fingerprint {
1151 return Err(ScannFormatError::new(
1152 "binary ScaNN segment model does not match the persisted artifact",
1153 ));
1154 }
1155 let mut runs = Vec::with_capacity(self.leaves.len());
1156 for leaf in &self.leaves {
1157 runs.push(ScannLeafRun::from_rows(
1158 leaf.leaf_id,
1159 0,
1160 &leaf.doc_ids,
1161 &leaf.ordinals,
1162 leaf.codes.clone(),
1163 ScannEncoding::BinaryHamming,
1164 self.dim_bits,
1165 )?);
1166 }
1167 ScannSegmentPayload::new(artifact, doc_count, runs)
1168 }
1169
1170 fn validate_for(&self, model: &BinaryScannModel) -> ScannResult<()> {
1171 if self.dim_bits != model.dim_bits
1172 || self.model_fingerprint != model.fingerprint
1173 || self.num_leaves != model.num_leaves
1174 {
1175 return Err(ScannFormatError::new(
1176 "binary ScaNN segment belongs to a different trained generation",
1177 ));
1178 }
1179 let byte_len = model.byte_len();
1180 let mut previous = None;
1181 let mut total = 0usize;
1182 for leaf in &self.leaves {
1183 if leaf.leaf_id >= self.num_leaves
1184 || previous.is_some_and(|previous| previous >= leaf.leaf_id)
1185 || leaf.doc_ids.len() != leaf.ordinals.len()
1186 || leaf.codes.len() != leaf.doc_ids.len().saturating_mul(byte_len)
1187 {
1188 return Err(ScannFormatError::new(
1189 "binary ScaNN leaf directory or columns are inconsistent",
1190 ));
1191 }
1192 previous = Some(leaf.leaf_id);
1193 total = total
1194 .checked_add(leaf.doc_ids.len())
1195 .ok_or_else(|| ScannFormatError::new("binary ScaNN segment count overflows"))?;
1196 }
1197 let maximum_stored = self
1198 .len
1199 .checked_mul(2)
1200 .ok_or_else(|| ScannFormatError::new("binary ScaNN segment count overflows"))?;
1201 if total != self.stored_len
1202 || self.stored_len < self.len
1203 || self.stored_len > maximum_stored
1204 {
1205 return Err(ScannFormatError::new(
1206 "binary ScaNN segment vector count is inconsistent",
1207 ));
1208 }
1209 Ok(())
1210 }
1211
1212 fn scan(
1213 &self,
1214 query: &[u8],
1215 plan: &BinaryScannProbePlan,
1216 doc_base: u32,
1217 k: usize,
1218 best: &mut BinaryHeap<BinaryScannHit>,
1219 scratch: &mut BinaryScannSearchScratch,
1220 ) -> ScannResult<()> {
1221 if plan.model_fingerprint != self.model_fingerprint {
1222 return Err(ScannFormatError::new(
1223 "binary ScaNN probe plan belongs to a different trained generation",
1224 ));
1225 }
1226 let byte_len = self.dim_bits as usize / 8;
1227 let kernel = HammingKernel::resolve();
1228 for &leaf_id in &plan.leaf_ids {
1229 let Ok(position) = self
1230 .leaves
1231 .binary_search_by_key(&leaf_id, |leaf| leaf.leaf_id)
1232 else {
1233 continue;
1234 };
1235 let leaf = &self.leaves[position];
1236 for start in (0..leaf.doc_ids.len()).step_by(HAMMING_SCAN_BLOCK) {
1237 let rows = HAMMING_SCAN_BLOCK.min(leaf.doc_ids.len() - start);
1238 scratch.distances.clear();
1239 scratch.distances.resize(rows, 0);
1240 kernel.distances(
1241 query,
1242 &leaf.codes[start * byte_len..(start + rows) * byte_len],
1243 byte_len,
1244 &mut scratch.distances,
1245 );
1246 for (local, &distance) in scratch.distances.iter().enumerate() {
1247 if k == 0 {
1248 continue;
1249 }
1250 let index = start + local;
1251 let doc_id = leaf.doc_ids[index].checked_add(doc_base).ok_or_else(|| {
1252 ScannFormatError::new("binary ScaNN query document ID overflows u32")
1253 })?;
1254 let ordinal = leaf.ordinals[index];
1255 if scratch.best_hit_keys.contains(&(doc_id, ordinal)) {
1256 continue;
1257 }
1258 let hit = BinaryScannHit {
1259 doc_id,
1260 ordinal,
1261 distance,
1262 };
1263 if best.len() < k {
1264 best.push(hit);
1265 scratch.best_hit_keys.insert((doc_id, ordinal));
1266 } else if best.peek().is_some_and(|worst| hit < *worst) {
1267 let evicted = best.pop().expect("non-empty top-k heap has a worst hit");
1268 scratch
1269 .best_hit_keys
1270 .remove(&(evicted.doc_id, evicted.ordinal));
1271 best.push(hit);
1272 scratch.best_hit_keys.insert((doc_id, ordinal));
1273 }
1274 }
1275 }
1276 }
1277 Ok(())
1278 }
1279}
1280
1281#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1284pub struct BinaryScannHit {
1285 pub doc_id: u32,
1286 pub ordinal: u16,
1287 pub distance: u32,
1288}
1289
1290impl Ord for BinaryScannHit {
1291 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
1292 self.distance
1293 .cmp(&other.distance)
1294 .then_with(|| self.doc_id.cmp(&other.doc_id))
1295 .then_with(|| self.ordinal.cmp(&other.ordinal))
1296 }
1297}
1298
1299impl PartialOrd for BinaryScannHit {
1300 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
1301 Some(self.cmp(other))
1302 }
1303}
1304
1305struct BinaryPartition {
1306 centroids: Vec<u8>,
1307 groups: Vec<BinaryTrainingRows>,
1308}
1309
1310#[derive(Debug)]
1314enum BinaryTrainingRows {
1315 Contiguous(Range<usize>),
1316 Indexed(Vec<usize>),
1317}
1318
1319impl BinaryTrainingRows {
1320 fn len(&self) -> usize {
1321 match self {
1322 Self::Contiguous(range) => range.len(),
1323 Self::Indexed(indices) => indices.len(),
1324 }
1325 }
1326
1327 fn source_row(&self, position: usize) -> usize {
1328 match self {
1329 Self::Contiguous(range) => range.start + position,
1330 Self::Indexed(indices) => indices[position],
1331 }
1332 }
1333
1334 fn from_indices(indices: Vec<usize>) -> Self {
1335 let Some(&first) = indices.first() else {
1336 return Self::Indexed(indices);
1337 };
1338 if indices
1339 .iter()
1340 .enumerate()
1341 .all(|(offset, &row)| row == first + offset)
1342 {
1343 Self::Contiguous(first..first + indices.len())
1344 } else {
1345 Self::Indexed(indices)
1346 }
1347 }
1348}
1349
1350#[allow(clippy::too_many_arguments)]
1351fn train_binary_partition(
1352 source_codes: &[u8],
1353 rows: &BinaryTrainingRows,
1354 byte_len: usize,
1355 dim_bits: u32,
1356 clusters: usize,
1357 train_iters: usize,
1358 seed: u64,
1359 depth: usize,
1360 retain_groups: bool,
1361 index_label: &str,
1362 stats: &mut BinaryScannTrainingStats,
1363) -> ScannResult<BinaryPartition> {
1364 let row_count = rows.len();
1365 if row_count == 0 || byte_len == 0 || clusters == 0 || clusters > row_count {
1366 return Err(ScannFormatError::new(
1367 "invalid recursive binary ScaNN partition shape",
1368 ));
1369 }
1370 stats.max_depth = stats.max_depth.max(depth);
1371 if clusters == row_count {
1372 let groups = if retain_groups {
1373 let groups: Vec<BinaryTrainingRows> = (0..row_count)
1374 .map(|position| {
1375 let row = rows.source_row(position);
1376 BinaryTrainingRows::Contiguous(row..row + 1)
1377 })
1378 .collect();
1379 stats.retained_groups = stats.retained_groups.saturating_add(groups.len());
1380 groups
1381 } else {
1382 Vec::new()
1383 };
1384 return Ok(BinaryPartition {
1385 centroids: materialize_training_rows(source_codes, rows, byte_len)?,
1386 groups,
1387 });
1388 }
1389
1390 let branches = training_branch_factor(clusters).min(row_count);
1391 let mut config = BinaryIvfConfig::new(dim_bits as usize, branches);
1392 config.routing = IvfRoutingMode::Flat;
1393 config.train_iters = train_iters;
1394 config.max_train_samples = row_count;
1395 config.seed = seed;
1396 let local_centroids =
1397 train_binary_codebook_for_rows(&config, source_codes, rows, byte_len, index_label, stats)?;
1398 stats.splits = stats.splits.saturating_add(1);
1399 stats.max_split_clusters = stats.max_split_clusters.max(branches);
1400 if branches == clusters {
1401 let groups = if retain_groups {
1402 let groups =
1403 partition_one_group_nonempty(source_codes, rows, &local_centroids, byte_len);
1404 stats.retained_groups = stats.retained_groups.saturating_add(groups.len());
1405 groups
1406 } else {
1407 Vec::new()
1408 };
1409 return Ok(BinaryPartition {
1410 groups,
1411 centroids: local_centroids,
1412 });
1413 }
1414
1415 let local_groups = partition_one_group_nonempty(source_codes, rows, &local_centroids, byte_len);
1416 let sizes: Vec<usize> = local_groups.iter().map(BinaryTrainingRows::len).collect();
1417 let allocations = allocate_child_clusters(&sizes, clusters);
1418 if allocations.iter().sum::<usize>() != clusters || allocations.contains(&0) {
1419 return Err(ScannFormatError::new(
1420 "recursive binary ScaNN centroid allocation is inconsistent",
1421 ));
1422 }
1423 let mut centroids = Vec::with_capacity(clusters.saturating_mul(byte_len));
1424 let mut groups = Vec::with_capacity(clusters);
1425 for (branch, (group, &allocation)) in local_groups.iter().zip(&allocations).enumerate() {
1426 let child = train_binary_partition(
1427 source_codes,
1428 group,
1429 byte_len,
1430 dim_bits,
1431 allocation,
1432 train_iters,
1433 derived_seed(seed, depth, branch),
1434 depth + 1,
1435 retain_groups,
1436 index_label,
1437 stats,
1438 )?;
1439 centroids.extend_from_slice(&child.centroids);
1440 if retain_groups {
1441 groups.extend(child.groups);
1442 }
1443 }
1444 Ok(BinaryPartition { centroids, groups })
1445}
1446
1447fn training_branch_factor(clusters: usize) -> usize {
1448 if clusters <= MAX_LOCAL_K_MAJORITY_BRANCHES {
1449 clusters
1450 } else {
1451 ((clusters as f64).sqrt().ceil() as usize).clamp(2, MAX_LOCAL_K_MAJORITY_BRANCHES)
1452 }
1453}
1454
1455fn deterministic_sample_rows(
1456 num_vectors: usize,
1457 sample_count: usize,
1458 seed: u64,
1459) -> BinaryTrainingRows {
1460 if sample_count >= num_vectors {
1461 return BinaryTrainingRows::Contiguous(0..num_vectors);
1462 }
1463 let mut rng = rand::rngs::StdRng::seed_from_u64(seed);
1464 let mut indices = rand::seq::index::sample(&mut rng, num_vectors, sample_count).into_vec();
1465 indices.sort_unstable();
1466 BinaryTrainingRows::from_indices(indices)
1467}
1468
1469fn materialize_training_rows(
1470 source_codes: &[u8],
1471 rows: &BinaryTrainingRows,
1472 byte_len: usize,
1473) -> ScannResult<Vec<u8>> {
1474 let capacity = rows
1475 .len()
1476 .checked_mul(byte_len)
1477 .ok_or_else(|| ScannFormatError::new("binary ScaNN group matrix overflows"))?;
1478 let mut packed = Vec::with_capacity(capacity);
1479 for position in 0..rows.len() {
1480 let row = rows.source_row(position);
1481 let start = row
1482 .checked_mul(byte_len)
1483 .ok_or_else(|| ScannFormatError::new("binary ScaNN source row overflows"))?;
1484 let code = source_codes
1485 .get(start..start + byte_len)
1486 .ok_or_else(|| ScannFormatError::new("binary ScaNN source row is truncated"))?;
1487 packed.extend_from_slice(code);
1488 }
1489 Ok(packed)
1490}
1491
1492fn train_binary_codebook_for_rows(
1493 config: &BinaryIvfConfig,
1494 source_codes: &[u8],
1495 rows: &BinaryTrainingRows,
1496 byte_len: usize,
1497 index_label: &str,
1498 stats: &mut BinaryScannTrainingStats,
1499) -> ScannResult<Vec<u8>> {
1500 match rows {
1501 BinaryTrainingRows::Contiguous(range) => {
1502 let start = range
1503 .start
1504 .checked_mul(byte_len)
1505 .ok_or_else(|| ScannFormatError::new("binary ScaNN group offset overflows"))?;
1506 let end = range
1507 .end
1508 .checked_mul(byte_len)
1509 .ok_or_else(|| ScannFormatError::new("binary ScaNN group offset overflows"))?;
1510 let codes = source_codes
1511 .get(start..end)
1512 .ok_or_else(|| ScannFormatError::new("binary ScaNN group is truncated"))?;
1513 train_binary_k_majority_codebook(config, codes, rows.len(), index_label)
1514 .map_err(|error| ScannFormatError::new(error.to_string()))
1515 }
1516 BinaryTrainingRows::Indexed(_) => {
1517 let packed = materialize_training_rows(source_codes, rows, byte_len)?;
1518 stats.max_materialized_training_bytes =
1519 stats.max_materialized_training_bytes.max(packed.len());
1520 train_binary_k_majority_codebook(config, &packed, rows.len(), index_label)
1521 .map_err(|error| ScannFormatError::new(error.to_string()))
1522 }
1523 }
1524}
1525
1526fn derived_seed(seed: u64, level: usize, parent: usize) -> u64 {
1527 seed ^ (level as u64).wrapping_mul(0xd6e8_feb8_6659_fd93)
1528 ^ (parent as u64).wrapping_mul(0x9e37_79b9_7f4a_7c15)
1529}
1530
1531fn partition_one_group_nonempty(
1532 source_codes: &[u8],
1533 rows: &BinaryTrainingRows,
1534 centroids: &[u8],
1535 byte_len: usize,
1536) -> Vec<BinaryTrainingRows> {
1537 let kernel = HammingKernel::resolve();
1538 let child_count = centroids.len() / byte_len;
1539 let row_count = rows.len();
1540 let mut assignments = vec![0usize; row_count];
1541 let mut assignment_distances = vec![0u32; row_count];
1542 let mut counts = vec![0usize; child_count];
1543 let mut distances = vec![0u32; child_count];
1544 for position in 0..row_count {
1545 let source_row = rows.source_row(position);
1546 let offset = source_row * byte_len;
1547 let code = &source_codes[offset..offset + byte_len];
1548 kernel.distances(code, centroids, byte_len, &mut distances);
1549 let (child, &distance) = distances
1550 .iter()
1551 .enumerate()
1552 .min_by_key(|&(child, distance)| (*distance, child))
1553 .expect("a populated routing parent has children");
1554 assignments[position] = child;
1555 assignment_distances[position] = distance;
1556 counts[child] += 1;
1557 }
1558 for empty in 0..child_count {
1559 if counts[empty] != 0 {
1560 continue;
1561 }
1562 let replacement = (0..row_count)
1563 .filter(|&row| counts[assignments[row]] > 1)
1564 .max_by_key(|&row| (assignment_distances[row], Reverse(row)))
1565 .expect("training readiness guarantees one sample per centroid");
1566 counts[assignments[replacement]] -= 1;
1567 assignments[replacement] = empty;
1568 assignment_distances[replacement] = 0;
1569 counts[empty] = 1;
1570 }
1571 let mut groups: Vec<Vec<usize>> = counts
1572 .iter()
1573 .map(|&count| Vec::with_capacity(count))
1574 .collect();
1575 for (position, &child) in assignments.iter().enumerate() {
1576 groups[child].push(rows.source_row(position));
1577 }
1578 groups
1579 .into_iter()
1580 .map(BinaryTrainingRows::from_indices)
1581 .collect()
1582}
1583
1584struct Fingerprint(u64);
1585
1586impl Fingerprint {
1587 fn new() -> Self {
1588 Self(0xcbf2_9ce4_8422_2325)
1589 }
1590
1591 fn write(&mut self, bytes: &[u8]) {
1592 for &byte in bytes {
1593 self.0 ^= u64::from(byte);
1594 self.0 = self.0.wrapping_mul(0x0000_0100_0000_01b3);
1595 }
1596 }
1597
1598 fn finish(self) -> u64 {
1599 self.0
1600 }
1601}
1602
1603#[cfg(test)]
1604mod tests {
1605 use super::*;
1606 use crate::structures::simd::hamming_distance;
1607
1608 fn corpus(rows: usize) -> Vec<u8> {
1609 let anchors = [
1610 [0x00, 0x00],
1611 [0xff, 0xff],
1612 [0x0f, 0x0f],
1613 [0xf0, 0xf0],
1614 [0xaa, 0xaa],
1615 [0x55, 0x55],
1616 [0x33, 0xcc],
1617 [0xcc, 0x33],
1618 ];
1619 let mut codes = Vec::with_capacity(rows * 2);
1620 for row in 0..rows {
1621 let mut code = anchors[row % anchors.len()];
1622 code[(row / anchors.len()) % 2] ^= 1 << ((row / 16) % 8);
1623 codes.extend_from_slice(&code);
1624 }
1625 codes
1626 }
1627
1628 fn training() -> BinaryScannTraining {
1629 BinaryScannTraining {
1630 dim_bits: 16,
1631 geometry: ScannGeometry {
1632 centroid_levels: 3,
1633 num_leaves: 8,
1634 level_counts: vec![2, 4, 8],
1635 },
1636 train_iters: 2,
1637 seed: 73,
1638 }
1639 }
1640
1641 fn two_leaf_model() -> BinaryScannModel {
1642 let mut model = BinaryScannModel {
1643 dim_bits: 8,
1644 num_leaves: 2,
1645 levels: vec![BinaryRoutingLevel {
1646 centroids: vec![0x00, 0xff],
1647 parent_offsets: vec![0, 2],
1648 }],
1649 fingerprint: 0,
1650 };
1651 model.fingerprint = model.compute_fingerprint();
1652 model.validate().unwrap();
1653 model
1654 }
1655
1656 #[test]
1657 fn readiness_is_derived_from_geometry_not_user_configuration() {
1658 let training = training();
1659 assert_eq!(
1660 training.training_state(99_999).unwrap(),
1661 ScannTrainingState::AwaitingData {
1662 observed: 99_999,
1663 required: 100_000,
1664 }
1665 );
1666 assert_eq!(training.desired_training_vectors(500_000).unwrap(), 100_000);
1667 }
1668
1669 #[test]
1670 fn large_binary_partition_never_flat_trains_terminal_leaf_count() {
1671 let codes = corpus(100_000);
1672 let training = BinaryScannTraining {
1673 dim_bits: 16,
1674 geometry: ScannGeometry {
1675 centroid_levels: 1,
1676 num_leaves: 1_024,
1677 level_counts: vec![1_024],
1678 },
1679 train_iters: 1,
1680 seed: 91,
1681 };
1682 let (model, stats) =
1683 BinaryScannModel::train_with_stats(&training, &codes, 100_000, "test").unwrap();
1684 assert_eq!(model.num_leaves(), 1_024);
1685 assert!(stats.splits > 1);
1686 assert!(
1687 stats.max_split_clusters <= MAX_LOCAL_K_MAJORITY_BRANCHES,
1688 "binary local split widened to {} clusters",
1689 stats.max_split_clusters,
1690 );
1691 assert!(stats.max_split_clusters < 1_024);
1692 assert_eq!(
1693 stats.retained_groups, 0,
1694 "terminal training must not allocate one Vec per leaf",
1695 );
1696 assert!(
1697 stats.max_materialized_training_bytes < codes.len(),
1698 "binary training cloned the complete {}-byte retained sample",
1699 codes.len(),
1700 );
1701 }
1702
1703 #[test]
1704 fn full_probe_widens_past_the_legacy_sixty_four_parent_beam() {
1705 let root_count = 65usize;
1706 let leaf_count = root_count * root_count;
1707 let mut leaf_offsets = Vec::with_capacity(root_count + 1);
1708 for parent in 0..=root_count {
1709 leaf_offsets.push((parent * root_count) as u32);
1710 }
1711 let mut model = BinaryScannModel {
1712 dim_bits: 8,
1713 num_leaves: leaf_count as u32,
1714 levels: vec![
1715 BinaryRoutingLevel {
1716 centroids: vec![0; root_count],
1717 parent_offsets: vec![0, root_count as u32],
1718 },
1719 BinaryRoutingLevel {
1720 centroids: vec![0; leaf_count],
1721 parent_offsets: leaf_offsets,
1722 },
1723 ],
1724 fingerprint: 0,
1725 };
1726 model.fingerprint = model.compute_fingerprint();
1727 model.validate().unwrap();
1728
1729 let mut scratch = BinaryScannSearchScratch::default();
1730 let owned = model.probe(&[0], leaf_count, 64, &mut scratch).unwrap();
1731 assert_eq!(owned.leaf_ids.len(), leaf_count);
1732 assert_eq!(owned.leaf_ids, (0..leaf_count as u32).collect::<Vec<_>>());
1733
1734 let artifact = model.to_artifact(7, 100_000).unwrap();
1735 let bytes = artifact.to_bytes().unwrap();
1736 let artifact = ScannTrainedArtifactView::parse(&bytes).unwrap();
1737 let quantized = QuantizedBinaryScannModel::from_artifact_view(&artifact).unwrap();
1738 let view = quantized.view(&bytes).unwrap();
1739 let mapped = view.probe(&[0], leaf_count, 64, &mut scratch).unwrap();
1740 assert_eq!(mapped.leaf_ids, owned.leaf_ids);
1741 }
1742
1743 #[test]
1744 fn packed_hamming_search_is_exact_and_merge_independent() {
1745 let training_codes = corpus(100_000);
1746 let model = BinaryScannModel::train(&training(), &training_codes, 100_000, "test").unwrap();
1747 let rebuilt =
1748 BinaryScannModel::train(&training(), &training_codes, 100_000, "test").unwrap();
1749 assert_eq!(rebuilt.fingerprint(), model.fingerprint());
1750 let mut scratch = BinaryScannSearchScratch::default();
1751 let query = [0b1010_1011, 0b1010_1010];
1752 let first = model.probe(&query, 4, 2, &mut scratch).unwrap();
1753 let second = rebuilt.probe(&query, 4, 2, &mut scratch).unwrap();
1754 assert_eq!(first, second);
1755 let artifact = model.to_artifact(11, 100_000).unwrap();
1756 let artifact_bytes = artifact.to_bytes().unwrap();
1757 let artifact_view = ScannTrainedArtifactView::parse(&artifact_bytes).unwrap();
1758 let quantized = QuantizedBinaryScannModel::from_artifact_view(&artifact_view).unwrap();
1759 let quantized_view = quantized.view(&artifact_bytes).unwrap();
1760 let range_backed = quantized_view.probe(&query, 4, 2, &mut scratch).unwrap();
1761 assert_eq!(range_backed, first);
1762 assert_eq!(quantized.fingerprint(), model.fingerprint());
1763 assert!(quantized.estimated_memory_bytes() < artifact_bytes.len());
1764
1765 let codes = corpus(512);
1766 let labels: Vec<_> = (0..512).map(|doc_id| (doc_id, 0)).collect();
1767 let monolith = BinaryScannSegment::build(&model, &codes, &labels, &mut scratch).unwrap();
1768
1769 let split = 193;
1770 let left_labels: Vec<_> = (0..split as u32).map(|doc_id| (doc_id, 0)).collect();
1771 let right_labels: Vec<_> = (0..(512 - split) as u32)
1772 .map(|doc_id| (doc_id, 0))
1773 .collect();
1774 let left =
1775 BinaryScannSegment::build(&model, &codes[..split * 2], &left_labels, &mut scratch)
1776 .unwrap();
1777 let right =
1778 BinaryScannSegment::build(&model, &codes[split * 2..], &right_labels, &mut scratch)
1779 .unwrap();
1780 let merged =
1781 BinaryScannSegment::merge_compatible(&model, &[(&left, 0), (&right, split as u32)])
1782 .unwrap();
1783
1784 let expected = model
1785 .search_segments(
1786 &query,
1787 25,
1788 model.num_leaves() as usize,
1789 8,
1790 &[(&monolith, 0)],
1791 &mut scratch,
1792 )
1793 .unwrap();
1794 let split_hits = model
1795 .search_segments(
1796 &query,
1797 25,
1798 model.num_leaves() as usize,
1799 8,
1800 &[(&left, 0), (&right, split as u32)],
1801 &mut scratch,
1802 )
1803 .unwrap();
1804 let merged_hits = model
1805 .search_segments(
1806 &query,
1807 25,
1808 model.num_leaves() as usize,
1809 8,
1810 &[(&merged, 0)],
1811 &mut scratch,
1812 )
1813 .unwrap();
1814 assert_eq!(split_hits, expected);
1815 assert_eq!(merged_hits, expected);
1816
1817 let mut brute_force: Vec<_> = codes
1818 .chunks_exact(2)
1819 .enumerate()
1820 .map(|(doc_id, code)| BinaryScannHit {
1821 doc_id: doc_id as u32,
1822 ordinal: 0,
1823 distance: hamming_distance(&query, code),
1824 })
1825 .collect();
1826 brute_force.sort_unstable();
1827 brute_force.truncate(25);
1828 assert_eq!(expected, brute_force);
1829 }
1830
1831 #[test]
1832 fn selective_binary_spill_is_bounded_deterministic_and_deduplicated() {
1833 let model = two_leaf_model();
1834 let artifact = model.to_artifact(17, 100_000).unwrap();
1835 let codes = vec![0x00, 0x01, 0x03, 0x07, 0x0f, 0xff, 0xfe, 0xfc, 0xf8, 0xf0];
1836 let labels: Vec<_> = (0..codes.len() as u32).map(|doc_id| (doc_id, 0)).collect();
1837 let soar = SoarConfig::new().target_spill_fraction(0.30);
1838 let mut scratch = BinaryScannSearchScratch::default();
1839
1840 let artifact_bytes = artifact.to_bytes().unwrap();
1841 let artifact_view = ScannTrainedArtifactView::parse(&artifact_bytes).unwrap();
1842 let quantized = QuantizedBinaryScannModel::from_artifact_view(&artifact_view).unwrap();
1843 let quantized = quantized.view(&artifact_bytes).unwrap();
1844 for code in &codes {
1845 let code = std::slice::from_ref(code);
1846 assert_eq!(
1847 model.spill_assignment(code, &mut scratch).unwrap(),
1848 quantized.spill_assignment(code, &mut scratch).unwrap(),
1849 );
1850 }
1851
1852 let first =
1853 BinaryScannSegment::build_with_soar(&model, &codes, &labels, &soar, &mut scratch)
1854 .unwrap();
1855 let second =
1856 BinaryScannSegment::build_with_soar(&model, &codes, &labels, &soar, &mut scratch)
1857 .unwrap();
1858 assert_eq!(first.len(), codes.len());
1859 assert_eq!(first.stored_len(), codes.len() + 3);
1860
1861 let first_payload = first
1862 .to_payload(&model, &artifact, codes.len() as u32)
1863 .unwrap();
1864 let second_payload = second
1865 .to_payload(&model, &artifact, codes.len() as u32)
1866 .unwrap();
1867 assert_eq!(first_payload, second_payload);
1868 let encoded = first_payload.to_bytes().unwrap();
1869 let decoded = ScannSegmentPayload::from_bytes(&encoded).unwrap();
1870 assert_eq!(decoded, first_payload);
1871 decoded.validate_against(&artifact).unwrap();
1872
1873 let query = [0x0f];
1874 let hits = model
1875 .search_segments(&query, codes.len(), 2, 2, &[(&first, 0)], &mut scratch)
1876 .unwrap();
1877 assert_eq!(
1878 hits.len(),
1879 codes.len(),
1880 "secondary postings must not duplicate hits"
1881 );
1882 let mut expected: Vec<_> = codes
1883 .iter()
1884 .enumerate()
1885 .map(|(doc_id, code)| BinaryScannHit {
1886 doc_id: doc_id as u32,
1887 ordinal: 0,
1888 distance: hamming_distance(&query, std::slice::from_ref(code)),
1889 })
1890 .collect();
1891 expected.sort_unstable();
1892 assert_eq!(hits, expected);
1893
1894 let split = 5;
1895 let local_labels: Vec<_> = (0..split as u32).map(|doc_id| (doc_id, 0)).collect();
1896 let left = BinaryScannSegment::build_with_soar(
1897 &model,
1898 &codes[..split],
1899 &local_labels,
1900 &soar,
1901 &mut scratch,
1902 )
1903 .unwrap();
1904 let right = BinaryScannSegment::build_with_soar(
1905 &model,
1906 &codes[split..],
1907 &local_labels,
1908 &soar,
1909 &mut scratch,
1910 )
1911 .unwrap();
1912 let fingerprint = model.fingerprint();
1913 let merged =
1914 BinaryScannSegment::merge_compatible(&model, &[(&left, 0), (&right, split as u32)])
1915 .unwrap();
1916 assert_eq!(model.fingerprint(), fingerprint, "merge must not retrain");
1917 assert_eq!(merged.len(), codes.len());
1918 assert_eq!(merged.stored_len(), codes.len() + 2);
1919 let merged_hits = model
1920 .search_segments(&query, codes.len(), 2, 2, &[(&merged, 0)], &mut scratch)
1921 .unwrap();
1922 assert_eq!(merged_hits, expected);
1923
1924 let boundary_code = [0x0f];
1927 let boundary_label = [(42, 0)];
1928 let primary_only =
1929 BinaryScannSegment::build(&model, &boundary_code, &boundary_label, &mut scratch)
1930 .unwrap();
1931 let fully_spilled = BinaryScannSegment::build_with_soar(
1932 &model,
1933 &boundary_code,
1934 &boundary_label,
1935 &SoarConfig::full(),
1936 &mut scratch,
1937 )
1938 .unwrap();
1939 let nearby_query = [0x1f];
1940 assert!(
1941 model
1942 .search_segments(&nearby_query, 1, 1, 1, &[(&primary_only, 0)], &mut scratch)
1943 .unwrap()
1944 .is_empty()
1945 );
1946 assert_eq!(
1947 model
1948 .search_segments(&nearby_query, 1, 1, 1, &[(&fully_spilled, 0)], &mut scratch)
1949 .unwrap(),
1950 vec![BinaryScannHit {
1951 doc_id: 42,
1952 ordinal: 0,
1953 distance: 1,
1954 }],
1955 );
1956 }
1957}