1use anyhow::{anyhow, Result};
12use serde::{Deserialize, Serialize};
13use std::collections::{BTreeMap, HashMap, VecDeque};
14use std::time::{Duration, SystemTime, UNIX_EPOCH};
15
16#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct QueryAnalytics {
19 pub query_id: String,
20 pub timestamp: u64,
21 pub query_vector: Vec<f32>,
22 pub similarity_metric: String,
23 pub top_k: usize,
24 pub response_time: Duration,
25 pub results_count: usize,
26 pub avg_similarity_score: f32,
27 pub min_similarity_score: f32,
28 pub max_similarity_score: f32,
29 pub cache_hit: bool,
30 pub index_type: String,
31}
32
33#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct VectorDistributionAnalysis {
36 pub total_vectors: usize,
37 pub dimensionality: usize,
38 pub density_estimate: f32,
39 pub cluster_count: usize,
40 pub cluster_sizes: Vec<usize>,
41 pub cluster_cohesion: Vec<f32>,
42 pub cluster_separation: f32,
43 pub outlier_count: usize,
44 pub outlier_threshold: f32,
45 pub sparsity_ratio: f32,
46 pub distribution_skewness: f32,
47}
48
49#[derive(Debug, Clone, Serialize, Deserialize)]
51pub struct PerformanceTrends {
52 pub time_window: Duration,
53 pub query_volume_trend: Vec<(u64, usize)>,
54 pub response_time_trend: Vec<(u64, f32)>,
55 pub cache_hit_rate_trend: Vec<(u64, f32)>,
56 pub error_rate_trend: Vec<(u64, f32)>,
57 pub predicted_peak_hours: Vec<u8>,
58 pub performance_score: f32,
59 pub bottleneck_analysis: Vec<BottleneckInsight>,
60}
61
62#[derive(Debug, Clone, Serialize, Deserialize)]
64pub struct BottleneckInsight {
65 pub component: String,
66 pub severity: BottleneckSeverity,
67 pub impact_score: f32,
68 pub recommendation: String,
69 pub estimated_improvement: f32,
70}
71
72#[derive(Debug, Clone, Serialize, Deserialize)]
74pub enum BottleneckSeverity {
75 Critical,
76 High,
77 Medium,
78 Low,
79}
80
81#[derive(Debug, Clone, Serialize, Deserialize)]
83pub struct OptimizationRecommendation {
84 pub recommendation_type: RecommendationType,
85 pub priority: Priority,
86 pub description: String,
87 pub expected_improvement: f32,
88 pub implementation_effort: ImplementationEffort,
89 pub affected_queries: Vec<String>,
90}
91
92#[derive(Debug, Clone, Serialize, Deserialize)]
94pub enum RecommendationType {
95 IndexOptimization,
96 CacheStrategy,
97 SimilarityMetric,
98 Preprocessing,
99 Batching,
100 Hardware,
101}
102
103#[derive(Debug, Clone, Serialize, Deserialize)]
105pub enum Priority {
106 Critical,
107 High,
108 Medium,
109 Low,
110}
111
112#[derive(Debug, Clone, Serialize, Deserialize)]
114pub enum ImplementationEffort {
115 Minimal,
116 Low,
117 Medium,
118 High,
119 Significant,
120}
121
122#[derive(Debug, Clone, Serialize, Deserialize)]
124pub struct AnomalyDetection {
125 pub anomalies: Vec<QueryAnomaly>,
126 pub detection_threshold: f32,
127 pub false_positive_rate: f32,
128 pub confidence_level: f32,
129}
130
131#[derive(Debug, Clone, Serialize, Deserialize)]
133pub struct QueryAnomaly {
134 pub query_id: String,
135 pub anomaly_type: AnomalyType,
136 pub severity_score: f32,
137 pub description: String,
138 pub suggested_action: String,
139}
140
141#[derive(Debug, Clone, Serialize, Deserialize)]
143pub enum AnomalyType {
144 UnusualLatency,
145 LowSimilarityScores,
146 HighErrorRate,
147 UnexpectedTraffic,
148 SuspiciousPattern,
149}
150
151#[derive(Debug, Clone, Serialize, Deserialize)]
153pub struct VectorQualityAssessment {
154 pub overall_quality_score: f32,
155 pub dimension_quality: Vec<f32>,
156 pub noise_level: f32,
157 pub embedding_consistency: f32,
158 pub semantic_coherence: f32,
159 pub recommendations: Vec<QualityRecommendation>,
160}
161
162#[derive(Debug, Clone, Serialize, Deserialize)]
164pub struct QualityRecommendation {
165 pub aspect: QualityAspect,
166 pub current_score: f32,
167 pub target_score: f32,
168 pub recommendation: String,
169 pub priority: Priority,
170}
171
172#[derive(Debug, Clone, Serialize, Deserialize)]
174pub enum QualityAspect {
175 DimensionalityReduction,
176 NoiseReduction,
177 EmbeddingModel,
178 Preprocessing,
179 DataCleaning,
180}
181
182#[derive(Debug)]
184pub struct VectorAnalyticsEngine {
185 query_history: VecDeque<QueryAnalytics>,
186 performance_metrics: BTreeMap<u64, PerformanceMetrics>,
187 max_history_size: usize,
188 analysis_window: Duration,
189 anomaly_detector: AnomalyDetector,
190}
191
192#[derive(Debug, Clone, Serialize, Deserialize)]
194pub struct PerformanceMetrics {
195 pub timestamp: u64,
196 pub query_count: usize,
197 pub avg_response_time: f32,
198 pub cache_hit_rate: f32,
199 pub error_rate: f32,
200 pub throughput: f32,
201}
202
203#[derive(Debug)]
205pub struct AnomalyDetector {
206 baseline_metrics: HashMap<String, f32>,
207 detection_sensitivity: f32,
208 learning_rate: f32,
209}
210
211impl VectorAnalyticsEngine {
212 pub fn new() -> Self {
214 Self {
215 query_history: VecDeque::new(),
216 performance_metrics: BTreeMap::new(),
217 max_history_size: 10000,
218 analysis_window: Duration::from_secs(24 * 60 * 60), anomaly_detector: AnomalyDetector::new(),
220 }
221 }
222
223 pub fn with_config(max_history: usize, window: Duration, sensitivity: f32) -> Self {
225 Self {
226 query_history: VecDeque::new(),
227 performance_metrics: BTreeMap::new(),
228 max_history_size: max_history,
229 analysis_window: window,
230 anomaly_detector: AnomalyDetector::with_sensitivity(sensitivity),
231 }
232 }
233
234 pub fn record_query(&mut self, analytics: QueryAnalytics) {
236 if self.query_history.len() >= self.max_history_size {
238 self.query_history.pop_front();
239 }
240
241 self.query_history.push_back(analytics.clone());
242
243 self.update_performance_metrics(&analytics);
245
246 self.anomaly_detector.update_baseline(&analytics);
248 }
249
250 pub fn analyze_vector_distribution(
252 &self,
253 vectors: &[Vec<f32>],
254 ) -> Result<VectorDistributionAnalysis> {
255 if vectors.is_empty() {
256 return Err(anyhow!("Cannot analyze empty vector set"));
257 }
258
259 let total_vectors = vectors.len();
260 let dimensionality = vectors[0].len();
261
262 let density_estimate = self.calculate_density_estimate(vectors);
264 let sparsity_ratio = self.calculate_sparsity_ratio(vectors);
265 let distribution_skewness = self.calculate_skewness(vectors);
266
267 let (cluster_count, cluster_sizes, cluster_cohesion, cluster_separation) =
269 self.analyze_clustering(vectors)?;
270
271 let (outlier_count, outlier_threshold) = self.detect_outliers(vectors);
273
274 Ok(VectorDistributionAnalysis {
275 total_vectors,
276 dimensionality,
277 density_estimate,
278 cluster_count,
279 cluster_sizes,
280 cluster_cohesion,
281 cluster_separation,
282 outlier_count,
283 outlier_threshold,
284 sparsity_ratio,
285 distribution_skewness,
286 })
287 }
288
289 pub fn analyze_performance_trends(&self) -> PerformanceTrends {
291 let cutoff_time = self.current_timestamp() - self.analysis_window.as_secs();
292
293 let query_volume_trend = self.calculate_query_volume_trend(cutoff_time);
294 let response_time_trend = self.calculate_response_time_trend(cutoff_time);
295 let cache_hit_rate_trend = self.calculate_cache_hit_rate_trend(cutoff_time);
296 let error_rate_trend = self.calculate_error_rate_trend(cutoff_time);
297
298 let predicted_peak_hours = self.predict_peak_hours();
299 let performance_score = self.calculate_performance_score();
300 let bottleneck_analysis = self.analyze_bottlenecks();
301
302 PerformanceTrends {
303 time_window: self.analysis_window,
304 query_volume_trend,
305 response_time_trend,
306 cache_hit_rate_trend,
307 error_rate_trend,
308 predicted_peak_hours,
309 performance_score,
310 bottleneck_analysis,
311 }
312 }
313
314 pub fn generate_optimization_recommendations(&self) -> Vec<OptimizationRecommendation> {
316 let mut recommendations = Vec::new();
317
318 recommendations.extend(self.analyze_index_optimization());
320
321 recommendations.extend(self.analyze_cache_optimization());
323
324 recommendations.extend(self.analyze_similarity_optimization());
326
327 recommendations.extend(self.analyze_batching_optimization());
329
330 recommendations.sort_by(|a, b| match (&a.priority, &b.priority) {
332 (Priority::Critical, Priority::Critical) => b
333 .expected_improvement
334 .partial_cmp(&a.expected_improvement)
335 .unwrap(),
336 (Priority::Critical, _) => std::cmp::Ordering::Less,
337 (_, Priority::Critical) => std::cmp::Ordering::Greater,
338 (Priority::High, Priority::High) => b
339 .expected_improvement
340 .partial_cmp(&a.expected_improvement)
341 .unwrap(),
342 (Priority::High, _) => std::cmp::Ordering::Less,
343 (_, Priority::High) => std::cmp::Ordering::Greater,
344 _ => b
345 .expected_improvement
346 .partial_cmp(&a.expected_improvement)
347 .unwrap(),
348 });
349
350 recommendations
351 }
352
353 pub fn detect_anomalies(&self) -> AnomalyDetection {
355 self.anomaly_detector.detect_anomalies(&self.query_history)
356 }
357
358 pub fn assess_vector_quality(&self, vectors: &[Vec<f32>]) -> Result<VectorQualityAssessment> {
360 if vectors.is_empty() {
361 return Err(anyhow!("Cannot assess quality of empty vector set"));
362 }
363
364 let overall_quality_score = self.calculate_overall_quality(vectors);
365 let dimension_quality = self.calculate_dimension_quality(vectors);
366 let noise_level = self.estimate_noise_level(vectors);
367 let embedding_consistency = self.calculate_embedding_consistency(vectors);
368 let semantic_coherence = self.calculate_semantic_coherence(vectors);
369 let recommendations = self.generate_quality_recommendations(
370 overall_quality_score,
371 &dimension_quality,
372 noise_level,
373 );
374
375 Ok(VectorQualityAssessment {
376 overall_quality_score,
377 dimension_quality,
378 noise_level,
379 embedding_consistency,
380 semantic_coherence,
381 recommendations,
382 })
383 }
384
385 pub fn export_analytics(&self) -> Result<String> {
387 #[derive(Serialize)]
388 struct AnalyticsExport {
389 query_count: usize,
390 performance_trends: PerformanceTrends,
391 recommendations: Vec<OptimizationRecommendation>,
392 anomalies: AnomalyDetection,
393 export_timestamp: u64,
394 }
395
396 let export = AnalyticsExport {
397 query_count: self.query_history.len(),
398 performance_trends: self.analyze_performance_trends(),
399 recommendations: self.generate_optimization_recommendations(),
400 anomalies: self.detect_anomalies(),
401 export_timestamp: self.current_timestamp(),
402 };
403
404 serde_json::to_string_pretty(&export)
405 .map_err(|e| anyhow!("Failed to export analytics: {}", e))
406 }
407
408 fn update_performance_metrics(&mut self, query: &QueryAnalytics) {
411 let time_bucket = (query.timestamp / 300) * 300; let metrics = self
414 .performance_metrics
415 .entry(time_bucket)
416 .or_insert(PerformanceMetrics {
417 timestamp: time_bucket,
418 query_count: 0,
419 avg_response_time: 0.0,
420 cache_hit_rate: 0.0,
421 error_rate: 0.0,
422 throughput: 0.0,
423 });
424
425 metrics.query_count += 1;
426 metrics.avg_response_time = (metrics.avg_response_time * (metrics.query_count - 1) as f32
427 + query.response_time.as_secs_f32())
428 / metrics.query_count as f32;
429
430 if query.cache_hit {
431 metrics.cache_hit_rate = (metrics.cache_hit_rate * (metrics.query_count - 1) as f32
432 + 1.0)
433 / metrics.query_count as f32;
434 } else {
435 metrics.cache_hit_rate = (metrics.cache_hit_rate * (metrics.query_count - 1) as f32)
436 / metrics.query_count as f32;
437 }
438
439 metrics.throughput = metrics.query_count as f32 / 300.0; }
441
442 fn calculate_density_estimate(&self, vectors: &[Vec<f32>]) -> f32 {
443 if vectors.len() < 2 {
445 return 0.0;
446 }
447
448 let mut total_distance = 0.0;
449 let mut count = 0;
450
451 for (i, v1) in vectors.iter().enumerate().take(100) {
452 for v2 in vectors.iter().skip(i + 1).take(10) {
454 total_distance += euclidean_distance(v1, v2);
455 count += 1;
456 }
457 }
458
459 if count > 0 {
460 1.0 / (total_distance / count as f32)
461 } else {
462 0.0
463 }
464 }
465
466 fn calculate_sparsity_ratio(&self, vectors: &[Vec<f32>]) -> f32 {
467 let mut zero_count = 0;
468 let mut total_elements = 0;
469
470 for vector in vectors {
471 for &value in vector {
472 if value.abs() < 1e-6 {
473 zero_count += 1;
474 }
475 total_elements += 1;
476 }
477 }
478
479 zero_count as f32 / total_elements as f32
480 }
481
482 fn calculate_skewness(&self, vectors: &[Vec<f32>]) -> f32 {
483 if vectors.is_empty() || vectors[0].is_empty() {
485 return 0.0;
486 }
487
488 let values: Vec<f32> = vectors.iter().map(|v| v[0]).collect();
489 let mean = values.iter().sum::<f32>() / values.len() as f32;
490 let variance = values.iter().map(|x| (x - mean).powi(2)).sum::<f32>() / values.len() as f32;
491 let std_dev = variance.sqrt();
492
493 if std_dev == 0.0 {
494 return 0.0;
495 }
496
497 let skewness = values
498 .iter()
499 .map(|x| ((x - mean) / std_dev).powi(3))
500 .sum::<f32>()
501 / values.len() as f32;
502
503 skewness
504 }
505
506 fn analyze_clustering(
507 &self,
508 vectors: &[Vec<f32>],
509 ) -> Result<(usize, Vec<usize>, Vec<f32>, f32)> {
510 let max_k = (vectors.len() as f32).sqrt() as usize;
512 let optimal_k = (max_k / 2).clamp(2, 10);
513
514 let cluster_count = optimal_k;
516 let cluster_sizes = vec![vectors.len() / cluster_count; cluster_count];
517 let cluster_cohesion = vec![0.8; cluster_count]; let cluster_separation = 0.6; Ok((
521 cluster_count,
522 cluster_sizes,
523 cluster_cohesion,
524 cluster_separation,
525 ))
526 }
527
528 fn detect_outliers(&self, vectors: &[Vec<f32>]) -> (usize, f32) {
529 let centroid = calculate_centroid(vectors);
531 let mut distances = Vec::new();
532
533 for vector in vectors {
534 distances.push(euclidean_distance(vector, ¢roid));
535 }
536
537 distances.sort_by(|a, b| a.partial_cmp(b).unwrap());
538 let q3_index = (distances.len() as f32 * 0.75) as usize;
539 let q1_index = (distances.len() as f32 * 0.25) as usize;
540
541 let iqr = distances[q3_index] - distances[q1_index];
542 let threshold = distances[q3_index] + 1.5 * iqr;
543
544 let outlier_count = distances.iter().filter(|&&d| d > threshold).count();
545
546 (outlier_count, threshold)
547 }
548
549 fn calculate_query_volume_trend(&self, cutoff_time: u64) -> Vec<(u64, usize)> {
550 let mut hourly_counts = BTreeMap::new();
551
552 for query in &self.query_history {
553 if query.timestamp > cutoff_time {
554 let hour_bucket = (query.timestamp / 3600) * 3600;
555 *hourly_counts.entry(hour_bucket).or_insert(0) += 1;
556 }
557 }
558
559 hourly_counts.into_iter().collect()
560 }
561
562 fn calculate_response_time_trend(&self, cutoff_time: u64) -> Vec<(u64, f32)> {
563 let mut hourly_times = BTreeMap::new();
564 let mut hourly_counts = BTreeMap::new();
565
566 for query in &self.query_history {
567 if query.timestamp > cutoff_time {
568 let hour_bucket = (query.timestamp / 3600) * 3600;
569 *hourly_times.entry(hour_bucket).or_insert(0.0) +=
570 query.response_time.as_secs_f32();
571 *hourly_counts.entry(hour_bucket).or_insert(0) += 1;
572 }
573 }
574
575 hourly_times
576 .into_iter()
577 .map(|(time, total)| (time, total / hourly_counts[&time] as f32))
578 .collect()
579 }
580
581 fn calculate_cache_hit_rate_trend(&self, cutoff_time: u64) -> Vec<(u64, f32)> {
582 let mut hourly_hits = BTreeMap::new();
583 let mut hourly_counts = BTreeMap::new();
584
585 for query in &self.query_history {
586 if query.timestamp > cutoff_time {
587 let hour_bucket = (query.timestamp / 3600) * 3600;
588 if query.cache_hit {
589 *hourly_hits.entry(hour_bucket).or_insert(0) += 1;
590 }
591 *hourly_counts.entry(hour_bucket).or_insert(0) += 1;
592 }
593 }
594
595 hourly_counts
596 .into_iter()
597 .map(|(time, count)| {
598 let hits = hourly_hits.get(&time).unwrap_or(&0);
599 (time, *hits as f32 / count as f32)
600 })
601 .collect()
602 }
603
604 fn calculate_error_rate_trend(&self, _cutoff_time: u64) -> Vec<(u64, f32)> {
605 vec![]
607 }
608
609 fn predict_peak_hours(&self) -> Vec<u8> {
610 let mut hour_volumes = [0; 24];
611
612 for query in &self.query_history {
613 let hour = ((query.timestamp % 86400) / 3600) as usize;
614 if hour < 24 {
615 hour_volumes[hour] += 1;
616 }
617 }
618
619 let avg_volume = hour_volumes.iter().sum::<usize>() as f32 / 24.0;
620
621 hour_volumes
622 .iter()
623 .enumerate()
624 .filter(|&(_, &volume)| volume as f32 > avg_volume * 1.5)
625 .map(|(hour, _)| hour as u8)
626 .collect()
627 }
628
629 fn calculate_performance_score(&self) -> f32 {
630 if self.query_history.is_empty() {
631 return 0.0;
632 }
633
634 let avg_response_time = self
635 .query_history
636 .iter()
637 .map(|q| q.response_time.as_secs_f32())
638 .sum::<f32>()
639 / self.query_history.len() as f32;
640
641 let cache_hit_rate = self.query_history.iter().filter(|q| q.cache_hit).count() as f32
642 / self.query_history.len() as f32;
643
644 let avg_similarity = self
645 .query_history
646 .iter()
647 .map(|q| q.avg_similarity_score)
648 .sum::<f32>()
649 / self.query_history.len() as f32;
650
651 let response_score = 1.0 / (1.0 + avg_response_time);
653 let cache_score = cache_hit_rate;
654 let similarity_score = avg_similarity;
655
656 (response_score + cache_score + similarity_score) / 3.0
657 }
658
659 fn analyze_bottlenecks(&self) -> Vec<BottleneckInsight> {
660 let mut bottlenecks = Vec::new();
661
662 let avg_response_time = self
664 .query_history
665 .iter()
666 .map(|q| q.response_time.as_secs_f32())
667 .sum::<f32>()
668 / self.query_history.len().max(1) as f32;
669
670 if avg_response_time > 0.1 {
671 bottlenecks.push(BottleneckInsight {
672 component: "Response Time".to_string(),
673 severity: if avg_response_time > 1.0 {
674 BottleneckSeverity::Critical
675 } else {
676 BottleneckSeverity::High
677 },
678 impact_score: avg_response_time * 10.0,
679 recommendation: "Consider index optimization or caching improvements".to_string(),
680 estimated_improvement: 0.3,
681 });
682 }
683
684 let cache_hit_rate = self.query_history.iter().filter(|q| q.cache_hit).count() as f32
686 / self.query_history.len().max(1) as f32;
687
688 if cache_hit_rate < 0.5 {
689 bottlenecks.push(BottleneckInsight {
690 component: "Cache Efficiency".to_string(),
691 severity: BottleneckSeverity::Medium,
692 impact_score: (1.0 - cache_hit_rate) * 5.0,
693 recommendation: "Improve cache strategy or increase cache size".to_string(),
694 estimated_improvement: 0.25,
695 });
696 }
697
698 bottlenecks
699 }
700
701 fn analyze_index_optimization(&self) -> Vec<OptimizationRecommendation> {
702 let mut recommendations = Vec::new();
704
705 let mut index_performance = HashMap::new();
707 for query in &self.query_history {
708 let entry = index_performance
709 .entry(&query.index_type)
710 .or_insert(Vec::new());
711 entry.push(query.response_time.as_secs_f32());
712 }
713
714 for (index_type, times) in index_performance {
715 let avg_time = times.iter().sum::<f32>() / times.len() as f32;
716 if avg_time > 0.05 {
717 recommendations.push(OptimizationRecommendation {
719 recommendation_type: RecommendationType::IndexOptimization,
720 priority: if avg_time > 0.2 {
721 Priority::High
722 } else {
723 Priority::Medium
724 },
725 description: format!("Optimize {index_type} index performance"),
726 expected_improvement: (avg_time * 0.3).min(0.8),
727 implementation_effort: ImplementationEffort::Medium,
728 affected_queries: vec![], });
730 }
731 }
732
733 recommendations
734 }
735
736 fn analyze_cache_optimization(&self) -> Vec<OptimizationRecommendation> {
737 let cache_hit_rate = self.query_history.iter().filter(|q| q.cache_hit).count() as f32
738 / self.query_history.len().max(1) as f32;
739
740 let mut recommendations = Vec::new();
741
742 if cache_hit_rate < 0.7 {
743 recommendations.push(OptimizationRecommendation {
744 recommendation_type: RecommendationType::CacheStrategy,
745 priority: Priority::Medium,
746 description: "Improve cache hit rate through better caching strategy".to_string(),
747 expected_improvement: (0.7 - cache_hit_rate) * 0.5,
748 implementation_effort: ImplementationEffort::Low,
749 affected_queries: vec![],
750 });
751 }
752
753 recommendations
754 }
755
756 fn analyze_similarity_optimization(&self) -> Vec<OptimizationRecommendation> {
757 let mut metric_performance = HashMap::new();
759
760 for query in &self.query_history {
761 let entry = metric_performance
762 .entry(&query.similarity_metric)
763 .or_insert(Vec::new());
764 entry.push((
765 query.response_time.as_secs_f32(),
766 query.avg_similarity_score,
767 ));
768 }
769
770 let mut recommendations = Vec::new();
771
772 for (metric, performance) in metric_performance {
773 let avg_time =
774 performance.iter().map(|(t, _)| t).sum::<f32>() / performance.len() as f32;
775 let avg_score =
776 performance.iter().map(|(_, s)| s).sum::<f32>() / performance.len() as f32;
777
778 if avg_time > 0.05 && avg_score < 0.8 {
779 recommendations.push(OptimizationRecommendation {
780 recommendation_type: RecommendationType::SimilarityMetric,
781 priority: Priority::Low,
782 description: format!("Consider alternative to {metric} similarity metric"),
783 expected_improvement: 0.15,
784 implementation_effort: ImplementationEffort::Low,
785 affected_queries: vec![],
786 });
787 }
788 }
789
790 recommendations
791 }
792
793 fn analyze_batching_optimization(&self) -> Vec<OptimizationRecommendation> {
794 let mut recommendations = Vec::new();
796
797 let single_query_count = self.query_history.iter().filter(|q| q.top_k == 1).count();
798
799 if single_query_count > self.query_history.len() / 3 {
800 recommendations.push(OptimizationRecommendation {
801 recommendation_type: RecommendationType::Batching,
802 priority: Priority::Medium,
803 description: "Consider batching single-result queries for better throughput"
804 .to_string(),
805 expected_improvement: 0.2,
806 implementation_effort: ImplementationEffort::Medium,
807 affected_queries: vec![],
808 });
809 }
810
811 recommendations
812 }
813
814 fn calculate_overall_quality(&self, vectors: &[Vec<f32>]) -> f32 {
815 let consistency = self.calculate_embedding_consistency(vectors);
817 let coherence = self.calculate_semantic_coherence(vectors);
818 let noise = 1.0 - self.estimate_noise_level(vectors);
819
820 (consistency + coherence + noise) / 3.0
821 }
822
823 fn calculate_dimension_quality(&self, vectors: &[Vec<f32>]) -> Vec<f32> {
824 if vectors.is_empty() || vectors[0].is_empty() {
825 return vec![];
826 }
827
828 let dim_count = vectors[0].len();
829 let mut quality_scores = vec![0.0; dim_count];
830
831 for dim in 0..dim_count {
832 let values: Vec<f32> = vectors.iter().map(|v| v[dim]).collect();
833 let variance = calculate_variance(&values);
834 let range = values
835 .iter()
836 .max_by(|a, b| a.partial_cmp(b).unwrap())
837 .unwrap()
838 - values
839 .iter()
840 .min_by(|a, b| a.partial_cmp(b).unwrap())
841 .unwrap();
842
843 quality_scores[dim] = (variance * range).min(1.0);
845 }
846
847 quality_scores
848 }
849
850 fn estimate_noise_level(&self, vectors: &[Vec<f32>]) -> f32 {
851 let mut noise_estimate = 0.0;
853 let sample_size = vectors.len().min(100);
854
855 for i in 0..sample_size {
856 let mut min_distance = f32::INFINITY;
857 for j in 0..sample_size {
858 if i != j {
859 let distance = euclidean_distance(&vectors[i], &vectors[j]);
860 min_distance = min_distance.min(distance);
861 }
862 }
863 noise_estimate += min_distance;
864 }
865
866 noise_estimate / sample_size as f32
867 }
868
869 fn calculate_embedding_consistency(&self, vectors: &[Vec<f32>]) -> f32 {
870 let centroid = calculate_centroid(vectors);
872 let mut total_distance = 0.0;
873
874 for vector in vectors {
875 total_distance += euclidean_distance(vector, ¢roid);
876 }
877
878 let avg_distance = total_distance / vectors.len() as f32;
879 1.0 / (1.0 + avg_distance) }
881
882 fn calculate_semantic_coherence(&self, _vectors: &[Vec<f32>]) -> f32 {
883 0.8
886 }
887
888 fn generate_quality_recommendations(
889 &self,
890 overall_score: f32,
891 dimension_quality: &[f32],
892 noise_level: f32,
893 ) -> Vec<QualityRecommendation> {
894 let mut recommendations = Vec::new();
895
896 if overall_score < 0.7 {
897 recommendations.push(QualityRecommendation {
898 aspect: QualityAspect::EmbeddingModel,
899 current_score: overall_score,
900 target_score: 0.8,
901 recommendation: "Consider using a higher-quality embedding model".to_string(),
902 priority: Priority::High,
903 });
904 }
905
906 if noise_level > 0.3 {
907 recommendations.push(QualityRecommendation {
908 aspect: QualityAspect::NoiseReduction,
909 current_score: 1.0 - noise_level,
910 target_score: 0.8,
911 recommendation: "Apply noise reduction techniques to improve vector quality"
912 .to_string(),
913 priority: Priority::Medium,
914 });
915 }
916
917 let low_quality_dims = dimension_quality
918 .iter()
919 .enumerate()
920 .filter(|&(_, &score)| score < 0.5)
921 .count();
922
923 if low_quality_dims > dimension_quality.len() / 4 {
924 recommendations.push(QualityRecommendation {
925 aspect: QualityAspect::DimensionalityReduction,
926 current_score: 1.0 - (low_quality_dims as f32 / dimension_quality.len() as f32),
927 target_score: 0.9,
928 recommendation: "Consider dimensionality reduction to remove low-quality dimensions".to_string(),
929 priority: Priority::Medium,
930 });
931 }
932
933 recommendations
934 }
935
936 fn current_timestamp(&self) -> u64 {
937 SystemTime::now()
938 .duration_since(UNIX_EPOCH)
939 .unwrap_or_default()
940 .as_secs()
941 }
942}
943
944impl AnomalyDetector {
945 fn new() -> Self {
946 Self {
947 baseline_metrics: HashMap::new(),
948 detection_sensitivity: 2.0,
949 learning_rate: 0.1,
950 }
951 }
952
953 fn with_sensitivity(sensitivity: f32) -> Self {
954 Self {
955 baseline_metrics: HashMap::new(),
956 detection_sensitivity: sensitivity,
957 learning_rate: 0.1,
958 }
959 }
960
961 fn update_baseline(&mut self, query: &QueryAnalytics) {
962 let response_time = query.response_time.as_secs_f32();
963
964 let current_baseline = self.baseline_metrics.get("response_time").unwrap_or(&0.1);
965 let new_baseline =
966 current_baseline * (1.0 - self.learning_rate) + response_time * self.learning_rate;
967 self.baseline_metrics
968 .insert("response_time".to_string(), new_baseline);
969
970 let current_similarity = self.baseline_metrics.get("avg_similarity").unwrap_or(&0.8);
971 let new_similarity = current_similarity * (1.0 - self.learning_rate)
972 + query.avg_similarity_score * self.learning_rate;
973 self.baseline_metrics
974 .insert("avg_similarity".to_string(), new_similarity);
975 }
976
977 fn detect_anomalies(&self, queries: &VecDeque<QueryAnalytics>) -> AnomalyDetection {
978 let mut anomalies = Vec::new();
979
980 let response_time_baseline = self.baseline_metrics.get("response_time").unwrap_or(&0.1);
981 let similarity_baseline = self.baseline_metrics.get("avg_similarity").unwrap_or(&0.8);
982
983 for query in queries {
984 let response_time_ratio = query.response_time.as_secs_f32() / response_time_baseline;
985 let similarity_ratio = query.avg_similarity_score / similarity_baseline;
986
987 if response_time_ratio > self.detection_sensitivity {
988 anomalies.push(QueryAnomaly {
989 query_id: query.query_id.clone(),
990 anomaly_type: AnomalyType::UnusualLatency,
991 severity_score: response_time_ratio,
992 description: format!(
993 "Query response time {response_time_ratio}x higher than baseline"
994 ),
995 suggested_action: "Investigate query complexity or system load".to_string(),
996 });
997 }
998
999 if similarity_ratio < (1.0 / self.detection_sensitivity) {
1000 anomalies.push(QueryAnomaly {
1001 query_id: query.query_id.clone(),
1002 anomaly_type: AnomalyType::LowSimilarityScores,
1003 severity_score: 1.0 / similarity_ratio,
1004 description: "Unusually low similarity scores detected".to_string(),
1005 suggested_action: "Check vector quality or similarity metric configuration"
1006 .to_string(),
1007 });
1008 }
1009 }
1010
1011 AnomalyDetection {
1012 anomalies,
1013 detection_threshold: self.detection_sensitivity,
1014 false_positive_rate: 0.05, confidence_level: 0.95,
1016 }
1017 }
1018}
1019
1020fn euclidean_distance(a: &[f32], b: &[f32]) -> f32 {
1023 a.iter()
1024 .zip(b.iter())
1025 .map(|(x, y)| (x - y).powi(2))
1026 .sum::<f32>()
1027 .sqrt()
1028}
1029
1030fn calculate_centroid(vectors: &[Vec<f32>]) -> Vec<f32> {
1031 if vectors.is_empty() {
1032 return vec![];
1033 }
1034
1035 let dim_count = vectors[0].len();
1036 let mut centroid = vec![0.0; dim_count];
1037
1038 for vector in vectors {
1039 for (i, &value) in vector.iter().enumerate() {
1040 centroid[i] += value;
1041 }
1042 }
1043
1044 for value in &mut centroid {
1045 *value /= vectors.len() as f32;
1046 }
1047
1048 centroid
1049}
1050
1051fn calculate_variance(values: &[f32]) -> f32 {
1052 if values.is_empty() {
1053 return 0.0;
1054 }
1055
1056 let mean = values.iter().sum::<f32>() / values.len() as f32;
1057 values.iter().map(|x| (x - mean).powi(2)).sum::<f32>() / values.len() as f32
1058}
1059
1060impl Default for VectorAnalyticsEngine {
1061 fn default() -> Self {
1062 Self::new()
1063 }
1064}
1065
1066#[cfg(test)]
1067mod tests {
1068 use super::*;
1069 use std::time::Duration;
1070
1071 #[test]
1072 fn test_analytics_engine_creation() {
1073 let engine = VectorAnalyticsEngine::new();
1074 assert_eq!(engine.query_history.len(), 0);
1075 assert_eq!(engine.max_history_size, 10000);
1076 }
1077
1078 #[test]
1079 fn test_query_recording() {
1080 let mut engine = VectorAnalyticsEngine::new();
1081
1082 let query = QueryAnalytics {
1083 query_id: "test_query_1".to_string(),
1084 timestamp: 1640995200, query_vector: vec![0.1, 0.2, 0.3],
1086 similarity_metric: "cosine".to_string(),
1087 top_k: 10,
1088 response_time: Duration::from_millis(50),
1089 results_count: 8,
1090 avg_similarity_score: 0.85,
1091 min_similarity_score: 0.7,
1092 max_similarity_score: 0.95,
1093 cache_hit: true,
1094 index_type: "hnsw".to_string(),
1095 };
1096
1097 engine.record_query(query);
1098 assert_eq!(engine.query_history.len(), 1);
1099 }
1100
1101 #[test]
1102 fn test_vector_distribution_analysis() {
1103 let engine = VectorAnalyticsEngine::new();
1104
1105 let vectors = vec![
1106 vec![1.0, 2.0, 3.0],
1107 vec![1.1, 2.1, 3.1],
1108 vec![0.9, 1.9, 2.9],
1109 vec![5.0, 6.0, 7.0],
1110 vec![5.1, 6.1, 7.1],
1111 ];
1112
1113 let analysis = engine.analyze_vector_distribution(&vectors).unwrap();
1114
1115 assert_eq!(analysis.total_vectors, 5);
1116 assert_eq!(analysis.dimensionality, 3);
1117 assert!(analysis.density_estimate > 0.0);
1118 assert!(analysis.sparsity_ratio >= 0.0);
1119 }
1120
1121 #[test]
1122 fn test_performance_score_calculation() {
1123 let mut engine = VectorAnalyticsEngine::new();
1124
1125 for i in 0..10 {
1127 let query = QueryAnalytics {
1128 query_id: format!("query_{i}"),
1129 timestamp: 1640995200 + i * 60,
1130 query_vector: vec![0.1 * i as f32, 0.2 * i as f32],
1131 similarity_metric: "cosine".to_string(),
1132 top_k: 5,
1133 response_time: Duration::from_millis(30 + i * 5),
1134 results_count: 5,
1135 avg_similarity_score: 0.8 + (i as f32 * 0.01),
1136 min_similarity_score: 0.7,
1137 max_similarity_score: 0.95,
1138 cache_hit: i % 2 == 0,
1139 index_type: "hnsw".to_string(),
1140 };
1141 engine.record_query(query);
1142 }
1143
1144 let score = engine.calculate_performance_score();
1145 assert!(score > 0.0 && score <= 1.0);
1146 }
1147
1148 #[test]
1149 fn test_anomaly_detection() {
1150 let engine = VectorAnalyticsEngine::new();
1151 let anomalies = engine.detect_anomalies();
1152
1153 assert_eq!(anomalies.anomalies.len(), 0); assert!(anomalies.confidence_level > 0.0);
1155 }
1156
1157 #[test]
1158 fn test_optimization_recommendations() {
1159 let mut engine = VectorAnalyticsEngine::new();
1160
1161 let slow_query = QueryAnalytics {
1163 query_id: "slow_query".to_string(),
1164 timestamp: 1640995200,
1165 query_vector: vec![0.1, 0.2, 0.3],
1166 similarity_metric: "cosine".to_string(),
1167 top_k: 10,
1168 response_time: Duration::from_millis(500), results_count: 8,
1170 avg_similarity_score: 0.85,
1171 min_similarity_score: 0.7,
1172 max_similarity_score: 0.95,
1173 cache_hit: false, index_type: "linear".to_string(),
1175 };
1176
1177 engine.record_query(slow_query);
1178 let recommendations = engine.generate_optimization_recommendations();
1179
1180 assert!(!recommendations.is_empty());
1181 }
1182
1183 #[test]
1184 fn test_vector_quality_assessment() {
1185 let engine = VectorAnalyticsEngine::new();
1186
1187 let vectors = vec![
1188 vec![1.0, 2.0, 3.0, 0.0],
1189 vec![1.1, 2.1, 3.1, 0.0], vec![0.9, 1.9, 2.9, 0.0],
1191 vec![1.05, 2.05, 3.05, 0.0],
1192 ];
1193
1194 let assessment = engine.assess_vector_quality(&vectors).unwrap();
1195
1196 assert!(assessment.overall_quality_score >= 0.0 && assessment.overall_quality_score <= 1.0);
1197 assert_eq!(assessment.dimension_quality.len(), 4);
1198 assert!(assessment.noise_level >= 0.0);
1199 assert!(!assessment.recommendations.is_empty());
1200 }
1201
1202 #[test]
1203 fn test_analytics_export() {
1204 let engine = VectorAnalyticsEngine::new();
1205 let json_result = engine.export_analytics();
1206
1207 assert!(json_result.is_ok());
1208 let json_data = json_result.unwrap();
1209 assert!(json_data.contains("query_count"));
1210 assert!(json_data.contains("performance_trends"));
1211 assert!(json_data.contains("recommendations"));
1212 }
1213}