Skip to main content

linkrs_query/optimizer/cost_based/
index.rs

1//! Index Selector Module
2//!
3//! Used to select the optimal index for the query.
4
5use std::sync::Arc;
6
7use crate::optimizer::cost::{CostCalculator, SelectivityEstimator};
8use linkrs_core::types::{Expression, Index};
9
10/// Index selector
11#[derive(Debug)]
12pub struct IndexSelector {
13    cost_calculator: Arc<CostCalculator>,
14    selectivity_estimator: Arc<SelectivityEstimator>,
15}
16
17/// Index selection results
18#[derive(Debug, Clone)]
19pub enum IndexSelection {
20    /// Attribute Index
21    PropertyIndex {
22        /// Index name
23        index_name: String,
24        /// Attribute name
25        property_name: String,
26        /// Estimated cost
27        estimated_cost: f64,
28        /// selectivity
29        selectivity: f64,
30    },
31    /// Tag Index
32    TagIndex {
33        /// Estimated cost
34        estimated_cost: f64,
35        /// Number of vertices
36        vertex_count: u64,
37    },
38    /// Full table scan
39    FullScan {
40        /// Estimated cost
41        estimated_cost: f64,
42        /// Number of vertices
43        vertex_count: u64,
44    },
45}
46
47/// Attribute predicate
48#[derive(Debug, Clone)]
49pub struct PropertyPredicate {
50    /// attribute name
51    pub property_name: String,
52    /// Operator
53    pub operator: PredicateOperator,
54    /// Value expression
55    pub value: Expression,
56}
57
58/// Predicate operator
59#[derive(Debug, Clone, Copy, PartialEq, Eq)]
60pub enum PredicateOperator {
61    /// equals
62    Equal,
63    /// Not equal to
64    NotEqual,
65    /// less than
66    LessThan,
67    /// less than or equal to
68    LessThanOrEqual,
69    /// greater than
70    GreaterThan,
71    /// greater than or equal to
72    GreaterThanOrEqual,
73    /// LIKE
74    Like,
75    /// IN
76    In,
77}
78
79impl IndexSelector {
80    /// Create a new index selector with Arc-wrapped dependencies.
81    ///
82    /// This constructor is suitable for long-lived selectors that need to be shared.
83    pub fn new(
84        cost_calculator: Arc<CostCalculator>,
85        selectivity_estimator: Arc<SelectivityEstimator>,
86    ) -> Self {
87        Self {
88            cost_calculator,
89            selectivity_estimator,
90        }
91    }
92
93    /// Create a new index selector with references (lightweight version).
94    ///
95    /// This constructor is suitable for short-lived selectors with clear lifetimes.
96    /// It avoids the overhead of Arc wrapping while maintaining the same functionality.
97    pub fn with_refs<'a>(
98        cost_calculator: &'a CostCalculator,
99        selectivity_estimator: &'a SelectivityEstimator,
100    ) -> Self {
101        Self {
102            cost_calculator: Arc::new(cost_calculator.clone()),
103            selectivity_estimator: Arc::new(selectivity_estimator.clone()),
104        }
105    }
106
107    /// Select the optimal index for the query.
108    pub fn select_index(
109        &self,
110        space: &str,
111        tag_name: &str,
112        predicates: &[PropertyPredicate],
113        available_indexes: &[Index],
114    ) -> IndexSelection {
115        // If there are no predicates, use a full table scan.
116        if predicates.is_empty() {
117            let vertex_count = self
118                .cost_calculator
119                .statistics_manager()
120                .get_vertex_count(space, tag_name);
121            let estimated_cost = self
122                .cost_calculator
123                .calculate_scan_vertices_cost(space, tag_name);
124            return IndexSelection::FullScan {
125                estimated_cost,
126                vertex_count,
127            };
128        }
129
130        // Evaluate each available index.
131        let mut best_selection: Option<IndexSelection> = None;
132
133        for index in available_indexes {
134            // Please provide the text that needs to be translated. I will then translate it into English, ensuring that only the content matching the specified tags is included in the translation.
135            if index.schema_name != tag_name {
136                continue;
137            }
138
139            if let Some(selection) = self.evaluate_index(space, index, predicates) {
140                match &best_selection {
141                    None => best_selection = Some(selection),
142                    Some(current_best) => {
143                        if selection.estimated_cost() < current_best.estimated_cost() {
144                            best_selection = Some(selection);
145                        }
146                    }
147                }
148            }
149        }
150
151        // If no suitable index is found, a full table scan is used.
152        best_selection.unwrap_or_else(|| {
153            let vertex_count = self
154                .cost_calculator
155                .statistics_manager()
156                .get_vertex_count(space, tag_name);
157            let estimated_cost = self
158                .cost_calculator
159                .calculate_scan_vertices_cost(space, tag_name);
160            IndexSelection::FullScan {
161                estimated_cost,
162                vertex_count,
163            }
164        })
165    }
166
167    /// Evaluating a single index
168    fn evaluate_index(
169        &self,
170        space: &str,
171        index: &Index,
172        predicates: &[PropertyPredicate],
173    ) -> Option<IndexSelection> {
174        // Check whether the index covers the predicate.
175        let covered_predicates: Vec<&PropertyPredicate> = predicates
176            .iter()
177            .filter(|p| index.properties.contains(&p.property_name))
178            .collect();
179
180        if covered_predicates.is_empty() {
181            return None;
182        }
183
184        // Calculating selectivity
185        let mut total_selectivity = 1.0;
186        for predicate in &covered_predicates {
187            let selectivity = match predicate.operator {
188                PredicateOperator::Equal => {
189                    // Try to extract the values from the expression.
190                    let value = if let Expression::Literal(v) = &predicate.value {
191                        Some(v.clone())
192                    } else {
193                        None
194                    };
195                    self.selectivity_estimator.estimate_equality_selectivity(
196                        Some(space),
197                        Some(&index.schema_name),
198                        &predicate.property_name,
199                        value.as_ref(),
200                    )
201                }
202                PredicateOperator::LessThan | PredicateOperator::LessThanOrEqual => {
203                    let bound = match &predicate.value {
204                        Expression::Literal(v) => numeric_literal_as_f64(v),
205                        _ => None,
206                    };
207                    self.selectivity_estimator.estimate_ordered_comparison(
208                        Some(space),
209                        Some(&index.schema_name),
210                        &predicate.property_name,
211                        bound,
212                        false,
213                    )
214                }
215                PredicateOperator::GreaterThan | PredicateOperator::GreaterThanOrEqual => {
216                    let bound = match &predicate.value {
217                        Expression::Literal(v) => numeric_literal_as_f64(v),
218                        _ => None,
219                    };
220                    self.selectivity_estimator.estimate_ordered_comparison(
221                        Some(space),
222                        Some(&index.schema_name),
223                        &predicate.property_name,
224                        bound,
225                        true,
226                    )
227                }
228                PredicateOperator::Like => {
229                    // Try to extract patterns from the expression.
230                    if let Expression::Literal(linkrs_core::value::Value::String(pattern)) =
231                        &predicate.value
232                    {
233                        self.selectivity_estimator
234                            .estimate_like_selectivity(pattern)
235                    } else {
236                        0.3
237                    }
238                }
239                _ => 0.3,
240            };
241            total_selectivity *= selectivity;
242        }
243
244        // Calculating the cost
245        let estimated_cost = self.cost_calculator.calculate_index_scan_cost(
246            space,
247            &index.schema_name,
248            &covered_predicates[0].property_name,
249            total_selectivity,
250        );
251
252        // Get the name of the first overridden attribute.
253        let property_name = covered_predicates[0].property_name.clone();
254
255        Some(IndexSelection::PropertyIndex {
256            index_name: index.name.clone(),
257            property_name,
258            estimated_cost,
259            selectivity: total_selectivity,
260        })
261    }
262
263    /// Choosing the optimal composite index strategy
264    pub fn select_composite_index_strategy(
265        &self,
266        space: &str,
267        tag_name: &str,
268        predicates: &[PropertyPredicate],
269        available_indexes: &[Index],
270    ) -> Vec<IndexSelection> {
271        let mut strategies = Vec::new();
272
273        // Add a full-table scan as a benchmark.
274        let vertex_count = self
275            .cost_calculator
276            .statistics_manager()
277            .get_vertex_count(space, tag_name);
278        let full_scan_cost = self
279            .cost_calculator
280            .calculate_scan_vertices_cost(space, tag_name);
281        strategies.push(IndexSelection::FullScan {
282            estimated_cost: full_scan_cost,
283            vertex_count,
284        });
285
286        // Evaluate each index.
287        for index in available_indexes {
288            if index.schema_name != tag_name {
289                continue;
290            }
291
292            if let Some(selection) = self.evaluate_index(space, index, predicates) {
293                strategies.push(selection);
294            }
295        }
296
297        // Sort by cost
298        strategies.sort_by(|a, b| {
299            a.estimated_cost()
300                .partial_cmp(&b.estimated_cost())
301                .unwrap_or(std::cmp::Ordering::Equal)
302        });
303
304        strategies
305    }
306}
307
308impl Clone for IndexSelector {
309    fn clone(&self) -> Self {
310        Self {
311            cost_calculator: self.cost_calculator.clone(),
312            selectivity_estimator: self.selectivity_estimator.clone(),
313        }
314    }
315}
316
317fn numeric_literal_as_f64(value: &linkrs_core::Value) -> Option<f64> {
318    match value {
319        linkrs_core::Value::SmallInt(v) => Some(*v as f64),
320        linkrs_core::Value::Int(v) => Some(*v as f64),
321        linkrs_core::Value::BigInt(v) => Some(*v as f64),
322        linkrs_core::Value::Float(v) => Some((*v).into()),
323        linkrs_core::Value::Double(v) => Some(*v),
324        _ => None,
325    }
326}
327
328impl IndexSelection {
329    /// Obtain the estimated cost.
330    pub fn estimated_cost(&self) -> f64 {
331        match self {
332            IndexSelection::PropertyIndex { estimated_cost, .. } => *estimated_cost,
333            IndexSelection::TagIndex { estimated_cost, .. } => *estimated_cost,
334            IndexSelection::FullScan { estimated_cost, .. } => *estimated_cost,
335        }
336    }
337
338    /// Obtain the options (if any).
339    pub fn selectivity(&self) -> Option<f64> {
340        match self {
341            IndexSelection::PropertyIndex { selectivity, .. } => Some(*selectivity),
342            _ => None,
343        }
344    }
345
346    /// Determine whether it is an index scan.
347    pub fn is_index_scan(&self) -> bool {
348        matches!(self, IndexSelection::PropertyIndex { .. })
349    }
350
351    /// Determine whether it is a full table scan.
352    pub fn is_full_scan(&self) -> bool {
353        matches!(self, IndexSelection::FullScan { .. })
354    }
355}