ruvector_dag/healing/
index_health.rs1#[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 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 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 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}