Skip to main content

hermes_core/structures/vector/scann/
binary.rs

1//! Binary ScaNN routing and exact leaf scanning.
2//!
3//! Binary embeddings stay packed from training through serving. Routing uses
4//! a configurable one-to-three-level k-majority tree and leaf scans compute
5//! exact Hamming distances with Hermes' resolved AVX-512/AVX2/NEON/scalar
6//! kernel. The trained tree is global; segment objects contain only leaf-local
7//! document columns and exact packed codes, so compatible merges never train.
8
9use 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    /// Largest temporary packed code matrix materialized for a non-contiguous
38    /// row group. The complete retained sample is borrowed in place.
39    pub max_materialized_training_bytes: usize,
40}
41
42/// Training controls for a global binary ScaNN tree.
43///
44/// Readiness and sample size are intentionally absent: both are hardcoded and
45/// derived from `geometry`, matching the float ScaNN builder contract.
46#[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    /// The corpus floor is fixed in code and raised only when the chosen
86    /// geometry needs the hardcoded minimum sample coverage for every
87    /// terminal leaf.
88    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    /// Packed child centroids, ordered by parent node.
112    centroids: Vec<u8>,
113    /// `parent_offsets[p]..parent_offsets[p + 1]` is parent `p`'s child run.
114    parent_offsets: Vec<u32>,
115}
116
117/// Index-generation-scoped packed Hamming routing model.
118#[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/// Small executable metadata for mmap-backed packed-Hamming routing. The
134/// potentially multi-gigabyte centroid planes remain in artifact storage.
135#[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    /// Route once against the global tree. The resulting plan can be reused
388    /// across every immutable segment in the active generation.
389    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    /// Choose the normal primary leaf and the best alternate leaf reachable by
479    /// a small widened tree probe. Packed bits do not have a meaningful float
480    /// residual projection, so binary spilling uses exact centroid Hamming
481    /// distance while retaining SOAR's one-secondary storage policy.
482    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    /// Search any number of compatible segments with one shared routing plan.
518    /// `doc_base` rebases segment-local IDs without touching their payload.
519    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                    &centroids[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    /// Mmap-backed equivalent of [`BinaryScannModel::spill_assignment`]. The
801    /// centroid plane remains borrowed from the artifact mapping.
802    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/// Per-query allocations retained by the caller and reused across segments.
866#[derive(Default, Debug)]
867pub struct BinaryScannSearchScratch {
868    frontier: Vec<u32>,
869    candidates: Vec<RouteCandidate>,
870    distances: Vec<u32>,
871    /// Logical vector IDs currently represented in the top-k heap. Tracking
872    /// only retained hits keeps secondary-posting deduplication bounded by k,
873    /// rather than by the number of postings scanned.
874    best_hit_keys: rustc_hash::FxHashSet<(u32, u16)>,
875}
876
877/// Deterministic packed-Hamming primary and optional secondary candidate.
878/// Policy code decides whether to retain the secondary under its storage cap.
879#[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/// Immutable segment-local exact binary payload.
913#[derive(Clone, Debug)]
914pub struct BinaryScannSegment {
915    dim_bits: u32,
916    model_fingerprint: u64,
917    num_leaves: u32,
918    leaves: Vec<BinaryScannLeaf>,
919    /// Logical vectors before optional secondary posting expansion.
920    len: usize,
921    /// Physical postings, bounded to at most two per logical vector.
922    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    /// Build with deterministic one-secondary binary spilling.
936    ///
937    /// A negative `spill_threshold` keeps `SoarConfig`'s target-fraction tag:
938    /// the most poorly represented primary assignments are retained up to a
939    /// strict segment-local storage budget. Explicit non-negative thresholds
940    /// use the same primary residual rule as float SOAR, with squared L2 over
941    /// bits represented exactly by Hamming distance.
942    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            // Preserve the allocation profile of the established primary-only
978            // builder. Spill ranking state is paid only when spilling is
979            // explicitly enabled for this segment.
980            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                // Floor, rather than round, makes the target a strict storage
1010                // ceiling even for tiny streaming segments.
1011                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    /// Leaf-wise compatible merge. Codes are copied verbatim and no routing or
1065    /// training runs; only segment-local document IDs are rebased.
1066    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    /// Number of physical leaf postings after secondary spill expansion.
1138    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/// Exact Hamming result. Lower distance wins; ties are stable by document and
1282/// ordinal so segment layout and merge order cannot change the answer.
1283#[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/// A partition is an ordered view into the caller-owned packed code matrix.
1311/// The initial complete sample is a range and costs no additional memory;
1312/// child partitions retain only row identifiers instead of cloning codes.
1313#[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        // A boundary vector assigned primarily to leaf zero remains reachable
1925        // when a nearby query routes only to leaf one.
1926        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}