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_or_else(|e| e.into_inner())
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_or_else(|e| e.into_inner())
77 .record(duration.as_micros() as u64);
78
79 let mut queries = self
81 .recent_queries
82 .write()
83 .unwrap_or_else(|e| e.into_inner());
84 if queries.len() >= self.max_recent_queries {
85 queries.pop_front();
86 }
87 queries.push_back(QueryRecord {
88 timestamp: SystemTime::now()
89 .duration_since(UNIX_EPOCH)
90 .unwrap_or_default()
91 .as_secs(),
92 latency_us: duration.as_micros() as u64,
93 k,
94 result_count,
95 });
96 }
97
98 pub fn record_cache_hit(&self) {
100 self.cache_hits.fetch_add(1, Ordering::Relaxed);
101 }
102
103 pub fn record_cache_miss(&self) {
105 self.cache_misses.fetch_add(1, Ordering::Relaxed);
106 }
107
108 pub fn snapshot(&self) -> StatsSnapshot {
110 let search_latencies = self
111 .search_latencies
112 .read()
113 .unwrap_or_else(|e| e.into_inner());
114 let insert_latencies = self
115 .insert_latencies
116 .read()
117 .unwrap_or_else(|e| e.into_inner());
118
119 let cache_hits = self.cache_hits.load(Ordering::Relaxed);
120 let cache_misses = self.cache_misses.load(Ordering::Relaxed);
121 let total_cache = cache_hits + cache_misses;
122
123 StatsSnapshot {
124 insert_count: self.insert_count.load(Ordering::Relaxed),
125 delete_count: self.delete_count.load(Ordering::Relaxed),
126 search_count: self.search_count.load(Ordering::Relaxed),
127 search_latency_p50: search_latencies.percentile(50),
128 search_latency_p90: search_latencies.percentile(90),
129 search_latency_p99: search_latencies.percentile(99),
130 search_latency_avg: search_latencies.average(),
131 insert_latency_avg: insert_latencies.average(),
132 cache_hit_rate: if total_cache > 0 {
133 cache_hits as f64 / total_cache as f64
134 } else {
135 0.0
136 },
137 uptime_seconds: SystemTime::now()
138 .duration_since(UNIX_EPOCH)
139 .unwrap_or_default()
140 .as_secs()
141 - self.start_time,
142 }
143 }
144
145 pub fn reset(&self) {
147 self.insert_count.store(0, Ordering::Relaxed);
148 self.delete_count.store(0, Ordering::Relaxed);
149 self.search_count.store(0, Ordering::Relaxed);
150 self.search_latencies
151 .write()
152 .unwrap_or_else(|e| e.into_inner())
153 .reset();
154 self.insert_latencies
155 .write()
156 .unwrap_or_else(|e| e.into_inner())
157 .reset();
158 self.cache_hits.store(0, Ordering::Relaxed);
159 self.cache_misses.store(0, Ordering::Relaxed);
160 self.recent_queries
161 .write()
162 .unwrap_or_else(|e| e.into_inner())
163 .clear();
164 }
165
166 pub fn recent_queries(&self) -> Vec<QueryRecord> {
168 self.recent_queries
169 .read()
170 .unwrap_or_else(|e| e.into_inner())
171 .iter()
172 .cloned()
173 .collect()
174 }
175
176 pub fn qps(&self) -> f64 {
178 let uptime = SystemTime::now()
179 .duration_since(UNIX_EPOCH)
180 .unwrap_or_default()
181 .as_secs()
182 - self.start_time;
183
184 if uptime > 0 {
185 self.search_count.load(Ordering::Relaxed) as f64 / uptime as f64
186 } else {
187 0.0
188 }
189 }
190}
191
192#[derive(Debug, Clone, Serialize, Deserialize)]
194pub struct StatsSnapshot {
195 pub insert_count: u64,
197 pub delete_count: u64,
199 pub search_count: u64,
201 pub search_latency_p50: u64,
203 pub search_latency_p90: u64,
205 pub search_latency_p99: u64,
207 pub search_latency_avg: u64,
209 pub insert_latency_avg: u64,
211 pub cache_hit_rate: f64,
213 pub uptime_seconds: u64,
215}
216
217impl StatsSnapshot {
218 pub fn format_latency(us: u64) -> String {
220 if us < 1000 {
221 format!("{}µs", us)
222 } else if us < 1_000_000 {
223 format!("{:.2}ms", us as f64 / 1000.0)
224 } else {
225 format!("{:.2}s", us as f64 / 1_000_000.0)
226 }
227 }
228
229 pub fn summary(&self) -> String {
231 format!(
232 "Searches: {} (P50: {}, P99: {}), Inserts: {}, Cache: {:.1}%",
233 self.search_count,
234 Self::format_latency(self.search_latency_p50),
235 Self::format_latency(self.search_latency_p99),
236 self.insert_count,
237 self.cache_hit_rate * 100.0
238 )
239 }
240}
241
242#[derive(Debug, Clone, Serialize, Deserialize)]
244pub struct QueryRecord {
245 pub timestamp: u64,
247 pub latency_us: u64,
249 pub k: usize,
251 pub result_count: usize,
253}
254
255#[derive(Default)]
257pub struct LatencyHistogram {
258 values: Vec<u64>,
260 sum: u64,
262 count: u64,
264}
265
266impl LatencyHistogram {
267 pub fn new() -> Self {
269 Self::default()
270 }
271
272 pub fn record(&mut self, value_us: u64) {
274 let pos = self.values.binary_search(&value_us).unwrap_or_else(|i| i);
276 self.values.insert(pos, value_us);
277
278 self.sum += value_us;
279 self.count += 1;
280
281 if self.values.len() > 10000 {
283 self.values.drain(0..1000);
285 }
286 }
287
288 pub fn percentile(&self, p: u8) -> u64 {
290 if self.values.is_empty() {
291 return 0;
292 }
293
294 let idx = ((p as usize) * self.values.len() / 100).min(self.values.len() - 1);
295 self.values[idx]
296 }
297
298 pub fn average(&self) -> u64 {
300 if self.count == 0 {
301 return 0;
302 }
303 self.sum / self.count
304 }
305
306 pub fn reset(&mut self) {
308 self.values.clear();
309 self.sum = 0;
310 self.count = 0;
311 }
312
313 pub fn count(&self) -> u64 {
315 self.count
316 }
317}
318
319#[derive(Debug, Clone, Serialize, Deserialize)]
321pub struct IndexHealth {
322 pub size: usize,
324 pub memory_bytes: usize,
326 pub dimension: usize,
328 pub avg_connectivity: Option<f32>,
330 pub recall_estimate: Option<f32>,
332 pub health_score: f32,
334 pub issues: Vec<HealthIssue>,
336}
337
338#[derive(Debug, Clone, Serialize, Deserialize)]
340pub struct HealthIssue {
341 pub severity: u8,
343 pub message: String,
345 pub recommendation: String,
347}
348
349impl IndexHealth {
350 pub fn analyze(size: usize, dimension: usize, stats: Option<&StatsSnapshot>) -> Self {
352 let mut issues = Vec::new();
353 let mut health_score = 1.0;
354
355 let memory_bytes = size * dimension * 4 + size * dimension * 4 * 16;
357
358 if size == 0 {
360 issues.push(HealthIssue {
361 severity: 0,
362 message: "Index is empty".to_string(),
363 recommendation: "Add vectors to enable semantic search".to_string(),
364 });
365 health_score *= 0.9;
366 }
367
368 if let Some(s) = stats {
369 if s.search_latency_p99 > 100_000 {
371 issues.push(HealthIssue {
373 severity: 2,
374 message: format!(
375 "High P99 search latency: {}",
376 StatsSnapshot::format_latency(s.search_latency_p99)
377 ),
378 recommendation: "Consider reducing ef_search or optimizing index parameters"
379 .to_string(),
380 });
381 health_score *= 0.7;
382 } else if s.search_latency_p99 > 10_000 {
383 issues.push(HealthIssue {
385 severity: 1,
386 message: format!(
387 "Elevated P99 search latency: {}",
388 StatsSnapshot::format_latency(s.search_latency_p99)
389 ),
390 recommendation: "Monitor latency trends".to_string(),
391 });
392 health_score *= 0.9;
393 }
394
395 if s.cache_hit_rate < 0.5 && s.search_count > 100 {
397 issues.push(HealthIssue {
398 severity: 1,
399 message: format!("Low cache hit rate: {:.1}%", s.cache_hit_rate * 100.0),
400 recommendation: "Consider increasing cache size".to_string(),
401 });
402 health_score *= 0.95;
403 }
404 }
405
406 if size > 1_000_000 {
408 issues.push(HealthIssue {
409 severity: 1,
410 message: format!("Large index size: {} vectors", size),
411 recommendation:
412 "Consider using DiskANN or quantization for better memory efficiency"
413 .to_string(),
414 });
415 }
416
417 Self {
418 size,
419 memory_bytes,
420 dimension,
421 avg_connectivity: None,
422 recall_estimate: None,
423 health_score,
424 issues,
425 }
426 }
427}
428
429pub struct PerfTimer {
431 start: Instant,
432}
433
434impl PerfTimer {
435 pub fn start() -> Self {
437 Self {
438 start: Instant::now(),
439 }
440 }
441
442 pub fn elapsed(&self) -> Duration {
444 self.start.elapsed()
445 }
446
447 pub fn stop(self) -> Duration {
449 self.start.elapsed()
450 }
451}
452
453#[derive(Debug, Clone, Serialize, Deserialize)]
455pub struct MemoryUsage {
456 pub vectors_bytes: usize,
458 pub index_bytes: usize,
460 pub metadata_bytes: usize,
462 pub cache_bytes: usize,
464 pub total_bytes: usize,
466}
467
468impl MemoryUsage {
469 pub fn estimate(
471 num_vectors: usize,
472 dimension: usize,
473 metadata_count: usize,
474 cache_size: usize,
475 ) -> Self {
476 let vectors_bytes = num_vectors * dimension * 4;
478
479 let index_bytes = 16 * num_vectors * 4 * 2;
482
483 let metadata_bytes = metadata_count * 200;
485
486 let cache_bytes = cache_size * dimension * 4 * 2;
488
489 let total_bytes = vectors_bytes + index_bytes + metadata_bytes + cache_bytes;
490
491 Self {
492 vectors_bytes,
493 index_bytes,
494 metadata_bytes,
495 cache_bytes,
496 total_bytes,
497 }
498 }
499
500 pub fn format_bytes(bytes: usize) -> String {
502 if bytes < 1024 {
503 format!("{} B", bytes)
504 } else if bytes < 1024 * 1024 {
505 format!("{:.2} KB", bytes as f64 / 1024.0)
506 } else if bytes < 1024 * 1024 * 1024 {
507 format!("{:.2} MB", bytes as f64 / (1024.0 * 1024.0))
508 } else {
509 format!("{:.2} GB", bytes as f64 / (1024.0 * 1024.0 * 1024.0))
510 }
511 }
512
513 pub fn summary(&self) -> String {
515 format!(
516 "Total: {} (Vectors: {}, Index: {}, Metadata: {}, Cache: {})",
517 Self::format_bytes(self.total_bytes),
518 Self::format_bytes(self.vectors_bytes),
519 Self::format_bytes(self.index_bytes),
520 Self::format_bytes(self.metadata_bytes),
521 Self::format_bytes(self.cache_bytes),
522 )
523 }
524}
525
526#[cfg(test)]
527mod tests {
528 use super::*;
529
530 #[test]
531 fn test_stats_recording() {
532 let stats = IndexStats::new();
533
534 stats.record_insert(Duration::from_micros(100));
536 stats.record_insert(Duration::from_micros(200));
537 stats.record_search(Duration::from_micros(50), 10, 10);
538 stats.record_search(Duration::from_micros(150), 10, 8);
539 stats.record_cache_hit();
540 stats.record_cache_miss();
541
542 let snapshot = stats.snapshot();
543
544 assert_eq!(snapshot.insert_count, 2);
545 assert_eq!(snapshot.search_count, 2);
546 assert!(snapshot.cache_hit_rate > 0.4 && snapshot.cache_hit_rate < 0.6);
547 }
548
549 #[test]
550 fn test_latency_histogram() {
551 let mut histogram = LatencyHistogram::new();
552
553 for i in 1..=100 {
554 histogram.record(i);
555 }
556
557 assert_eq!(histogram.count(), 100);
558 let p50 = histogram.percentile(50);
560 assert!((50..=52).contains(&p50), "P50 was {}", p50);
561 assert!(histogram.percentile(99) >= 99);
562 assert!(histogram.average() >= 50 && histogram.average() <= 51);
564 }
565
566 #[test]
567 fn test_index_health() {
568 let health = IndexHealth::analyze(1000, 768, None);
569
570 assert!(health.health_score > 0.0);
571 assert_eq!(health.size, 1000);
572 assert_eq!(health.dimension, 768);
573 }
574
575 #[test]
576 fn test_memory_usage() {
577 let usage = MemoryUsage::estimate(10000, 768, 10000, 1000);
578
579 assert!(usage.total_bytes > 1024 * 1024);
581 assert!(usage.vectors_bytes > 0);
582 }
583
584 #[test]
585 fn test_perf_timer() {
586 let timer = PerfTimer::start();
587 std::thread::sleep(Duration::from_millis(10));
588 let elapsed = timer.stop();
589
590 assert!(elapsed >= Duration::from_millis(10));
591 }
592}