qntz 0.2.1

Vector quantization
Documentation
use qntz::adaptive::AdaptiveQuantizer;

const DIM: usize = 96;
const N_DOCS: usize = 256;
const TOP_K: usize = 10;

fn main() -> qntz::Result<()> {
    let docs = make_vectors(N_DOCS, DIM);
    let query = make_query(&docs[37]);
    let exact = exact_top_k(&docs, &query, TOP_K);

    println!("dataset: {N_DOCS} docs, dim={DIM}, top-{TOP_K}");
    println!("bits  recall@10  mean distance relative error  stored code bytes/doc");

    let mut distances = Vec::with_capacity(N_DOCS);
    for bits in [2u8, 4, 8] {
        let quantizer = AdaptiveQuantizer::new(bits)?;
        let batch = quantizer.quantize_packed(&docs)?;
        let scan_plan = batch.scan_plan();
        scan_plan.asymmetric_distances_into(&query, &mut distances)?;
        let approx = top_k_from_distances(&distances, TOP_K);

        let recall = recall_at_k(&exact, &approx);
        let mean_rel_err = mean_relative_error(&docs, &query, &distances);
        let bytes_per_doc = batch.codes.len() / batch.len();

        println!("{bits:>4}  {recall:>8.3}  {mean_rel_err:>28.4}  {bytes_per_doc:>9}");
    }

    Ok(())
}

fn top_k_from_distances(distances: &[f32], k: usize) -> Vec<(usize, f32)> {
    let mut scores: Vec<(usize, f32)> = distances.iter().copied().enumerate().collect();
    scores.sort_unstable_by(|a, b| a.1.total_cmp(&b.1).then_with(|| a.0.cmp(&b.0)));
    scores.truncate(k);
    scores
}

fn exact_top_k(docs: &[Vec<f32>], query: &[f32], k: usize) -> Vec<(usize, f32)> {
    let mut scores: Vec<(usize, f32)> = docs
        .iter()
        .enumerate()
        .map(|(doc_id, doc)| (doc_id, l2_sqr(query, doc)))
        .collect();
    scores.sort_unstable_by(|a, b| a.1.total_cmp(&b.1).then_with(|| a.0.cmp(&b.0)));
    scores.truncate(k);
    scores
}

fn recall_at_k(exact: &[(usize, f32)], approx: &[(usize, f32)]) -> f32 {
    let hits = approx
        .iter()
        .filter(|(doc_id, _)| exact.iter().any(|(exact_id, _)| exact_id == doc_id))
        .count();
    hits as f32 / exact.len() as f32
}

fn mean_relative_error(docs: &[Vec<f32>], query: &[f32], approx_distances: &[f32]) -> f32 {
    let total: f32 = docs
        .iter()
        .zip(approx_distances.iter())
        .map(|(doc, &approx)| {
            let exact = l2_sqr(query, doc).max(1e-6);
            ((approx - exact).abs() / exact).min(9.99)
        })
        .sum();
    total / docs.len() as f32
}

fn l2_sqr(a: &[f32], b: &[f32]) -> f32 {
    a.iter()
        .zip(b.iter())
        .map(|(x, y)| {
            let d = x - y;
            d * d
        })
        .sum()
}

fn make_vectors(n: usize, dim: usize) -> Vec<Vec<f32>> {
    let mut rng = Rng::new(0x1234_5678);
    (0..n)
        .map(|i| {
            let scale = 0.5 + (i % 11) as f32 * 0.2;
            let offset = (i % 7) as f32 * 0.15 - 0.45;
            (0..dim)
                .map(|d| {
                    let periodic = ((d as f32 + i as f32 * 0.03) * 0.11).sin();
                    offset + scale * (periodic + 0.2 * rng.normal())
                })
                .collect()
        })
        .collect()
}

fn make_query(source: &[f32]) -> Vec<f32> {
    let mut rng = Rng::new(0xCAFE);
    source.iter().map(|&x| x + 0.05 * rng.normal()).collect()
}

struct Rng(u64);

impl Rng {
    fn new(seed: u64) -> Self {
        Self(seed.max(1))
    }

    fn next_u64(&mut self) -> u64 {
        self.0 ^= self.0 << 13;
        self.0 ^= self.0 >> 7;
        self.0 ^= self.0 << 17;
        self.0
    }

    fn uniform(&mut self) -> f32 {
        ((self.next_u64() >> 11) as f64 / (1u64 << 53) as f64) as f32
    }

    fn normal(&mut self) -> f32 {
        let u1 = self.uniform().max(1e-7);
        let u2 = self.uniform();
        (-2.0 * u1.ln()).sqrt() * (2.0 * std::f32::consts::PI * u2).cos()
    }
}