1use serde::{Deserialize, Serialize};
28use std::cmp::Reverse;
29use std::collections::HashMap;
30use std::time::{Duration, SystemTime};
31
32#[derive(Debug, Clone)]
34pub struct AnalyzerConfig {
35 pub trend_window: Duration,
37 pub min_frequency: usize,
39 pub max_patterns: usize,
41 pub ngram_size: usize,
43}
44
45impl AnalyzerConfig {
46 pub fn new() -> Self {
48 Self {
49 trend_window: Duration::from_secs(3600), min_frequency: 5,
51 max_patterns: 1000,
52 ngram_size: 3,
53 }
54 }
55
56 pub fn with_trend_window(mut self, window: Duration) -> Self {
58 self.trend_window = window;
59 self
60 }
61
62 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#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)]
77pub struct QueryPattern {
78 pub operation: String,
80 pub fields: Vec<String>,
82}
83
84impl QueryPattern {
85 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 pub fn signature(&self) -> String {
96 format!("{}:{}", self.operation, self.fields.join(","))
97 }
98}
99
100#[derive(Debug, Clone, Serialize, Deserialize)]
102pub struct PatternOccurrence {
103 pub pattern: QueryPattern,
105 pub count: usize,
107 pub last_seen: SystemTime,
109 pub first_seen: SystemTime,
111 pub recent_timestamps: Vec<SystemTime>,
113}
114
115#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
117pub enum TrendDirection {
118 Increasing,
120 Decreasing,
122 Stable,
124}
125
126#[derive(Debug, Clone, Serialize, Deserialize)]
128pub struct PatternTrend {
129 pub pattern: QueryPattern,
131 pub direction: TrendDirection,
133 pub change_rate: f64,
135 pub confidence: f64,
137}
138
139#[derive(Debug, Clone, Serialize, Deserialize)]
141pub struct FieldCombination {
142 pub fields: Vec<String>,
144 pub count: usize,
146}
147
148#[derive(Debug, Clone, Serialize, Deserialize)]
150pub struct TemporalPattern {
151 pub hour: u8,
153 pub day_of_week: u8,
155 pub count: usize,
157}
158
159pub 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 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 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 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 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 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 self.record_field_combinations(&pattern.fields);
217
218 self.record_temporal_pattern(timestamp);
220
221 if self.patterns.len() > self.config.max_patterns {
223 self.trim_patterns();
224 }
225 }
226
227 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 fn record_temporal_pattern(&mut self, timestamp: SystemTime) {
241 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 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 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 fn analyze_pattern_trend(&self, occ: &PatternOccurrence) -> Option<PatternTrend> {
274 if occ.recent_timestamps.len() < 2 {
275 return None;
276 }
277
278 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 let hours = span.as_secs_f64() / 3600.0;
289 let rate = occ.recent_timestamps.len() as f64 / hours;
290
291 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 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 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 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 pub fn find_correlated_patterns(&self, pattern_signature: &str, threshold: f64) -> Vec<String> {
344 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 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 fn calculate_temporal_correlation(
366 &self,
367 pattern1: &PatternOccurrence,
368 pattern2: &PatternOccurrence,
369 ) -> f64 {
370 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 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 pub fn clear(&mut self) {
412 self.patterns.clear();
413 self.field_combinations.clear();
414 self.temporal_patterns.clear();
415 }
416
417 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 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 analyzer.config.max_patterns = 5;
592
593 for i in 0..10 {
595 analyzer.record_query(format!("Query{}", i), vec!["field".to_string()]);
596 }
597
598 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}