1use std::sync::Arc;
7
8use crate::optimizer::cost::config::{
9 DEFAULT_DEGREE_FALLBACK, SKEW_MODERATE_GINI, SKEW_SEVERE_GINI,
10};
11use crate::optimizer::cost::CostCalculator;
12use crate::optimizer::stats::{EdgeTypeStatistics, StatisticsManager};
13use linkrs_core::types::EdgeDirection;
14
15#[derive(Debug, Clone)]
17pub struct DepthAllocationContext {
18 pub start_variable: String,
20 pub end_variable: String,
22 pub edge_types: Vec<String>,
24 pub total_depth: u32,
26 pub start_tag: Option<String>,
28 pub end_tag: Option<String>,
30 pub start_degree_hint: Option<f64>,
32 pub end_degree_hint: Option<f64>,
34}
35
36impl DepthAllocationContext {
37 pub fn new(
39 start_variable: impl Into<String>,
40 end_variable: impl Into<String>,
41 edge_types: Vec<String>,
42 total_depth: u32,
43 ) -> Self {
44 Self {
45 start_variable: start_variable.into(),
46 end_variable: end_variable.into(),
47 edge_types,
48 total_depth,
49 start_tag: None,
50 end_tag: None,
51 start_degree_hint: None,
52 end_degree_hint: None,
53 }
54 }
55
56 pub fn with_start_tag(mut self, tag: impl Into<String>) -> Self {
58 self.start_tag = Some(tag.into());
59 self
60 }
61
62 pub fn with_end_tag(mut self, tag: impl Into<String>) -> Self {
64 self.end_tag = Some(tag.into());
65 self
66 }
67
68 pub fn with_start_degree(mut self, degree: f64) -> Self {
70 self.start_degree_hint = Some(degree);
71 self
72 }
73
74 pub fn with_end_degree(mut self, degree: f64) -> Self {
76 self.end_degree_hint = Some(degree);
77 self
78 }
79}
80
81#[derive(Debug, Clone)]
83pub struct BidirectionalDecision {
84 pub use_bidirectional: bool,
86 pub forward_start: String,
88 pub backward_start: String,
90 pub estimated_savings: f64,
92 pub recommended_depth: u32,
94}
95
96impl BidirectionalDecision {
97 pub fn unidirectional(forward_start: String) -> Self {
99 Self {
100 use_bidirectional: false,
101 forward_start,
102 backward_start: String::new(),
103 estimated_savings: 0.0,
104 recommended_depth: 0,
105 }
106 }
107
108 pub fn bidirectional(
110 forward_start: String,
111 backward_start: String,
112 estimated_savings: f64,
113 recommended_depth: u32,
114 ) -> Self {
115 Self {
116 use_bidirectional: true,
117 forward_start,
118 backward_start,
119 estimated_savings,
120 recommended_depth,
121 }
122 }
123}
124
125pub struct BidirectionalTraversalOptimizer {
127 cost_calculator: Arc<CostCalculator>,
129 stats_manager: Arc<StatisticsManager>,
131}
132
133impl BidirectionalTraversalOptimizer {
134 pub fn new(
136 cost_calculator: Arc<CostCalculator>,
137 stats_manager: Arc<StatisticsManager>,
138 ) -> Self {
139 Self {
140 cost_calculator,
141 stats_manager,
142 }
143 }
144
145 pub fn evaluate(
156 &self,
157 space: &str,
158 start_variable: &str,
159 end_variable: &str,
160 edge_types: &[String],
161 max_depth: u32,
162 ) -> BidirectionalDecision {
163 if max_depth < 2 {
165 return BidirectionalDecision::unidirectional(start_variable.to_string());
166 }
167
168 let avg_branching = self.estimate_average_branching(space, edge_types);
170
171 let unidirectional_cost = avg_branching.powi(max_depth as i32);
173
174 let half_depth = (max_depth as f64 / 2.0).ceil() as i32;
176 let bidirectional_cost = 2.0 * avg_branching.powi(half_depth);
177
178 let savings = if unidirectional_cost > 0.0 {
180 1.0 - (bidirectional_cost / unidirectional_cost)
181 } else {
182 0.0
183 };
184
185 let threshold = self
187 .cost_calculator
188 .config()
189 .strategy_thresholds
190 .bidirectional_savings_threshold;
191 if savings > threshold {
192 BidirectionalDecision::bidirectional(
193 start_variable.to_string(),
194 end_variable.to_string(),
195 savings,
196 max_depth,
197 )
198 } else {
199 BidirectionalDecision::unidirectional(start_variable.to_string())
200 }
201 }
202
203 pub fn evaluate_path_query(
205 &self,
206 space: &str,
207 start_variable: &str,
208 end_variable: &str,
209 edge_types: &[String],
210 min_depth: u32,
211 max_depth: u32,
212 ) -> BidirectionalDecision {
213 let avg_depth = ((min_depth + max_depth) as f64 / 2.0) as u32;
215
216 if min_depth >= 2 {
218 let decision =
219 self.evaluate(space, start_variable, end_variable, edge_types, avg_depth);
220 if decision.use_bidirectional {
221 return decision;
222 }
223 }
224
225 self.evaluate(space, start_variable, end_variable, edge_types, max_depth)
227 }
228
229 fn estimate_average_branching(&self, space: &str, edge_types: &[String]) -> f64 {
231 if edge_types.is_empty() {
232 return DEFAULT_DEGREE_FALLBACK;
233 }
234
235 let mut total_branching = 0.0;
236 let mut count = 0;
237
238 for edge_type in edge_types {
239 if let Some(stats) = self.stats_manager.get_edge_stats(space, edge_type) {
240 let branching = stats.avg_out_degree;
242 total_branching += branching;
243 count += 1;
244 }
245 }
246
247 if count > 0 {
248 total_branching / count as f64
249 } else {
250 DEFAULT_DEGREE_FALLBACK
251 }
252 }
253
254 pub fn calculate_depth_allocation(
259 &self,
260 space: &str,
261 context: &DepthAllocationContext,
262 ) -> (u32, u32) {
263 let total_depth = context.total_depth;
264
265 if total_depth < 2 {
267 return (total_depth, 0);
268 }
269
270 let edge_stats: Vec<EdgeTypeStatistics> = context
272 .edge_types
273 .iter()
274 .filter_map(|et| self.stats_manager.get_edge_stats(space, et))
275 .collect();
276
277 let start_degree = context
279 .start_degree_hint
280 .or_else(|| {
281 self.estimate_vertex_degree(
282 space,
283 &context.start_tag,
284 &edge_stats,
285 EdgeDirection::Out,
286 )
287 })
288 .unwrap_or_else(|| self.estimate_average_branching(space, &context.edge_types));
289
290 let end_degree = context
291 .end_degree_hint
292 .or_else(|| {
293 self.estimate_vertex_degree(space, &context.end_tag, &edge_stats, EdgeDirection::In)
294 })
295 .unwrap_or_else(|| self.estimate_average_branching(space, &context.edge_types));
296
297 self.allocate_depth_by_degree(start_degree, end_degree, total_depth, &edge_stats)
300 }
301
302 fn allocate_depth_by_degree(
304 &self,
305 start_degree: f64,
306 end_degree: f64,
307 total_depth: u32,
308 edge_stats: &[EdgeTypeStatistics],
309 ) -> (u32, u32) {
310 let skewness_adjustment = self.calculate_skewness_adjustment(edge_stats);
312
313 let log_start = (start_degree + 1.0).ln();
316 let log_end = (end_degree + 1.0).ln();
317 let log_total = log_start + log_end;
318
319 let base_forward_ratio = if log_total > 0.0 {
321 log_end / log_total
322 } else {
323 0.5
324 };
325
326 let adjusted_ratio = base_forward_ratio * (1.0 + skewness_adjustment);
328 let forward_ratio = adjusted_ratio.clamp(0.2, 0.8); let forward_depth =
331 ((forward_ratio * total_depth as f64).round() as u32).clamp(1, total_depth - 1);
332 let backward_depth = total_depth - forward_depth;
333
334 (forward_depth, backward_depth)
335 }
336
337 fn calculate_skewness_adjustment(&self, edge_stats: &[EdgeTypeStatistics]) -> f64 {
339 if edge_stats.is_empty() {
340 return 0.0;
341 }
342
343 let total_skewness: f64 = edge_stats.iter().map(|s| s.degree_gini_coefficient).sum();
344
345 let avg_skewness = total_skewness / edge_stats.len() as f64;
346
347 if avg_skewness > SKEW_SEVERE_GINI {
350 -0.1 } else if avg_skewness > SKEW_MODERATE_GINI {
352 -0.05 } else {
354 0.0 }
356 }
357
358 fn estimate_vertex_degree(
360 &self,
361 space: &str,
362 tag: &Option<String>,
363 _edge_stats: &[EdgeTypeStatistics],
364 direction: EdgeDirection,
365 ) -> Option<f64> {
366 let tag_name = tag.as_ref()?;
367
368 let tag_stats = self.stats_manager.get_tag_stats(space, tag_name)?;
370
371 match direction {
373 EdgeDirection::Out => Some(tag_stats.avg_out_degree),
374 EdgeDirection::In => Some(tag_stats.avg_in_degree),
375 EdgeDirection::Both => Some((tag_stats.avg_out_degree + tag_stats.avg_in_degree) / 2.0),
376 }
377 }
378
379 pub fn should_use_bidirectional(&self, space: &str, edge_types: &[String]) -> bool {
383 if edge_types.is_empty() {
384 return false;
385 }
386
387 let mut total_out_degree = 0.0;
388 let mut total_in_degree = 0.0;
389 let mut count = 0;
390
391 for edge_type in edge_types {
392 if let Some(stats) = self.stats_manager.get_edge_stats(space, edge_type) {
393 total_out_degree += stats.avg_out_degree;
394 total_in_degree += stats.avg_in_degree;
395 count += 1;
396
397 if stats.is_heavily_skewed() {
399 return false;
400 }
401 }
402 }
403
404 if count == 0 {
405 return false;
406 }
407
408 let avg_out = total_out_degree / count as f64;
409 let avg_in = total_in_degree / count as f64;
410
411 (avg_out < 10.0 && avg_in < 10.0) || ((avg_out - avg_in).abs() / (avg_out + avg_in) < 0.3)
414 }
415}
416
417#[cfg(test)]
418mod tests {
419 use super::*;
420 use crate::optimizer::cost::config::CostModelConfig;
421 use crate::optimizer::stats::{EdgeTypeStatistics, TagStatistics};
422
423 fn create_test_optimizer() -> BidirectionalTraversalOptimizer {
424 let stats_manager = Arc::new(crate::optimizer::stats::StatisticsManager::new());
425 let config = CostModelConfig::default();
426 let cost_calculator = Arc::new(CostCalculator::with_config(stats_manager.clone(), config));
427
428 let edge_stats = EdgeTypeStatistics::new("friend".to_string());
430 stats_manager.update_edge_stats("test", edge_stats);
431
432 BidirectionalTraversalOptimizer::new(cost_calculator, stats_manager)
433 }
434
435 fn create_test_optimizer_with_stats() -> BidirectionalTraversalOptimizer {
436 let stats_manager = Arc::new(crate::optimizer::stats::StatisticsManager::new());
437 let config = CostModelConfig::default();
438 let cost_calculator = Arc::new(CostCalculator::with_config(stats_manager.clone(), config));
439
440 let person_tag = TagStatistics {
442 tag_name: "Person".to_string(),
443 vertex_count: 1000,
444 avg_out_degree: 5.0,
445 avg_in_degree: 4.0,
446 ..TagStatistics::default()
447 };
448 stats_manager.update_tag_stats("test", person_tag);
449
450 let company_tag = TagStatistics {
451 tag_name: "Company".to_string(),
452 vertex_count: 100,
453 avg_out_degree: 2.0,
454 avg_in_degree: 50.0,
455 ..TagStatistics::default()
456 };
457 stats_manager.update_tag_stats("test", company_tag);
458
459 let edge_stats = EdgeTypeStatistics {
461 edge_type: "works_at".to_string(),
462 edge_count: 500,
463 avg_out_degree: 1.0,
464 avg_in_degree: 5.0,
465 max_out_degree: 1,
466 max_in_degree: 10,
467 unique_src_vertices: 500,
468 out_degree_std_dev: 0.5,
469 in_degree_std_dev: 2.0,
470 degree_gini_coefficient: 0.2,
471 hot_vertices: Vec::new(),
472 ..EdgeTypeStatistics::default()
473 };
474 stats_manager.update_edge_stats("test", edge_stats);
475
476 BidirectionalTraversalOptimizer::new(cost_calculator, stats_manager)
477 }
478
479 #[test]
480 fn test_unidirectional_for_shallow_depth() {
481 let optimizer = create_test_optimizer();
482 let decision = optimizer.evaluate("test", "a", "b", &[], 1);
483
484 assert!(!decision.use_bidirectional);
485 assert_eq!(decision.forward_start, "a");
486 }
487
488 #[test]
489 fn test_bidirectional_for_deep_depth() {
490 let optimizer = create_test_optimizer();
491 let decision = optimizer.evaluate("test", "a", "b", &[], 4);
493
494 if decision.use_bidirectional {
496 assert!(decision.estimated_savings > 0.0);
497 assert_eq!(decision.forward_start, "a");
498 assert_eq!(decision.backward_start, "b");
499 }
500 }
501
502 #[test]
503 fn test_estimate_average_branching() {
504 let optimizer = create_test_optimizer();
505 let branching = optimizer.estimate_average_branching("test", &[]);
506
507 assert_eq!(branching, 2.0);
509 }
510
511 #[test]
512 fn test_depth_allocation_with_context() {
513 let optimizer = create_test_optimizer_with_stats();
514
515 let context = DepthAllocationContext::new("a", "b", vec!["works_at".to_string()], 4);
517 let (forward, backward) = optimizer.calculate_depth_allocation("test", &context);
518
519 assert_eq!(forward + backward, 4);
520 assert!(forward >= 1);
521 assert!(backward >= 1);
522 }
523
524 #[test]
525 fn test_depth_allocation_with_tags() {
526 let optimizer = create_test_optimizer_with_stats();
527
528 let context = DepthAllocationContext::new("a", "b", vec!["works_at".to_string()], 4)
530 .with_start_tag("Person")
531 .with_end_tag("Company");
532
533 let (forward, backward) = optimizer.calculate_depth_allocation("test", &context);
534
535 assert_eq!(forward + backward, 4);
537 assert!(
540 forward >= backward,
541 "The end with the smaller degree should be assigned more depth."
542 );
543 }
544
545 #[test]
546 fn test_depth_allocation_with_degree_hints() {
547 let optimizer = create_test_optimizer();
548
549 let context = DepthAllocationContext::new("a", "b", vec![], 6)
551 .with_start_degree(2.0)
552 .with_end_degree(8.0);
553
554 let (forward, backward) = optimizer.calculate_depth_allocation("test", &context);
555
556 assert!(
558 forward > backward,
559 "Endpoints with smaller degrees should be assigned more depth."
560 );
561 assert_eq!(forward + backward, 6);
562 }
563
564 #[test]
565 fn test_should_use_bidirectional() {
566 let optimizer = create_test_optimizer_with_stats();
567
568 assert!(optimizer.should_use_bidirectional("test", &["works_at".to_string()]));
570
571 assert!(!optimizer.should_use_bidirectional("test", &[]));
573 }
574
575 #[test]
576 fn test_depth_allocation_shallow_depth() {
577 let optimizer = create_test_optimizer();
578
579 let context = DepthAllocationContext::new("a", "b", vec![], 1);
581 let (forward, backward) = optimizer.calculate_depth_allocation("test", &context);
582
583 assert_eq!(forward, 1);
584 assert_eq!(backward, 0);
585 }
586}