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 .unwrap();
277
278 let best_recall = results
279 .iter()
280 .max_by(|a, b| a.recall_at_10.partial_cmp(&b.recall_at_10).unwrap())
281 .map(|r| r.config_name.clone())
282 .unwrap();
283
284 let best_memory = results
285 .iter()
286 .min_by(|a, b| a.memory_mb.partial_cmp(&b.memory_mb).unwrap())
287 .map(|r| r.config_name.clone())
288 .unwrap();
289
290 let mut recommendations = Vec::new();
292 recommendations.push(format!(
293 "For lowest latency: {} ({:.2}ms avg)",
294 best_latency,
295 results
296 .iter()
297 .find(|r| r.config_name == best_latency)
298 .unwrap()
299 .avg_latency
300 .as_micros() as f64
301 / 1000.0
302 ));
303
304 recommendations.push(format!(
305 "For highest recall: {} ({:.2}% recall@10)",
306 best_recall,
307 results
308 .iter()
309 .find(|r| r.config_name == best_recall)
310 .unwrap()
311 .recall_at_10
312 * 100.0
313 ));
314
315 recommendations.push(format!(
316 "For lowest memory: {} ({:.2}MB)",
317 best_memory,
318 results
319 .iter()
320 .find(|r| r.config_name == best_memory)
321 .unwrap()
322 .memory_mb
323 ));
324
325 Ok(ComparisonReport {
326 results: results.to_vec(),
327 best_latency,
328 best_recall,
329 best_memory,
330 recommendations,
331 })
332 }
333
334 pub fn print_comparison(&self, report: &ComparisonReport) {
336 println!("\n=== Benchmark Comparison Report ===\n");
337 println!(
338 "{:<20} {:>10} {:>10} {:>10} {:>10} {:>10}",
339 "Config", "Avg(ms)", "P99(ms)", "Recall@10", "QPS", "Memory(MB)"
340 );
341 println!("{:-<80}", "");
342
343 for result in &report.results {
344 println!(
345 "{:<20} {:>10.2} {:>10.2} {:>10.2} {:>10.0} {:>10.2}",
346 result.config_name,
347 result.avg_latency.as_micros() as f64 / 1000.0,
348 result.p99_latency.as_micros() as f64 / 1000.0,
349 result.recall_at_10 * 100.0,
350 result.qps,
351 result.memory_mb
352 );
353 }
354
355 println!("\n=== Recommendations ===\n");
356 for rec in &report.recommendations {
357 println!(" • {}", rec);
358 }
359 println!();
360 }
361}
362
363impl Default for BenchmarkSuite {
364 fn default() -> Self {
365 Self::new()
366 }
367}
368
369pub struct ParameterSweep {
371 base_config: IndexConfig,
373 parameter: String,
375 values: Vec<usize>,
377}
378
379impl ParameterSweep {
380 pub fn new(base_config: IndexConfig, parameter: String, values: Vec<usize>) -> Self {
382 Self {
383 base_config,
384 parameter,
385 values,
386 }
387 }
388
389 pub fn generate_configs(&self) -> Vec<IndexConfig> {
391 self.values
392 .iter()
393 .map(|&value| {
394 let mut config = self.base_config.clone();
395 config.name = format!("{}_{}", self.parameter, value);
396
397 match self.parameter.as_str() {
398 "m" => config.m = value,
399 "ef_construction" => config.ef_construction = value,
400 "ef_search" => config.ef_search = value,
401 _ => {}
402 }
403
404 config
405 })
406 .collect()
407 }
408}
409
410#[cfg(test)]
411mod tests {
412 use super::*;
413
414 #[test]
415 fn test_index_config_presets() {
416 let low_lat = IndexConfig::low_latency();
417 assert_eq!(low_lat.name, "low_latency");
418 assert_eq!(low_lat.m, 8);
419
420 let high_rec = IndexConfig::high_recall();
421 assert_eq!(high_rec.name, "high_recall");
422 assert_eq!(high_rec.m, 32);
423
424 let balanced = IndexConfig::balanced();
425 assert_eq!(balanced.name, "balanced");
426 assert_eq!(balanced.m, 16);
427
428 let mem_eff = IndexConfig::memory_efficient();
429 assert_eq!(mem_eff.name, "memory_efficient");
430 assert!(mem_eff.use_quantization);
431 }
432
433 #[test]
434 fn test_benchmark_suite_creation() {
435 let suite = BenchmarkSuite::new();
436 assert_eq!(suite.configs.len(), 0);
437 }
438
439 #[test]
440 fn test_add_config() {
441 let mut suite = BenchmarkSuite::new();
442 let config = IndexConfig::low_latency();
443
444 suite.add_config("test", config).unwrap();
445 assert_eq!(suite.configs.len(), 1);
446 }
447
448 #[test]
449 fn test_parameter_sweep() {
450 let base = IndexConfig::balanced();
451 let sweep = ParameterSweep::new(base, "m".to_string(), vec![8, 16, 32, 64]);
452
453 let configs = sweep.generate_configs();
454 assert_eq!(configs.len(), 4);
455 assert_eq!(configs[0].m, 8);
456 assert_eq!(configs[1].m, 16);
457 assert_eq!(configs[2].m, 32);
458 assert_eq!(configs[3].m, 64);
459 }
460
461 #[test]
462 fn test_ef_construction_sweep() {
463 let base = IndexConfig::balanced();
464 let sweep = ParameterSweep::new(
465 base,
466 "ef_construction".to_string(),
467 vec![100, 200, 400, 800],
468 );
469
470 let configs = sweep.generate_configs();
471 assert_eq!(configs.len(), 4);
472 assert_eq!(configs[0].ef_construction, 100);
473 assert_eq!(configs[3].ef_construction, 800);
474 }
475
476 #[test]
477 fn test_ef_search_sweep() {
478 let base = IndexConfig::balanced();
479 let sweep = ParameterSweep::new(base, "ef_search".to_string(), vec![16, 32, 64, 128]);
480
481 let configs = sweep.generate_configs();
482 assert_eq!(configs.len(), 4);
483 assert_eq!(configs[0].ef_search, 16);
484 assert_eq!(configs[3].ef_search, 128);
485 }
486}