Skip to main content

oxirs_shacl/analytics/
pattern_detection.rs

1//! Pattern Detection Module
2//!
3//! This module provides advanced pattern detection capabilities for SHACL validation,
4//! including failure pattern analysis, performance bottleneck detection, and data quality insights.
5
6use chrono::{DateTime, Utc};
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9use std::time::Duration;
10
11/// Pattern detection configuration
12#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct PatternDetectionConfig {
14    /// Enable failure pattern detection
15    pub enable_failure_patterns: bool,
16    /// Enable performance pattern detection
17    pub enable_performance_patterns: bool,
18    /// Enable data quality pattern detection  
19    pub enable_data_quality_patterns: bool,
20    /// Minimum pattern occurrence threshold
21    pub min_pattern_threshold: usize,
22    /// Pattern detection window duration
23    pub detection_window: Duration,
24    /// Enable anomaly detection
25    pub enable_anomaly_detection: bool,
26}
27
28impl Default for PatternDetectionConfig {
29    fn default() -> Self {
30        Self {
31            enable_failure_patterns: true,
32            enable_performance_patterns: true,
33            enable_data_quality_patterns: true,
34            min_pattern_threshold: 3,
35            detection_window: Duration::from_secs(3600), // 1 hour
36            enable_anomaly_detection: true,
37        }
38    }
39}
40
41/// Types of patterns that can be detected
42#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
43pub enum PatternType {
44    /// Validation failure patterns
45    ValidationFailure,
46    /// Performance bottleneck patterns
47    PerformanceBottleneck,
48    /// Data quality issues
49    DataQuality,
50    /// Constraint usage patterns
51    ConstraintUsage,
52    /// Target selection patterns
53    TargetSelection,
54    /// Anomalous behavior
55    Anomaly,
56}
57
58/// Detected pattern information
59#[derive(Debug, Clone, Serialize, Deserialize)]
60pub struct DetectedPattern {
61    /// Pattern type
62    pub pattern_type: PatternType,
63    /// Pattern identifier
64    pub pattern_id: String,
65    /// Pattern description
66    pub description: String,
67    /// Pattern severity (0.0 to 1.0)
68    pub severity: f64,
69    /// Number of occurrences
70    pub occurrence_count: usize,
71    /// First detected timestamp
72    pub first_detected: DateTime<Utc>,
73    /// Last detected timestamp
74    pub last_detected: DateTime<Utc>,
75    /// Pattern metadata
76    pub metadata: HashMap<String, String>,
77    /// Suggested actions
78    pub suggested_actions: Vec<String>,
79}
80
81/// Pattern detection engine
82#[derive(Debug)]
83pub struct PatternDetector {
84    config: PatternDetectionConfig,
85    detected_patterns: HashMap<String, DetectedPattern>,
86    failure_patterns: HashMap<String, usize>,
87    performance_patterns: HashMap<String, Vec<Duration>>,
88    data_quality_patterns: HashMap<String, usize>,
89    last_cleanup: DateTime<Utc>,
90}
91
92impl PatternDetector {
93    /// Create a new pattern detector
94    pub fn new(config: PatternDetectionConfig) -> Self {
95        Self {
96            config,
97            detected_patterns: HashMap::new(),
98            failure_patterns: HashMap::new(),
99            performance_patterns: HashMap::new(),
100            data_quality_patterns: HashMap::new(),
101            last_cleanup: Utc::now(),
102        }
103    }
104
105    /// Create with default configuration
106    pub fn with_default_config() -> Self {
107        Self::new(PatternDetectionConfig::default())
108    }
109
110    /// Record a validation failure for pattern analysis
111    pub fn record_validation_failure(
112        &mut self,
113        shape_iri: &str,
114        constraint_type: &str,
115        failure_reason: &str,
116    ) {
117        if !self.config.enable_failure_patterns {
118            return;
119        }
120
121        let pattern_key = format!("{shape_iri}:{constraint_type}:{failure_reason}");
122        let new_count = {
123            let count = self
124                .failure_patterns
125                .entry(pattern_key.clone())
126                .or_insert(0);
127            *count += 1;
128            *count
129        };
130
131        if new_count >= self.config.min_pattern_threshold {
132            self.create_or_update_pattern(
133                PatternType::ValidationFailure,
134                pattern_key,
135                format!("Recurring validation failure in shape {shape_iri} for constraint {constraint_type} with reason: {failure_reason}"),
136                0.7, // Medium-high severity
137                new_count,
138                vec![
139                    "Review shape definition for potential issues".to_string(),
140                    "Check data quality for this constraint".to_string(),
141                    "Consider relaxing constraint if appropriate".to_string(),
142                ],
143            );
144        }
145    }
146
147    /// Record performance data for pattern analysis
148    pub fn record_performance_data(&mut self, operation: &str, duration: Duration) {
149        if !self.config.enable_performance_patterns {
150            return;
151        }
152
153        let (durations_len, avg_duration) = {
154            let durations = self
155                .performance_patterns
156                .entry(operation.to_string())
157                .or_default();
158            durations.push(duration);
159
160            // Keep only recent measurements
161            durations.retain(|d| d.as_millis() > 0);
162
163            let len = durations.len();
164            let avg_duration = if len > 0 {
165                Duration::from_millis(
166                    durations.iter().map(|d| d.as_millis() as u64).sum::<u64>() / len as u64,
167                )
168            } else {
169                Duration::from_millis(0)
170            };
171            (len, avg_duration)
172        };
173
174        // Detect slow operation patterns
175        if durations_len >= self.config.min_pattern_threshold
176            && avg_duration > Duration::from_millis(1000)
177        {
178            self.create_or_update_pattern(
179                PatternType::PerformanceBottleneck,
180                format!("slow_{operation}"),
181                format!("Performance bottleneck detected in operation: {operation} (avg: {avg_duration:?})"),
182                0.8, // High severity
183                durations_len,
184                vec![
185                    "Optimize the operation implementation".to_string(),
186                    "Consider caching strategies".to_string(),
187                    "Review data structures and algorithms".to_string(),
188                ],
189            );
190        }
191    }
192
193    /// Record data quality issues for pattern analysis
194    pub fn record_data_quality_issue(&mut self, issue_type: &str, resource_iri: &str) {
195        if !self.config.enable_data_quality_patterns {
196            return;
197        }
198
199        let pattern_key = format!("{issue_type}:{resource_iri}");
200        let new_count = {
201            let count = self
202                .data_quality_patterns
203                .entry(pattern_key.clone())
204                .or_insert(0);
205            *count += 1;
206            *count
207        };
208
209        if new_count >= self.config.min_pattern_threshold {
210            self.create_or_update_pattern(
211                PatternType::DataQuality,
212                pattern_key,
213                format!("Data quality issue detected: {issue_type} for resource {resource_iri}"),
214                0.6, // Medium severity
215                new_count,
216                vec![
217                    "Review data source quality".to_string(),
218                    "Implement data validation rules".to_string(),
219                    "Consider data cleansing processes".to_string(),
220                ],
221            );
222        }
223    }
224
225    /// Detect anomalous behavior
226    pub fn detect_anomalies(&mut self) -> Vec<DetectedPattern> {
227        if !self.config.enable_anomaly_detection {
228            return Vec::new();
229        }
230
231        let mut anomalies = Vec::new();
232
233        // Detect sudden spikes in failure patterns
234        for (pattern_key, count) in &self.failure_patterns {
235            if *count > self.config.min_pattern_threshold * 5 {
236                // 5x threshold
237                let anomaly = DetectedPattern {
238                    pattern_type: PatternType::Anomaly,
239                    pattern_id: format!("anomaly_spike_{pattern_key}"),
240                    description: format!("Anomalous spike in failures: {count} occurrences"),
241                    severity: 0.9,
242                    occurrence_count: *count,
243                    first_detected: Utc::now(),
244                    last_detected: Utc::now(),
245                    metadata: HashMap::new(),
246                    suggested_actions: vec![
247                        "Investigate root cause immediately".to_string(),
248                        "Check for data source changes".to_string(),
249                        "Review recent system changes".to_string(),
250                    ],
251                };
252                anomalies.push(anomaly);
253            }
254        }
255
256        anomalies
257    }
258
259    /// Get all detected patterns
260    pub fn get_detected_patterns(&self) -> Vec<&DetectedPattern> {
261        self.detected_patterns.values().collect()
262    }
263
264    /// Get patterns by type
265    pub fn get_patterns_by_type(&self, pattern_type: PatternType) -> Vec<&DetectedPattern> {
266        self.detected_patterns
267            .values()
268            .filter(|p| p.pattern_type == pattern_type)
269            .collect()
270    }
271
272    /// Clear old patterns based on detection window
273    pub fn cleanup_old_patterns(&mut self) {
274        let cutoff = Utc::now()
275            - chrono::Duration::from_std(self.config.detection_window).unwrap_or_default();
276
277        self.detected_patterns
278            .retain(|_, pattern| pattern.last_detected > cutoff);
279
280        // Also clean up raw data
281        if Utc::now()
282            .signed_duration_since(self.last_cleanup)
283            .num_minutes()
284            > 60
285        {
286            self.failure_patterns.clear();
287            self.performance_patterns.clear();
288            self.data_quality_patterns.clear();
289            self.last_cleanup = Utc::now();
290        }
291    }
292
293    /// Generate pattern analysis report
294    pub fn generate_report(&self) -> PatternAnalysisReport {
295        let patterns_by_type: HashMap<PatternType, Vec<&DetectedPattern>> = self
296            .detected_patterns
297            .values()
298            .fold(HashMap::new(), |mut acc, pattern| {
299                acc.entry(pattern.pattern_type.clone())
300                    .or_default()
301                    .push(pattern);
302                acc
303            });
304
305        let high_severity_patterns = self
306            .detected_patterns
307            .values()
308            .filter(|p| p.severity >= 0.8)
309            .cloned()
310            .collect();
311
312        PatternAnalysisReport {
313            total_patterns: self.detected_patterns.len(),
314            patterns_by_type: patterns_by_type
315                .into_iter()
316                .map(|(k, v)| (k, v.len()))
317                .collect(),
318            high_severity_patterns,
319            analysis_timestamp: Utc::now(),
320            summary: self.generate_summary(),
321        }
322    }
323
324    /// Create or update a detected pattern
325    fn create_or_update_pattern(
326        &mut self,
327        pattern_type: PatternType,
328        pattern_id: String,
329        description: String,
330        severity: f64,
331        occurrence_count: usize,
332        suggested_actions: Vec<String>,
333    ) {
334        let now = Utc::now();
335
336        match self.detected_patterns.get_mut(&pattern_id) {
337            Some(existing_pattern) => {
338                existing_pattern.occurrence_count = occurrence_count;
339                existing_pattern.last_detected = now;
340                existing_pattern.severity = severity;
341            }
342            None => {
343                let pattern = DetectedPattern {
344                    pattern_type,
345                    pattern_id: pattern_id.clone(),
346                    description,
347                    severity,
348                    occurrence_count,
349                    first_detected: now,
350                    last_detected: now,
351                    metadata: HashMap::new(),
352                    suggested_actions,
353                };
354                self.detected_patterns.insert(pattern_id, pattern);
355            }
356        }
357    }
358
359    /// Generate summary of detected patterns
360    fn generate_summary(&self) -> String {
361        let total = self.detected_patterns.len();
362        let high_severity = self
363            .detected_patterns
364            .values()
365            .filter(|p| p.severity >= 0.8)
366            .count();
367        let medium_severity = self
368            .detected_patterns
369            .values()
370            .filter(|p| p.severity >= 0.5 && p.severity < 0.8)
371            .count();
372        let low_severity = total - high_severity - medium_severity;
373
374        format!(
375            "Pattern Analysis Summary: {total} total patterns detected ({high_severity} high severity, {medium_severity} medium severity, {low_severity} low severity)"
376        )
377    }
378}
379
380/// Pattern analysis report
381#[derive(Debug, Clone, Serialize, Deserialize)]
382pub struct PatternAnalysisReport {
383    /// Total number of patterns detected
384    pub total_patterns: usize,
385    /// Patterns grouped by type
386    pub patterns_by_type: HashMap<PatternType, usize>,
387    /// High severity patterns requiring immediate attention
388    pub high_severity_patterns: Vec<DetectedPattern>,
389    /// Analysis timestamp
390    pub analysis_timestamp: DateTime<Utc>,
391    /// Summary description
392    pub summary: String,
393}
394
395#[cfg(test)]
396mod tests {
397    use super::*;
398
399    #[test]
400    fn test_pattern_detector_creation() {
401        let detector = PatternDetector::with_default_config();
402        assert_eq!(detector.detected_patterns.len(), 0);
403    }
404
405    #[test]
406    fn test_validation_failure_pattern_detection() {
407        let mut detector = PatternDetector::with_default_config();
408
409        // Record multiple failures for the same pattern
410        for _ in 0..5 {
411            detector.record_validation_failure(
412                "http://example.org/shape1",
413                "minCount",
414                "insufficient_values",
415            );
416        }
417
418        let patterns = detector.get_detected_patterns();
419        assert!(!patterns.is_empty());
420        assert!(patterns
421            .iter()
422            .any(|p| p.pattern_type == PatternType::ValidationFailure));
423    }
424
425    #[test]
426    fn test_performance_pattern_detection() {
427        let mut detector = PatternDetector::with_default_config();
428
429        // Record multiple slow operations
430        for _ in 0..5 {
431            detector.record_performance_data("shape_validation", Duration::from_millis(2000));
432        }
433
434        let patterns = detector.get_detected_patterns();
435        assert!(patterns
436            .iter()
437            .any(|p| p.pattern_type == PatternType::PerformanceBottleneck));
438    }
439
440    #[test]
441    fn test_data_quality_pattern_detection() {
442        let mut detector = PatternDetector::with_default_config();
443
444        // Record multiple data quality issues
445        for _ in 0..5 {
446            detector.record_data_quality_issue(
447                "missing_required_property",
448                "http://example.org/resource1",
449            );
450        }
451
452        let patterns = detector.get_detected_patterns();
453        assert!(patterns
454            .iter()
455            .any(|p| p.pattern_type == PatternType::DataQuality));
456    }
457
458    #[test]
459    fn test_anomaly_detection() {
460        let mut detector = PatternDetector::with_default_config();
461
462        // Create an anomalous spike in failures
463        for _ in 0..20 {
464            // Way above threshold
465            detector.record_validation_failure(
466                "http://example.org/shape1",
467                "minCount",
468                "insufficient_values",
469            );
470        }
471
472        let anomalies = detector.detect_anomalies();
473        assert!(!anomalies.is_empty());
474        assert!(anomalies
475            .iter()
476            .any(|a| a.pattern_type == PatternType::Anomaly));
477    }
478
479    #[test]
480    fn test_pattern_cleanup() {
481        let mut detector = PatternDetector::with_default_config();
482
483        // Add some patterns
484        detector.record_validation_failure("shape1", "constraint1", "reason1");
485        assert!(!detector.failure_patterns.is_empty());
486
487        // Cleanup should remove old data
488        detector.cleanup_old_patterns();
489        // Since we just added the data, it shouldn't be cleaned up yet
490        assert!(!detector.failure_patterns.is_empty());
491    }
492
493    #[test]
494    fn test_report_generation() {
495        let mut detector = PatternDetector::with_default_config();
496
497        // Add various patterns
498        for _ in 0..5 {
499            detector.record_validation_failure("shape1", "constraint1", "reason1");
500            detector.record_performance_data("operation1", Duration::from_millis(2000));
501            detector.record_data_quality_issue("issue1", "resource1");
502        }
503
504        let report = detector.generate_report();
505        assert!(report.total_patterns > 0);
506        assert!(!report.patterns_by_type.is_empty());
507        assert!(!report.summary.is_empty());
508    }
509
510    #[test]
511    fn test_patterns_by_type_filtering() {
512        let mut detector = PatternDetector::with_default_config();
513
514        // Add different types of patterns
515        for _ in 0..5 {
516            detector.record_validation_failure("shape1", "constraint1", "reason1");
517            detector.record_performance_data("operation1", Duration::from_millis(2000));
518        }
519
520        let validation_patterns = detector.get_patterns_by_type(PatternType::ValidationFailure);
521        let performance_patterns =
522            detector.get_patterns_by_type(PatternType::PerformanceBottleneck);
523
524        assert!(!validation_patterns.is_empty());
525        assert!(!performance_patterns.is_empty());
526    }
527}