1use std::time::Instant;
10
11use recern_vector::{CollectionConfig, Database, Metric, RecallOptions};
12
13struct 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}