1use 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
35pub enum TraversalDirection {
36 Forward,
39 Backward,
42 Bidirectional,
45}
46
47impl TraversalDirection {
48 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 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 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#[derive(Debug, Clone)]
78pub enum DirectionSelectionReason {
79 OutDegreeLower { out_degree: f64, in_degree: f64 },
81 InDegreeLower { in_degree: f64, out_degree: f64 },
83 DegreesEqual { out_degree: f64, in_degree: f64 },
85 CostBased {
87 forward_cost: f64,
88 backward_cost: f64,
89 },
90 AvoidSuperNode {
92 super_node_direction: TraversalDirection,
93 threshold: f64,
94 },
95 StatsUnavailable,
97 ExplicitDirection,
99}
100
101#[derive(Debug, Clone)]
103pub struct TraversalDirectionDecision {
104 pub direction: TraversalDirection,
106 pub estimated_output_rows: u64,
108 pub estimated_cost: f64,
110 pub reason: DirectionSelectionReason,
112 pub avg_degree: f64,
114 pub involves_super_node: bool,
116}
117
118#[derive(Debug)]
122pub struct TraversalDirectionOptimizer {
123 cost_calculator: Arc<CostCalculator>,
124 space: String,
126 super_node_threshold: f64,
128 degree_equality_threshold: f64,
130}
131
132#[derive(Debug, Clone)]
134pub struct DirectionContext {
135 pub edge_type: String,
137 pub start_nodes: u64,
139 pub explicit_direction: Option<TraversalDirection>,
141 pub allow_bidirectional: bool,
143 pub steps: u32,
145}
146
147impl TraversalDirectionOptimizer {
148 pub fn new(cost_calculator: Arc<CostCalculator>) -> Self {
150 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 pub fn with_space(mut self, space: &str) -> Self {
162 self.space = space.to_string();
163 self
164 }
165
166 pub fn with_super_node_threshold(mut self, threshold: f64) -> Self {
168 self.super_node_threshold = threshold;
169 self
170 }
171
172 pub fn with_equality_threshold(mut self, threshold: f64) -> Self {
174 self.degree_equality_threshold = threshold;
175 self
176 }
177
178 pub fn optimize_direction(&self, context: &DirectionContext) -> TraversalDirectionDecision {
186 if let Some(explicit) = context.explicit_direction {
188 return self.create_explicit_decision(context, explicit);
189 }
190
191 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 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 let forward_is_super = out_degree > self.super_node_threshold;
214 let backward_is_super = in_degree > self.super_node_threshold;
215
216 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 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 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 self.select_by_cost(context, out_degree, in_degree)
283 } else if out_degree < in_degree {
284 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 fn select_by_cost(
313 &self,
314 context: &DirectionContext,
315 out_degree: f64,
316 in_degree: f64,
317 ) -> TraversalDirectionDecision {
318 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 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 let degree_factor = if degree > self.super_node_threshold {
354 self.cost_calculator.config().super_node_penalty
355 } else {
356 1.0 + (degree / self.super_node_threshold).ln_1p()
358 };
359
360 base_cost * degree_factor
361 }
362
363 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 fn create_explicit_decision(
380 &self,
381 context: &DirectionContext,
382 direction: TraversalDirection,
383 ) -> TraversalDirectionDecision {
384 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 fn create_default_decision(&self, context: &DirectionContext) -> TraversalDirectionDecision {
410 let default_degree = DEFAULT_DEGREE_FALLBACK;
411
412 TraversalDirectionDecision {
413 direction: TraversalDirection::Forward, 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 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 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#[derive(Debug, Clone)]
466pub struct DegreeInfo {
467 pub avg_out_degree: f64,
469 pub avg_in_degree: f64,
471 pub max_out_degree: u64,
473 pub max_in_degree: u64,
475 pub is_out_super: bool,
477 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 let info = optimizer.get_degree_info("UNKNOWN");
572 assert!(info.is_none());
573 }
574}