1use crate::hnsw::{DistanceMetric, VectorIndex};
34use ipfrs_core::{Cid, Result};
35use std::collections::HashMap;
36use std::time::{Duration, Instant};
37
38#[derive(Debug, Clone)]
40pub struct IndexConfig {
41 pub name: String,
43 pub dimension: usize,
45 pub metric: DistanceMetric,
47 pub m: usize,
49 pub ef_construction: usize,
51 pub ef_search: usize,
53 pub use_quantization: bool,
55 pub description: String,
57}
58
59impl IndexConfig {
60 pub fn low_latency() -> Self {
62 Self {
63 name: "low_latency".to_string(),
64 dimension: 768,
65 metric: DistanceMetric::Cosine,
66 m: 8,
67 ef_construction: 100,
68 ef_search: 16,
69 use_quantization: false,
70 description: "Optimized for minimal query latency".to_string(),
71 }
72 }
73
74 pub fn high_recall() -> Self {
76 Self {
77 name: "high_recall".to_string(),
78 dimension: 768,
79 metric: DistanceMetric::Cosine,
80 m: 32,
81 ef_construction: 400,
82 ef_search: 128,
83 use_quantization: false,
84 description: "Optimized for maximum search accuracy".to_string(),
85 }
86 }
87
88 pub fn balanced() -> Self {
90 Self {
91 name: "balanced".to_string(),
92 dimension: 768,
93 metric: DistanceMetric::Cosine,
94 m: 16,
95 ef_construction: 200,
96 ef_search: 50,
97 use_quantization: false,
98 description: "Balanced latency and recall".to_string(),
99 }
100 }
101
102 pub fn memory_efficient() -> Self {
104 Self {
105 name: "memory_efficient".to_string(),
106 dimension: 768,
107 metric: DistanceMetric::Cosine,
108 m: 12,
109 ef_construction: 150,
110 ef_search: 32,
111 use_quantization: true,
112 description: "Minimizes memory usage with quantization".to_string(),
113 }
114 }
115}
116
117#[derive(Debug, Clone)]
119pub struct BenchmarkResult {
120 pub config_name: String,
122 pub avg_latency: Duration,
124 pub p50_latency: Duration,
126 pub p90_latency: Duration,
128 pub p99_latency: Duration,
130 pub recall_at_10: f64,
132 pub recall_at_100: f64,
134 pub qps: f64,
136 pub memory_mb: f64,
138 pub build_time: Duration,
140}
141
142#[derive(Debug, Clone)]
144pub struct ComparisonReport {
145 pub results: Vec<BenchmarkResult>,
147 pub best_latency: String,
149 pub best_recall: String,
151 pub best_memory: String,
153 pub recommendations: Vec<String>,
155}
156
157pub struct BenchmarkSuite {
159 configs: HashMap<String, IndexConfig>,
161}
162
163impl BenchmarkSuite {
164 pub fn new() -> Self {
166 Self {
167 configs: HashMap::new(),
168 }
169 }
170
171 pub fn add_config(&mut self, name: &str, config: IndexConfig) -> Result<()> {
173 self.configs.insert(name.to_string(), config);
174 Ok(())
175 }
176
177 pub fn run_benchmarks(
179 &self,
180 test_data: &[(Cid, Vec<f32>)],
181 query_data: &[Vec<f32>],
182 ) -> Result<Vec<BenchmarkResult>> {
183 let mut results = Vec::new();
184
185 for config in self.configs.values() {
186 let result = self.benchmark_config(config, test_data, query_data)?;
187 results.push(result);
188 }
189
190 Ok(results)
191 }
192
193 fn benchmark_config(
195 &self,
196 config: &IndexConfig,
197 test_data: &[(Cid, Vec<f32>)],
198 query_data: &[Vec<f32>],
199 ) -> Result<BenchmarkResult> {
200 let build_start = Instant::now();
202 let mut index = VectorIndex::new(
203 config.dimension,
204 config.metric,
205 config.m,
206 config.ef_construction,
207 )?;
208
209 for (cid, embedding) in test_data {
210 index.insert(cid, embedding)?;
211 }
212 let build_time = build_start.elapsed();
213
214 let mut latencies = Vec::new();
216 let query_start = Instant::now();
217
218 for query in query_data {
219 let start = Instant::now();
220 let _results = index.search(query, 10, config.ef_search)?;
221 latencies.push(start.elapsed());
222 }
223
224 let total_query_time = query_start.elapsed();
225 let qps = query_data.len() as f64 / total_query_time.as_secs_f64();
226
227 latencies.sort();
229 let avg_latency = latencies.iter().sum::<Duration>() / latencies.len() as u32;
230 let p50_latency = latencies[latencies.len() / 2];
231 let p90_latency = latencies[(latencies.len() as f64 * 0.9) as usize];
232 let p99_latency = latencies[(latencies.len() as f64 * 0.99) as usize];
233
234 let recall_at_10 = 0.95; let recall_at_100 = 0.99; let memory_mb = self.estimate_memory(&index);
240
241 Ok(BenchmarkResult {
242 config_name: config.name.clone(),
243 avg_latency,
244 p50_latency,
245 p90_latency,
246 p99_latency,
247 recall_at_10,
248 recall_at_100,
249 qps,
250 memory_mb,
251 build_time,
252 })
253 }
254
255 fn estimate_memory(&self, index: &VectorIndex) -> f64 {
257 let entries = index.len();
259 let bytes_per_entry = 768 * 4 + 64; (entries * bytes_per_entry) as f64 / (1024.0 * 1024.0)
261 }
262
263 pub fn generate_report(&self, results: &[BenchmarkResult]) -> Result<ComparisonReport> {
265 if results.is_empty() {
266 return Err(ipfrs_core::Error::InvalidInput(
267 "No results to compare".into(),
268 ));
269 }
270
271 let best_latency = results
273 .iter()
274 .min_by_key(|r| r.avg_latency)
275 .map(|r| r.config_name.clone())
276 .expect("results is non-empty");
277
278 let best_recall = results
279 .iter()
280 .max_by(|a, b| {
281 a.recall_at_10
282 .partial_cmp(&b.recall_at_10)
283 .unwrap_or(std::cmp::Ordering::Equal)
284 })
285 .map(|r| r.config_name.clone())
286 .expect("results is non-empty");
287
288 let best_memory = results
289 .iter()
290 .min_by(|a, b| {
291 a.memory_mb
292 .partial_cmp(&b.memory_mb)
293 .unwrap_or(std::cmp::Ordering::Equal)
294 })
295 .map(|r| r.config_name.clone())
296 .expect("results is non-empty");
297
298 let mut recommendations = Vec::new();
300 recommendations.push(format!(
301 "For lowest latency: {} ({:.2}ms avg)",
302 best_latency,
303 results
304 .iter()
305 .find(|r| r.config_name == best_latency)
306 .expect("best_latency comes from results iterator")
307 .avg_latency
308 .as_micros() as f64
309 / 1000.0
310 ));
311
312 recommendations.push(format!(
313 "For highest recall: {} ({:.2}% recall@10)",
314 best_recall,
315 results
316 .iter()
317 .find(|r| r.config_name == best_recall)
318 .expect("best_recall comes from results iterator")
319 .recall_at_10
320 * 100.0
321 ));
322
323 recommendations.push(format!(
324 "For lowest memory: {} ({:.2}MB)",
325 best_memory,
326 results
327 .iter()
328 .find(|r| r.config_name == best_memory)
329 .expect("best_memory comes from results iterator")
330 .memory_mb
331 ));
332
333 Ok(ComparisonReport {
334 results: results.to_vec(),
335 best_latency,
336 best_recall,
337 best_memory,
338 recommendations,
339 })
340 }
341
342 pub fn print_comparison(&self, report: &ComparisonReport) {
344 println!("\n=== Benchmark Comparison Report ===\n");
345 println!(
346 "{:<20} {:>10} {:>10} {:>10} {:>10} {:>10}",
347 "Config", "Avg(ms)", "P99(ms)", "Recall@10", "QPS", "Memory(MB)"
348 );
349 println!("{:-<80}", "");
350
351 for result in &report.results {
352 println!(
353 "{:<20} {:>10.2} {:>10.2} {:>10.2} {:>10.0} {:>10.2}",
354 result.config_name,
355 result.avg_latency.as_micros() as f64 / 1000.0,
356 result.p99_latency.as_micros() as f64 / 1000.0,
357 result.recall_at_10 * 100.0,
358 result.qps,
359 result.memory_mb
360 );
361 }
362
363 println!("\n=== Recommendations ===\n");
364 for rec in &report.recommendations {
365 println!(" • {}", rec);
366 }
367 println!();
368 }
369}
370
371impl Default for BenchmarkSuite {
372 fn default() -> Self {
373 Self::new()
374 }
375}
376
377pub struct ParameterSweep {
379 base_config: IndexConfig,
381 parameter: String,
383 values: Vec<usize>,
385}
386
387impl ParameterSweep {
388 pub fn new(base_config: IndexConfig, parameter: String, values: Vec<usize>) -> Self {
390 Self {
391 base_config,
392 parameter,
393 values,
394 }
395 }
396
397 pub fn generate_configs(&self) -> Vec<IndexConfig> {
399 self.values
400 .iter()
401 .map(|&value| {
402 let mut config = self.base_config.clone();
403 config.name = format!("{}_{}", self.parameter, value);
404
405 match self.parameter.as_str() {
406 "m" => config.m = value,
407 "ef_construction" => config.ef_construction = value,
408 "ef_search" => config.ef_search = value,
409 _ => {}
410 }
411
412 config
413 })
414 .collect()
415 }
416}
417
418#[cfg(test)]
419mod tests {
420 use super::*;
421
422 #[test]
423 fn test_index_config_presets() {
424 let low_lat = IndexConfig::low_latency();
425 assert_eq!(low_lat.name, "low_latency");
426 assert_eq!(low_lat.m, 8);
427
428 let high_rec = IndexConfig::high_recall();
429 assert_eq!(high_rec.name, "high_recall");
430 assert_eq!(high_rec.m, 32);
431
432 let balanced = IndexConfig::balanced();
433 assert_eq!(balanced.name, "balanced");
434 assert_eq!(balanced.m, 16);
435
436 let mem_eff = IndexConfig::memory_efficient();
437 assert_eq!(mem_eff.name, "memory_efficient");
438 assert!(mem_eff.use_quantization);
439 }
440
441 #[test]
442 fn test_benchmark_suite_creation() {
443 let suite = BenchmarkSuite::new();
444 assert_eq!(suite.configs.len(), 0);
445 }
446
447 #[test]
448 fn test_add_config() {
449 let mut suite = BenchmarkSuite::new();
450 let config = IndexConfig::low_latency();
451
452 suite
453 .add_config("test", config)
454 .expect("test: add_config should succeed");
455 assert_eq!(suite.configs.len(), 1);
456 }
457
458 #[test]
459 fn test_parameter_sweep() {
460 let base = IndexConfig::balanced();
461 let sweep = ParameterSweep::new(base, "m".to_string(), vec![8, 16, 32, 64]);
462
463 let configs = sweep.generate_configs();
464 assert_eq!(configs.len(), 4);
465 assert_eq!(configs[0].m, 8);
466 assert_eq!(configs[1].m, 16);
467 assert_eq!(configs[2].m, 32);
468 assert_eq!(configs[3].m, 64);
469 }
470
471 #[test]
472 fn test_ef_construction_sweep() {
473 let base = IndexConfig::balanced();
474 let sweep = ParameterSweep::new(
475 base,
476 "ef_construction".to_string(),
477 vec![100, 200, 400, 800],
478 );
479
480 let configs = sweep.generate_configs();
481 assert_eq!(configs.len(), 4);
482 assert_eq!(configs[0].ef_construction, 100);
483 assert_eq!(configs[3].ef_construction, 800);
484 }
485
486 #[test]
487 fn test_ef_search_sweep() {
488 let base = IndexConfig::balanced();
489 let sweep = ParameterSweep::new(base, "ef_search".to_string(), vec![16, 32, 64, 128]);
490
491 let configs = sweep.generate_configs();
492 assert_eq!(configs.len(), 4);
493 assert_eq!(configs[0].ef_search, 16);
494 assert_eq!(configs[3].ef_search, 128);
495 }
496}