Skip to main content

zeppelin_embed/
graph.rs

1//! Per-segment vector graphs.
2
3/// Persisted fixed-stride graph node blocks.
4pub mod block;
5/// Deterministic, checkpointable flat Vamana construction.
6pub mod build;
7/// Copy-on-write consolidation of sealed graph segments (Task 19-M8).
8pub mod consolidate;
9/// Copy-on-write post-consolidation graph refinement passes.
10pub mod refine;
11/// Single-core fixed-stride graph traversal.
12pub mod search;
13
14/// Compatibility name for the tier policy's single provisional threshold.
15pub const MIN_GRAPH_ROWS: u32 = crate::tier::PROVISIONAL_TIER_THRESHOLDS.graph_min_rows;
16
17/// Validated construction controls for one flat per-segment graph.
18#[derive(Clone, Copy, Debug, PartialEq)]
19pub struct GraphParams {
20    r_target: u8,
21    r_max: u8,
22    alpha_build: f32,
23    alpha_refine: f32,
24    l_build: u16,
25    checkpoint_batch_rows: u32,
26}
27
28impl GraphParams {
29    /// Creates validated Vamana construction controls.
30    pub fn new(
31        r_target: u8,
32        r_max: u8,
33        alpha_build: f32,
34        alpha_refine: f32,
35        l_build: u16,
36        checkpoint_batch_rows: u32,
37    ) -> Result<Self, GraphParamsError> {
38        if r_target == 0 || r_target > r_max {
39            return Err(GraphParamsError::DegreeOrder { r_target, r_max });
40        }
41        if !alpha_build.is_finite()
42            || !alpha_refine.is_finite()
43            || alpha_build < 1.0
44            || alpha_refine < alpha_build
45        {
46            return Err(GraphParamsError::AlphaOrder {
47                alpha_build,
48                alpha_refine,
49            });
50        }
51        if l_build == 0 || usize::from(l_build) < usize::from(r_target) {
52            return Err(GraphParamsError::ConstructionWidth { l_build, r_target });
53        }
54        if checkpoint_batch_rows == 0 {
55            return Err(GraphParamsError::CheckpointBatchRows);
56        }
57        Ok(Self {
58            r_target,
59            r_max,
60            alpha_build,
61            alpha_refine,
62            l_build,
63            checkpoint_batch_rows,
64        })
65    }
66
67    /// Returns the Task 19-M3 SIFT-1M construction contract.
68    #[must_use]
69    pub const fn sift_1m() -> Self {
70        Self {
71            // M3/M5 SIFT measurements: `docs/19-m5-defaults.md`.
72            r_target: 32,
73            // M3/M5 SIFT measurements: `docs/19-m5-defaults.md`.
74            r_max: 44,
75            // Shipped one-pass alpha measured in `docs/19-m5-defaults.md`.
76            alpha_build: 1.0,
77            // Explicit two-pass arm only; rejected as a default by the matched
78            // measurement in `docs/19-m5-defaults.md`.
79            alpha_refine: 1.2,
80            // M3/M5 SIFT measurements: `docs/19-m5-defaults.md`.
81            l_build: 100,
82            // Operational M3 choice, NOT MEASURED; documented plainly in
83            // `docs/19-m5-defaults.md`.
84            checkpoint_batch_rows: 65_536,
85        }
86    }
87
88    /// Returns the Task 19-M10 angular construction contract.
89    #[must_use]
90    pub const fn angular() -> Self {
91        Self {
92            // M10's owner-approved angular mechanism profile.
93            r_target: 48,
94            // M10 reserves sixteen slots above the normal pruned degree for
95            // reverse-edge stitching and hub densification.
96            r_max: 64,
97            // M10 retains the shipped one-pass build pending the deferred
98            // external-dataset alpha sweep.
99            alpha_build: 1.0,
100            // Existing explicit refinement arm used by the M9 machinery.
101            alpha_refine: 1.2,
102            // No external-dataset build-width sweep is in this mechanism pass;
103            // retain the existing validated construction width.
104            l_build: 100,
105            // Preserve the established resumable checkpoint discipline.
106            checkpoint_batch_rows: 65_536,
107        }
108    }
109
110    /// Selects a different positive checkpoint batch size without changing graph quality.
111    pub fn with_checkpoint_batch_rows(
112        self,
113        checkpoint_batch_rows: u32,
114    ) -> Result<Self, GraphParamsError> {
115        Self::new(
116            self.r_target,
117            self.r_max,
118            self.alpha_build,
119            self.alpha_refine,
120            self.l_build,
121            checkpoint_batch_rows,
122        )
123    }
124
125    /// Returns whether a sealed segment crosses Amendment A1's graph threshold.
126    #[must_use]
127    pub const fn should_build(self, row_count: u32) -> bool {
128        row_count >= MIN_GRAPH_ROWS
129    }
130
131    /// Returns the normal pruned out-degree.
132    #[must_use]
133    pub const fn r_target(self) -> u8 {
134        self.r_target
135    }
136
137    /// Returns the persisted hard degree cap.
138    #[must_use]
139    pub const fn r_max(self) -> u8 {
140        self.r_max
141    }
142
143    /// Returns the first-pass occlusion factor.
144    #[must_use]
145    pub const fn alpha_build(self) -> f32 {
146        self.alpha_build
147    }
148
149    /// Returns the second-pass occlusion factor.
150    #[must_use]
151    pub const fn alpha_refine(self) -> f32 {
152        self.alpha_refine
153    }
154
155    /// Returns the construction beam width.
156    #[must_use]
157    pub const fn l_build(self) -> u16 {
158        self.l_build
159    }
160
161    /// Returns rows processed between resumable checkpoints.
162    #[must_use]
163    pub const fn checkpoint_batch_rows(self) -> u32 {
164        self.checkpoint_batch_rows
165    }
166}
167
168/// Rejection of loose or contradictory graph-construction controls.
169#[derive(Clone, Copy, Debug, PartialEq)]
170pub enum GraphParamsError {
171    /// The target degree was zero or exceeded the hard persisted cap.
172    DegreeOrder {
173        /// Normal pruned degree.
174        r_target: u8,
175        /// Hard persisted cap.
176        r_max: u8,
177    },
178    /// Alpha values were non-finite, below one, or decreased on refinement.
179    AlphaOrder {
180        /// First-pass alpha.
181        alpha_build: f32,
182        /// Second-pass alpha.
183        alpha_refine: f32,
184    },
185    /// The construction beam was zero or narrower than the target degree.
186    ConstructionWidth {
187        /// Construction beam width.
188        l_build: u16,
189        /// Normal pruned degree.
190        r_target: u8,
191    },
192    /// A checkpoint batch must contain at least one row.
193    CheckpointBatchRows,
194}
195
196impl std::fmt::Display for GraphParamsError {
197    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
198        match self {
199            Self::DegreeOrder { r_target, r_max } => write!(
200                formatter,
201                "graph target degree {r_target} must be positive and at most hard cap {r_max}"
202            ),
203            Self::AlphaOrder {
204                alpha_build,
205                alpha_refine,
206            } => write!(
207                formatter,
208                "graph alpha must be finite and ordered 1 <= build <= refine, got {alpha_build} then {alpha_refine}"
209            ),
210            Self::ConstructionWidth { l_build, r_target } => write!(
211                formatter,
212                "graph construction width {l_build} must be at least target degree {r_target}"
213            ),
214            Self::CheckpointBatchRows => {
215                formatter.write_str("graph checkpoint batch rows must be positive")
216            }
217        }
218    }
219}
220
221impl std::error::Error for GraphParamsError {}
222
223use crate::scan::{ScanCandidate, ScanError, ScanRequest};
224use crate::segment::SegmentError;
225use crate::segment::reader::SegmentReader;
226
227/// Query-tier decision after validating a segment's derived graph accelerator.
228#[derive(Debug)]
229pub enum GraphLoadOutcome<'a> {
230    /// The graph region is valid and may be consumed by the later traversal milestone.
231    Graph(block::GraphNodeBlocks<'a>),
232    /// The derived graph failed loudly and correctness was served by exact scan.
233    ExactScanFallback {
234        /// Typed graph/segment failure retained for diagnostics and callers.
235        error: SegmentError,
236        /// Exact candidates including every k-th-score boundary tie.
237        candidates: Vec<ScanCandidate>,
238    },
239}
240
241/// Failure of the correctness-preserving exact tier after graph-load failure.
242#[derive(Clone, Debug, Eq, PartialEq)]
243pub enum GraphQueryError {
244    /// The exact scan request itself was invalid or scoring failed.
245    ExactScan(ScanError),
246}
247
248impl std::fmt::Display for GraphQueryError {
249    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
250        match self {
251            Self::ExactScan(error) => {
252                write!(formatter, "graph exact-scan fallback failed: {error}")
253            }
254        }
255    }
256}
257
258impl std::error::Error for GraphQueryError {
259    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
260        match self {
261            Self::ExactScan(error) => Some(error),
262        }
263    }
264}
265
266/// Loads a graph for a query, or loudly returns its typed error with exact results.
267///
268/// This is the single sanctioned recovery path for a corrupt derived accelerator.
269/// No other segment contract is downgraded to best-effort behavior.
270pub fn load_graph_or_exact_scan<'a>(
271    reader: &'a SegmentReader,
272    exact_request: ScanRequest<'_>,
273    k: usize,
274) -> Result<GraphLoadOutcome<'a>, GraphQueryError> {
275    match reader.graph_node_blocks() {
276        Ok(graph) => Ok(GraphLoadOutcome::Graph(graph)),
277        Err(error) => crate::scan::top_k_with_ties(exact_request, k)
278            .map(|candidates| GraphLoadOutcome::ExactScanFallback { error, candidates })
279            .map_err(GraphQueryError::ExactScan),
280    }
281}
282
283#[cfg(test)]
284#[allow(clippy::expect_used)]
285mod params_tests {
286    use super::{GraphParams, GraphParamsError, MIN_GRAPH_ROWS};
287
288    #[test]
289    fn profile_params_and_graph_crossover_are_literal() {
290        let params = GraphParams::sift_1m();
291        assert_eq!(params.r_target(), 32);
292        assert_eq!(params.r_max(), 44);
293        assert_eq!(params.alpha_build(), 1.0);
294        assert_eq!(params.alpha_refine(), 1.2);
295        assert_eq!(params.l_build(), 100);
296        assert_eq!(params.checkpoint_batch_rows(), 65_536);
297        assert_eq!(
298            params
299                .with_checkpoint_batch_rows(17)
300                .expect("positive checkpoint batch")
301                .checkpoint_batch_rows(),
302            17
303        );
304        assert!(!params.should_build(MIN_GRAPH_ROWS - 1));
305        assert!(params.should_build(MIN_GRAPH_ROWS));
306
307        let angular = GraphParams::angular();
308        assert_eq!(angular.r_target(), 48);
309        assert_eq!(angular.r_max(), 64);
310        assert_eq!(angular.alpha_build(), 1.0);
311        assert_eq!(angular.alpha_refine(), 1.2);
312        assert_eq!(angular.l_build(), 100);
313        assert_eq!(angular.checkpoint_batch_rows(), 65_536);
314    }
315
316    #[test]
317    fn graph_params_reject_loose_invalid_controls() {
318        assert!(GraphParams::new(0, 44, 1.0, 1.2, 100, 1).is_err());
319        assert!(GraphParams::new(45, 44, 1.0, 1.2, 100, 1).is_err());
320        assert!(GraphParams::new(32, 44, 0.9, 1.2, 100, 1).is_err());
321        assert!(GraphParams::new(32, 44, 1.2, 1.1, 100, 1).is_err());
322        assert!(GraphParams::new(32, 44, 1.0, 1.2, 31, 1).is_err());
323        assert!(GraphParams::new(32, 44, 1.0, 1.2, 100, 0).is_err());
324
325        let errors = [
326            GraphParamsError::DegreeOrder {
327                r_target: 0,
328                r_max: 44,
329            },
330            GraphParamsError::AlphaOrder {
331                alpha_build: 0.9,
332                alpha_refine: 1.2,
333            },
334            GraphParamsError::ConstructionWidth {
335                l_build: 31,
336                r_target: 32,
337            },
338            GraphParamsError::CheckpointBatchRows,
339        ];
340        for error in errors {
341            assert!(!error.to_string().is_empty());
342        }
343    }
344}