#![cfg(test)]
use crate::partition::{Partitions, Tuning, Vectors};
use crate::rabitq::Bits;
use std::time::Instant;
struct Base {
dim: usize,
data: Vec<f32>,
}
impl Vectors for Base {
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
}
}
trait Le: Copy {
fn read(b: &[u8]) -> Self;
}
impl Le for f32 {
fn read(b: &[u8]) -> f32 {
f32::from_le_bytes([b[0], b[1], b[2], b[3]])
}
}
impl Le for i32 {
fn read(b: &[u8]) -> i32 {
i32::from_le_bytes([b[0], b[1], b[2], b[3]])
}
}
fn read_vecs<T: Le>(path: &str) -> (usize, Vec<T>) {
let bytes = std::fs::read(path).unwrap_or_else(|e| panic!("reading {path}: {e}"));
assert!(bytes.len() >= 4, "{path} is too short to hold a vector");
let dim = i32::read(&bytes[..4]) as usize;
let stride = 4 + dim * 4;
assert_eq!(
bytes.len() % stride,
0,
"{path} is not a whole number of {dim} dimensional vectors"
);
let n = bytes.len() / stride;
let mut out = Vec::with_capacity(n * dim);
for v in 0..n {
let at = v * stride + 4;
for d in 0..dim {
out.push(T::read(&bytes[at + d * 4..at + d * 4 + 4]));
}
}
(dim, out)
}
fn prefix(dir: &str) -> &str {
let dir = dir.trim_end_matches(['/', '\\']);
dir.rsplit(['/', '\\']).next().unwrap_or(dir)
}
#[test]
#[ignore = "needs a dataset in YO_DATASET and prints a table rather than asserting"]
fn how_far_down_the_probe_order_the_answers_are() {
let Ok(dir) = std::env::var("YO_DATASET") else {
panic!("set YO_DATASET to a directory holding <name>_base.fvecs and the two beside it");
};
let dir = dir.trim().to_string();
let queries: usize = std::env::var("YO_QUERIES")
.ok()
.and_then(|q| q.parse().ok())
.unwrap_or(500);
let set = prefix(&dir);
let t = Instant::now();
let (dim, data) = read_vecs::<f32>(&format!("{dir}/{set}_base.fvecs"));
let (qdim, query) = read_vecs::<f32>(&format!("{dir}/{set}_query.fvecs"));
let (gdim, truth) = read_vecs::<i32>(&format!("{dir}/{set}_groundtruth.ivecs"));
assert_eq!(dim, qdim, "base and query dimensions differ");
let n = data.len() / dim;
let queries = queries.min(query.len() / dim);
println!(
"{dir}: {n} base at {dim} dimensions, {queries} queries, read in {:?}",
t.elapsed()
);
let base = Base { dim, data };
let t = Instant::now();
let mut ix = Partitions::new(dim, Bits::One, 0x51f7, Tuning::default());
let mut buf = vec![0f32; dim];
for id in 0..n as u64 {
base.get(id, &mut buf);
ix.insert(id, &buf);
if ix.needs_maintenance() {
ix.maintain(&base, 4);
}
}
let parts = ix.partitions();
println!("{parts} partitions, built in {:?}", t.elapsed());
const K: usize = 10;
let edges = [8usize, 16, 32, 64, 128, 256, 512, 1024];
let mut bucket = vec![0usize; edges.len() + 1];
let mut worst = 0usize;
let mut total = 0usize;
let mut order = Vec::new();
let mut place = vec![0usize; parts];
for q in 0..queries {
let v = &query[q * dim..(q + 1) * dim];
ix.probe_order(v, &mut order);
for (rank, &p) in order.iter().enumerate() {
place[p] = rank;
}
for &id in &truth[q * gdim..q * gdim + K] {
let Some(p) = ix.holder(id as u64) else {
continue;
};
let rank = place[p] + 1;
worst = worst.max(rank);
total += 1;
let at = edges.iter().position(|&e| rank <= e).unwrap_or(edges.len());
bucket[at] += 1;
}
}
println!();
println!("{:>10}{:>12}{:>12}", "found by", "share", "running");
let mut running = 0usize;
for (i, &count) in bucket.iter().enumerate() {
running += count;
let name = edges
.get(i)
.map_or("beyond".to_string(), |e| format!("probe {e}"));
println!(
"{name:>10}{:>11.4}{:>12.4}",
count as f64 / total as f64,
running as f64 / total as f64
);
}
println!();
println!("furthest a true neighbour sat down the order: {worst} of {parts}");
}