use std::io::Write;
use std::time::Instant;
fn main() {
let mut args: Vec<String> = std::env::args().skip(1).collect();
let check = args.iter().any(|a| a == "--check");
args.retain(|a| a != "--check");
let mut args = args.into_iter();
let Some(dir) = args.next() else {
eprintln!("usage: truth <dataset directory> [neighbours] [threads] [--check]");
std::process::exit(2);
};
let k: usize = args.next().map_or(100, |a| {
a.parse().expect("the second argument is a neighbour count")
});
let threads: usize = args.next().map_or_else(
|| {
std::thread::available_parallelism()
.map(std::num::NonZero::get)
.unwrap_or(1)
},
|a| a.parse().expect("the third argument is a thread count"),
);
let set = prefix(&dir);
let out = format!("{dir}/{set}_groundtruth.ivecs");
let there = std::fs::metadata(&out).is_ok();
if there && !check {
eprintln!("{out} is already there, and this will not overwrite one");
eprintln!("pass --check to compare against it instead");
std::process::exit(1);
}
if check && !there {
eprintln!("{out} is not there, so there is nothing to check against");
std::process::exit(1);
}
let t = Instant::now();
let (dim, base) = read_fvecs(&format!("{dir}/{set}_base.fvecs"));
let (qdim, query) = read_fvecs(&format!("{dir}/{set}_query.fvecs"));
assert_eq!(dim, qdim, "base and query dimensions differ");
let n = base.len() / dim;
let queries = query.len() / dim;
assert!(k <= n, "asked for {k} neighbours out of {n} vectors");
println!(
"{dir}: {n} base at {dim} dimensions, {queries} query, read in {:?}",
t.elapsed()
);
println!("{k} neighbours each on {threads} threads");
let mut found = vec![0i32; queries * k];
let t = Instant::now();
let per = queries.div_ceil(threads);
std::thread::scope(|scope| {
for (t, rows) in found.chunks_mut(per * k).enumerate() {
let (base, query) = (&base, &query);
scope.spawn(move || {
let mut near = Nearest::new(k);
for (q, row) in rows.chunks_mut(k).enumerate() {
let q = t * per + q;
let v = &query[q * dim..(q + 1) * dim];
near.clear();
for id in 0..n {
near.offer(id as i32, sqdist(v, &base[id * dim..(id + 1) * dim]));
}
near.take_into(row);
}
});
}
});
println!("searched in {:?}", t.elapsed());
if check {
compare(&out, &found, &base, &query, dim, k);
return;
}
let mut file = std::io::BufWriter::new(std::fs::File::create(&out).expect("could not write"));
let width = (k as i32).to_le_bytes();
for row in found.chunks(k) {
file.write_all(&width).expect("write");
for id in row {
file.write_all(&id.to_le_bytes()).expect("write");
}
}
file.flush().expect("flush");
println!("wrote {out}");
}
fn compare(out: &str, found: &[i32], base: &[f32], query: &[f32], dim: usize, k: usize) {
let (fk, theirs) = read_ivecs(out);
assert!(
fk >= k,
"{out} lists {fk} neighbours a query and this worked out {k}"
);
let queries = found.len() / k;
assert_eq!(
theirs.len() / fk,
queries,
"{out} covers a different number of queries"
);
let at = |row: &[i32], q: usize| -> Vec<f64> {
let v = &query[q * dim..(q + 1) * dim];
row.iter()
.map(|&id| {
let id = id as usize;
sqdist(v, &base[id * dim..(id + 1) * dim])
})
.collect()
};
let (mut same_ids, mut same_dist, mut theirs_worse, mut mine_worse) = (0, 0, 0, 0);
let mut first = None;
for q in 0..queries {
let mine = &found[q * k..(q + 1) * k];
let file = &theirs[q * fk..q * fk + k];
if mine == file {
same_ids += 1;
same_dist += 1;
continue;
}
let (a, b) = (at(mine, q), at(file, q));
if a == b {
same_dist += 1;
} else {
if first.is_none() {
first = Some(q);
}
let split = a.iter().zip(&b).position(|(x, y)| x != y).unwrap_or(0);
if a[split] < b[split] {
theirs_worse += 1;
} else {
mine_worse += 1;
}
}
}
println!("{queries} queries against {out}, at {k} neighbours each");
println!(" same ids in the same order: {same_ids}");
println!(" same distances in the same order: {same_dist}");
println!(" the file has something further away than it could: {theirs_worse}");
println!(" this program has something nearer than the file: {mine_worse}");
if let Some(q) = first {
println!(" first query whose distances differ: {q}");
}
if same_dist == queries {
println!("agreed on every query, and the ids differ only where the distances tie");
} else {
std::process::exit(1);
}
}
struct Nearest {
k: usize,
have: Vec<(f64, i32)>,
}
impl Nearest {
fn new(k: usize) -> Nearest {
Nearest {
k,
have: Vec::with_capacity(k + 1),
}
}
fn clear(&mut self) {
self.have.clear();
}
fn offer(&mut self, id: i32, d: f64) {
if self.have.len() == self.k && d >= self.have[self.k - 1].0 {
return;
}
let at = self.have.partition_point(|&(had, _)| had <= d);
self.have.insert(at, (d, id));
self.have.truncate(self.k);
}
fn take_into(&self, row: &mut [i32]) {
for (slot, &(_, id)) in row.iter_mut().zip(&self.have) {
*slot = id;
}
}
}
fn sqdist(a: &[f32], b: &[f32]) -> f64 {
const LANES: usize = 8;
let (xs, x_tail) = a.as_chunks::<LANES>();
let (ys, y_tail) = b.as_chunks::<LANES>();
let mut totals = [0.0f64; LANES];
for (x, y) in xs.iter().zip(ys) {
for k in 0..LANES {
let d = f64::from(x[k]) - f64::from(y[k]);
totals[k] += d * d;
}
}
let mut sum = 0.0f64;
for total in totals {
sum += total;
}
for (x, y) in x_tail.iter().zip(y_tail) {
let d = f64::from(*x) - f64::from(*y);
sum += d * d;
}
sum
}
fn prefix(dir: &str) -> &str {
dir.trim_end_matches('/')
.rsplit(['/', '\\'])
.next()
.unwrap_or(dir)
}
fn read_ivecs(path: &str) -> (usize, Vec<i32>) {
let (dim, floats) = read_fvecs(path);
let ids = floats.into_iter().map(|f| f.to_bits() as i32).collect();
(dim, ids)
}
fn read_fvecs(path: &str) -> (usize, Vec<f32>) {
let bytes = std::fs::read(path).unwrap_or_else(|e| {
eprintln!("{path}: {e}");
std::process::exit(1);
});
assert!(bytes.len() >= 4, "{path} is too short to hold a dimension");
let dim = i32::from_le_bytes(bytes[..4].try_into().unwrap()) as usize;
let record = 4 + dim * 4;
assert!(
dim > 0 && bytes.len().is_multiple_of(record),
"{path} is not {dim} dimensional records of {record} bytes"
);
let mut out = Vec::with_capacity(bytes.len() / record * dim);
for (i, rec) in bytes.chunks_exact(record).enumerate() {
let d = i32::from_le_bytes(rec[..4].try_into().unwrap()) as usize;
assert_eq!(d, dim, "{path} record {i} has {d} dimensions, not {dim}");
out.extend(
rec[4..]
.as_chunks::<4>()
.0
.iter()
.copied()
.map(f32::from_le_bytes),
);
}
(dim, out)
}