use std::collections::HashSet;
use std::fs::File;
use std::io::{self, BufReader, Read};
use std::path::Path;
use std::time::Instant;
use vicinity::sparse_mips::{SparseMipsIndex, SparseMipsParams, SparseVector};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let data_dir = std::env::args()
.nth(1)
.unwrap_or_else(|| "data/sparse-mips/smoke".to_string());
let data_dir = Path::new(&data_dir);
if !data_dir.join("train.spv1").exists() {
println!(
"SparseMIPS smoke data not found at {}. Run: uv run scripts/generate_sparse_mips_smoke_data.py {}",
data_dir.display(),
data_dir.display()
);
return Ok(());
}
let train = load_spv1(&data_dir.join("train.spv1"))?;
let test = load_spv1(&data_dir.join("test.spv1"))?;
let (neighbors, gt_k) = load_nbr1(&data_dir.join("neighbors.bin"))?;
let k = gt_k.min(10);
let params = SparseMipsParams {
max_degree: 24,
ef_construction: 80,
ef_search: 80,
alpha: 1.2,
};
let build_start = Instant::now();
let mut index = SparseMipsIndex::new(params);
for (doc_id, vector) in train.iter().enumerate() {
index.add(doc_id as u32, vector.clone())?;
}
index.build()?;
let build_time = build_start.elapsed();
let search_start = Instant::now();
let mut results = Vec::with_capacity(test.len());
for query in &test {
results.push(index.search(query, k)?);
}
let search_time = search_start.elapsed();
let recall = recall_at_k(&results, &neighbors, k);
let qps = if search_time.as_secs_f64() > 0.0 {
test.len() as f64 / search_time.as_secs_f64()
} else {
f64::INFINITY
};
println!("SparseMIPS smoke benchmark");
println!("vectors={} queries={} k={}", train.len(), test.len(), k);
println!("build_time_s={:.6}", build_time.as_secs_f64());
println!("recall_at_{k}={recall:.4}");
println!("qps={qps:.1}");
Ok(())
}
fn load_spv1(path: &Path) -> Result<Vec<SparseVector>, Box<dyn std::error::Error>> {
let mut reader = BufReader::new(File::open(path)?);
let mut magic = [0_u8; 4];
reader.read_exact(&mut magic)?;
if &magic != b"SPV1" {
return Err(io::Error::new(io::ErrorKind::InvalidData, "invalid SPV1 magic").into());
}
let rows = read_u32(&mut reader)? as usize;
let total_nnz = read_u64(&mut reader)? as usize;
let mut offsets = Vec::with_capacity(rows + 1);
for _ in 0..=rows {
offsets.push(read_u64(&mut reader)? as usize);
}
if offsets.first().copied() != Some(0) || offsets.last().copied() != Some(total_nnz) {
return Err(io::Error::new(io::ErrorKind::InvalidData, "invalid SPV1 offsets").into());
}
if !offsets.windows(2).all(|window| window[0] <= window[1]) {
return Err(
io::Error::new(io::ErrorKind::InvalidData, "non-monotonic SPV1 offsets").into(),
);
}
let mut indices = Vec::with_capacity(total_nnz);
for _ in 0..total_nnz {
indices.push(read_u32(&mut reader)?);
}
let mut values = Vec::with_capacity(total_nnz);
for _ in 0..total_nnz {
values.push(read_f32(&mut reader)?);
}
let mut vectors = Vec::with_capacity(rows);
for window in offsets.windows(2) {
let start = window[0];
let end = window[1];
let vector_indices = indices[start..end].to_vec();
if !vector_indices.windows(2).all(|pair| pair[0] < pair[1]) {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"SPV1 vector indices must be strictly increasing",
)
.into());
}
vectors.push(SparseVector {
indices: vector_indices,
values: values[start..end].to_vec(),
});
}
Ok(vectors)
}
fn load_nbr1(path: &Path) -> Result<(Vec<Vec<i32>>, usize), Box<dyn std::error::Error>> {
let mut reader = BufReader::new(File::open(path)?);
let mut magic = [0_u8; 4];
reader.read_exact(&mut magic)?;
if &magic != b"NBR1" {
return Err(io::Error::new(io::ErrorKind::InvalidData, "invalid NBR1 magic").into());
}
let rows = read_u32(&mut reader)? as usize;
let width = read_u32(&mut reader)? as usize;
let mut neighbors = Vec::with_capacity(rows);
for _ in 0..rows {
let mut row = Vec::with_capacity(width);
for _ in 0..width {
row.push(read_i32(&mut reader)?);
}
neighbors.push(row);
}
Ok((neighbors, width))
}
fn read_u32(reader: &mut impl Read) -> io::Result<u32> {
let mut bytes = [0_u8; 4];
reader.read_exact(&mut bytes)?;
Ok(u32::from_le_bytes(bytes))
}
fn read_i32(reader: &mut impl Read) -> io::Result<i32> {
let mut bytes = [0_u8; 4];
reader.read_exact(&mut bytes)?;
Ok(i32::from_le_bytes(bytes))
}
fn read_u64(reader: &mut impl Read) -> io::Result<u64> {
let mut bytes = [0_u8; 8];
reader.read_exact(&mut bytes)?;
Ok(u64::from_le_bytes(bytes))
}
fn read_f32(reader: &mut impl Read) -> io::Result<f32> {
let mut bytes = [0_u8; 4];
reader.read_exact(&mut bytes)?;
Ok(f32::from_le_bytes(bytes))
}
fn recall_at_k(results: &[Vec<(u32, f32)>], neighbors: &[Vec<i32>], k: usize) -> f64 {
let mut hits = 0usize;
let mut total = 0usize;
for (result_row, truth_row) in results.iter().zip(neighbors.iter()) {
let truth: HashSet<u32> = truth_row
.iter()
.take(k)
.filter_map(|&id| u32::try_from(id).ok())
.collect();
for &(id, _) in result_row.iter().take(k) {
if truth.contains(&id) {
hits += 1;
}
}
total += k;
}
hits as f64 / total.max(1) as f64
}