Skip to main content

oxirs_gql/
query_pattern_analyzer.rs

1//! Real-time Query Pattern Analysis
2//!
3//! This module provides real-time analysis of GraphQL query patterns,
4//! identifying trends, common patterns, and optimization opportunities.
5//!
6//! # Features
7//!
8//! - **Pattern Detection**: Identify frequently executed query patterns
9//! - **Trend Analysis**: Detect increasing/decreasing query patterns
10//! - **N-gram Analysis**: Find common query field combinations
11//! - **Temporal Patterns**: Time-of-day and day-of-week patterns
12//! - **Correlation Analysis**: Find correlated queries
13//! - **Real-time Alerts**: Trigger alerts on pattern changes
14//!
15//! # Example
16//!
17//! ```rust,ignore
18//! use oxirs_gql::query_pattern_analyzer::{PatternAnalyzer, AnalyzerConfig};
19//!
20//! let mut analyzer = PatternAnalyzer::new(AnalyzerConfig::default());
21//! analyzer.record_query("GetUser", vec!["id", "name", "email"]);
22//!
23//! let patterns = analyzer.get_top_patterns(10);
24//! let trends = analyzer.analyze_trends();
25//! ```
26
27use serde::{Deserialize, Serialize};
28use std::cmp::Reverse;
29use std::collections::HashMap;
30use std::time::{Duration, SystemTime};
31
32/// Pattern analyzer configuration
33#[derive(Debug, Clone)]
34pub struct AnalyzerConfig {
35    /// Window size for trend analysis
36    pub trend_window: Duration,
37    /// Minimum pattern frequency to track
38    pub min_frequency: usize,
39    /// Maximum patterns to retain
40    pub max_patterns: usize,
41    /// N-gram size for field combinations
42    pub ngram_size: usize,
43}
44
45impl AnalyzerConfig {
46    /// Create new analyzer configuration
47    pub fn new() -> Self {
48        Self {
49            trend_window: Duration::from_secs(3600), // 1 hour
50            min_frequency: 5,
51            max_patterns: 1000,
52            ngram_size: 3,
53        }
54    }
55
56    /// Set trend window
57    pub fn with_trend_window(mut self, window: Duration) -> Self {
58        self.trend_window = window;
59        self
60    }
61
62    /// Set minimum frequency
63    pub fn with_min_frequency(mut self, min: usize) -> Self {
64        self.min_frequency = min;
65        self
66    }
67}
68
69impl Default for AnalyzerConfig {
70    fn default() -> Self {
71        Self::new()
72    }
73}
74
75/// Query pattern
76#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)]
77pub struct QueryPattern {
78    /// Operation name
79    pub operation: String,
80    /// Sorted field list
81    pub fields: Vec<String>,
82}
83
84impl QueryPattern {
85    /// Create a new query pattern
86    pub fn new(operation: impl Into<String>, mut fields: Vec<String>) -> Self {
87        fields.sort();
88        Self {
89            operation: operation.into(),
90            fields,
91        }
92    }
93
94    /// Get pattern signature
95    pub fn signature(&self) -> String {
96        format!("{}:{}", self.operation, self.fields.join(","))
97    }
98}
99
100/// Pattern occurrence
101#[derive(Debug, Clone, Serialize, Deserialize)]
102pub struct PatternOccurrence {
103    /// Pattern
104    pub pattern: QueryPattern,
105    /// Occurrence count
106    pub count: usize,
107    /// Last seen timestamp
108    pub last_seen: SystemTime,
109    /// First seen timestamp
110    pub first_seen: SystemTime,
111    /// Recent timestamps (for trend analysis)
112    pub recent_timestamps: Vec<SystemTime>,
113}
114
115/// Trend direction
116#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
117pub enum TrendDirection {
118    /// Increasing trend
119    Increasing,
120    /// Decreasing trend
121    Decreasing,
122    /// Stable trend
123    Stable,
124}
125
126/// Pattern trend
127#[derive(Debug, Clone, Serialize, Deserialize)]
128pub struct PatternTrend {
129    /// Pattern
130    pub pattern: QueryPattern,
131    /// Trend direction
132    pub direction: TrendDirection,
133    /// Change rate (queries per hour)
134    pub change_rate: f64,
135    /// Confidence (0.0 to 1.0)
136    pub confidence: f64,
137}
138
139/// Field combination (N-gram)
140#[derive(Debug, Clone, Serialize, Deserialize)]
141pub struct FieldCombination {
142    /// Fields in combination
143    pub fields: Vec<String>,
144    /// Occurrence count
145    pub count: usize,
146}
147
148/// Temporal pattern
149#[derive(Debug, Clone, Serialize, Deserialize)]
150pub struct TemporalPattern {
151    /// Hour of day (0-23)
152    pub hour: u8,
153    /// Day of week (0=Sunday, 6=Saturday)
154    pub day_of_week: u8,
155    /// Query count
156    pub count: usize,
157}
158
159/// Pattern analyzer
160pub struct PatternAnalyzer {
161    config: AnalyzerConfig,
162    patterns: HashMap<String, PatternOccurrence>,
163    field_combinations: HashMap<Vec<String>, usize>,
164    temporal_patterns: HashMap<(u8, u8), usize>,
165}
166
167impl PatternAnalyzer {
168    /// Create new pattern analyzer
169    pub fn new(config: AnalyzerConfig) -> Self {
170        Self {
171            config,
172            patterns: HashMap::new(),
173            field_combinations: HashMap::new(),
174            temporal_patterns: HashMap::new(),
175        }
176    }
177
178    /// Record a query pattern
179    pub fn record_query(&mut self, operation: impl Into<String>, fields: Vec<String>) {
180        self.record_query_at(operation, fields, SystemTime::now());
181    }
182
183    /// Record query at specific time
184    pub fn record_query_at(
185        &mut self,
186        operation: impl Into<String>,
187        fields: Vec<String>,
188        timestamp: SystemTime,
189    ) {
190        let pattern = QueryPattern::new(operation, fields.clone());
191        let signature = pattern.signature();
192
193        // Update pattern occurrence
194        self.patterns
195            .entry(signature.clone())
196            .and_modify(|occ| {
197                occ.count += 1;
198                occ.last_seen = timestamp;
199                occ.recent_timestamps.push(timestamp);
200
201                // Keep only recent timestamps for trend analysis
202                let cutoff = timestamp
203                    .checked_sub(self.config.trend_window)
204                    .unwrap_or(timestamp);
205                occ.recent_timestamps.retain(|&ts| ts >= cutoff);
206            })
207            .or_insert_with(|| PatternOccurrence {
208                pattern: pattern.clone(),
209                count: 1,
210                last_seen: timestamp,
211                first_seen: timestamp,
212                recent_timestamps: vec![timestamp],
213            });
214
215        // Record field combinations (N-grams)
216        self.record_field_combinations(&pattern.fields);
217
218        // Record temporal pattern
219        self.record_temporal_pattern(timestamp);
220
221        // Trim patterns if exceeds max
222        if self.patterns.len() > self.config.max_patterns {
223            self.trim_patterns();
224        }
225    }
226
227    /// Record field combinations
228    fn record_field_combinations(&mut self, fields: &[String]) {
229        if fields.len() < self.config.ngram_size {
230            return;
231        }
232
233        for window in fields.windows(self.config.ngram_size) {
234            let combo = window.to_vec();
235            *self.field_combinations.entry(combo).or_insert(0) += 1;
236        }
237    }
238
239    /// Record temporal pattern
240    fn record_temporal_pattern(&mut self, timestamp: SystemTime) {
241        // This is simplified - in production, use chrono for proper time handling
242        let elapsed = timestamp
243            .duration_since(SystemTime::UNIX_EPOCH)
244            .expect("SystemTime should be after UNIX_EPOCH");
245        let hours = (elapsed.as_secs() / 3600) % 24;
246        let days = (elapsed.as_secs() / 86400) % 7;
247
248        let hour = hours as u8;
249        let day_of_week = days as u8;
250
251        *self
252            .temporal_patterns
253            .entry((hour, day_of_week))
254            .or_insert(0) += 1;
255    }
256
257    /// Get top N patterns by frequency
258    pub fn get_top_patterns(&self, n: usize) -> Vec<PatternOccurrence> {
259        let mut patterns: Vec<_> = self.patterns.values().cloned().collect();
260        patterns.sort_by_key(|p| Reverse(p.count));
261        patterns.into_iter().take(n).collect()
262    }
263
264    /// Analyze trends for all patterns
265    pub fn analyze_trends(&self) -> Vec<PatternTrend> {
266        self.patterns
267            .values()
268            .filter_map(|occ| self.analyze_pattern_trend(occ))
269            .collect()
270    }
271
272    /// Analyze trend for a specific pattern
273    fn analyze_pattern_trend(&self, occ: &PatternOccurrence) -> Option<PatternTrend> {
274        if occ.recent_timestamps.len() < 2 {
275            return None;
276        }
277
278        // Calculate time span
279        let first = occ.recent_timestamps.first()?;
280        let last = occ.recent_timestamps.last()?;
281        let span = last.duration_since(*first).ok()?;
282
283        if span.as_secs() == 0 {
284            return None;
285        }
286
287        // Calculate rate (queries per hour)
288        let hours = span.as_secs_f64() / 3600.0;
289        let rate = occ.recent_timestamps.len() as f64 / hours;
290
291        // Determine trend direction
292        let mid_point = occ.recent_timestamps.len() / 2;
293        let first_half_count = mid_point;
294        let second_half_count = occ.recent_timestamps.len() - mid_point;
295
296        let direction = if second_half_count > first_half_count * 12 / 10 {
297            TrendDirection::Increasing
298        } else if second_half_count < first_half_count * 8 / 10 {
299            TrendDirection::Decreasing
300        } else {
301            TrendDirection::Stable
302        };
303
304        // Calculate confidence based on sample size
305        let confidence = (occ.recent_timestamps.len() as f64 / 100.0).min(1.0);
306
307        Some(PatternTrend {
308            pattern: occ.pattern.clone(),
309            direction,
310            change_rate: rate,
311            confidence,
312        })
313    }
314
315    /// Get top field combinations
316    pub fn get_top_field_combinations(&self, n: usize) -> Vec<FieldCombination> {
317        let mut combos: Vec<_> = self
318            .field_combinations
319            .iter()
320            .map(|(fields, &count)| FieldCombination {
321                fields: fields.clone(),
322                count,
323            })
324            .collect();
325
326        combos.sort_by_key(|c| Reverse(c.count));
327        combos.into_iter().take(n).collect()
328    }
329
330    /// Get temporal patterns
331    pub fn get_temporal_patterns(&self) -> Vec<TemporalPattern> {
332        self.temporal_patterns
333            .iter()
334            .map(|((hour, day_of_week), &count)| TemporalPattern {
335                hour: *hour,
336                day_of_week: *day_of_week,
337                count,
338            })
339            .collect()
340    }
341
342    /// Find correlated patterns
343    pub fn find_correlated_patterns(&self, pattern_signature: &str, threshold: f64) -> Vec<String> {
344        // Simplified correlation - in production use proper statistical correlation
345        let mut correlations = Vec::new();
346
347        if let Some(base_pattern) = self.patterns.get(pattern_signature) {
348            for (sig, occ) in &self.patterns {
349                if sig == pattern_signature {
350                    continue;
351                }
352
353                // Check if patterns occur in similar time windows
354                let correlation = self.calculate_temporal_correlation(base_pattern, occ);
355                if correlation >= threshold {
356                    correlations.push(sig.clone());
357                }
358            }
359        }
360
361        correlations
362    }
363
364    /// Calculate temporal correlation between two patterns
365    fn calculate_temporal_correlation(
366        &self,
367        pattern1: &PatternOccurrence,
368        pattern2: &PatternOccurrence,
369    ) -> f64 {
370        // Simplified: count how many timestamps are within 1 minute
371        let mut matches = 0;
372        let window = Duration::from_secs(60);
373
374        for ts1 in &pattern1.recent_timestamps {
375            for ts2 in &pattern2.recent_timestamps {
376                if let Ok(diff) = ts1.duration_since(*ts2) {
377                    if diff < window {
378                        matches += 1;
379                        break;
380                    }
381                }
382                if let Ok(diff) = ts2.duration_since(*ts1) {
383                    if diff < window {
384                        matches += 1;
385                        break;
386                    }
387                }
388            }
389        }
390
391        matches as f64 / pattern1.recent_timestamps.len().max(1) as f64
392    }
393
394    /// Trim least frequent patterns
395    fn trim_patterns(&mut self) {
396        let mut patterns: Vec<_> = self.patterns.iter().collect();
397        patterns.sort_by_key(|p| Reverse(p.1.count));
398
399        let to_remove: Vec<_> = patterns
400            .into_iter()
401            .skip(self.config.max_patterns)
402            .map(|(sig, _)| sig.clone())
403            .collect();
404
405        for sig in to_remove {
406            self.patterns.remove(&sig);
407        }
408    }
409
410    /// Clear all data
411    pub fn clear(&mut self) {
412        self.patterns.clear();
413        self.field_combinations.clear();
414        self.temporal_patterns.clear();
415    }
416
417    /// Get pattern count
418    pub fn pattern_count(&self) -> usize {
419        self.patterns.len()
420    }
421}
422
423#[cfg(test)]
424mod tests {
425    use super::*;
426
427    #[test]
428    fn test_analyzer_config_creation() {
429        let config = AnalyzerConfig::new()
430            .with_trend_window(Duration::from_secs(1800))
431            .with_min_frequency(10);
432
433        assert_eq!(config.trend_window.as_secs(), 1800);
434        assert_eq!(config.min_frequency, 10);
435    }
436
437    #[test]
438    fn test_query_pattern_creation() {
439        let pattern = QueryPattern::new("GetUser", vec!["id".to_string(), "name".to_string()]);
440
441        assert_eq!(pattern.operation, "GetUser");
442        assert_eq!(pattern.fields.len(), 2);
443    }
444
445    #[test]
446    fn test_query_pattern_signature() {
447        let pattern = QueryPattern::new("GetUser", vec!["name".to_string(), "id".to_string()]);
448
449        let sig = pattern.signature();
450        assert!(sig.contains("GetUser"));
451        assert!(sig.contains("id"));
452        assert!(sig.contains("name"));
453    }
454
455    #[test]
456    fn test_pattern_analyzer_creation() {
457        let analyzer = PatternAnalyzer::new(AnalyzerConfig::default());
458
459        assert_eq!(analyzer.pattern_count(), 0);
460    }
461
462    #[test]
463    fn test_record_query() {
464        let mut analyzer = PatternAnalyzer::new(AnalyzerConfig::default());
465
466        analyzer.record_query("GetUser", vec!["id".to_string(), "name".to_string()]);
467
468        assert_eq!(analyzer.pattern_count(), 1);
469    }
470
471    #[test]
472    fn test_record_multiple_queries() {
473        let mut analyzer = PatternAnalyzer::new(AnalyzerConfig::default());
474
475        analyzer.record_query("GetUser", vec!["id".to_string()]);
476        analyzer.record_query("GetUser", vec!["id".to_string()]);
477        analyzer.record_query("GetPosts", vec!["title".to_string()]);
478
479        assert_eq!(analyzer.pattern_count(), 2);
480    }
481
482    #[test]
483    fn test_get_top_patterns() {
484        let mut analyzer = PatternAnalyzer::new(AnalyzerConfig::default());
485
486        for _ in 0..10 {
487            analyzer.record_query("GetUser", vec!["id".to_string()]);
488        }
489        for _ in 0..5 {
490            analyzer.record_query("GetPosts", vec!["title".to_string()]);
491        }
492
493        let top = analyzer.get_top_patterns(2);
494
495        assert_eq!(top.len(), 2);
496        assert_eq!(top[0].count, 10);
497        assert_eq!(top[1].count, 5);
498    }
499
500    #[test]
501    fn test_field_combinations() {
502        let mut analyzer = PatternAnalyzer::new(AnalyzerConfig::default().with_min_frequency(1));
503
504        analyzer.record_query(
505            "GetUser",
506            vec![
507                "id".to_string(),
508                "name".to_string(),
509                "email".to_string(),
510                "age".to_string(),
511            ],
512        );
513
514        let combos = analyzer.get_top_field_combinations(5);
515
516        assert!(!combos.is_empty());
517    }
518
519    #[test]
520    fn test_temporal_patterns() {
521        let mut analyzer = PatternAnalyzer::new(AnalyzerConfig::default());
522
523        analyzer.record_query("GetUser", vec!["id".to_string()]);
524
525        let temporal = analyzer.get_temporal_patterns();
526
527        assert_eq!(temporal.len(), 1);
528    }
529
530    #[test]
531    fn test_trend_direction() {
532        let directions = [
533            TrendDirection::Increasing,
534            TrendDirection::Decreasing,
535            TrendDirection::Stable,
536        ];
537
538        assert_eq!(directions.len(), 3);
539    }
540
541    #[test]
542    fn test_analyze_trends() {
543        let mut analyzer = PatternAnalyzer::new(AnalyzerConfig::default());
544
545        let now = SystemTime::now();
546        for i in 0..20 {
547            let ts = now + Duration::from_secs(i * 60);
548            analyzer.record_query_at("GetUser", vec!["id".to_string()], ts);
549        }
550
551        let trends = analyzer.analyze_trends();
552
553        assert!(!trends.is_empty());
554    }
555
556    #[test]
557    fn test_analyze_trends_insufficient_data() {
558        let mut analyzer = PatternAnalyzer::new(AnalyzerConfig::default());
559
560        analyzer.record_query("GetUser", vec!["id".to_string()]);
561
562        let trends = analyzer.analyze_trends();
563
564        // Should have no trends with only 1 data point
565        assert!(trends.is_empty());
566    }
567
568    #[test]
569    fn test_find_correlated_patterns() {
570        let mut analyzer = PatternAnalyzer::new(AnalyzerConfig::default());
571
572        let now = SystemTime::now();
573        for i in 0..10 {
574            let ts = now + Duration::from_secs(i * 30);
575            analyzer.record_query_at("GetUser", vec!["id".to_string()], ts);
576            analyzer.record_query_at("GetPosts", vec!["title".to_string()], ts);
577        }
578
579        let pattern_sig = QueryPattern::new("GetUser", vec!["id".to_string()]).signature();
580        let correlated = analyzer.find_correlated_patterns(&pattern_sig, 0.3);
581
582        assert!(!correlated.is_empty());
583    }
584
585    #[test]
586    fn test_pattern_trimming() {
587        let config = AnalyzerConfig::default().with_min_frequency(1);
588        let mut analyzer = PatternAnalyzer::new(config);
589
590        // Set max to 5
591        analyzer.config.max_patterns = 5;
592
593        // Add 10 patterns
594        for i in 0..10 {
595            analyzer.record_query(format!("Query{}", i), vec!["field".to_string()]);
596        }
597
598        // Should trim to 5
599        assert_eq!(analyzer.pattern_count(), 5);
600    }
601
602    #[test]
603    fn test_clear() {
604        let mut analyzer = PatternAnalyzer::new(AnalyzerConfig::default());
605
606        analyzer.record_query("GetUser", vec!["id".to_string()]);
607        assert_eq!(analyzer.pattern_count(), 1);
608
609        analyzer.clear();
610        assert_eq!(analyzer.pattern_count(), 0);
611    }
612
613    #[test]
614    fn test_pattern_occurrence_tracking() {
615        let mut analyzer = PatternAnalyzer::new(AnalyzerConfig::default());
616
617        let now = SystemTime::now();
618        analyzer.record_query_at("GetUser", vec!["id".to_string()], now);
619        analyzer.record_query_at(
620            "GetUser",
621            vec!["id".to_string()],
622            now + Duration::from_secs(60),
623        );
624
625        let top = analyzer.get_top_patterns(1);
626        assert_eq!(top[0].count, 2);
627        assert_eq!(top[0].recent_timestamps.len(), 2);
628    }
629
630    #[test]
631    fn test_field_combination_count() {
632        let mut analyzer = PatternAnalyzer::new(AnalyzerConfig::default());
633
634        analyzer.record_query(
635            "GetUser",
636            vec!["a".to_string(), "b".to_string(), "c".to_string()],
637        );
638        analyzer.record_query(
639            "GetUser",
640            vec!["a".to_string(), "b".to_string(), "c".to_string()],
641        );
642
643        let combos = analyzer.get_top_field_combinations(1);
644        assert_eq!(combos[0].count, 2);
645    }
646
647    #[test]
648    fn test_temporal_correlation() {
649        let occ1 = PatternOccurrence {
650            pattern: QueryPattern::new("Query1", vec!["field".to_string()]),
651            count: 2,
652            last_seen: SystemTime::now(),
653            first_seen: SystemTime::now(),
654            recent_timestamps: vec![
655                SystemTime::now(),
656                SystemTime::now() + Duration::from_secs(30),
657            ],
658        };
659
660        let occ2 = PatternOccurrence {
661            pattern: QueryPattern::new("Query2", vec!["field".to_string()]),
662            count: 2,
663            last_seen: SystemTime::now(),
664            first_seen: SystemTime::now(),
665            recent_timestamps: vec![
666                SystemTime::now(),
667                SystemTime::now() + Duration::from_secs(30),
668            ],
669        };
670
671        let analyzer = PatternAnalyzer::new(AnalyzerConfig::default());
672        let correlation = analyzer.calculate_temporal_correlation(&occ1, &occ2);
673
674        assert!((0.0..=1.0).contains(&correlation));
675    }
676
677    #[test]
678    fn test_pattern_trend_confidence() {
679        let mut analyzer = PatternAnalyzer::new(AnalyzerConfig::default());
680
681        let now = SystemTime::now();
682        for i in 0..100 {
683            let ts = now + Duration::from_secs(i * 60);
684            analyzer.record_query_at("GetUser", vec!["id".to_string()], ts);
685        }
686
687        let trends = analyzer.analyze_trends();
688        assert!(!trends.is_empty());
689        assert!(trends[0].confidence > 0.0);
690    }
691}