Skip to main content

bench/
bench.rs

1//! Builds an index over synthetic clustered vectors and reports build speed,
2//! recall and latency.
3//!
4//! cargo run --release --example bench -- [records] [dim] [noise]
5//!
6//! `noise` is the per-dimension spread around cluster centers (default 1.0);
7//! higher values make neighbors harder to tell apart.
8
9use std::time::Instant;
10
11use recern_vector::{CollectionConfig, Database, Metric, RecallOptions};
12
13/// Clustered data resembles real embeddings far better than uniform noise.
14struct Clusters {
15    state: u64,
16    noise: f32,
17    centers: Vec<Vec<f32>>,
18}
19
20impl Clusters {
21    fn new(count: usize, dim: usize, noise: f32, seed: u64) -> Self {
22        let mut this = Self {
23            state: seed,
24            noise,
25            centers: Vec::new(),
26        };
27        this.centers = (0..count)
28            .map(|_| (0..dim).map(|_| this.gaussian()).collect())
29            .collect();
30        this
31    }
32
33    fn uniform(&mut self) -> f32 {
34        self.state ^= self.state << 13;
35        self.state ^= self.state >> 7;
36        self.state ^= self.state << 17;
37        ((self.state >> 40) as f32 + 1.0) / (1u64 << 24) as f32
38    }
39
40    fn gaussian(&mut self) -> f32 {
41        let (u, v) = (self.uniform(), self.uniform());
42        (-2.0 * u.ln()).sqrt() * (std::f32::consts::TAU * v).cos()
43    }
44
45    fn sample(&mut self) -> Vec<f32> {
46        let center = (self.uniform() * self.centers.len() as f32) as usize % self.centers.len();
47        let center = self.centers[center].clone();
48        center
49            .iter()
50            .map(|c| c + self.noise * self.gaussian())
51            .collect()
52    }
53}
54
55fn main() -> recern_vector::Result<()> {
56    let args: Vec<String> = std::env::args().skip(1).collect();
57    let arg = |i: usize| {
58        args.get(i)
59            .map(|a| a.parse::<f64>().expect("numeric argument"))
60    };
61    let records = arg(0).map_or(50_000, |v| v as usize);
62    let dim = arg(1).map_or(128, |v| v as usize);
63    let noise = arg(2).map_or(1.0, |v| v as f32);
64    let path = std::env::temp_dir().join("recern-vector-bench.rvec");
65    let _ = std::fs::remove_file(&path);
66
67    let mut data = Clusters::new(100, dim, noise, 0x5EED);
68    let vectors: Vec<Vec<f32>> = (0..records).map(|_| data.sample()).collect();
69
70    let mut db = Database::create(&path)?;
71    let collection = db.create_collection("bench", CollectionConfig::new(dim, Metric::Cosine))?;
72    let start = Instant::now();
73    for (i, v) in vectors.iter().enumerate() {
74        collection.upsert(&i.to_string(), v, None)?;
75    }
76    let build = start.elapsed();
77
78    let start = Instant::now();
79    db.save()?;
80    let save = start.elapsed();
81    let start = Instant::now();
82    let db = Database::open(&path)?;
83    let open = start.elapsed();
84    let collection = db.collection("bench")?;
85    let stats = collection.stats();
86
87    println!("records {records} · dim {dim} · noise {noise} · cosine · m=16 ef_construction=200");
88    println!(
89        "build   {:.2} s ({:.0} inserts/s)",
90        build.as_secs_f64(),
91        records as f64 / build.as_secs_f64()
92    );
93    println!(
94        "file    {:.1} MB · save {:.0} ms · open {:.0} ms",
95        std::fs::metadata(&path)?.len() as f64 / 1_048_576.0,
96        save.as_secs_f64() * 1e3,
97        open.as_secs_f64() * 1e3
98    );
99    println!(
100        "graph   layers {:?} · avg degree L0 {:.1} · unreachable {}",
101        stats.nodes_per_layer, stats.avg_degree_layer0, stats.unreachable
102    );
103
104    let report = collection.estimate_recall(&RecallOptions {
105        sample: 200,
106        k: 10,
107        ef_values: vec![10, 16, 32, 64, 128, 256],
108        seed: 7,
109    })?;
110    println!();
111    println!(
112        "exact scan p50 {:.3} ms",
113        report.exact_p50.as_secs_f64() * 1e3
114    );
115    println!(
116        "{:>6}  {:>9}  {:>10}  {:>10}  {:>9}",
117        "ef", "recall@10", "p50 ms", "p95 ms", "speedup"
118    );
119    for p in &report.points {
120        println!(
121            "{:>6}  {:>9.3}  {:>10.3}  {:>10.3}  {:>8.0}x",
122            p.ef,
123            p.recall,
124            p.p50.as_secs_f64() * 1e3,
125            p.p95.as_secs_f64() * 1e3,
126            report.exact_p50.as_secs_f64() / p.p50.as_secs_f64()
127        );
128    }
129
130    let _ = std::fs::remove_file(&path);
131    Ok(())
132}