Skip to main content

hermes_core/structures/vector/scann/
geometry.rs

1//! Recall-oriented ScaNN partition geometry.
2//!
3//! The constants mirror the freshly validated Keenable ScaNN builder: do not
4//! expose a configurable minimum-training count. Readiness and sample size are
5//! derived from the selected geometry.
6
7use super::{
8    MAX_SCANN_LEAVES, MAX_SCANN_TREE_LEVELS, MIN_PARTITION_TRAINING_POINTS_PER_LEAF,
9    MIN_POINTS_FOR_PARTITIONING, ScannFormatError, ScannResult,
10};
11
12/// Higher-quality explicit geometry target documented by AlloyDB. Operators
13/// can ask for `rows / QUALITY_OPTIMIZED_POINTS_PER_LEAF` leaves explicitly
14/// when the additional build cost is justified by measurements on their
15/// corpus.
16pub const QUALITY_OPTIMIZED_POINTS_PER_LEAF: u64 = 100;
17/// AlloyDB's recall-oriented balanced guidance changes the leaf exponent with
18/// tree depth. Keep exactly one billion rows in the three-level band: it is a
19/// useful, measured recall point, while the four-level band prioritizes build
20/// scalability above that boundary.
21const THREE_LEVEL_MIN_POINTS: u64 = 100_000_000;
22const FOUR_LEVEL_MIN_POINTS_EXCLUSIVE: u64 = 1_000_000_000;
23
24#[derive(Clone, Debug, Eq, PartialEq)]
25pub struct ScannGeometry {
26    pub centroid_levels: u8,
27    pub num_leaves: u32,
28    /// Centroid count at each routing level, ending in `num_leaves`.
29    pub level_counts: Vec<u32>,
30}
31
32/// Derive automatic production geometry for an observed corpus.
33pub fn derive_geometry(points: u64, dimension: u32) -> ScannResult<ScannGeometry> {
34    derive_geometry_with_levels(points, dimension, None)
35}
36
37/// Derive geometry while allowing the schema-selected tree depth to override
38/// automatic depth. A zero-level tree is returned below the hardcoded
39/// partitioning floor; callers should keep serving the exact/flat generation.
40pub fn derive_geometry_with_levels(
41    points: u64,
42    dimension: u32,
43    requested_levels: Option<u8>,
44) -> ScannResult<ScannGeometry> {
45    derive_geometry_with_levels_and_sample_limit(points, dimension, requested_levels, points)
46}
47
48/// Derive automatic geometry and validate the number of training rows the
49/// builder can actually retain. The selected topology depends only on corpus
50/// geometry, never on a transient resource ceiling. A machine that cannot
51/// retain the hardcoded minimum sample must fail/defer the build rather than
52/// publish a different shared codebook shape.
53pub fn derive_geometry_with_levels_and_sample_limit(
54    points: u64,
55    dimension: u32,
56    requested_levels: Option<u8>,
57    sample_limit: u64,
58) -> ScannResult<ScannGeometry> {
59    if points == 0 {
60        return Err(ScannFormatError::new(
61            "ScaNN geometry requires at least one vector",
62        ));
63    }
64    if dimension == 0 {
65        return Err(ScannFormatError::new(
66            "ScaNN geometry requires a non-zero vector dimension",
67        ));
68    }
69    if let Some(levels) = requested_levels
70        && !(1..=MAX_SCANN_TREE_LEVELS).contains(&levels)
71    {
72        return Err(ScannFormatError::new(format!(
73            "ScaNN tree levels must be in 1..={MAX_SCANN_TREE_LEVELS}"
74        )));
75    }
76    if points < MIN_POINTS_FOR_PARTITIONING {
77        return Ok(ScannGeometry {
78            centroid_levels: 0,
79            num_leaves: 1,
80            level_counts: Vec::new(),
81        });
82    }
83
84    let (desired_leaves, corpus_min_levels) = automatic_leaf_target(points);
85    let leaves = desired_leaves.min(u64::from(MAX_SCANN_LEAVES)) as u32;
86    let levels = requested_levels
87        .unwrap_or_else(|| corpus_min_levels.max(width_required_levels(leaves, dimension)));
88    let geometry = geometry_for_leaves(leaves, levels)?;
89    let required_sample = u64::from(leaves)
90        .checked_mul(MIN_PARTITION_TRAINING_POINTS_PER_LEAF)
91        .ok_or_else(|| ScannFormatError::new("ScaNN minimum training sample overflows u64"))?
92        .max(MIN_POINTS_FOR_PARTITIONING);
93    let achievable_sample = points.min(sample_limit);
94    if achievable_sample < required_sample {
95        return Err(ScannFormatError::new(format!(
96            "ScaNN automatic geometry selected {leaves} leaves and needs at least {required_sample} training samples (hardcoded {MIN_PARTITION_TRAINING_POINTS_PER_LEAF} samples/leaf), but the builder can supply {achievable_sample}"
97        )));
98    }
99    Ok(geometry)
100}
101
102/// Construct and validate an explicit trained geometry.
103pub fn geometry_for_leaves(num_leaves: u32, levels: u8) -> ScannResult<ScannGeometry> {
104    if !(2..=MAX_SCANN_LEAVES).contains(&num_leaves) {
105        return Err(ScannFormatError::new(format!(
106            "ScaNN leaf count must be in 2..={MAX_SCANN_LEAVES}"
107        )));
108    }
109    if !(1..=MAX_SCANN_TREE_LEVELS).contains(&levels) {
110        return Err(ScannFormatError::new(format!(
111            "ScaNN tree levels must be in 1..={MAX_SCANN_TREE_LEVELS}"
112        )));
113    }
114    let factors = balanced_branching_factors(num_leaves, levels);
115    let mut product = 1u64;
116    let mut level_counts = Vec::with_capacity(usize::from(levels));
117    for (level, factor) in factors.into_iter().enumerate() {
118        product = product.saturating_mul(u64::from(factor));
119        level_counts.push(if level + 1 == usize::from(levels) {
120            num_leaves
121        } else {
122            product.min(u64::from(num_leaves)) as u32
123        });
124    }
125    Ok(ScannGeometry {
126        centroid_levels: levels,
127        num_leaves,
128        level_counts,
129    })
130}
131
132/// Construct explicit leaf geometry, deriving only its routing depth when the
133/// schema does not pin one. Explicit leaf counts must not inherit corpus-size
134/// depth bands: those bands choose automatic leaf counts, not the shape of an
135/// operator-selected codebook.
136pub fn geometry_for_leaves_with_auto_depth(
137    num_leaves: u32,
138    dimension: u32,
139    requested_levels: Option<u8>,
140) -> ScannResult<ScannGeometry> {
141    if dimension == 0 {
142        return Err(ScannFormatError::new(
143            "ScaNN geometry requires a non-zero vector dimension",
144        ));
145    }
146    geometry_for_leaves(
147        num_leaves,
148        requested_levels.unwrap_or_else(|| width_required_levels(num_leaves, dimension)),
149    )
150}
151
152/// Hardcoded recall-oriented training sample derived from geometry.
153pub fn desired_training_sample(observed: u64, num_leaves: u32) -> u64 {
154    let desired = u64::from(num_leaves)
155        .saturating_mul(super::PARTITION_TRAINING_POINTS_PER_CENTROID)
156        .max(super::DEFAULT_TRAINING_SAMPLE_SIZE);
157    observed.min(desired)
158}
159
160fn automatic_leaf_target(points: u64) -> (u64, u8) {
161    if points < THREE_LEVEL_MIN_POINTS {
162        (fractional_power_ceil(points, 1, 2), 1)
163    } else if points <= FOUR_LEVEL_MIN_POINTS_EXCLUSIVE {
164        (fractional_power_ceil(points, 2, 3), 2)
165    } else {
166        (fractional_power_ceil(points, 3, 4), 3)
167    }
168}
169
170fn width_required_levels(leaves: u32, dimension: u32) -> u8 {
171    let max_branching = flat_training_width_bound(dimension);
172    for levels in 1..MAX_SCANN_TREE_LEVELS {
173        if nth_root_ceil(leaves, levels) <= max_branching {
174            return levels;
175        }
176    }
177    MAX_SCANN_TREE_LEVELS
178}
179
180/// ScaNN autopilot's bounded flat k-means work estimate.
181fn flat_training_width_bound(dimension: u32) -> u32 {
182    let numerator = 60.0 * 32.0 * 2.0e9;
183    let denominator = f64::from(dimension) * super::PARTITION_TRAINING_POINTS_PER_CENTROID as f64;
184    (numerator / denominator).sqrt().ceil().max(1.0) as u32
185}
186
187fn balanced_branching_factors(leaves: u32, levels: u8) -> Vec<u32> {
188    let mut remaining = leaves;
189    let mut factors = Vec::with_capacity(usize::from(levels));
190    for levels_left in (1..=levels).rev() {
191        let factor = nth_root_ceil(remaining, levels_left);
192        factors.push(factor);
193        remaining = remaining.div_ceil(factor);
194    }
195    factors
196}
197
198fn nth_root_ceil(value: u32, degree: u8) -> u32 {
199    if value <= 1 || degree == 1 {
200        return value;
201    }
202    let mut low = 1u32;
203    let mut high = value;
204    while low < high {
205        let middle = low + (high - low) / 2;
206        if pow_reaches(middle, degree, value) {
207            high = middle;
208        } else {
209            low = middle + 1;
210        }
211    }
212    low
213}
214
215fn fractional_power_ceil(value: u64, numerator: u8, denominator: u8) -> u64 {
216    debug_assert!(value > 0 && numerator > 0 && numerator < denominator);
217    let target = saturating_pow_u128(u128::from(value), numerator);
218    let mut low = 1u64;
219    // Automatic geometry clamps at the format cap. One past that cap is enough
220    // to tell the caller the fractional-power target is larger.
221    let mut high = value.min(u64::from(MAX_SCANN_LEAVES) + 1).max(2);
222    while low < high {
223        let middle = low + (high - low) / 2;
224        if saturating_pow_u128(u128::from(middle), denominator) >= target {
225            high = middle;
226        } else {
227            low = middle + 1;
228        }
229    }
230    low
231}
232
233fn saturating_pow_u128(base: u128, exponent: u8) -> u128 {
234    (0..exponent).fold(1u128, |product, _| product.saturating_mul(base))
235}
236
237fn pow_reaches(base: u32, degree: u8, target: u32) -> bool {
238    let mut product = 1u64;
239    for _ in 0..degree {
240        product = product.saturating_mul(u64::from(base));
241        if product >= u64::from(target) {
242            return true;
243        }
244    }
245    false
246}
247
248#[cfg(test)]
249mod tests {
250    use super::*;
251
252    #[test]
253    fn geometry_matches_billion_scale_balanced_width() {
254        let geometry = derive_geometry(1_000_000_000, 1_024).unwrap();
255        assert_eq!(geometry.centroid_levels, 2);
256        assert_eq!(geometry.level_counts, [1_000, 1_000_000]);
257        assert_eq!(
258            desired_training_sample(1_000_000_000, 1_000_000),
259            200_000_000
260        );
261    }
262
263    #[test]
264    fn billion_scale_float_geometry_rejects_an_inadequate_default_sample() {
265        let sample_limit = (4_u64 * 1024 * 1024 * 1024) / (1_024 * 4);
266        assert_eq!(sample_limit, 1_048_576);
267        let error =
268            derive_geometry_with_levels_and_sample_limit(1_000_000_000, 1_024, None, sample_limit)
269                .unwrap_err()
270                .to_string();
271        assert!(error.contains("1000000 leaves"));
272        assert!(error.contains("8000000 training samples"));
273    }
274
275    #[test]
276    fn billion_scale_binary_geometry_fits_default_training_budget() {
277        // The default 10M row cap is tighter than 4 GiB / 320-byte rows.
278        let sample_limit = 10_000_000_u64;
279        let geometry =
280            derive_geometry_with_levels_and_sample_limit(1_000_000_000, 2_560, None, sample_limit)
281                .unwrap();
282        assert_eq!(sample_limit, 10_000_000);
283        assert_eq!(geometry.centroid_levels, 2);
284        assert_eq!(geometry.level_counts, [1_000, 1_000_000]);
285        assert!(
286            u64::from(geometry.num_leaves) * MIN_PARTITION_TRAINING_POINTS_PER_LEAF <= sample_limit
287        );
288    }
289
290    #[test]
291    fn fifteen_million_rows_use_the_measured_balanced_geometry() {
292        let geometry = derive_geometry(15_000_000, 2_560).unwrap();
293        assert_eq!(geometry.centroid_levels, 2);
294        assert_eq!(geometry.level_counts, [63, 3_873]);
295        assert_eq!(desired_training_sample(15_000_000, 3_873), 774_600);
296    }
297
298    #[test]
299    fn automatic_geometry_follows_google_tree_depth_bands() {
300        assert_eq!(
301            derive_geometry(99_999_999, 2_560).unwrap().level_counts,
302            [100, 10_000]
303        );
304        assert_eq!(
305            derive_geometry(100_000_000, 2_560).unwrap().level_counts,
306            [465, 215_444]
307        );
308        assert_eq!(
309            derive_geometry(1_000_000_001, 2_560).unwrap().level_counts,
310            [178, 31_684, 5_623_414]
311        );
312        assert_eq!(
313            derive_geometry(10_000_000_000, 2_560).unwrap().level_counts,
314            [311, 96_721, 30_000_000]
315        );
316    }
317
318    #[test]
319    fn sample_limit_never_changes_the_selected_topology() {
320        let expected = derive_geometry(1_000_000_000, 2_560).unwrap();
321        assert!(
322            derive_geometry_with_levels_and_sample_limit(1_000_000_000, 2_560, None, 7_999_999,)
323                .is_err()
324        );
325        assert_eq!(
326            derive_geometry_with_levels_and_sample_limit(1_000_000_000, 2_560, None, 8_000_000,)
327                .unwrap(),
328            expected
329        );
330    }
331
332    #[test]
333    fn fractional_power_is_exact_at_and_between_perfect_powers() {
334        assert_eq!(fractional_power_ceil(1_000_000_000, 2, 3), 1_000_000);
335        assert_eq!(fractional_power_ceil(1_000_000_001, 2, 3), 1_000_001);
336        assert_eq!(fractional_power_ceil(1_000_000_000, 3, 4), 5_623_414);
337        assert_eq!(
338            fractional_power_ceil(u64::MAX, 3, 4),
339            u64::from(MAX_SCANN_LEAVES) + 1
340        );
341    }
342
343    #[test]
344    fn geometry_defers_partitioning_below_hardcoded_floor() {
345        let geometry = derive_geometry(99_999, 1_024).unwrap();
346        assert_eq!(geometry.centroid_levels, 0);
347        assert!(geometry.level_counts.is_empty());
348    }
349
350    #[test]
351    fn explicit_depth_is_validated_and_balanced() {
352        assert_eq!(
353            geometry_for_leaves(10_000, 3).unwrap().level_counts,
354            [22, 484, 10_000]
355        );
356        assert!(geometry_for_leaves(10_000, 4).is_err());
357    }
358
359    #[test]
360    fn explicit_leaf_depth_depends_on_width_not_synthetic_corpus_bands() {
361        assert_eq!(
362            geometry_for_leaves_with_auto_depth(31_622, 2_560, None)
363                .unwrap()
364                .level_counts,
365            [178, 31_622]
366        );
367        assert_eq!(
368            geometry_for_leaves_with_auto_depth(31_623, 2_560, None)
369                .unwrap()
370                .level_counts,
371            [178, 31_623]
372        );
373        assert_eq!(
374            geometry_for_leaves_with_auto_depth(1_000_000, 1_024, None)
375                .unwrap()
376                .level_counts,
377            [1_000, 1_000_000]
378        );
379    }
380
381    #[test]
382    fn automatic_geometry_partitions_at_the_exact_hardcoded_floor() {
383        assert_eq!(
384            derive_geometry(MIN_POINTS_FOR_PARTITIONING, 1_024)
385                .unwrap()
386                .level_counts,
387            [317]
388        );
389    }
390}