1pub mod block;
5pub mod build;
7pub mod consolidate;
9pub mod refine;
11pub mod search;
13
14pub const MIN_GRAPH_ROWS: u32 = crate::tier::PROVISIONAL_TIER_THRESHOLDS.graph_min_rows;
16
17#[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 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 #[must_use]
69 pub const fn sift_1m() -> Self {
70 Self {
71 r_target: 32,
73 r_max: 44,
75 alpha_build: 1.0,
77 alpha_refine: 1.2,
80 l_build: 100,
82 checkpoint_batch_rows: 65_536,
85 }
86 }
87
88 #[must_use]
90 pub const fn angular() -> Self {
91 Self {
92 r_target: 48,
94 r_max: 64,
97 alpha_build: 1.0,
100 alpha_refine: 1.2,
102 l_build: 100,
105 checkpoint_batch_rows: 65_536,
107 }
108 }
109
110 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 #[must_use]
127 pub const fn should_build(self, row_count: u32) -> bool {
128 row_count >= MIN_GRAPH_ROWS
129 }
130
131 #[must_use]
133 pub const fn r_target(self) -> u8 {
134 self.r_target
135 }
136
137 #[must_use]
139 pub const fn r_max(self) -> u8 {
140 self.r_max
141 }
142
143 #[must_use]
145 pub const fn alpha_build(self) -> f32 {
146 self.alpha_build
147 }
148
149 #[must_use]
151 pub const fn alpha_refine(self) -> f32 {
152 self.alpha_refine
153 }
154
155 #[must_use]
157 pub const fn l_build(self) -> u16 {
158 self.l_build
159 }
160
161 #[must_use]
163 pub const fn checkpoint_batch_rows(self) -> u32 {
164 self.checkpoint_batch_rows
165 }
166}
167
168#[derive(Clone, Copy, Debug, PartialEq)]
170pub enum GraphParamsError {
171 DegreeOrder {
173 r_target: u8,
175 r_max: u8,
177 },
178 AlphaOrder {
180 alpha_build: f32,
182 alpha_refine: f32,
184 },
185 ConstructionWidth {
187 l_build: u16,
189 r_target: u8,
191 },
192 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#[derive(Debug)]
229pub enum GraphLoadOutcome<'a> {
230 Graph(block::GraphNodeBlocks<'a>),
232 ExactScanFallback {
234 error: SegmentError,
236 candidates: Vec<ScanCandidate>,
238 },
239}
240
241#[derive(Clone, Debug, Eq, PartialEq)]
243pub enum GraphQueryError {
244 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
266pub 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}