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 mut views = [BinaryLevelView::EMPTY; MAX_SCANN_TREE_LEVELS as usize];
407 for (view, level) in views.iter_mut().zip(&self.levels) {
408 *view = BinaryLevelView {
409 centroids: &level.centroids,
410 parent_offsets: &level.parent_offsets,
411 };
412 }
413 let leaf_ids = probe_binary_tree(
414 &views[..self.levels.len()],
415 self.byte_len(),
416 self.num_leaves as usize,
417 query,
418 nprobe,
419 beam_width,
420 scratch,
421 )?;
422 Ok(BinaryScannProbePlan {
423 model_fingerprint: self.fingerprint,
424 leaf_ids,
425 })
426 }
427
428 pub fn assign(&self, code: &[u8], scratch: &mut BinaryScannSearchScratch) -> ScannResult<u32> {
429 self.probe(code, 1, 1, scratch)?
430 .leaf_ids
431 .first()
432 .copied()
433 .ok_or_else(|| ScannFormatError::new("binary ScaNN assignment returned no leaf"))
434 }
435
436 pub fn spill_assignment(
441 &self,
442 code: &[u8],
443 scratch: &mut BinaryScannSearchScratch,
444 ) -> ScannResult<BinaryScannSpillAssignment> {
445 let primary_leaf = self.assign(code, scratch)?;
446 let candidate_count = BINARY_SPILL_ASSIGNMENT_CANDIDATES
447 .min(self.num_leaves as usize)
448 .max(1);
449 let plan = self.probe(code, candidate_count, candidate_count, scratch)?;
450 let kernel = HammingKernel::resolve();
451 let leaf_centroids = &self
452 .levels
453 .last()
454 .expect("validated binary ScaNN model has a terminal level")
455 .centroids;
456 let centroid = |leaf_id: u32| {
457 let start = leaf_id as usize * self.byte_len();
458 &leaf_centroids[start..start + self.byte_len()]
459 };
460 let primary_distance = kernel.distance(code, centroid(primary_leaf));
461 let secondary_leaf = plan
462 .leaf_ids
463 .into_iter()
464 .filter(|&leaf_id| leaf_id != primary_leaf)
465 .map(|leaf_id| (kernel.distance(code, centroid(leaf_id)), leaf_id))
466 .min()
467 .map(|(_, leaf_id)| leaf_id);
468 Ok(BinaryScannSpillAssignment {
469 primary_leaf,
470 secondary_leaf,
471 primary_distance,
472 })
473 }
474
475 pub fn search_segments(
478 &self,
479 query: &[u8],
480 k: usize,
481 nprobe: usize,
482 beam_width: usize,
483 segments: &[(&BinaryScannSegment, u32)],
484 scratch: &mut BinaryScannSearchScratch,
485 ) -> ScannResult<Vec<BinaryScannHit>> {
486 let plan = self.probe(query, nprobe, beam_width, scratch)?;
487 scratch.best_hit_keys.clear();
488 let mut best = BinaryHeap::with_capacity(k.min(8_192));
489 for &(segment, doc_base) in segments {
490 segment.validate_for(self)?;
491 segment.scan(query, &plan, doc_base, k, &mut best, scratch)?;
492 }
493 let mut hits = best.into_vec();
494 hits.sort_unstable();
495 Ok(hits)
496 }
497
498 fn byte_len(&self) -> usize {
499 self.dim_bits as usize / 8
500 }
501
502 fn compute_fingerprint(&self) -> u64 {
503 let mut hash = Fingerprint::new();
504 hash.write(&self.dim_bits.to_le_bytes());
505 hash.write(&self.num_leaves.to_le_bytes());
506 hash.write(&(self.levels.len() as u32).to_le_bytes());
507 for level in &self.levels {
508 for offset in &level.parent_offsets {
509 hash.write(&offset.to_le_bytes());
510 }
511 hash.write(&level.centroids);
512 }
513 hash.finish()
514 }
515}
516
517impl QuantizedBinaryScannModel {
518 pub fn from_artifact_view(artifact: &ScannTrainedArtifactView<'_>) -> ScannResult<Self> {
519 if artifact.config.encoding != ScannEncoding::BinaryHamming {
520 return Err(ScannFormatError::new(
521 "float ScaNN artifact cannot be opened as a binary mmap model",
522 ));
523 }
524 let byte_len = artifact.config.dimension as usize / 8;
525 let mut levels = Vec::with_capacity(artifact.level_count());
526 for index in 0..artifact.level_count() {
527 let level = artifact.level(index).ok_or_else(|| {
528 ScannFormatError::new("binary ScaNN artifact routing level disappeared")
529 })?;
530 let centroid_codes = artifact.level_centroid_codes_range(index).ok_or_else(|| {
531 ScannFormatError::new("binary ScaNN artifact centroid range disappeared")
532 })?;
533 if centroid_codes.len() != level.centroid_count as usize * byte_len {
534 return Err(ScannFormatError::new(
535 "binary ScaNN artifact centroid plane is inconsistent",
536 ));
537 }
538 let parent_offsets = if index == 0 {
539 vec![0, level.centroid_count]
540 } else {
541 artifact
542 .level(index - 1)
543 .expect("previous validated routing level exists")
544 .child_offsets()
545 .collect()
546 };
547 levels.push(QuantizedBinaryRoutingLevel {
548 centroid_count: level.centroid_count as usize,
549 centroid_codes,
550 parent_offsets,
551 });
552 }
553 let mut model = Self {
554 dim_bits: artifact.config.dimension,
555 num_leaves: artifact.config.num_leaves,
556 artifact_id: artifact.artifact_id,
557 artifact_len: artifact.bytes().len(),
558 levels,
559 fingerprint: 0,
560 };
561 model.fingerprint = model.compute_fingerprint(artifact.bytes());
562 model.validate_metadata()?;
563 Ok(model)
564 }
565
566 pub fn view<'a>(
567 &'a self,
568 artifact_bytes: &'a [u8],
569 ) -> ScannResult<QuantizedBinaryScannModelView<'a>> {
570 let stored_id = artifact_bytes
571 .get(12..20)
572 .and_then(|bytes| <[u8; 8]>::try_from(bytes).ok())
573 .map(u64::from_le_bytes);
574 if artifact_bytes.len() != self.artifact_len || stored_id != Some(self.artifact_id) {
575 return Err(ScannFormatError::new(
576 "quantized binary ScaNN model was paired with a different artifact mapping",
577 ));
578 }
579 Ok(QuantizedBinaryScannModelView {
580 model: self,
581 artifact_bytes,
582 })
583 }
584
585 pub fn dim_bits(&self) -> u32 {
586 self.dim_bits
587 }
588
589 pub fn num_leaves(&self) -> u32 {
590 self.num_leaves
591 }
592
593 pub fn fingerprint(&self) -> u64 {
594 self.fingerprint
595 }
596
597 pub fn estimated_memory_bytes(&self) -> usize {
598 self.levels.iter().fold(0usize, |total, level| {
599 total.saturating_add(level.parent_offsets.len() * std::mem::size_of::<u32>())
600 })
601 }
602
603 fn byte_len(&self) -> usize {
604 self.dim_bits as usize / 8
605 }
606
607 fn validate_metadata(&self) -> ScannResult<()> {
608 if self.dim_bits == 0
609 || !self.dim_bits.is_multiple_of(8)
610 || self.levels.is_empty()
611 || self.levels.len() > usize::from(MAX_SCANN_TREE_LEVELS)
612 || self.levels.last().map(|level| level.centroid_count)
613 != Some(self.num_leaves as usize)
614 {
615 return Err(ScannFormatError::new(
616 "invalid quantized binary ScaNN model header",
617 ));
618 }
619 let mut parents = 1usize;
620 for level in &self.levels {
621 if level.centroid_codes.end > self.artifact_len
622 || level.centroid_codes.len()
623 != level.centroid_count.saturating_mul(self.byte_len())
624 || level.parent_offsets.len() != parents + 1
625 || level.parent_offsets.first() != Some(&0)
626 || level.parent_offsets.last().copied() != Some(level.centroid_count as u32)
627 || level
628 .parent_offsets
629 .windows(2)
630 .any(|pair| pair[0] > pair[1])
631 {
632 return Err(ScannFormatError::new(
633 "invalid quantized binary ScaNN routing level",
634 ));
635 }
636 parents = level.centroid_count;
637 }
638 Ok(())
639 }
640
641 fn compute_fingerprint(&self, artifact_bytes: &[u8]) -> u64 {
642 let mut hash = Fingerprint::new();
643 hash.write(&self.dim_bits.to_le_bytes());
644 hash.write(&self.num_leaves.to_le_bytes());
645 hash.write(&(self.levels.len() as u32).to_le_bytes());
646 for level in &self.levels {
647 for offset in &level.parent_offsets {
648 hash.write(&offset.to_le_bytes());
649 }
650 hash.write(&artifact_bytes[level.centroid_codes.clone()]);
651 }
652 hash.finish()
653 }
654}
655
656impl QuantizedBinaryScannModelView<'_> {
657 pub fn dim_bits(&self) -> u32 {
658 self.model.dim_bits
659 }
660
661 pub fn num_leaves(&self) -> u32 {
662 self.model.num_leaves
663 }
664
665 pub fn fingerprint(&self) -> u64 {
666 self.model.fingerprint
667 }
668
669 pub fn probe(
670 &self,
671 query: &[u8],
672 nprobe: usize,
673 beam_width: usize,
674 scratch: &mut BinaryScannSearchScratch,
675 ) -> ScannResult<BinaryScannProbePlan> {
676 if query.len() != self.model.byte_len() {
677 return Err(ScannFormatError::new(
678 "binary ScaNN query dimension does not match the model",
679 ));
680 }
681 if nprobe == 0 || beam_width == 0 {
682 return Err(ScannFormatError::new(
683 "binary ScaNN nprobe and beam width must be positive",
684 ));
685 }
686 let mut views = [BinaryLevelView::EMPTY; MAX_SCANN_TREE_LEVELS as usize];
687 for (view, level) in views.iter_mut().zip(&self.model.levels) {
688 *view = BinaryLevelView {
689 centroids: &self.artifact_bytes[level.centroid_codes.clone()],
690 parent_offsets: &level.parent_offsets,
691 };
692 }
693 let leaf_ids = probe_binary_tree(
694 &views[..self.model.levels.len()],
695 self.model.byte_len(),
696 self.model.num_leaves as usize,
697 query,
698 nprobe,
699 beam_width,
700 scratch,
701 )?;
702 Ok(BinaryScannProbePlan {
703 model_fingerprint: self.model.fingerprint,
704 leaf_ids,
705 })
706 }
707
708 pub fn assign(&self, code: &[u8], scratch: &mut BinaryScannSearchScratch) -> ScannResult<u32> {
709 self.probe(code, 1, 1, scratch)?
710 .leaf_ids
711 .first()
712 .copied()
713 .ok_or_else(|| ScannFormatError::new("binary ScaNN assignment returned no leaf"))
714 }
715
716 pub fn spill_assignment(
719 &self,
720 code: &[u8],
721 scratch: &mut BinaryScannSearchScratch,
722 ) -> ScannResult<BinaryScannSpillAssignment> {
723 let primary_leaf = self.assign(code, scratch)?;
724 let candidate_count = BINARY_SPILL_ASSIGNMENT_CANDIDATES
725 .min(self.model.num_leaves as usize)
726 .max(1);
727 let plan = self.probe(code, candidate_count, candidate_count, scratch)?;
728 let kernel = HammingKernel::resolve();
729 let terminal = self
730 .model
731 .levels
732 .last()
733 .expect("validated binary ScaNN model has a terminal level");
734 let leaf_centroids = &self.artifact_bytes[terminal.centroid_codes.clone()];
735 let centroid = |leaf_id: u32| {
736 let start = leaf_id as usize * self.model.byte_len();
737 &leaf_centroids[start..start + self.model.byte_len()]
738 };
739 let primary_distance = kernel.distance(code, centroid(primary_leaf));
740 let secondary_leaf = plan
741 .leaf_ids
742 .into_iter()
743 .filter(|&leaf_id| leaf_id != primary_leaf)
744 .map(|leaf_id| (kernel.distance(code, centroid(leaf_id)), leaf_id))
745 .min()
746 .map(|(_, leaf_id)| leaf_id);
747 Ok(BinaryScannSpillAssignment {
748 primary_leaf,
749 secondary_leaf,
750 primary_distance,
751 })
752 }
753}
754
755#[derive(Clone, Debug, Eq, PartialEq)]
756pub struct BinaryScannProbePlan {
757 pub model_fingerprint: u64,
758 pub leaf_ids: Vec<u32>,
759}
760
761#[derive(Clone, Copy)]
764struct BinaryLevelView<'a> {
765 centroids: &'a [u8],
766 parent_offsets: &'a [u32],
767}
768
769impl BinaryLevelView<'_> {
770 const EMPTY: Self = Self {
771 centroids: &[],
772 parent_offsets: &[],
773 };
774}
775
776fn probe_binary_tree(
786 levels: &[BinaryLevelView<'_>],
787 byte_len: usize,
788 num_leaves: usize,
789 query: &[u8],
790 nprobe: usize,
791 beam_width: usize,
792 scratch: &mut BinaryScannSearchScratch,
793) -> ScannResult<Vec<u32>> {
794 let kernel = HammingKernel::resolve();
795 scratch.frontier.clear();
796 scratch.frontier.push(0);
797 let mut leaf_ids = Vec::new();
798 for (level_index, level) in levels.iter().enumerate() {
799 scratch.candidates.clear();
800 for &parent in &scratch.frontier {
801 let parent = parent as usize;
802 let start = level.parent_offsets[parent] as usize;
803 let end = level.parent_offsets[parent + 1] as usize;
804 let rows = end - start;
805 scratch.distances.clear();
806 scratch.distances.resize(rows, 0);
807 kernel.distances(
808 query,
809 &level.centroids[start * byte_len..end * byte_len],
810 byte_len,
811 &mut scratch.distances,
812 );
813 scratch
814 .candidates
815 .extend(
816 scratch
817 .distances
818 .iter()
819 .enumerate()
820 .map(|(local, &distance)| RouteCandidate {
821 node: (start + local) as u32,
822 distance,
823 }),
824 );
825 }
826 let is_leaf_level = level_index + 1 == levels.len();
827 let width = if is_leaf_level {
828 let width = nprobe.min(num_leaves).min(scratch.candidates.len());
829 rank_best_candidates(&mut scratch.candidates, width);
830 width
831 } else {
832 let child_offsets = levels[level_index + 1].parent_offsets;
833 select_intermediate_frontier(&mut scratch.candidates, child_offsets, beam_width, nprobe)
834 };
835 if width == 0 {
836 return Err(ScannFormatError::new(
837 "binary ScaNN routing reached an empty branch",
838 ));
839 }
840 let selected = scratch.candidates[..width]
841 .iter()
842 .map(|candidate| candidate.node);
843 if is_leaf_level {
844 leaf_ids.reserve_exact(width);
845 leaf_ids.extend(selected);
846 } else {
847 scratch.frontier.clear();
848 scratch.frontier.extend(selected);
849 }
850 }
851 Ok(leaf_ids)
852}
853
854fn rank_best_candidates(candidates: &mut [RouteCandidate], width: usize) {
857 if width < candidates.len() {
858 candidates.select_nth_unstable(width);
859 }
860 candidates[..width].sort_unstable();
861}
862
863fn select_intermediate_frontier(
867 candidates: &mut [RouteCandidate],
868 child_offsets: &[u32],
869 beam_width: usize,
870 nprobe: usize,
871) -> usize {
872 let total = candidates.len();
873 if total == 0 {
874 return 0;
875 }
876 let mut ranked = beam_width.clamp(1, total);
877 loop {
878 rank_best_candidates(candidates, ranked);
879 let width = super::routing_prefix_for_child_coverage(
880 &candidates[..ranked],
881 child_offsets,
882 beam_width,
883 nprobe,
884 |candidate| candidate.node as usize,
885 );
886 let covered = candidates[..width].iter().fold(0usize, |total, candidate| {
887 let node = candidate.node as usize;
888 total.saturating_add((child_offsets[node + 1] - child_offsets[node]) as usize)
889 });
890 if covered >= nprobe || ranked == total {
891 return width;
892 }
893 ranked = ranked.saturating_mul(2).min(total);
894 }
895}
896
897#[derive(Clone, Copy, Debug, Eq, PartialEq)]
898struct RouteCandidate {
899 node: u32,
900 distance: u32,
901}
902
903impl Ord for RouteCandidate {
904 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
905 self.distance
906 .cmp(&other.distance)
907 .then_with(|| self.node.cmp(&other.node))
908 }
909}
910
911impl PartialOrd for RouteCandidate {
912 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
913 Some(self.cmp(other))
914 }
915}
916
917#[derive(Default, Debug)]
919pub struct BinaryScannSearchScratch {
920 frontier: Vec<u32>,
921 candidates: Vec<RouteCandidate>,
922 distances: Vec<u32>,
923 best_hit_keys: rustc_hash::FxHashSet<(u32, u16)>,
927}
928
929#[derive(Clone, Copy, Debug, Eq, PartialEq)]
932pub struct BinaryScannSpillAssignment {
933 pub primary_leaf: u32,
934 pub secondary_leaf: Option<u32>,
935 pub primary_distance: u32,
936}
937
938#[derive(Clone, Debug)]
939struct BinaryScannLeaf {
940 leaf_id: u32,
941 doc_ids: Vec<u32>,
942 ordinals: Vec<u16>,
943 codes: Vec<u8>,
944}
945
946fn push_binary_posting(
947 grouped: &mut rustc_hash::FxHashMap<u32, BinaryScannLeaf>,
948 leaf_id: u32,
949 doc_id: u32,
950 ordinal: u16,
951 code: &[u8],
952) {
953 let leaf = grouped.entry(leaf_id).or_insert_with(|| BinaryScannLeaf {
954 leaf_id,
955 doc_ids: Vec::new(),
956 ordinals: Vec::new(),
957 codes: Vec::new(),
958 });
959 leaf.doc_ids.push(doc_id);
960 leaf.ordinals.push(ordinal);
961 leaf.codes.extend_from_slice(code);
962}
963
964#[derive(Clone, Debug)]
966pub struct BinaryScannSegment {
967 dim_bits: u32,
968 model_fingerprint: u64,
969 num_leaves: u32,
970 leaves: Vec<BinaryScannLeaf>,
971 len: usize,
973 stored_len: usize,
975}
976
977impl BinaryScannSegment {
978 pub fn build(
979 model: &BinaryScannModel,
980 codes: &[u8],
981 doc_id_ordinals: &[(u32, u16)],
982 scratch: &mut BinaryScannSearchScratch,
983 ) -> ScannResult<Self> {
984 Self::build_internal(model, codes, doc_id_ordinals, None, scratch)
985 }
986
987 pub fn build_with_soar(
995 model: &BinaryScannModel,
996 codes: &[u8],
997 doc_id_ordinals: &[(u32, u16)],
998 soar: &SoarConfig,
999 scratch: &mut BinaryScannSearchScratch,
1000 ) -> ScannResult<Self> {
1001 Self::build_internal(model, codes, doc_id_ordinals, Some(soar), scratch)
1002 }
1003
1004 fn build_internal(
1005 model: &BinaryScannModel,
1006 codes: &[u8],
1007 doc_id_ordinals: &[(u32, u16)],
1008 soar: Option<&SoarConfig>,
1009 scratch: &mut BinaryScannSearchScratch,
1010 ) -> ScannResult<Self> {
1011 let expected = doc_id_ordinals
1012 .len()
1013 .checked_mul(model.byte_len())
1014 .ok_or_else(|| ScannFormatError::new("binary ScaNN segment size overflows"))?;
1015 if codes.len() != expected {
1016 return Err(ScannFormatError::new(
1017 "binary ScaNN segment code and label columns are inconsistent",
1018 ));
1019 }
1020 if soar.is_some_and(|config| !config.spill_threshold.is_finite()) {
1021 return Err(ScannFormatError::new(
1022 "binary ScaNN spill threshold must be finite",
1023 ));
1024 }
1025
1026 let spill_enabled =
1027 soar.is_some_and(|config| config.num_secondary > 0) && model.num_leaves > 1;
1028 if !spill_enabled {
1029 let mut grouped = rustc_hash::FxHashMap::<u32, BinaryScannLeaf>::default();
1033 for (&(doc_id, ordinal), code) in doc_id_ordinals
1034 .iter()
1035 .zip(codes.chunks_exact(model.byte_len()))
1036 {
1037 let primary_leaf = model.assign(code, scratch)?;
1038 push_binary_posting(&mut grouped, primary_leaf, doc_id, ordinal, code);
1039 }
1040 let mut leaves: Vec<_> = grouped.into_values().collect();
1041 leaves.sort_unstable_by_key(|leaf| leaf.leaf_id);
1042 let segment = Self {
1043 dim_bits: model.dim_bits,
1044 model_fingerprint: model.fingerprint,
1045 num_leaves: model.num_leaves,
1046 leaves,
1047 len: doc_id_ordinals.len(),
1048 stored_len: doc_id_ordinals.len(),
1049 };
1050 segment.validate_for(model)?;
1051 return Ok(segment);
1052 }
1053
1054 let mut assignments = Vec::with_capacity(doc_id_ordinals.len());
1055 for code in codes.chunks_exact(model.byte_len()) {
1056 assignments.push(model.spill_assignment(code, scratch)?);
1057 }
1058
1059 if let Some(config) = soar {
1060 if let Some(target_fraction) = config.calibration_target() {
1061 let spill_budget = ((assignments.len() as f64 * f64::from(target_fraction)).floor()
1064 as usize)
1065 .min(assignments.len());
1066 let mut ranked: Vec<usize> = assignments
1067 .iter()
1068 .enumerate()
1069 .filter_map(|(row, assignment)| assignment.secondary_leaf.map(|_| row))
1070 .collect();
1071 ranked.sort_unstable_by(|&left, &right| {
1072 assignments[right]
1073 .primary_distance
1074 .cmp(&assignments[left].primary_distance)
1075 .then_with(|| left.cmp(&right))
1076 });
1077 for &row in ranked.iter().skip(spill_budget) {
1078 assignments[row].secondary_leaf = None;
1079 }
1080 } else if config.selective {
1081 let threshold_sq = f64::from(config.spill_threshold).powi(2);
1082 for assignment in &mut assignments {
1083 if f64::from(assignment.primary_distance) < threshold_sq {
1084 assignment.secondary_leaf = None;
1085 }
1086 }
1087 }
1088 }
1089
1090 let mut grouped = rustc_hash::FxHashMap::<u32, BinaryScannLeaf>::default();
1091 for ((&(doc_id, ordinal), code), assignment) in doc_id_ordinals
1092 .iter()
1093 .zip(codes.chunks_exact(model.byte_len()))
1094 .zip(assignments)
1095 {
1096 push_binary_posting(&mut grouped, assignment.primary_leaf, doc_id, ordinal, code);
1097 if let Some(secondary_leaf) = assignment.secondary_leaf {
1098 push_binary_posting(&mut grouped, secondary_leaf, doc_id, ordinal, code);
1099 }
1100 }
1101 let mut leaves: Vec<_> = grouped.into_values().collect();
1102 leaves.sort_unstable_by_key(|leaf| leaf.leaf_id);
1103 let stored_len = leaves.iter().map(|leaf| leaf.doc_ids.len()).sum();
1104 let segment = Self {
1105 dim_bits: model.dim_bits,
1106 model_fingerprint: model.fingerprint,
1107 num_leaves: model.num_leaves,
1108 leaves,
1109 len: doc_id_ordinals.len(),
1110 stored_len,
1111 };
1112 segment.validate_for(model)?;
1113 Ok(segment)
1114 }
1115
1116 pub fn merge_compatible(
1119 model: &BinaryScannModel,
1120 segments: &[(&Self, u32)],
1121 ) -> ScannResult<Self> {
1122 for &(segment, _) in segments {
1123 segment.validate_for(model)?;
1124 }
1125 let mut cursors = vec![0usize; segments.len()];
1126 let mut queue = BinaryHeap::new();
1127 for (segment_index, (segment, _)) in segments.iter().enumerate() {
1128 if let Some(first) = segment.leaves.first() {
1129 queue.push(Reverse((first.leaf_id, segment_index)));
1130 }
1131 }
1132 let mut leaves: Vec<BinaryScannLeaf> = Vec::new();
1133 let len = segments.iter().try_fold(0usize, |total, (segment, _)| {
1134 total
1135 .checked_add(segment.len)
1136 .ok_or_else(|| ScannFormatError::new("binary ScaNN merge count overflows"))
1137 })?;
1138 let mut stored_len = 0usize;
1139 while let Some(Reverse((leaf_id, segment_index))) = queue.pop() {
1140 let (segment, doc_base) = segments[segment_index];
1141 let source = &segment.leaves[cursors[segment_index]];
1142 if leaves.last().is_none_or(|leaf| leaf.leaf_id != leaf_id) {
1143 leaves.push(BinaryScannLeaf {
1144 leaf_id,
1145 doc_ids: Vec::new(),
1146 ordinals: Vec::new(),
1147 codes: Vec::new(),
1148 });
1149 }
1150 let target = leaves.last_mut().expect("leaf was just inserted");
1151 target.doc_ids.reserve(source.doc_ids.len());
1152 for &doc_id in &source.doc_ids {
1153 target
1154 .doc_ids
1155 .push(doc_id.checked_add(doc_base).ok_or_else(|| {
1156 ScannFormatError::new("binary ScaNN merge document ID overflows u32")
1157 })?);
1158 }
1159 target.ordinals.extend_from_slice(&source.ordinals);
1160 target.codes.extend_from_slice(&source.codes);
1161 stored_len = stored_len
1162 .checked_add(source.doc_ids.len())
1163 .ok_or_else(|| ScannFormatError::new("binary ScaNN merge count overflows"))?;
1164 cursors[segment_index] += 1;
1165 if let Some(next) = segment.leaves.get(cursors[segment_index]) {
1166 queue.push(Reverse((next.leaf_id, segment_index)));
1167 }
1168 }
1169 let merged = Self {
1170 dim_bits: model.dim_bits,
1171 model_fingerprint: model.fingerprint,
1172 num_leaves: model.num_leaves,
1173 leaves,
1174 len,
1175 stored_len,
1176 };
1177 merged.validate_for(model)?;
1178 Ok(merged)
1179 }
1180
1181 pub fn len(&self) -> usize {
1182 self.len
1183 }
1184
1185 pub fn is_empty(&self) -> bool {
1186 self.len == 0
1187 }
1188
1189 pub fn stored_len(&self) -> usize {
1191 self.stored_len
1192 }
1193
1194 pub fn to_payload(
1195 &self,
1196 model: &BinaryScannModel,
1197 artifact: &ScannTrainedArtifact,
1198 doc_count: u32,
1199 ) -> ScannResult<ScannSegmentPayload> {
1200 self.validate_for(model)?;
1201 let reopened = BinaryScannModel::from_artifact(artifact)?;
1202 if reopened.fingerprint != model.fingerprint {
1203 return Err(ScannFormatError::new(
1204 "binary ScaNN segment model does not match the persisted artifact",
1205 ));
1206 }
1207 let mut runs = Vec::with_capacity(self.leaves.len());
1208 for leaf in &self.leaves {
1209 runs.push(ScannLeafRun::from_rows(
1210 leaf.leaf_id,
1211 0,
1212 &leaf.doc_ids,
1213 &leaf.ordinals,
1214 leaf.codes.clone(),
1215 ScannEncoding::BinaryHamming,
1216 self.dim_bits,
1217 )?);
1218 }
1219 ScannSegmentPayload::new(artifact, doc_count, runs)
1220 }
1221
1222 fn validate_for(&self, model: &BinaryScannModel) -> ScannResult<()> {
1223 if self.dim_bits != model.dim_bits
1224 || self.model_fingerprint != model.fingerprint
1225 || self.num_leaves != model.num_leaves
1226 {
1227 return Err(ScannFormatError::new(
1228 "binary ScaNN segment belongs to a different trained generation",
1229 ));
1230 }
1231 let byte_len = model.byte_len();
1232 let mut previous = None;
1233 let mut total = 0usize;
1234 for leaf in &self.leaves {
1235 if leaf.leaf_id >= self.num_leaves
1236 || previous.is_some_and(|previous| previous >= leaf.leaf_id)
1237 || leaf.doc_ids.len() != leaf.ordinals.len()
1238 || leaf.codes.len() != leaf.doc_ids.len().saturating_mul(byte_len)
1239 {
1240 return Err(ScannFormatError::new(
1241 "binary ScaNN leaf directory or columns are inconsistent",
1242 ));
1243 }
1244 previous = Some(leaf.leaf_id);
1245 total = total
1246 .checked_add(leaf.doc_ids.len())
1247 .ok_or_else(|| ScannFormatError::new("binary ScaNN segment count overflows"))?;
1248 }
1249 let maximum_stored = self
1250 .len
1251 .checked_mul(2)
1252 .ok_or_else(|| ScannFormatError::new("binary ScaNN segment count overflows"))?;
1253 if total != self.stored_len
1254 || self.stored_len < self.len
1255 || self.stored_len > maximum_stored
1256 {
1257 return Err(ScannFormatError::new(
1258 "binary ScaNN segment vector count is inconsistent",
1259 ));
1260 }
1261 Ok(())
1262 }
1263
1264 fn scan(
1265 &self,
1266 query: &[u8],
1267 plan: &BinaryScannProbePlan,
1268 doc_base: u32,
1269 k: usize,
1270 best: &mut BinaryHeap<BinaryScannHit>,
1271 scratch: &mut BinaryScannSearchScratch,
1272 ) -> ScannResult<()> {
1273 if plan.model_fingerprint != self.model_fingerprint {
1274 return Err(ScannFormatError::new(
1275 "binary ScaNN probe plan belongs to a different trained generation",
1276 ));
1277 }
1278 let byte_len = self.dim_bits as usize / 8;
1279 let kernel = HammingKernel::resolve();
1280 for &leaf_id in &plan.leaf_ids {
1281 let Ok(position) = self
1282 .leaves
1283 .binary_search_by_key(&leaf_id, |leaf| leaf.leaf_id)
1284 else {
1285 continue;
1286 };
1287 let leaf = &self.leaves[position];
1288 for start in (0..leaf.doc_ids.len()).step_by(HAMMING_SCAN_BLOCK) {
1289 let rows = HAMMING_SCAN_BLOCK.min(leaf.doc_ids.len() - start);
1290 scratch.distances.clear();
1291 scratch.distances.resize(rows, 0);
1292 kernel.distances(
1293 query,
1294 &leaf.codes[start * byte_len..(start + rows) * byte_len],
1295 byte_len,
1296 &mut scratch.distances,
1297 );
1298 for (local, &distance) in scratch.distances.iter().enumerate() {
1299 if k == 0 {
1300 continue;
1301 }
1302 let index = start + local;
1303 let doc_id = leaf.doc_ids[index].checked_add(doc_base).ok_or_else(|| {
1304 ScannFormatError::new("binary ScaNN query document ID overflows u32")
1305 })?;
1306 let ordinal = leaf.ordinals[index];
1307 if scratch.best_hit_keys.contains(&(doc_id, ordinal)) {
1308 continue;
1309 }
1310 let hit = BinaryScannHit {
1311 doc_id,
1312 ordinal,
1313 distance,
1314 };
1315 if best.len() < k {
1316 best.push(hit);
1317 scratch.best_hit_keys.insert((doc_id, ordinal));
1318 } else if best.peek().is_some_and(|worst| hit < *worst) {
1319 let evicted = best.pop().expect("non-empty top-k heap has a worst hit");
1320 scratch
1321 .best_hit_keys
1322 .remove(&(evicted.doc_id, evicted.ordinal));
1323 best.push(hit);
1324 scratch.best_hit_keys.insert((doc_id, ordinal));
1325 }
1326 }
1327 }
1328 }
1329 Ok(())
1330 }
1331}
1332
1333#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1336pub struct BinaryScannHit {
1337 pub doc_id: u32,
1338 pub ordinal: u16,
1339 pub distance: u32,
1340}
1341
1342impl Ord for BinaryScannHit {
1343 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
1344 self.distance
1345 .cmp(&other.distance)
1346 .then_with(|| self.doc_id.cmp(&other.doc_id))
1347 .then_with(|| self.ordinal.cmp(&other.ordinal))
1348 }
1349}
1350
1351impl PartialOrd for BinaryScannHit {
1352 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
1353 Some(self.cmp(other))
1354 }
1355}
1356
1357struct BinaryPartition {
1358 centroids: Vec<u8>,
1359 groups: Vec<BinaryTrainingRows>,
1360}
1361
1362#[derive(Debug)]
1366enum BinaryTrainingRows {
1367 Contiguous(Range<usize>),
1368 Indexed(Vec<usize>),
1369}
1370
1371impl BinaryTrainingRows {
1372 fn len(&self) -> usize {
1373 match self {
1374 Self::Contiguous(range) => range.len(),
1375 Self::Indexed(indices) => indices.len(),
1376 }
1377 }
1378
1379 fn source_row(&self, position: usize) -> usize {
1380 match self {
1381 Self::Contiguous(range) => range.start + position,
1382 Self::Indexed(indices) => indices[position],
1383 }
1384 }
1385
1386 fn from_indices(indices: Vec<usize>) -> Self {
1387 let Some(&first) = indices.first() else {
1388 return Self::Indexed(indices);
1389 };
1390 if indices
1391 .iter()
1392 .enumerate()
1393 .all(|(offset, &row)| row == first + offset)
1394 {
1395 Self::Contiguous(first..first + indices.len())
1396 } else {
1397 Self::Indexed(indices)
1398 }
1399 }
1400}
1401
1402#[allow(clippy::too_many_arguments)]
1403fn train_binary_partition(
1404 source_codes: &[u8],
1405 rows: &BinaryTrainingRows,
1406 byte_len: usize,
1407 dim_bits: u32,
1408 clusters: usize,
1409 train_iters: usize,
1410 seed: u64,
1411 depth: usize,
1412 retain_groups: bool,
1413 index_label: &str,
1414 stats: &mut BinaryScannTrainingStats,
1415) -> ScannResult<BinaryPartition> {
1416 let row_count = rows.len();
1417 if row_count == 0 || byte_len == 0 || clusters == 0 || clusters > row_count {
1418 return Err(ScannFormatError::new(
1419 "invalid recursive binary ScaNN partition shape",
1420 ));
1421 }
1422 stats.max_depth = stats.max_depth.max(depth);
1423 if clusters == row_count {
1424 let groups = if retain_groups {
1425 let groups: Vec<BinaryTrainingRows> = (0..row_count)
1426 .map(|position| {
1427 let row = rows.source_row(position);
1428 BinaryTrainingRows::Contiguous(row..row + 1)
1429 })
1430 .collect();
1431 stats.retained_groups = stats.retained_groups.saturating_add(groups.len());
1432 groups
1433 } else {
1434 Vec::new()
1435 };
1436 return Ok(BinaryPartition {
1437 centroids: materialize_training_rows(source_codes, rows, byte_len)?,
1438 groups,
1439 });
1440 }
1441
1442 let branches = training_branch_factor(clusters).min(row_count);
1443 let mut config = BinaryIvfConfig::new(dim_bits as usize, branches);
1444 config.routing = IvfRoutingMode::Flat;
1445 config.train_iters = train_iters;
1446 config.max_train_samples = row_count;
1447 config.seed = seed;
1448 let local_centroids =
1449 train_binary_codebook_for_rows(&config, source_codes, rows, byte_len, index_label, stats)?;
1450 stats.splits = stats.splits.saturating_add(1);
1451 stats.max_split_clusters = stats.max_split_clusters.max(branches);
1452 if branches == clusters {
1453 let groups = if retain_groups {
1454 let groups =
1455 partition_one_group_nonempty(source_codes, rows, &local_centroids, byte_len);
1456 stats.retained_groups = stats.retained_groups.saturating_add(groups.len());
1457 groups
1458 } else {
1459 Vec::new()
1460 };
1461 return Ok(BinaryPartition {
1462 groups,
1463 centroids: local_centroids,
1464 });
1465 }
1466
1467 let local_groups = partition_one_group_nonempty(source_codes, rows, &local_centroids, byte_len);
1468 let sizes: Vec<usize> = local_groups.iter().map(BinaryTrainingRows::len).collect();
1469 let allocations = allocate_child_clusters(&sizes, clusters);
1470 if allocations.iter().sum::<usize>() != clusters || allocations.contains(&0) {
1471 return Err(ScannFormatError::new(
1472 "recursive binary ScaNN centroid allocation is inconsistent",
1473 ));
1474 }
1475 let mut centroids = Vec::with_capacity(clusters.saturating_mul(byte_len));
1476 let mut groups = Vec::with_capacity(clusters);
1477 for (branch, (group, &allocation)) in local_groups.iter().zip(&allocations).enumerate() {
1478 let child = train_binary_partition(
1479 source_codes,
1480 group,
1481 byte_len,
1482 dim_bits,
1483 allocation,
1484 train_iters,
1485 derived_seed(seed, depth, branch),
1486 depth + 1,
1487 retain_groups,
1488 index_label,
1489 stats,
1490 )?;
1491 centroids.extend_from_slice(&child.centroids);
1492 if retain_groups {
1493 groups.extend(child.groups);
1494 }
1495 }
1496 Ok(BinaryPartition { centroids, groups })
1497}
1498
1499fn training_branch_factor(clusters: usize) -> usize {
1500 if clusters <= MAX_LOCAL_K_MAJORITY_BRANCHES {
1501 clusters
1502 } else {
1503 ((clusters as f64).sqrt().ceil() as usize).clamp(2, MAX_LOCAL_K_MAJORITY_BRANCHES)
1504 }
1505}
1506
1507fn deterministic_sample_rows(
1508 num_vectors: usize,
1509 sample_count: usize,
1510 seed: u64,
1511) -> BinaryTrainingRows {
1512 if sample_count >= num_vectors {
1513 return BinaryTrainingRows::Contiguous(0..num_vectors);
1514 }
1515 let mut rng = rand::rngs::StdRng::seed_from_u64(seed);
1516 let mut indices = rand::seq::index::sample(&mut rng, num_vectors, sample_count).into_vec();
1517 indices.sort_unstable();
1518 BinaryTrainingRows::from_indices(indices)
1519}
1520
1521fn materialize_training_rows(
1522 source_codes: &[u8],
1523 rows: &BinaryTrainingRows,
1524 byte_len: usize,
1525) -> ScannResult<Vec<u8>> {
1526 let capacity = rows
1527 .len()
1528 .checked_mul(byte_len)
1529 .ok_or_else(|| ScannFormatError::new("binary ScaNN group matrix overflows"))?;
1530 let mut packed = Vec::with_capacity(capacity);
1531 for position in 0..rows.len() {
1532 let row = rows.source_row(position);
1533 let start = row
1534 .checked_mul(byte_len)
1535 .ok_or_else(|| ScannFormatError::new("binary ScaNN source row overflows"))?;
1536 let code = source_codes
1537 .get(start..start + byte_len)
1538 .ok_or_else(|| ScannFormatError::new("binary ScaNN source row is truncated"))?;
1539 packed.extend_from_slice(code);
1540 }
1541 Ok(packed)
1542}
1543
1544fn train_binary_codebook_for_rows(
1545 config: &BinaryIvfConfig,
1546 source_codes: &[u8],
1547 rows: &BinaryTrainingRows,
1548 byte_len: usize,
1549 index_label: &str,
1550 stats: &mut BinaryScannTrainingStats,
1551) -> ScannResult<Vec<u8>> {
1552 match rows {
1553 BinaryTrainingRows::Contiguous(range) => {
1554 let start = range
1555 .start
1556 .checked_mul(byte_len)
1557 .ok_or_else(|| ScannFormatError::new("binary ScaNN group offset overflows"))?;
1558 let end = range
1559 .end
1560 .checked_mul(byte_len)
1561 .ok_or_else(|| ScannFormatError::new("binary ScaNN group offset overflows"))?;
1562 let codes = source_codes
1563 .get(start..end)
1564 .ok_or_else(|| ScannFormatError::new("binary ScaNN group is truncated"))?;
1565 train_binary_k_majority_codebook(config, codes, rows.len(), index_label)
1566 .map_err(|error| ScannFormatError::new(error.to_string()))
1567 }
1568 BinaryTrainingRows::Indexed(_) => {
1569 let packed = materialize_training_rows(source_codes, rows, byte_len)?;
1570 stats.max_materialized_training_bytes =
1571 stats.max_materialized_training_bytes.max(packed.len());
1572 train_binary_k_majority_codebook(config, &packed, rows.len(), index_label)
1573 .map_err(|error| ScannFormatError::new(error.to_string()))
1574 }
1575 }
1576}
1577
1578fn derived_seed(seed: u64, level: usize, parent: usize) -> u64 {
1579 seed ^ (level as u64).wrapping_mul(0xd6e8_feb8_6659_fd93)
1580 ^ (parent as u64).wrapping_mul(0x9e37_79b9_7f4a_7c15)
1581}
1582
1583fn partition_one_group_nonempty(
1584 source_codes: &[u8],
1585 rows: &BinaryTrainingRows,
1586 centroids: &[u8],
1587 byte_len: usize,
1588) -> Vec<BinaryTrainingRows> {
1589 let kernel = HammingKernel::resolve();
1590 let child_count = centroids.len() / byte_len;
1591 let row_count = rows.len();
1592 let mut assignments = vec![0usize; row_count];
1593 let mut assignment_distances = vec![0u32; row_count];
1594 let mut counts = vec![0usize; child_count];
1595 let mut distances = vec![0u32; child_count];
1596 for position in 0..row_count {
1597 let source_row = rows.source_row(position);
1598 let offset = source_row * byte_len;
1599 let code = &source_codes[offset..offset + byte_len];
1600 kernel.distances(code, centroids, byte_len, &mut distances);
1601 let (child, &distance) = distances
1602 .iter()
1603 .enumerate()
1604 .min_by_key(|&(child, distance)| (*distance, child))
1605 .expect("a populated routing parent has children");
1606 assignments[position] = child;
1607 assignment_distances[position] = distance;
1608 counts[child] += 1;
1609 }
1610 for empty in 0..child_count {
1611 if counts[empty] != 0 {
1612 continue;
1613 }
1614 let replacement = (0..row_count)
1615 .filter(|&row| counts[assignments[row]] > 1)
1616 .max_by_key(|&row| (assignment_distances[row], Reverse(row)))
1617 .expect("training readiness guarantees one sample per centroid");
1618 counts[assignments[replacement]] -= 1;
1619 assignments[replacement] = empty;
1620 assignment_distances[replacement] = 0;
1621 counts[empty] = 1;
1622 }
1623 let mut groups: Vec<Vec<usize>> = counts
1624 .iter()
1625 .map(|&count| Vec::with_capacity(count))
1626 .collect();
1627 for (position, &child) in assignments.iter().enumerate() {
1628 groups[child].push(rows.source_row(position));
1629 }
1630 groups
1631 .into_iter()
1632 .map(BinaryTrainingRows::from_indices)
1633 .collect()
1634}
1635
1636struct Fingerprint(u64);
1637
1638impl Fingerprint {
1639 fn new() -> Self {
1640 Self(0xcbf2_9ce4_8422_2325)
1641 }
1642
1643 fn write(&mut self, bytes: &[u8]) {
1644 for &byte in bytes {
1645 self.0 ^= u64::from(byte);
1646 self.0 = self.0.wrapping_mul(0x0000_0100_0000_01b3);
1647 }
1648 }
1649
1650 fn finish(self) -> u64 {
1651 self.0
1652 }
1653}
1654
1655#[cfg(test)]
1656mod tests {
1657 use super::*;
1658 use crate::structures::simd::hamming_distance;
1659
1660 fn corpus(rows: usize) -> Vec<u8> {
1661 let anchors = [
1662 [0x00, 0x00],
1663 [0xff, 0xff],
1664 [0x0f, 0x0f],
1665 [0xf0, 0xf0],
1666 [0xaa, 0xaa],
1667 [0x55, 0x55],
1668 [0x33, 0xcc],
1669 [0xcc, 0x33],
1670 ];
1671 let mut codes = Vec::with_capacity(rows * 2);
1672 for row in 0..rows {
1673 let mut code = anchors[row % anchors.len()];
1674 code[(row / anchors.len()) % 2] ^= 1 << ((row / 16) % 8);
1675 codes.extend_from_slice(&code);
1676 }
1677 codes
1678 }
1679
1680 fn training() -> BinaryScannTraining {
1681 BinaryScannTraining {
1682 dim_bits: 16,
1683 geometry: ScannGeometry {
1684 centroid_levels: 3,
1685 num_leaves: 8,
1686 level_counts: vec![2, 4, 8],
1687 },
1688 train_iters: 2,
1689 seed: 73,
1690 }
1691 }
1692
1693 fn two_leaf_model() -> BinaryScannModel {
1694 let mut model = BinaryScannModel {
1695 dim_bits: 8,
1696 num_leaves: 2,
1697 levels: vec![BinaryRoutingLevel {
1698 centroids: vec![0x00, 0xff],
1699 parent_offsets: vec![0, 2],
1700 }],
1701 fingerprint: 0,
1702 };
1703 model.fingerprint = model.compute_fingerprint();
1704 model.validate().unwrap();
1705 model
1706 }
1707
1708 #[test]
1709 fn readiness_is_derived_from_geometry_not_user_configuration() {
1710 let training = training();
1711 assert_eq!(
1712 training.training_state(99_999).unwrap(),
1713 ScannTrainingState::AwaitingData {
1714 observed: 99_999,
1715 required: 100_000,
1716 }
1717 );
1718 assert_eq!(training.desired_training_vectors(500_000).unwrap(), 100_000);
1719 }
1720
1721 #[test]
1722 fn large_binary_partition_never_flat_trains_terminal_leaf_count() {
1723 let codes = corpus(100_000);
1724 let training = BinaryScannTraining {
1725 dim_bits: 16,
1726 geometry: ScannGeometry {
1727 centroid_levels: 1,
1728 num_leaves: 1_024,
1729 level_counts: vec![1_024],
1730 },
1731 train_iters: 1,
1732 seed: 91,
1733 };
1734 let (model, stats) =
1735 BinaryScannModel::train_with_stats(&training, &codes, 100_000, "test").unwrap();
1736 assert_eq!(model.num_leaves(), 1_024);
1737 assert!(stats.splits > 1);
1738 assert!(
1739 stats.max_split_clusters <= MAX_LOCAL_K_MAJORITY_BRANCHES,
1740 "binary local split widened to {} clusters",
1741 stats.max_split_clusters,
1742 );
1743 assert!(stats.max_split_clusters < 1_024);
1744 assert_eq!(
1745 stats.retained_groups, 0,
1746 "terminal training must not allocate one Vec per leaf",
1747 );
1748 assert!(
1749 stats.max_materialized_training_bytes < codes.len(),
1750 "binary training cloned the complete {}-byte retained sample",
1751 codes.len(),
1752 );
1753 }
1754
1755 #[test]
1756 fn full_probe_widens_past_the_legacy_sixty_four_parent_beam() {
1757 let root_count = 65usize;
1758 let leaf_count = root_count * root_count;
1759 let mut leaf_offsets = Vec::with_capacity(root_count + 1);
1760 for parent in 0..=root_count {
1761 leaf_offsets.push((parent * root_count) as u32);
1762 }
1763 let mut model = BinaryScannModel {
1764 dim_bits: 8,
1765 num_leaves: leaf_count as u32,
1766 levels: vec![
1767 BinaryRoutingLevel {
1768 centroids: vec![0; root_count],
1769 parent_offsets: vec![0, root_count as u32],
1770 },
1771 BinaryRoutingLevel {
1772 centroids: vec![0; leaf_count],
1773 parent_offsets: leaf_offsets,
1774 },
1775 ],
1776 fingerprint: 0,
1777 };
1778 model.fingerprint = model.compute_fingerprint();
1779 model.validate().unwrap();
1780
1781 let mut scratch = BinaryScannSearchScratch::default();
1782 let owned = model.probe(&[0], leaf_count, 64, &mut scratch).unwrap();
1783 assert_eq!(owned.leaf_ids.len(), leaf_count);
1784 assert_eq!(owned.leaf_ids, (0..leaf_count as u32).collect::<Vec<_>>());
1785
1786 let artifact = model.to_artifact(7, 100_000).unwrap();
1787 let bytes = artifact.to_bytes().unwrap();
1788 let artifact = ScannTrainedArtifactView::parse(&bytes).unwrap();
1789 let quantized = QuantizedBinaryScannModel::from_artifact_view(&artifact).unwrap();
1790 let view = quantized.view(&bytes).unwrap();
1791 let mapped = view.probe(&[0], leaf_count, 64, &mut scratch).unwrap();
1792 assert_eq!(mapped.leaf_ids, owned.leaf_ids);
1793 }
1794
1795 #[test]
1796 fn packed_hamming_search_is_exact_and_merge_independent() {
1797 let training_codes = corpus(100_000);
1798 let model = BinaryScannModel::train(&training(), &training_codes, 100_000, "test").unwrap();
1799 let rebuilt =
1800 BinaryScannModel::train(&training(), &training_codes, 100_000, "test").unwrap();
1801 assert_eq!(rebuilt.fingerprint(), model.fingerprint());
1802 let mut scratch = BinaryScannSearchScratch::default();
1803 let query = [0b1010_1011, 0b1010_1010];
1804 let first = model.probe(&query, 4, 2, &mut scratch).unwrap();
1805 let second = rebuilt.probe(&query, 4, 2, &mut scratch).unwrap();
1806 assert_eq!(first, second);
1807 let artifact = model.to_artifact(11, 100_000).unwrap();
1808 let artifact_bytes = artifact.to_bytes().unwrap();
1809 let artifact_view = ScannTrainedArtifactView::parse(&artifact_bytes).unwrap();
1810 let quantized = QuantizedBinaryScannModel::from_artifact_view(&artifact_view).unwrap();
1811 let quantized_view = quantized.view(&artifact_bytes).unwrap();
1812 let range_backed = quantized_view.probe(&query, 4, 2, &mut scratch).unwrap();
1813 assert_eq!(range_backed, first);
1814 assert_eq!(quantized.fingerprint(), model.fingerprint());
1815 assert!(quantized.estimated_memory_bytes() < artifact_bytes.len());
1816
1817 let codes = corpus(512);
1818 let labels: Vec<_> = (0..512).map(|doc_id| (doc_id, 0)).collect();
1819 let monolith = BinaryScannSegment::build(&model, &codes, &labels, &mut scratch).unwrap();
1820
1821 let split = 193;
1822 let left_labels: Vec<_> = (0..split as u32).map(|doc_id| (doc_id, 0)).collect();
1823 let right_labels: Vec<_> = (0..(512 - split) as u32)
1824 .map(|doc_id| (doc_id, 0))
1825 .collect();
1826 let left =
1827 BinaryScannSegment::build(&model, &codes[..split * 2], &left_labels, &mut scratch)
1828 .unwrap();
1829 let right =
1830 BinaryScannSegment::build(&model, &codes[split * 2..], &right_labels, &mut scratch)
1831 .unwrap();
1832 let merged =
1833 BinaryScannSegment::merge_compatible(&model, &[(&left, 0), (&right, split as u32)])
1834 .unwrap();
1835
1836 let expected = model
1837 .search_segments(
1838 &query,
1839 25,
1840 model.num_leaves() as usize,
1841 8,
1842 &[(&monolith, 0)],
1843 &mut scratch,
1844 )
1845 .unwrap();
1846 let split_hits = model
1847 .search_segments(
1848 &query,
1849 25,
1850 model.num_leaves() as usize,
1851 8,
1852 &[(&left, 0), (&right, split as u32)],
1853 &mut scratch,
1854 )
1855 .unwrap();
1856 let merged_hits = model
1857 .search_segments(
1858 &query,
1859 25,
1860 model.num_leaves() as usize,
1861 8,
1862 &[(&merged, 0)],
1863 &mut scratch,
1864 )
1865 .unwrap();
1866 assert_eq!(split_hits, expected);
1867 assert_eq!(merged_hits, expected);
1868
1869 let mut brute_force: Vec<_> = codes
1870 .chunks_exact(2)
1871 .enumerate()
1872 .map(|(doc_id, code)| BinaryScannHit {
1873 doc_id: doc_id as u32,
1874 ordinal: 0,
1875 distance: hamming_distance(&query, code),
1876 })
1877 .collect();
1878 brute_force.sort_unstable();
1879 brute_force.truncate(25);
1880 assert_eq!(expected, brute_force);
1881 }
1882
1883 #[test]
1884 fn selective_binary_spill_is_bounded_deterministic_and_deduplicated() {
1885 let model = two_leaf_model();
1886 let artifact = model.to_artifact(17, 100_000).unwrap();
1887 let codes = vec![0x00, 0x01, 0x03, 0x07, 0x0f, 0xff, 0xfe, 0xfc, 0xf8, 0xf0];
1888 let labels: Vec<_> = (0..codes.len() as u32).map(|doc_id| (doc_id, 0)).collect();
1889 let soar = SoarConfig::new().target_spill_fraction(0.30);
1890 let mut scratch = BinaryScannSearchScratch::default();
1891
1892 let artifact_bytes = artifact.to_bytes().unwrap();
1893 let artifact_view = ScannTrainedArtifactView::parse(&artifact_bytes).unwrap();
1894 let quantized = QuantizedBinaryScannModel::from_artifact_view(&artifact_view).unwrap();
1895 let quantized = quantized.view(&artifact_bytes).unwrap();
1896 for code in &codes {
1897 let code = std::slice::from_ref(code);
1898 assert_eq!(
1899 model.spill_assignment(code, &mut scratch).unwrap(),
1900 quantized.spill_assignment(code, &mut scratch).unwrap(),
1901 );
1902 }
1903
1904 let first =
1905 BinaryScannSegment::build_with_soar(&model, &codes, &labels, &soar, &mut scratch)
1906 .unwrap();
1907 let second =
1908 BinaryScannSegment::build_with_soar(&model, &codes, &labels, &soar, &mut scratch)
1909 .unwrap();
1910 assert_eq!(first.len(), codes.len());
1911 assert_eq!(first.stored_len(), codes.len() + 3);
1912
1913 let first_payload = first
1914 .to_payload(&model, &artifact, codes.len() as u32)
1915 .unwrap();
1916 let second_payload = second
1917 .to_payload(&model, &artifact, codes.len() as u32)
1918 .unwrap();
1919 assert_eq!(first_payload, second_payload);
1920 let encoded = first_payload.to_bytes().unwrap();
1921 let decoded = ScannSegmentPayload::from_bytes(&encoded).unwrap();
1922 assert_eq!(decoded, first_payload);
1923 decoded.validate_against(&artifact).unwrap();
1924
1925 let query = [0x0f];
1926 let hits = model
1927 .search_segments(&query, codes.len(), 2, 2, &[(&first, 0)], &mut scratch)
1928 .unwrap();
1929 assert_eq!(
1930 hits.len(),
1931 codes.len(),
1932 "secondary postings must not duplicate hits"
1933 );
1934 let mut expected: Vec<_> = codes
1935 .iter()
1936 .enumerate()
1937 .map(|(doc_id, code)| BinaryScannHit {
1938 doc_id: doc_id as u32,
1939 ordinal: 0,
1940 distance: hamming_distance(&query, std::slice::from_ref(code)),
1941 })
1942 .collect();
1943 expected.sort_unstable();
1944 assert_eq!(hits, expected);
1945
1946 let split = 5;
1947 let local_labels: Vec<_> = (0..split as u32).map(|doc_id| (doc_id, 0)).collect();
1948 let left = BinaryScannSegment::build_with_soar(
1949 &model,
1950 &codes[..split],
1951 &local_labels,
1952 &soar,
1953 &mut scratch,
1954 )
1955 .unwrap();
1956 let right = BinaryScannSegment::build_with_soar(
1957 &model,
1958 &codes[split..],
1959 &local_labels,
1960 &soar,
1961 &mut scratch,
1962 )
1963 .unwrap();
1964 let fingerprint = model.fingerprint();
1965 let merged =
1966 BinaryScannSegment::merge_compatible(&model, &[(&left, 0), (&right, split as u32)])
1967 .unwrap();
1968 assert_eq!(model.fingerprint(), fingerprint, "merge must not retrain");
1969 assert_eq!(merged.len(), codes.len());
1970 assert_eq!(merged.stored_len(), codes.len() + 2);
1971 let merged_hits = model
1972 .search_segments(&query, codes.len(), 2, 2, &[(&merged, 0)], &mut scratch)
1973 .unwrap();
1974 assert_eq!(merged_hits, expected);
1975
1976 let boundary_code = [0x0f];
1979 let boundary_label = [(42, 0)];
1980 let primary_only =
1981 BinaryScannSegment::build(&model, &boundary_code, &boundary_label, &mut scratch)
1982 .unwrap();
1983 let fully_spilled = BinaryScannSegment::build_with_soar(
1984 &model,
1985 &boundary_code,
1986 &boundary_label,
1987 &SoarConfig::full(),
1988 &mut scratch,
1989 )
1990 .unwrap();
1991 let nearby_query = [0x1f];
1992 assert!(
1993 model
1994 .search_segments(&nearby_query, 1, 1, 1, &[(&primary_only, 0)], &mut scratch)
1995 .unwrap()
1996 .is_empty()
1997 );
1998 assert_eq!(
1999 model
2000 .search_segments(&nearby_query, 1, 1, 1, &[(&fully_spilled, 0)], &mut scratch)
2001 .unwrap(),
2002 vec![BinaryScannHit {
2003 doc_id: 42,
2004 ordinal: 0,
2005 distance: 1,
2006 }],
2007 );
2008 }
2009}