Skip to main content

linkrs_query/optimizer/cost_based/
traversal_direction.rs

1//! Image traversal direction optimizer module
2//!
3//! Select the optimal traversal direction (forward or backward) based on edge statistics.
4//!
5//! ## Optimization Strategies
6//!
7//! Choose the direction with a smaller degree value in order to reduce the intermediate results.
8//! Consider the impact of super nodes.
9//! Support for cost-based direction selection
10//!
11//! ## Usage Examples
12//!
13//! ```rust
14//! use linkrs_query::optimizer::cost::CostCalculator;
15//! use linkrs_query::optimizer::cost_based::TraversalDirectionOptimizer;
16//! use linkrs_query::optimizer::stats::StatisticsManager;
17//! use std::sync::Arc;
18//!
19//! let stats_manager = Arc::new(StatisticsManager::new());
20//! let cost_calculator = Arc::new(CostCalculator::new(stats_manager));
21//! let optimizer = TraversalDirectionOptimizer::new(cost_calculator);
22//!
23//! // let decision = optimizer.optimize_direction("KNOWS", None);
24//! ```
25
26use std::sync::Arc;
27
28use crate::optimizer::cost::config::DEFAULT_DEGREE_FALLBACK;
29use crate::optimizer::cost::CostCalculator;
30use crate::optimizer::stats::EdgeTypeStatistics;
31use linkrs_core::types::EdgeDirection;
32
33/// Decision on the direction of traversal
34#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
35pub enum TraversalDirection {
36    /// Forward traversal (outward direction)
37    /// Traverse from the source vertex to the target vertex.
38    Forward,
39    /// Reverse traversal (inward direction)
40    /// Traverse from the target vertex to the source vertex.
41    Backward,
42    /// Bidirectional traversal
43    /// Consider both directions at the same time.
44    Bidirectional,
45}
46
47impl TraversalDirection {
48    /// Obtain the name of the direction
49    pub fn name(&self) -> &'static str {
50        match self {
51            TraversalDirection::Forward => "Forward",
52            TraversalDirection::Backward => "Backward",
53            TraversalDirection::Bidirectional => "Bidirectional",
54        }
55    }
56
57    /// Convert to EdgeDirection
58    pub fn to_edge_direction(&self) -> EdgeDirection {
59        match self {
60            TraversalDirection::Forward => EdgeDirection::Out,
61            TraversalDirection::Backward => EdgeDirection::In,
62            TraversalDirection::Bidirectional => EdgeDirection::Both,
63        }
64    }
65
66    /// Conversion from EdgeDirection
67    pub fn from_edge_direction(direction: &EdgeDirection) -> Self {
68        match direction {
69            EdgeDirection::Out => TraversalDirection::Forward,
70            EdgeDirection::In => TraversalDirection::Backward,
71            EdgeDirection::Both => TraversalDirection::Bidirectional,
72        }
73    }
74}
75
76/// Reasons for the choice of direction
77#[derive(Debug, Clone)]
78pub enum DirectionSelectionReason {
79    /// Outdegree is less than indegree.
80    OutDegreeLower { out_degree: f64, in_degree: f64 },
81    /// The in-degree is less than the out-degree.
82    InDegreeLower { in_degree: f64, out_degree: f64 },
83    /// The degrees are equal or very similar.
84    DegreesEqual { out_degree: f64, in_degree: f64 },
85    /// Selection based on cost comparison
86    CostBased {
87        forward_cost: f64,
88        backward_cost: f64,
89    },
90    /// Avoid using super nodes.
91    AvoidSuperNode {
92        super_node_direction: TraversalDirection,
93        threshold: f64,
94    },
95    /// Statistical information is not available; the default orientation will be used.
96    StatsUnavailable,
97    /// Explicitly specifying the direction
98    ExplicitDirection,
99}
100
101/// Decision on the traversal direction
102#[derive(Debug, Clone)]
103pub struct TraversalDirectionDecision {
104    /// The chosen direction
105    pub direction: TraversalDirection,
106    /// Estimated number of output lines
107    pub estimated_output_rows: u64,
108    /// Estimated cost
109    pub estimated_cost: f64,
110    /// Reason for the choice
111    pub reason: DirectionSelectionReason,
112    /// Average degree (selected direction)
113    pub avg_degree: f64,
114    /// Does it involve super nodes?
115    pub involves_super_node: bool,
116}
117
118/// Traversal Direction Optimizer
119///
120/// Selecting the optimal traversal direction based on edge-based statistical information
121#[derive(Debug)]
122pub struct TraversalDirectionOptimizer {
123    cost_calculator: Arc<CostCalculator>,
124    /// Space name used to look up statistics.
125    space: String,
126    /// Super Node Threshold (A node is considered a super node if its degree exceeds this value.)
127    super_node_threshold: f64,
128    /// Threshold for degree difference: Differences smaller than this value are considered equal.
129    degree_equality_threshold: f64,
130}
131
132/// Direction optimization context
133#[derive(Debug, Clone)]
134pub struct DirectionContext {
135    /// Edge type
136    pub edge_type: String,
137    /// Number of starting nodes
138    pub start_nodes: u64,
139    /// The explicitly specified direction (if any)
140    pub explicit_direction: Option<TraversalDirection>,
141    /// Is bidirectional traversal allowed?
142    pub allow_bidirectional: bool,
143    /// Number of iterations
144    pub steps: u32,
145}
146
147impl TraversalDirectionOptimizer {
148    /// Create a new optimizer for optimizing traversal directions.
149    pub fn new(cost_calculator: Arc<CostCalculator>) -> Self {
150        // Single super-node source: CostModelConfig::super_node_threshold.
151        let thresholds = cost_calculator.config().strategy_thresholds;
152        Self {
153            cost_calculator: Arc::clone(&cost_calculator),
154            space: String::new(),
155            super_node_threshold: cost_calculator.config().super_node_threshold as f64,
156            degree_equality_threshold: thresholds.bidirectional_savings_threshold,
157        }
158    }
159
160    /// Set the space name used to look up statistics.
161    pub fn with_space(mut self, space: &str) -> Self {
162        self.space = space.to_string();
163        self
164    }
165
166    /// Setting the threshold for super nodes
167    pub fn with_super_node_threshold(mut self, threshold: f64) -> Self {
168        self.super_node_threshold = threshold;
169        self
170    }
171
172    /// Set a threshold for equal degrees
173    pub fn with_equality_threshold(mut self, threshold: f64) -> Self {
174        self.degree_equality_threshold = threshold;
175        self
176    }
177
178    /// Optimize the direction of the traversal
179    ///
180    /// # Parameters
181    /// “Direction optimization context”
182    ///
183    /// # Return
184    /// Directional decision-making outcome
185    pub fn optimize_direction(&self, context: &DirectionContext) -> TraversalDirectionDecision {
186        // If a specific direction is explicitly specified, that direction should be given priority.
187        if let Some(explicit) = context.explicit_direction {
188            return self.create_explicit_decision(context, explicit);
189        }
190
191        // Obtain edge statistics information
192        let stats = self
193            .cost_calculator
194            .statistics_manager()
195            .get_edge_stats(&self.space, &context.edge_type);
196
197        match stats {
198            Some(edge_stats) => self.optimize_with_stats(context, &edge_stats),
199            None => self.create_default_decision(context),
200        }
201    }
202
203    /// Optimization directions based on statistical information
204    fn optimize_with_stats(
205        &self,
206        context: &DirectionContext,
207        stats: &EdgeTypeStatistics,
208    ) -> TraversalDirectionDecision {
209        let out_degree = stats.avg_out_degree;
210        let in_degree = stats.avg_in_degree;
211
212        // Check whether super nodes are involved.
213        let forward_is_super = out_degree > self.super_node_threshold;
214        let backward_is_super = in_degree > self.super_node_threshold;
215
216        // If both directions involve super nodes, choose the one with the smaller degree (i.e., the one with fewer connections).
217        if forward_is_super && backward_is_super {
218            let direction = if out_degree <= in_degree {
219                TraversalDirection::Forward
220            } else {
221                TraversalDirection::Backward
222            };
223
224            let avg_degree = if out_degree <= in_degree {
225                out_degree
226            } else {
227                in_degree
228            };
229
230            return TraversalDirectionDecision {
231                direction,
232                estimated_output_rows: (context.start_nodes as f64 * avg_degree) as u64,
233                estimated_cost: self.calculate_cost(context, true),
234                reason: DirectionSelectionReason::AvoidSuperNode {
235                    super_node_direction: if out_degree > in_degree {
236                        TraversalDirection::Forward
237                    } else {
238                        TraversalDirection::Backward
239                    },
240                    threshold: self.super_node_threshold,
241                },
242                avg_degree,
243                involves_super_node: true,
244            };
245        }
246
247        // If there is only one direction in which the node is a super node, avoid that direction.
248        if forward_is_super {
249            return TraversalDirectionDecision {
250                direction: TraversalDirection::Backward,
251                estimated_output_rows: (context.start_nodes as f64 * in_degree) as u64,
252                estimated_cost: self.calculate_cost(context, false),
253                reason: DirectionSelectionReason::AvoidSuperNode {
254                    super_node_direction: TraversalDirection::Forward,
255                    threshold: self.super_node_threshold,
256                },
257                avg_degree: in_degree,
258                involves_super_node: true,
259            };
260        }
261
262        if backward_is_super {
263            return TraversalDirectionDecision {
264                direction: TraversalDirection::Forward,
265                estimated_output_rows: (context.start_nodes as f64 * out_degree) as u64,
266                estimated_cost: self.calculate_cost(context, false),
267                reason: DirectionSelectionReason::AvoidSuperNode {
268                    super_node_direction: TraversalDirection::Backward,
269                    threshold: self.super_node_threshold,
270                },
271                avg_degree: out_degree,
272                involves_super_node: true,
273            };
274        }
275
276        // Choosing the direction based on the comparison of degrees
277        let degree_diff = (out_degree - in_degree).abs();
278        let degree_ratio = degree_diff / ((out_degree + in_degree) / 2.0);
279
280        if degree_ratio < self.degree_equality_threshold {
281            // The degrees are close; the choice is made based on cost calculations.
282            self.select_by_cost(context, out_degree, in_degree)
283        } else if out_degree < in_degree {
284            // The degree of deviation is small; therefore, the positive option should be chosen.
285            TraversalDirectionDecision {
286                direction: TraversalDirection::Forward,
287                estimated_output_rows: (context.start_nodes as f64 * out_degree) as u64,
288                estimated_cost: self.calculate_cost(context, false),
289                reason: DirectionSelectionReason::OutDegreeLower {
290                    out_degree,
291                    in_degree,
292                },
293                avg_degree: out_degree,
294                involves_super_node: false,
295            }
296        } else {
297            TraversalDirectionDecision {
298                direction: TraversalDirection::Backward,
299                estimated_output_rows: (context.start_nodes as f64 * in_degree) as u64,
300                estimated_cost: self.calculate_cost(context, false),
301                reason: DirectionSelectionReason::InDegreeLower {
302                    in_degree,
303                    out_degree,
304                },
305                avg_degree: in_degree,
306                involves_super_node: false,
307            }
308        }
309    }
310
311    /// Choosing the direction based on cost
312    fn select_by_cost(
313        &self,
314        context: &DirectionContext,
315        out_degree: f64,
316        in_degree: f64,
317    ) -> TraversalDirectionDecision {
318        // Calculate costs with respective degrees
319        let forward_cost = self.calculate_cost_with_degree(context, out_degree);
320        let backward_cost = self.calculate_cost_with_degree(context, in_degree);
321
322        let (direction, avg_degree) = if forward_cost <= backward_cost {
323            (TraversalDirection::Forward, out_degree)
324        } else {
325            (TraversalDirection::Backward, in_degree)
326        };
327
328        let involves_super_node =
329            out_degree > self.super_node_threshold || in_degree > self.super_node_threshold;
330
331        TraversalDirectionDecision {
332            direction,
333            estimated_output_rows: (context.start_nodes as f64 * avg_degree) as u64,
334            estimated_cost: forward_cost.min(backward_cost),
335            reason: DirectionSelectionReason::CostBased {
336                forward_cost,
337                backward_cost,
338            },
339            avg_degree,
340            involves_super_node,
341        }
342    }
343
344    /// Calculating the cost of traversal with a specific degree
345    fn calculate_cost_with_degree(&self, context: &DirectionContext, degree: f64) -> f64 {
346        let base_cost = self.cost_calculator.calculate_expand_cost(
347            &self.space,
348            context.start_nodes,
349            Some(&context.edge_type),
350        );
351
352        // Apply penalty for high-degree (super node) scenarios
353        let degree_factor = if degree > self.super_node_threshold {
354            self.cost_calculator.config().super_node_penalty
355        } else {
356            // Gradual increase in cost based on degree ratio
357            1.0 + (degree / self.super_node_threshold).ln_1p()
358        };
359
360        base_cost * degree_factor
361    }
362
363    /// Calculating the cost of traversal
364    fn calculate_cost(&self, context: &DirectionContext, is_super: bool) -> f64 {
365        let base_cost = self.cost_calculator.calculate_expand_cost(
366            &self.space,
367            context.start_nodes,
368            Some(&context.edge_type),
369        );
370
371        if is_super {
372            base_cost * self.cost_calculator.config().super_node_penalty
373        } else {
374            base_cost
375        }
376    }
377
378    /// Making decisions that involve a clear direction
379    fn create_explicit_decision(
380        &self,
381        context: &DirectionContext,
382        direction: TraversalDirection,
383    ) -> TraversalDirectionDecision {
384        // Try to obtain statistical information.
385        let avg_degree = self
386            .cost_calculator
387            .statistics_manager()
388            .get_edge_stats(&self.space, &context.edge_type)
389            .map(|s| match direction {
390                TraversalDirection::Forward => s.avg_out_degree,
391                TraversalDirection::Backward => s.avg_in_degree,
392                TraversalDirection::Bidirectional => (s.avg_out_degree + s.avg_in_degree) / 2.0,
393            })
394            .unwrap_or(DEFAULT_DEGREE_FALLBACK);
395
396        let is_super = avg_degree > self.super_node_threshold;
397
398        TraversalDirectionDecision {
399            direction,
400            estimated_output_rows: (context.start_nodes as f64 * avg_degree) as u64,
401            estimated_cost: self.calculate_cost(context, is_super),
402            reason: DirectionSelectionReason::ExplicitDirection,
403            avg_degree,
404            involves_super_node: is_super,
405        }
406    }
407
408    /// Create a default decision (statistical information not available).
409    fn create_default_decision(&self, context: &DirectionContext) -> TraversalDirectionDecision {
410        let default_degree = DEFAULT_DEGREE_FALLBACK;
411
412        TraversalDirectionDecision {
413            direction: TraversalDirection::Forward, // Default forward direction
414            estimated_output_rows: (context.start_nodes as f64 * default_degree) as u64,
415            estimated_cost: self.calculate_cost(context, false),
416            reason: DirectionSelectionReason::StatsUnavailable,
417            avg_degree: default_degree,
418            involves_super_node: false,
419        }
420    }
421
422    /// Quick direction selection (simplified version, for decision-making caching)
423    pub fn select_direction_quick(&self, edge_type: &str) -> TraversalDirection {
424        let stats = self
425            .cost_calculator
426            .statistics_manager()
427            .get_edge_stats(&self.space, edge_type);
428
429        match stats {
430            Some(s) => {
431                if s.avg_out_degree > self.super_node_threshold
432                    && s.avg_in_degree <= self.super_node_threshold
433                {
434                    TraversalDirection::Backward
435                } else if s.avg_in_degree > self.super_node_threshold
436                    && s.avg_out_degree <= self.super_node_threshold
437                    || s.avg_out_degree <= s.avg_in_degree
438                {
439                    TraversalDirection::Forward
440                } else {
441                    TraversalDirection::Backward
442                }
443            }
444            None => TraversalDirection::Forward,
445        }
446    }
447
448    /// Obtaining the degree information of the edges
449    pub fn get_degree_info(&self, edge_type: &str) -> Option<DegreeInfo> {
450        self.cost_calculator
451            .statistics_manager()
452            .get_edge_stats(&self.space, edge_type)
453            .map(|s| DegreeInfo {
454                avg_out_degree: s.avg_out_degree,
455                avg_in_degree: s.avg_in_degree,
456                max_out_degree: s.max_out_degree,
457                max_in_degree: s.max_in_degree,
458                is_out_super: s.avg_out_degree > self.super_node_threshold,
459                is_in_super: s.avg_in_degree > self.super_node_threshold,
460            })
461    }
462}
463
464/// Degree information
465#[derive(Debug, Clone)]
466pub struct DegreeInfo {
467    /// Average attendance
468    pub avg_out_degree: f64,
469    /// Average Indegree
470    pub avg_in_degree: f64,
471    /// Maximum Outdegree
472    pub max_out_degree: u64,
473    /// Maximum In-degree
474    pub max_in_degree: u64,
475    /// Is the outbound direction a super node?
476    pub is_out_super: bool,
477    /// Is the inbound direction a super node?
478    pub is_in_super: bool,
479}
480
481#[cfg(test)]
482mod tests {
483    use super::*;
484    use crate::optimizer::stats::StatisticsManager;
485
486    fn create_test_optimizer() -> TraversalDirectionOptimizer {
487        let stats_manager = Arc::new(StatisticsManager::new());
488        let cost_calculator = Arc::new(CostCalculator::new(stats_manager));
489        TraversalDirectionOptimizer::new(cost_calculator).with_super_node_threshold(100.0)
490    }
491
492    #[test]
493    fn test_explicit_direction() {
494        let optimizer = create_test_optimizer();
495        let context = DirectionContext {
496            edge_type: "KNOWS".to_string(),
497            start_nodes: 100,
498            explicit_direction: Some(TraversalDirection::Backward),
499            allow_bidirectional: false,
500            steps: 1,
501        };
502
503        let decision = optimizer.optimize_direction(&context);
504        assert_eq!(decision.direction, TraversalDirection::Backward);
505        matches!(decision.reason, DirectionSelectionReason::ExplicitDirection);
506    }
507
508    #[test]
509    fn test_default_direction_when_no_stats() {
510        let optimizer = create_test_optimizer();
511        let context = DirectionContext {
512            edge_type: "UNKNOWN".to_string(),
513            start_nodes: 100,
514            explicit_direction: None,
515            allow_bidirectional: false,
516            steps: 1,
517        };
518
519        let decision = optimizer.optimize_direction(&context);
520        assert_eq!(decision.direction, TraversalDirection::Forward);
521        matches!(decision.reason, DirectionSelectionReason::StatsUnavailable);
522    }
523
524    #[test]
525    fn test_traversal_direction_name() {
526        assert_eq!(TraversalDirection::Forward.name(), "Forward");
527        assert_eq!(TraversalDirection::Backward.name(), "Backward");
528        assert_eq!(TraversalDirection::Bidirectional.name(), "Bidirectional");
529    }
530
531    #[test]
532    fn test_traversal_direction_conversion() {
533        assert_eq!(
534            TraversalDirection::from_edge_direction(&EdgeDirection::Out),
535            TraversalDirection::Forward
536        );
537        assert_eq!(
538            TraversalDirection::from_edge_direction(&EdgeDirection::In),
539            TraversalDirection::Backward
540        );
541        assert_eq!(
542            TraversalDirection::from_edge_direction(&EdgeDirection::Both),
543            TraversalDirection::Bidirectional
544        );
545
546        assert_eq!(
547            TraversalDirection::Forward.to_edge_direction(),
548            EdgeDirection::Out
549        );
550        assert_eq!(
551            TraversalDirection::Backward.to_edge_direction(),
552            EdgeDirection::In
553        );
554        assert_eq!(
555            TraversalDirection::Bidirectional.to_edge_direction(),
556            EdgeDirection::Both
557        );
558    }
559
560    #[test]
561    fn test_quick_selection_no_stats() {
562        let optimizer = create_test_optimizer();
563        let direction = optimizer.select_direction_quick("UNKNOWN");
564        assert_eq!(direction, TraversalDirection::Forward);
565    }
566
567    #[test]
568    fn test_degree_info() {
569        let optimizer = create_test_optimizer();
570        // The unknown edge type should return None.
571        let info = optimizer.get_degree_info("UNKNOWN");
572        assert!(info.is_none());
573    }
574}