use std::time::{Duration, Instant};
use yo_common::Rng;
use yo_vector::{Bits, Partitions, Tuning, Vectors};
const QUERIES: usize = 200;
const K: usize = 10;
struct Store {
dim: usize,
data: Vec<f32>,
}
impl Vectors for Store {
fn get(&self, id: u64, into: &mut [f32]) -> bool {
let at = id as usize * self.dim;
let Some(v) = self.data.get(at..at + self.dim) else {
return false;
};
into.copy_from_slice(v);
true
}
}
fn main() {
let n: usize = std::env::args()
.nth(1)
.and_then(|a| a.parse().ok())
.unwrap_or(500_000);
let dim = 128;
let store = corpus(dim, n, 200, 0x9e37);
let mut ix = Partitions::new(dim, Bits::One, 0x51f7, Tuning::default());
let mut buf = vec![0f32; dim];
let built = Instant::now();
for id in 0..n as u64 {
store.get(id, &mut buf);
ix.insert(id, &buf);
if ix.needs_maintenance() {
ix.maintain(&store, 4);
}
}
let built = built.elapsed();
let mut rng = Rng::new(0xbeef);
let queries: Vec<Vec<f32>> = (0..QUERIES)
.map(|_| {
let at = (rng.next_u64() as usize % n) * dim;
let mut q = store.data[at..at + dim].to_vec();
let noise = draw(dim, &mut rng);
for (x, e) in q.iter_mut().zip(&noise) {
*x += e * 0.08;
}
unit(&mut q);
q
})
.collect();
let per = n as f64 / ix.partitions() as f64;
println!(
"{n} vectors, {dim} dimensions, {} partitions, {per:.0} members a partition, built in {built:?}",
ix.partitions()
);
println!();
println!(
"{:>7}{:>12}{:>12}{:>12}{:>12}",
"probe", "scanned", "candidates", "search", "rerank"
);
let probes = [1usize, 2, 4, 8, 16, 32, 64, 128];
let mut points = Vec::new();
for probe in probes {
ix.retune(Tuning {
probe,
rerank: 16,
..Tuning::default()
});
let cand = median(&queries, |q| {
let t = Instant::now();
let out = ix.candidates(q, K * 16);
assert!(!out.is_empty());
t.elapsed()
});
let full = median(&queries, |q| {
let t = Instant::now();
let out = ix.search(q, K, &store);
assert_eq!(out.len(), K);
t.elapsed()
});
let rerank = full.saturating_sub(cand);
println!(
"{probe:>7}{:>12.0}{:>11}us{:>11}us{:>11}us",
probe as f64 * per,
cand.as_micros(),
full.as_micros(),
rerank.as_micros(),
);
points.push((probe as f64, cand.as_secs_f64() * 1e6));
}
let (rank, each) = fit(&points);
println!();
println!("ranking {} centroids: {rank:.0} us", ix.partitions());
println!("scanning one partition of {per:.0}: {each:.1} us");
println!(
"which puts a probe 64 search at {:.0} us before rerank",
rank + each * 64.0
);
}
fn median(queries: &[Vec<f32>], mut f: impl FnMut(&[f32]) -> Duration) -> Duration {
for q in queries.iter().take(16) {
f(q);
}
let mut took: Vec<Duration> = queries.iter().map(|q| f(q)).collect();
took.sort_unstable();
took[took.len() / 2]
}
fn fit(points: &[(f64, f64)]) -> (f64, f64) {
let n = points.len() as f64;
let sx: f64 = points.iter().map(|p| p.0).sum();
let sy: f64 = points.iter().map(|p| p.1).sum();
let sxx: f64 = points.iter().map(|p| p.0 * p.0).sum();
let sxy: f64 = points.iter().map(|p| p.0 * p.1).sum();
let d = n * sxx - sx * sx;
if d == 0.0 {
return (sy / n, 0.0);
}
let slope = (n * sxy - sx * sy) / d;
((sy - slope * sx) / n, slope)
}
fn corpus(dim: usize, n: usize, clusters: usize, seed: u64) -> Store {
let mut rng = Rng::new(seed);
let centres: Vec<Vec<f32>> = (0..clusters).map(|_| draw(dim, &mut rng)).collect();
let mut data = Vec::with_capacity(n * dim);
for i in 0..n {
let centre = ¢res[i % clusters];
let mut v = draw(dim, &mut rng);
for (x, c) in v.iter_mut().zip(centre) {
*x = *x * 0.35 + c;
}
unit(&mut v);
data.extend_from_slice(&v);
}
Store { dim, data }
}
fn draw(dim: usize, rng: &mut Rng) -> Vec<f32> {
let mut v = Vec::with_capacity(dim);
while v.len() < dim {
let u1 = ((rng.next_u64() >> 11) as f64 / (1u64 << 53) as f64).max(1e-12);
let u2 = (rng.next_u64() >> 11) as f64 / (1u64 << 53) as f64;
let r = (-2.0 * u1.ln()).sqrt();
v.push((r * (std::f64::consts::TAU * u2).cos()) as f32);
if v.len() < dim {
v.push((r * (std::f64::consts::TAU * u2).sin()) as f32);
}
}
v
}
fn unit(v: &mut [f32]) {
let n = v.iter().map(|x| x * x).sum::<f32>().sqrt();
if n > 0.0 {
for x in v {
*x /= n;
}
}
}