Skip to main content

ruvector_dag/healing/
index_health.rs

1//! Index Health Monitoring for HNSW and IVFFlat
2
3#[derive(Debug, Clone)]
4pub struct IndexHealth {
5    pub index_name: String,
6    pub index_type: IndexType,
7    pub fragmentation: f64,
8    pub recall_estimate: f64,
9    pub node_count: usize,
10    pub last_rebalanced: Option<std::time::Instant>,
11}
12
13#[derive(Debug, Clone, Copy, PartialEq)]
14pub enum IndexType {
15    Hnsw,
16    IvfFlat,
17    BTree,
18    Other,
19}
20
21pub struct IndexHealthChecker {
22    thresholds: IndexThresholds,
23}
24
25#[derive(Debug, Clone)]
26pub struct IndexThresholds {
27    pub max_fragmentation: f64,
28    pub min_recall: f64,
29    pub rebalance_interval_secs: u64,
30}
31
32impl Default for IndexThresholds {
33    fn default() -> Self {
34        Self {
35            max_fragmentation: 0.3,
36            min_recall: 0.95,
37            rebalance_interval_secs: 3600,
38        }
39    }
40}
41
42impl IndexHealthChecker {
43    pub fn new(thresholds: IndexThresholds) -> Self {
44        Self { thresholds }
45    }
46
47    pub fn check_health(&self, health: &IndexHealth) -> IndexCheckResult {
48        let mut issues = Vec::new();
49        let mut recommendations = Vec::new();
50
51        // Check fragmentation
52        if health.fragmentation > self.thresholds.max_fragmentation {
53            issues.push(format!(
54                "High fragmentation: {:.1}% (threshold: {:.1}%)",
55                health.fragmentation * 100.0,
56                self.thresholds.max_fragmentation * 100.0
57            ));
58            recommendations.push("Run REINDEX or vacuum".to_string());
59        }
60
61        // Check recall
62        if health.recall_estimate < self.thresholds.min_recall {
63            issues.push(format!(
64                "Low recall estimate: {:.1}% (threshold: {:.1}%)",
65                health.recall_estimate * 100.0,
66                self.thresholds.min_recall * 100.0
67            ));
68
69            match health.index_type {
70                IndexType::Hnsw => {
71                    recommendations.push("Increase ef_construction or M parameter".to_string());
72                }
73                IndexType::IvfFlat => {
74                    recommendations.push("Increase nprobe or rebuild with more lists".to_string());
75                }
76                _ => {
77                    recommendations.push("Consider rebuilding index".to_string());
78                }
79            }
80        }
81
82        // Check rebalance interval
83        if let Some(last_rebalanced) = health.last_rebalanced {
84            let elapsed = last_rebalanced.elapsed().as_secs();
85            if elapsed > self.thresholds.rebalance_interval_secs {
86                issues.push(format!(
87                    "Index not rebalanced for {} seconds (threshold: {})",
88                    elapsed, self.thresholds.rebalance_interval_secs
89                ));
90                recommendations.push("Schedule index rebalance".to_string());
91            }
92        }
93
94        let status = if issues.is_empty() {
95            HealthStatus::Healthy
96        } else if issues.len() == 1 {
97            HealthStatus::Warning
98        } else {
99            HealthStatus::Critical
100        };
101
102        IndexCheckResult {
103            status,
104            issues,
105            recommendations,
106            needs_rebalance: health.fragmentation > self.thresholds.max_fragmentation,
107        }
108    }
109}
110
111#[derive(Debug, Clone)]
112pub struct IndexCheckResult {
113    pub status: HealthStatus,
114    pub issues: Vec<String>,
115    pub recommendations: Vec<String>,
116    pub needs_rebalance: bool,
117}
118
119#[derive(Debug, Clone, Copy, PartialEq)]
120pub enum HealthStatus {
121    Healthy,
122    Warning,
123    Critical,
124}
125
126#[cfg(test)]
127mod tests {
128    use super::*;
129
130    #[test]
131    fn test_healthy_index() {
132        let checker = IndexHealthChecker::new(IndexThresholds::default());
133        let health = IndexHealth {
134            index_name: "test_index".to_string(),
135            index_type: IndexType::Hnsw,
136            fragmentation: 0.1,
137            recall_estimate: 0.98,
138            node_count: 1000,
139            last_rebalanced: Some(std::time::Instant::now()),
140        };
141
142        let result = checker.check_health(&health);
143        assert_eq!(result.status, HealthStatus::Healthy);
144        assert!(result.issues.is_empty());
145    }
146
147    #[test]
148    fn test_fragmented_index() {
149        let checker = IndexHealthChecker::new(IndexThresholds::default());
150        let health = IndexHealth {
151            index_name: "test_index".to_string(),
152            index_type: IndexType::Hnsw,
153            fragmentation: 0.5,
154            recall_estimate: 0.98,
155            node_count: 1000,
156            last_rebalanced: Some(std::time::Instant::now()),
157        };
158
159        let result = checker.check_health(&health);
160        assert_eq!(result.status, HealthStatus::Warning);
161        assert!(!result.issues.is_empty());
162        assert!(result.needs_rebalance);
163    }
164
165    #[test]
166    fn test_low_recall_index() {
167        let checker = IndexHealthChecker::new(IndexThresholds::default());
168        let health = IndexHealth {
169            index_name: "test_index".to_string(),
170            index_type: IndexType::IvfFlat,
171            fragmentation: 0.1,
172            recall_estimate: 0.85,
173            node_count: 1000,
174            last_rebalanced: Some(std::time::Instant::now()),
175        };
176
177        let result = checker.check_health(&health);
178        assert_eq!(result.status, HealthStatus::Warning);
179        assert!(!result.recommendations.is_empty());
180    }
181}