use std::fs;
use std::io::{BufRead, BufReader};
use clap::{Parser, Subcommand};
use serde::Deserialize;
use vicinity::hnsw::HNSWIndex;
#[derive(Parser)]
#[command(
name = "vicinity",
about = "Build and query an HNSW nearest-neighbor index"
)]
struct Cli {
#[command(subcommand)]
cmd: Cmd,
}
#[derive(Subcommand)]
enum Cmd {
Build {
vectors: String,
#[arg(short, long)]
out: String,
#[arg(long, default_value_t = 16)]
m: usize,
#[arg(long = "m-max", default_value_t = 32)]
m_max: usize,
},
Search {
index: String,
#[arg(short, long)]
query: String,
#[arg(short, default_value_t = 10)]
k: usize,
#[arg(long, default_value_t = 64)]
ef: usize,
},
}
#[derive(Deserialize)]
struct VecLine {
id: u32,
vec: Vec<f32>,
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
match Cli::parse().cmd {
Cmd::Build {
vectors,
out,
m,
m_max,
} => {
let file = BufReader::new(fs::File::open(&vectors)?);
let mut docs: Vec<VecLine> = Vec::new();
for line in file.lines() {
let line = line?;
if line.trim().is_empty() {
continue;
}
docs.push(serde_json::from_str(&line)?);
}
let dim = docs.first().ok_or("no vectors in input")?.vec.len();
let n = docs.len();
let mut index = HNSWIndex::new(dim, m, m_max)?;
for d in docs {
index.add(d.id, vicinity::distance::normalize(&d.vec))?;
}
index.build()?;
index.save_to_file(&out)?;
eprintln!("indexed {n} vectors -> {out}");
}
Cmd::Search {
index,
query,
k,
ef,
} => {
let idx = HNSWIndex::load_from_file(&index)?;
let parsed: Vec<f32> = serde_json::from_str(&query)?;
let q = vicinity::distance::normalize(&parsed);
for (id, score) in idx.search(&q, k, ef)? {
println!("{id}\t{score:.6}");
}
}
}
Ok(())
}