feox-vector 0.1.1

Embedded vector store on FeOxDB - exact and ANN cosine search with metadata filtering
Documentation
use std::collections::BTreeMap;

use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion, Throughput};
use feox_vector::{
    Store, StoreConfig, VectorQueryInput, VectorQueryMode, VectorStore, VectorUpsertInput,
    VectorUpsertRecord,
};
use serde_json::json;

const DIMENSIONS: usize = 128;
const RECORDS: usize = 10_000;
const UPSERT_BATCH: usize = 100;

struct XorShift(u64);

impl XorShift {
    fn next_f32(&mut self) -> f32 {
        self.0 ^= self.0 << 13;
        self.0 ^= self.0 >> 7;
        self.0 ^= self.0 << 17;
        ((self.0 >> 40) as f32 / (1 << 24) as f32) * 2.0 - 1.0
    }

    fn vector(&mut self, dimensions: usize) -> Vec<f32> {
        (0..dimensions).map(|_| self.next_f32()).collect()
    }
}

fn populated_store(records: usize) -> VectorStore {
    let store = Store::open(StoreConfig::default()).unwrap();
    let vectors = VectorStore::new(store);
    let mut rng = XorShift(0x5eed_f0f5 ^ records as u64);

    let mut batch = Vec::with_capacity(UPSERT_BATCH);
    for i in 0..records {
        let mut metadata = BTreeMap::new();
        metadata.insert("shard".to_string(), json!(format!("shard-{}", i % 8)));
        batch.push(VectorUpsertRecord {
            id: format!("record-{i:06}"),
            values: rng.vector(DIMENSIONS),
            metadata,
        });
        if batch.len() == UPSERT_BATCH {
            vectors
                .upsert(
                    "bench",
                    "docs",
                    "main",
                    VectorUpsertInput {
                        records: std::mem::take(&mut batch),
                    },
                )
                .unwrap();
        }
    }
    vectors
}

fn query_input(vector: Vec<f32>, mode: Option<VectorQueryMode>) -> VectorQueryInput {
    VectorQueryInput {
        vector,
        top_k: Some(10),
        filter: BTreeMap::new(),
        min_score: None,
        mode,
        ef_search: None,
        candidate_limit: None,
    }
}

fn bench_upsert(c: &mut Criterion) {
    let mut group = c.benchmark_group("upsert");
    group.sample_size(10);
    group.throughput(Throughput::Elements(RECORDS as u64));
    group.bench_function(BenchmarkId::new("batched", RECORDS), |b| {
        b.iter(|| populated_store(RECORDS));
    });
    group.finish();
}

fn bench_query(c: &mut Criterion) {
    let vectors = populated_store(RECORDS);
    vectors
        .rebuild_ann("bench", "docs", "main", DIMENSIONS)
        .unwrap();
    let mut rng = XorShift(0xfeed_beef);
    let queries: Vec<Vec<f32>> = (0..256).map(|_| rng.vector(DIMENSIONS)).collect();

    let mut group = c.benchmark_group("query");
    group.throughput(Throughput::Elements(1));
    group.bench_function("exact_top10", |b| {
        let mut cursor = 0;
        b.iter(|| {
            let query = queries[cursor % queries.len()].clone();
            cursor += 1;
            vectors
                .query("bench", "docs", "main", query_input(query, None))
                .unwrap()
        });
    });
    group.bench_function("ann_top10", |b| {
        let mut cursor = 0;
        b.iter(|| {
            let query = queries[cursor % queries.len()].clone();
            cursor += 1;
            vectors
                .query(
                    "bench",
                    "docs",
                    "main",
                    query_input(query, Some(VectorQueryMode::Ann)),
                )
                .unwrap()
        });
    });
    group.bench_function("ann_top10_filtered", |b| {
        let mut cursor = 0;
        b.iter(|| {
            let query = queries[cursor % queries.len()].clone();
            cursor += 1;
            let mut input = query_input(query, Some(VectorQueryMode::Ann));
            input.filter.insert(
                "shard".to_string(),
                json!({ "$in": ["shard-0", "shard-1"] }),
            );
            vectors.query("bench", "docs", "main", input).unwrap()
        });
    });
    group.finish();
}

criterion_group!(benches, bench_upsert, bench_query);
criterion_main!(benches);