linkrs_query/planning/statements/seeks/
seek_strategy_base.rs1use std::sync::Arc;
15
16use crate::optimizer::cost::{CostModelConfig, SelectivityEstimator};
17use linkrs_core::types::expr::visitor_checkers::PropertyContainsChecker;
18use linkrs_core::types::Expression;
19use linkrs_core::Value;
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub enum SeekStrategyType {
23 VertexSeek,
24 IndexSeek,
25 PropIndexSeek,
26 VariablePropIndexSeek,
27 EdgeSeek,
28 ScanSeek,
29}
30
31#[derive(Debug)]
32pub struct SeekStrategyContext {
33 pub space_id: u64,
34 pub node_pattern: NodePattern,
35 pub predicates: Vec<Expression>,
36 pub estimated_rows: usize,
37 pub available_indexes: Vec<IndexInfo>,
38 pub total_vertices: usize,
39}
40
41#[derive(Debug, Clone, PartialEq)]
42pub struct NodePattern {
43 pub vid: Option<Value>,
44 pub labels: Vec<String>,
45 pub properties: Vec<(Arc<str>, Value)>,
46}
47
48#[derive(Debug, Clone)]
49pub struct IndexInfo {
50 pub name: String,
51 pub target_type: String,
52 pub target_name: String,
53 pub properties: Vec<String>,
54 pub selectivity: f32,
55 pub field_count: usize,
56 pub is_composite: bool,
57 pub composite_targets: Vec<String>,
58}
59
60impl IndexInfo {
61 pub fn new(
62 name: String,
63 target_type: String,
64 target_name: String,
65 properties: Vec<String>,
66 ) -> Self {
67 let field_count = properties.len();
68 Self {
69 name,
70 target_type,
71 target_name,
72 properties,
73 selectivity: 0.5,
74 field_count,
75 is_composite: false,
76 composite_targets: Vec::new(),
77 }
78 }
79
80 pub fn with_selectivity(mut self, selectivity: f32) -> Self {
81 self.selectivity = selectivity;
82 self
83 }
84
85 pub fn with_composite(mut self, targets: Vec<String>) -> Self {
86 self.is_composite = true;
87 self.composite_targets = targets;
88 self
89 }
90
91 pub fn covers_labels(&self, labels: &[String]) -> bool {
92 if self.is_composite {
93 self.composite_targets.iter().all(|t| labels.contains(t))
94 } else {
95 labels.contains(&self.target_name)
96 }
97 }
98
99 pub fn coverage_score(&self, labels: &[String]) -> f32 {
100 if labels.is_empty() {
101 return 0.0;
102 }
103
104 let covered = if self.is_composite {
105 self.composite_targets
106 .iter()
107 .filter(|t| labels.contains(t))
108 .count()
109 } else {
110 if labels.contains(&self.target_name) {
111 1
112 } else {
113 0
114 }
115 };
116
117 covered as f32 / labels.len() as f32
118 }
119}
120
121#[derive(Debug)]
122pub struct SeekResult {
123 pub vertex_ids: Vec<Value>,
124 pub strategy_used: SeekStrategyType,
125 pub rows_scanned: usize,
126}
127
128impl SeekStrategyContext {
129 pub fn new(space_id: u64, node_pattern: NodePattern, predicates: Vec<Expression>) -> Self {
130 Self {
131 space_id,
132 node_pattern,
133 predicates,
134 estimated_rows: 0,
135 available_indexes: Vec::new(),
136 total_vertices: 10000,
137 }
138 }
139
140 pub fn with_estimated_rows(mut self, rows: usize) -> Self {
141 self.estimated_rows = rows;
142 self
143 }
144
145 pub fn with_indexes(mut self, indexes: Vec<IndexInfo>) -> Self {
146 self.available_indexes = indexes;
147 self
148 }
149
150 pub fn with_total_vertices(mut self, total: usize) -> Self {
151 self.total_vertices = total;
152 self
153 }
154
155 pub fn has_explicit_vid(&self) -> bool {
156 self.node_pattern.vid.is_some()
157 }
158
159 pub fn has_labels(&self) -> bool {
160 !self.node_pattern.labels.is_empty()
161 }
162
163 pub fn has_predicates(&self) -> bool {
164 !self.predicates.is_empty()
165 }
166
167 pub fn get_index_for_labels(&self, labels: &[String]) -> Option<&IndexInfo> {
168 self.available_indexes
169 .iter()
170 .find(|idx| idx.target_type == "tag" && labels.contains(&idx.target_name))
171 }
172
173 pub fn get_index_for_property(&self, property: &str) -> Option<&IndexInfo> {
174 self.available_indexes
175 .iter()
176 .find(|idx| idx.properties.contains(&property.to_string()))
177 }
178
179 pub fn has_property_predicates(&self) -> bool {
180 self.predicates
181 .iter()
182 .any(|pred| matches!(pred, Expression::Binary { .. }))
183 }
184
185 pub fn has_index_for_properties(&self) -> bool {
186 !self.available_indexes.is_empty() && !self.predicates.is_empty()
187 }
188}
189
190#[derive(Debug)]
191pub struct SeekStrategySelector {
192 use_index_threshold: usize,
193 scan_threshold: usize,
194 cost_config: CostModelConfig,
195 selectivity_estimator: Option<Arc<SelectivityEstimator>>,
196}
197
198impl SeekStrategySelector {
199 pub fn new(cost_config: CostModelConfig) -> Self {
200 Self {
201 use_index_threshold: 1000,
202 scan_threshold: 10000,
203 cost_config,
204 selectivity_estimator: None,
205 }
206 }
207
208 pub fn with_thresholds(mut self, use_index: usize, scan: usize) -> Self {
209 self.use_index_threshold = use_index;
210 self.scan_threshold = scan;
211 self
212 }
213
214 pub fn with_selectivity_estimator(mut self, estimator: Arc<SelectivityEstimator>) -> Self {
215 self.selectivity_estimator = Some(estimator);
216 self
217 }
218
219 pub fn select_best_index<'a>(
220 &self,
221 indexes: &'a [IndexInfo],
222 predicates: &[Expression],
223 ) -> Option<&'a IndexInfo> {
224 if indexes.is_empty() {
225 return None;
226 }
227
228 let candidate_indexes: Vec<&IndexInfo> = indexes
229 .iter()
230 .filter(|idx| {
231 idx.properties.iter().any(|prop| {
232 predicates.iter().any(|pred| {
233 PropertyContainsChecker::check(pred, std::slice::from_ref(prop))
234 })
235 })
236 })
237 .collect();
238
239 if candidate_indexes.is_empty() {
240 return indexes.iter().min_by_key(|idx| idx.field_count);
241 }
242
243 candidate_indexes.into_iter().min_by(|a, b| {
244 let field_cmp = a.field_count.cmp(&b.field_count);
245 if field_cmp != std::cmp::Ordering::Equal {
246 return field_cmp;
247 }
248 b.selectivity
249 .partial_cmp(&a.selectivity)
250 .unwrap_or(std::cmp::Ordering::Equal)
251 })
252 }
253
254 pub fn select_strategy(&self, context: &SeekStrategyContext) -> SeekStrategyType {
255 self.select_strategy_with_estimation(context).strategy_type
256 }
257
258 pub fn select_strategy_with_estimation(
259 &self,
260 context: &SeekStrategyContext,
261 ) -> StrategySelection {
262 let candidates = self.evaluate_all_strategies(context);
263
264 candidates
265 .into_iter()
266 .flatten()
267 .min_by(|a, b| {
268 a.cost
269 .partial_cmp(&b.cost)
270 .unwrap_or(std::cmp::Ordering::Equal)
271 })
272 .map(|e| StrategySelection {
273 strategy_type: e.strategy_type,
274 estimated_rows: e.estimated_rows,
275 })
276 .unwrap_or(StrategySelection {
277 strategy_type: SeekStrategyType::ScanSeek,
278 estimated_rows: context.total_vertices,
279 })
280 }
281
282 fn evaluate_all_strategies(
283 &self,
284 context: &SeekStrategyContext,
285 ) -> Vec<Option<StrategyEvaluation>> {
286 vec![
287 self.evaluate_vertex_seek(context),
288 self.evaluate_index_seek(context),
289 self.evaluate_prop_index_seek(context),
290 self.evaluate_scan_seek(context),
291 ]
292 }
293
294 fn evaluate_vertex_seek(&self, context: &SeekStrategyContext) -> Option<StrategyEvaluation> {
295 if !context.has_explicit_vid() {
296 return None;
297 }
298
299 let cost = self.cost_config.random_page_cost;
300 Some(StrategyEvaluation {
301 strategy_type: SeekStrategyType::VertexSeek,
302 cost,
303 estimated_rows: 1,
304 })
305 }
306
307 fn evaluate_index_seek(&self, context: &SeekStrategyContext) -> Option<StrategyEvaluation> {
308 let index = context.get_index_for_labels(&context.node_pattern.labels)?;
309 let total_rows = context.total_vertices;
310 let selectivity = index.selectivity as f64;
311 let matching = ((total_rows as f64 * selectivity).max(1.0)) as u64;
312 let cost = Self::shared_index_seek_cost(&self.cost_config, matching);
313 let estimated_rows = matching as usize;
314
315 Some(StrategyEvaluation {
316 strategy_type: SeekStrategyType::IndexSeek,
317 cost,
318 estimated_rows,
319 })
320 }
321
322 fn shared_index_seek_cost(cost_config: &CostModelConfig, matching_rows: u64) -> f64 {
325 use crate::optimizer::cost::config::ROWS_PER_PAGE;
326 let pages = (matching_rows / ROWS_PER_PAGE).max(1);
327 pages as f64 * cost_config.seq_page_cost
328 + matching_rows as f64 * cost_config.cpu_index_tuple_cost
329 + matching_rows as f64 * cost_config.random_page_cost
330 + matching_rows as f64 * cost_config.cpu_tuple_cost
331 }
332
333 fn evaluate_prop_index_seek(
334 &self,
335 context: &SeekStrategyContext,
336 ) -> Option<StrategyEvaluation> {
337 if !context.has_property_predicates() || !context.has_index_for_properties() {
338 return None;
339 }
340
341 let _index = self.select_best_index(&context.available_indexes, &context.predicates)?;
342 let selectivity = self.estimate_predicate_selectivity(context);
343 let total_rows = context.total_vertices;
344 let matching = ((total_rows as f64 * selectivity).max(1.0)) as u64;
345
346 let cost = Self::shared_index_seek_cost(&self.cost_config, matching);
347 let estimated_rows = matching as usize;
348
349 Some(StrategyEvaluation {
350 strategy_type: SeekStrategyType::PropIndexSeek,
351 cost,
352 estimated_rows,
353 })
354 }
355
356 fn evaluate_scan_seek(&self, context: &SeekStrategyContext) -> Option<StrategyEvaluation> {
357 let total_rows = context.total_vertices;
358 let cost = self.cost_config.seq_page_cost * total_rows as f64
359 + self.cost_config.cpu_tuple_cost * total_rows as f64;
360
361 Some(StrategyEvaluation {
362 strategy_type: SeekStrategyType::ScanSeek,
363 cost,
364 estimated_rows: total_rows,
365 })
366 }
367
368 fn estimate_predicate_selectivity(&self, context: &SeekStrategyContext) -> f64 {
369 if let Some(ref estimator) = self.selectivity_estimator {
370 let mut total_selectivity = 1.0;
371 for pred in &context.predicates {
372 let sel = estimator.estimate_from_expression(None, pred, None);
373 total_selectivity *= sel;
374 }
375 return total_selectivity;
376 }
377
378 if context.predicates.len() == 1 {
379 return 0.1;
380 }
381
382 0.1_f64.powi(context.predicates.len() as i32)
383 }
384}
385
386#[derive(Debug, Clone)]
387struct StrategyEvaluation {
388 strategy_type: SeekStrategyType,
389 cost: f64,
390 estimated_rows: usize,
391}
392
393#[derive(Debug, Clone)]
394pub struct StrategySelection {
395 pub strategy_type: SeekStrategyType,
396 pub estimated_rows: usize,
397}
398
399#[cfg(test)]
400mod tests {
401 use super::*;
402
403 #[test]
404 fn test_index_info_creation() {
405 let index = IndexInfo::new(
406 "person_name_idx".to_string(),
407 "tag".to_string(),
408 "person".to_string(),
409 vec!["name".to_string()],
410 );
411 assert_eq!(index.name, "person_name_idx");
412 assert_eq!(index.field_count, 1);
413 assert!(!index.is_composite);
414 }
415
416 #[test]
417 fn test_index_info_with_composite() {
418 let index = IndexInfo::new(
419 "composite_idx".to_string(),
420 "tag".to_string(),
421 "person".to_string(),
422 vec!["name".to_string(), "age".to_string()],
423 )
424 .with_composite(vec!["person".to_string(), "employee".to_string()]);
425
426 assert!(index.is_composite);
427 assert!(index.covers_labels(&["person".to_string(), "employee".to_string()]));
428 assert!(!index.covers_labels(&["person".to_string(), "other".to_string()]));
429 }
430
431 #[test]
432 fn test_seek_strategy_context() {
433 let node_pattern = NodePattern {
434 vid: Some(Value::Int(1)),
435 labels: vec!["person".to_string()],
436 properties: vec![],
437 };
438 let context = SeekStrategyContext::new(1, node_pattern, vec![]);
439
440 assert!(context.has_explicit_vid());
441 assert!(context.has_labels());
442 assert!(!context.has_predicates());
443 }
444
445 #[test]
446 fn test_seek_strategy_selector_vertex_seek() {
447 let selector = SeekStrategySelector::new(CostModelConfig::default());
448
449 let node_pattern = NodePattern {
450 vid: Some(Value::Int(1)),
451 labels: vec![],
452 properties: vec![],
453 };
454 let context = SeekStrategyContext::new(1, node_pattern, vec![]);
455
456 let strategy = selector.select_strategy(&context);
457 assert_eq!(strategy, SeekStrategyType::VertexSeek);
458 }
459
460 #[test]
461 fn test_seek_strategy_selector_scan_seek() {
462 let selector = SeekStrategySelector::new(CostModelConfig::default());
463
464 let node_pattern = NodePattern {
465 vid: None,
466 labels: vec![],
467 properties: vec![],
468 };
469 let context = SeekStrategyContext::new(1, node_pattern, vec![]);
470
471 let strategy = selector.select_strategy(&context);
472 assert_eq!(strategy, SeekStrategyType::ScanSeek);
473 }
474}