#![allow(clippy::unwrap_used)] //! Criterion benchmarks for the vector index subsystem. //! //! Measures ANN search latency across the adaptive query planner's strategy //! spectrum: unfiltered, in-graph filtered (20%), widened filtered (5%), //! and pre-filter brute-force (0.5%). Also benchmarks recall@100, single //! insert, and single delete. //! //! All setup (index construction, vector insertion) is done OUTSIDE the //! `b.iter()` closure. Only the search/insert/delete call is measured. use criterion::{Criterion, black_box, criterion_group, criterion_main}; use rand::Rng; use tidaldb::storage::vector::{ AdaptiveQueryPlanner, BruteForceIndex, DistanceMetric, QuantizationLevel, VectorId, VectorIndex, VectorIndexConfig, }; // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- /// Generate a random unit vector of the given dimensionality. fn random_unit_vector(dim: usize, rng: &mut impl Rng) -> Vec { let v: Vec = (0..dim) .map(|_| { let x: f32 = rng.random(); x - 0.5 }) .collect(); let norm: f32 = v.iter().map(|x| x * x).sum::().sqrt(); if norm < f32::EPSILON { // Degenerate case: return a unit vector along the first axis. let mut fallback = vec![0.0_f32; dim]; fallback[0] = 1.0; return fallback; } v.iter().map(|x| x / norm).collect() } /// Build a brute-force index with `n` random unit vectors of dimension `dim`. fn build_brute_index(n: u64, dim: usize) -> BruteForceIndex { let config = VectorIndexConfig { dimensions: dim, metric: DistanceMetric::L2, quantization: QuantizationLevel::F32, connectivity: 16, ef_construction: 200, ef_search: 200, }; let index = BruteForceIndex::new(config); let mut rng = rand::rng(); for id in 0..n { let vec = random_unit_vector(dim, &mut rng); index.insert(id, &vec).unwrap(); } index } // --------------------------------------------------------------------------- // Benchmarks // --------------------------------------------------------------------------- /// Benchmark: unfiltered ANN search over 10K vectors, dim=128, k=100. /// Measures baseline search latency without any filter overhead. fn bench_ann_search_unfiltered(c: &mut Criterion) { let dim = 128; let n = 10_000_u64; let index = build_brute_index(n, dim); let planner = AdaptiveQueryPlanner::with_defaults(); let mut rng = rand::rng(); let query = random_unit_vector(dim, &mut rng); c.bench_function("ann_search_unfiltered_10k", |b| { b.iter(|| { planner .execute( black_box(&index), black_box(&query), black_box(100), None, 1.0, None, ) .unwrap() }); }); } /// Benchmark: filtered ANN search with 20% selectivity (in-graph filter). /// 10K vectors, dim=128, k=100. fn bench_ann_search_filtered_20pct(c: &mut Criterion) { let dim = 128; let n = 10_000_u64; let index = build_brute_index(n, dim); let planner = AdaptiveQueryPlanner::with_defaults(); let mut rng = rand::rng(); let query = random_unit_vector(dim, &mut rng); // ~20% selectivity: IDs 0..1999 pass (20% of 10K). let filter = |id: VectorId| id < 2000; c.bench_function("ann_search_filtered_20pct_10k", |b| { b.iter(|| { planner .execute( black_box(&index), black_box(&query), black_box(100), Some(black_box(&filter)), 0.20, None, ) .unwrap() }); }); } /// Benchmark: filtered ANN search with 5% selectivity (widened filter, ef=400). /// 10K vectors, dim=128, k=100. fn bench_ann_search_filtered_5pct(c: &mut Criterion) { let dim = 128; let n = 10_000_u64; let index = build_brute_index(n, dim); let planner = AdaptiveQueryPlanner::with_defaults(); let mut rng = rand::rng(); let query = random_unit_vector(dim, &mut rng); // ~5% selectivity: IDs 0..499 pass (5% of 10K). let filter = |id: VectorId| id < 500; c.bench_function("ann_search_filtered_5pct_10k", |b| { b.iter(|| { planner .execute( black_box(&index), black_box(&query), black_box(100), Some(black_box(&filter)), 0.05, None, ) .unwrap() }); }); } /// Benchmark: pre-filter brute-force search with 0.5% selectivity. /// 10K vectors, dim=128, k=100. Uses a separate brute-force index. fn bench_ann_search_brute_force(c: &mut Criterion) { let dim = 128; let n = 10_000_u64; let index = build_brute_index(n, dim); let brute = build_brute_index(n, dim); let planner = AdaptiveQueryPlanner::with_defaults(); let mut rng = rand::rng(); let query = random_unit_vector(dim, &mut rng); // ~0.5% selectivity: IDs 0..49 pass (0.5% of 10K). let filter = |id: VectorId| id < 50; c.bench_function("ann_search_brute_force_10k", |b| { b.iter(|| { planner .execute( black_box(&index), black_box(&query), black_box(100), Some(black_box(&filter)), 0.005, Some(black_box(&brute as &dyn VectorIndex)), ) .unwrap() }); }); } /// Benchmark: recall@100 measurement. /// Builds a 10K brute-force index, runs search, and compares against /// ground truth (which for brute-force is exact). This benchmarks the /// search + comparison loop to establish a baseline measurement cost. fn bench_ann_recall_at_100(c: &mut Criterion) { let dim = 128; let n = 10_000_u64; let k = 100; let index = build_brute_index(n, dim); let mut rng = rand::rng(); let query = random_unit_vector(dim, &mut rng); // Pre-compute ground truth. let ground_truth = index.search(&query, k, 200).unwrap(); let gt_ids: Vec = ground_truth.iter().map(|r| r.id).collect(); c.bench_function("ann_recall_at_100_10k", |b| { b.iter(|| { let results = index .search(black_box(&query), black_box(k), black_box(200)) .unwrap(); let result_ids: Vec = results.iter().map(|r| r.id).collect(); // Compute recall: fraction of ground truth IDs found in results. let hits = result_ids.iter().filter(|id| gt_ids.contains(id)).count(); #[allow(clippy::cast_precision_loss)] let recall = hits as f64 / gt_ids.len() as f64; black_box(recall) }); }); } /// Benchmark: single vector insert into a pre-filled 10K index. fn bench_ann_insert_single(c: &mut Criterion) { let dim = 128; let n = 10_000_u64; let index = build_brute_index(n, dim); let mut rng = rand::rng(); let vec = random_unit_vector(dim, &mut rng); // Use an ID outside the pre-filled range to avoid replacement overhead. let mut next_id = n; c.bench_function("ann_insert_single_10k", |b| { b.iter(|| { index.insert(black_box(next_id), black_box(&vec)).unwrap(); next_id += 1; }); }); } /// Benchmark: single vector delete from a pre-filled 10K index. /// After each delete, re-inserts the vector so the bench remains iterable. fn bench_ann_delete_single(c: &mut Criterion) { let dim = 128; let n = 10_000_u64; let index = build_brute_index(n, dim); let mut rng = rand::rng(); let vec = random_unit_vector(dim, &mut rng); // Target a fixed ID for delete/reinsert cycle. let target_id = 0_u64; c.bench_function("ann_delete_single_10k", |b| { b.iter(|| { index.delete(black_box(target_id)).unwrap(); // Re-insert so the next iteration can delete it again. index.insert(black_box(target_id), black_box(&vec)).unwrap(); }); }); } // --------------------------------------------------------------------------- // Criterion group + main // --------------------------------------------------------------------------- criterion_group!( benches, bench_ann_search_unfiltered, bench_ann_search_filtered_20pct, bench_ann_search_filtered_5pct, bench_ann_search_brute_force, bench_ann_recall_at_100, bench_ann_insert_single, bench_ann_delete_single, ); criterion_main!(benches);