1use serde::{Deserialize, Serialize};
7use std::collections::VecDeque;
8use std::sync::atomic::{AtomicU64, Ordering};
9use std::sync::{Arc, RwLock};
10use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
11
12#[derive(Default)]
14pub struct IndexStats {
15 insert_count: AtomicU64,
17 delete_count: AtomicU64,
19 search_count: AtomicU64,
21 search_latencies: Arc<RwLock<LatencyHistogram>>,
23 insert_latencies: Arc<RwLock<LatencyHistogram>>,
25 cache_hits: AtomicU64,
27 cache_misses: AtomicU64,
29 start_time: u64,
31 recent_queries: Arc<RwLock<VecDeque<QueryRecord>>>,
33 max_recent_queries: usize,
35}
36
37impl IndexStats {
38 pub fn new() -> Self {
40 Self {
41 insert_count: AtomicU64::new(0),
42 delete_count: AtomicU64::new(0),
43 search_count: AtomicU64::new(0),
44 search_latencies: Arc::new(RwLock::new(LatencyHistogram::new())),
45 insert_latencies: Arc::new(RwLock::new(LatencyHistogram::new())),
46 cache_hits: AtomicU64::new(0),
47 cache_misses: AtomicU64::new(0),
48 start_time: SystemTime::now()
49 .duration_since(UNIX_EPOCH)
50 .unwrap_or_default()
51 .as_secs(),
52 recent_queries: Arc::new(RwLock::new(VecDeque::new())),
53 max_recent_queries: 1000,
54 }
55 }
56
57 pub fn record_insert(&self, duration: Duration) {
59 self.insert_count.fetch_add(1, Ordering::Relaxed);
60 self.insert_latencies
61 .write()
62 .unwrap()
63 .record(duration.as_micros() as u64);
64 }
65
66 pub fn record_delete(&self) {
68 self.delete_count.fetch_add(1, Ordering::Relaxed);
69 }
70
71 pub fn record_search(&self, duration: Duration, k: usize, result_count: usize) {
73 self.search_count.fetch_add(1, Ordering::Relaxed);
74 self.search_latencies
75 .write()
76 .unwrap()
77 .record(duration.as_micros() as u64);
78
79 let mut queries = self.recent_queries.write().unwrap();
81 if queries.len() >= self.max_recent_queries {
82 queries.pop_front();
83 }
84 queries.push_back(QueryRecord {
85 timestamp: SystemTime::now()
86 .duration_since(UNIX_EPOCH)
87 .unwrap_or_default()
88 .as_secs(),
89 latency_us: duration.as_micros() as u64,
90 k,
91 result_count,
92 });
93 }
94
95 pub fn record_cache_hit(&self) {
97 self.cache_hits.fetch_add(1, Ordering::Relaxed);
98 }
99
100 pub fn record_cache_miss(&self) {
102 self.cache_misses.fetch_add(1, Ordering::Relaxed);
103 }
104
105 pub fn snapshot(&self) -> StatsSnapshot {
107 let search_latencies = self.search_latencies.read().unwrap();
108 let insert_latencies = self.insert_latencies.read().unwrap();
109
110 let cache_hits = self.cache_hits.load(Ordering::Relaxed);
111 let cache_misses = self.cache_misses.load(Ordering::Relaxed);
112 let total_cache = cache_hits + cache_misses;
113
114 StatsSnapshot {
115 insert_count: self.insert_count.load(Ordering::Relaxed),
116 delete_count: self.delete_count.load(Ordering::Relaxed),
117 search_count: self.search_count.load(Ordering::Relaxed),
118 search_latency_p50: search_latencies.percentile(50),
119 search_latency_p90: search_latencies.percentile(90),
120 search_latency_p99: search_latencies.percentile(99),
121 search_latency_avg: search_latencies.average(),
122 insert_latency_avg: insert_latencies.average(),
123 cache_hit_rate: if total_cache > 0 {
124 cache_hits as f64 / total_cache as f64
125 } else {
126 0.0
127 },
128 uptime_seconds: SystemTime::now()
129 .duration_since(UNIX_EPOCH)
130 .unwrap_or_default()
131 .as_secs()
132 - self.start_time,
133 }
134 }
135
136 pub fn reset(&self) {
138 self.insert_count.store(0, Ordering::Relaxed);
139 self.delete_count.store(0, Ordering::Relaxed);
140 self.search_count.store(0, Ordering::Relaxed);
141 self.search_latencies.write().unwrap().reset();
142 self.insert_latencies.write().unwrap().reset();
143 self.cache_hits.store(0, Ordering::Relaxed);
144 self.cache_misses.store(0, Ordering::Relaxed);
145 self.recent_queries.write().unwrap().clear();
146 }
147
148 pub fn recent_queries(&self) -> Vec<QueryRecord> {
150 self.recent_queries
151 .read()
152 .unwrap()
153 .iter()
154 .cloned()
155 .collect()
156 }
157
158 pub fn qps(&self) -> f64 {
160 let uptime = SystemTime::now()
161 .duration_since(UNIX_EPOCH)
162 .unwrap_or_default()
163 .as_secs()
164 - self.start_time;
165
166 if uptime > 0 {
167 self.search_count.load(Ordering::Relaxed) as f64 / uptime as f64
168 } else {
169 0.0
170 }
171 }
172}
173
174#[derive(Debug, Clone, Serialize, Deserialize)]
176pub struct StatsSnapshot {
177 pub insert_count: u64,
179 pub delete_count: u64,
181 pub search_count: u64,
183 pub search_latency_p50: u64,
185 pub search_latency_p90: u64,
187 pub search_latency_p99: u64,
189 pub search_latency_avg: u64,
191 pub insert_latency_avg: u64,
193 pub cache_hit_rate: f64,
195 pub uptime_seconds: u64,
197}
198
199impl StatsSnapshot {
200 pub fn format_latency(us: u64) -> String {
202 if us < 1000 {
203 format!("{}µs", us)
204 } else if us < 1_000_000 {
205 format!("{:.2}ms", us as f64 / 1000.0)
206 } else {
207 format!("{:.2}s", us as f64 / 1_000_000.0)
208 }
209 }
210
211 pub fn summary(&self) -> String {
213 format!(
214 "Searches: {} (P50: {}, P99: {}), Inserts: {}, Cache: {:.1}%",
215 self.search_count,
216 Self::format_latency(self.search_latency_p50),
217 Self::format_latency(self.search_latency_p99),
218 self.insert_count,
219 self.cache_hit_rate * 100.0
220 )
221 }
222}
223
224#[derive(Debug, Clone, Serialize, Deserialize)]
226pub struct QueryRecord {
227 pub timestamp: u64,
229 pub latency_us: u64,
231 pub k: usize,
233 pub result_count: usize,
235}
236
237#[derive(Default)]
239pub struct LatencyHistogram {
240 values: Vec<u64>,
242 sum: u64,
244 count: u64,
246}
247
248impl LatencyHistogram {
249 pub fn new() -> Self {
251 Self::default()
252 }
253
254 pub fn record(&mut self, value_us: u64) {
256 let pos = self.values.binary_search(&value_us).unwrap_or_else(|i| i);
258 self.values.insert(pos, value_us);
259
260 self.sum += value_us;
261 self.count += 1;
262
263 if self.values.len() > 10000 {
265 self.values.drain(0..1000);
267 }
268 }
269
270 pub fn percentile(&self, p: u8) -> u64 {
272 if self.values.is_empty() {
273 return 0;
274 }
275
276 let idx = ((p as usize) * self.values.len() / 100).min(self.values.len() - 1);
277 self.values[idx]
278 }
279
280 pub fn average(&self) -> u64 {
282 if self.count == 0 {
283 return 0;
284 }
285 self.sum / self.count
286 }
287
288 pub fn reset(&mut self) {
290 self.values.clear();
291 self.sum = 0;
292 self.count = 0;
293 }
294
295 pub fn count(&self) -> u64 {
297 self.count
298 }
299}
300
301#[derive(Debug, Clone, Serialize, Deserialize)]
303pub struct IndexHealth {
304 pub size: usize,
306 pub memory_bytes: usize,
308 pub dimension: usize,
310 pub avg_connectivity: Option<f32>,
312 pub recall_estimate: Option<f32>,
314 pub health_score: f32,
316 pub issues: Vec<HealthIssue>,
318}
319
320#[derive(Debug, Clone, Serialize, Deserialize)]
322pub struct HealthIssue {
323 pub severity: u8,
325 pub message: String,
327 pub recommendation: String,
329}
330
331impl IndexHealth {
332 pub fn analyze(size: usize, dimension: usize, stats: Option<&StatsSnapshot>) -> Self {
334 let mut issues = Vec::new();
335 let mut health_score = 1.0;
336
337 let memory_bytes = size * dimension * 4 + size * dimension * 4 * 16;
339
340 if size == 0 {
342 issues.push(HealthIssue {
343 severity: 0,
344 message: "Index is empty".to_string(),
345 recommendation: "Add vectors to enable semantic search".to_string(),
346 });
347 health_score *= 0.9;
348 }
349
350 if let Some(s) = stats {
351 if s.search_latency_p99 > 100_000 {
353 issues.push(HealthIssue {
355 severity: 2,
356 message: format!(
357 "High P99 search latency: {}",
358 StatsSnapshot::format_latency(s.search_latency_p99)
359 ),
360 recommendation: "Consider reducing ef_search or optimizing index parameters"
361 .to_string(),
362 });
363 health_score *= 0.7;
364 } else if s.search_latency_p99 > 10_000 {
365 issues.push(HealthIssue {
367 severity: 1,
368 message: format!(
369 "Elevated P99 search latency: {}",
370 StatsSnapshot::format_latency(s.search_latency_p99)
371 ),
372 recommendation: "Monitor latency trends".to_string(),
373 });
374 health_score *= 0.9;
375 }
376
377 if s.cache_hit_rate < 0.5 && s.search_count > 100 {
379 issues.push(HealthIssue {
380 severity: 1,
381 message: format!("Low cache hit rate: {:.1}%", s.cache_hit_rate * 100.0),
382 recommendation: "Consider increasing cache size".to_string(),
383 });
384 health_score *= 0.95;
385 }
386 }
387
388 if size > 1_000_000 {
390 issues.push(HealthIssue {
391 severity: 1,
392 message: format!("Large index size: {} vectors", size),
393 recommendation:
394 "Consider using DiskANN or quantization for better memory efficiency"
395 .to_string(),
396 });
397 }
398
399 Self {
400 size,
401 memory_bytes,
402 dimension,
403 avg_connectivity: None,
404 recall_estimate: None,
405 health_score,
406 issues,
407 }
408 }
409}
410
411pub struct PerfTimer {
413 start: Instant,
414}
415
416impl PerfTimer {
417 pub fn start() -> Self {
419 Self {
420 start: Instant::now(),
421 }
422 }
423
424 pub fn elapsed(&self) -> Duration {
426 self.start.elapsed()
427 }
428
429 pub fn stop(self) -> Duration {
431 self.start.elapsed()
432 }
433}
434
435#[derive(Debug, Clone, Serialize, Deserialize)]
437pub struct MemoryUsage {
438 pub vectors_bytes: usize,
440 pub index_bytes: usize,
442 pub metadata_bytes: usize,
444 pub cache_bytes: usize,
446 pub total_bytes: usize,
448}
449
450impl MemoryUsage {
451 pub fn estimate(
453 num_vectors: usize,
454 dimension: usize,
455 metadata_count: usize,
456 cache_size: usize,
457 ) -> Self {
458 let vectors_bytes = num_vectors * dimension * 4;
460
461 let index_bytes = 16 * num_vectors * 4 * 2;
464
465 let metadata_bytes = metadata_count * 200;
467
468 let cache_bytes = cache_size * dimension * 4 * 2;
470
471 let total_bytes = vectors_bytes + index_bytes + metadata_bytes + cache_bytes;
472
473 Self {
474 vectors_bytes,
475 index_bytes,
476 metadata_bytes,
477 cache_bytes,
478 total_bytes,
479 }
480 }
481
482 pub fn format_bytes(bytes: usize) -> String {
484 if bytes < 1024 {
485 format!("{} B", bytes)
486 } else if bytes < 1024 * 1024 {
487 format!("{:.2} KB", bytes as f64 / 1024.0)
488 } else if bytes < 1024 * 1024 * 1024 {
489 format!("{:.2} MB", bytes as f64 / (1024.0 * 1024.0))
490 } else {
491 format!("{:.2} GB", bytes as f64 / (1024.0 * 1024.0 * 1024.0))
492 }
493 }
494
495 pub fn summary(&self) -> String {
497 format!(
498 "Total: {} (Vectors: {}, Index: {}, Metadata: {}, Cache: {})",
499 Self::format_bytes(self.total_bytes),
500 Self::format_bytes(self.vectors_bytes),
501 Self::format_bytes(self.index_bytes),
502 Self::format_bytes(self.metadata_bytes),
503 Self::format_bytes(self.cache_bytes),
504 )
505 }
506}
507
508#[cfg(test)]
509mod tests {
510 use super::*;
511
512 #[test]
513 fn test_stats_recording() {
514 let stats = IndexStats::new();
515
516 stats.record_insert(Duration::from_micros(100));
518 stats.record_insert(Duration::from_micros(200));
519 stats.record_search(Duration::from_micros(50), 10, 10);
520 stats.record_search(Duration::from_micros(150), 10, 8);
521 stats.record_cache_hit();
522 stats.record_cache_miss();
523
524 let snapshot = stats.snapshot();
525
526 assert_eq!(snapshot.insert_count, 2);
527 assert_eq!(snapshot.search_count, 2);
528 assert!(snapshot.cache_hit_rate > 0.4 && snapshot.cache_hit_rate < 0.6);
529 }
530
531 #[test]
532 fn test_latency_histogram() {
533 let mut histogram = LatencyHistogram::new();
534
535 for i in 1..=100 {
536 histogram.record(i);
537 }
538
539 assert_eq!(histogram.count(), 100);
540 let p50 = histogram.percentile(50);
542 assert!((50..=52).contains(&p50), "P50 was {}", p50);
543 assert!(histogram.percentile(99) >= 99);
544 assert!(histogram.average() >= 50 && histogram.average() <= 51);
546 }
547
548 #[test]
549 fn test_index_health() {
550 let health = IndexHealth::analyze(1000, 768, None);
551
552 assert!(health.health_score > 0.0);
553 assert_eq!(health.size, 1000);
554 assert_eq!(health.dimension, 768);
555 }
556
557 #[test]
558 fn test_memory_usage() {
559 let usage = MemoryUsage::estimate(10000, 768, 10000, 1000);
560
561 assert!(usage.total_bytes > 1024 * 1024);
563 assert!(usage.vectors_bytes > 0);
564 }
565
566 #[test]
567 fn test_perf_timer() {
568 let timer = PerfTimer::start();
569 std::thread::sleep(Duration::from_millis(10));
570 let elapsed = timer.stop();
571
572 assert!(elapsed >= Duration::from_millis(10));
573 }
574}