feox-ann 0.1.0

Dependency-free HNSW approximate nearest neighbor index with deterministic, reproducible builds
Documentation
use crate::{AnnConfig, AnnIndex, AnnQuery};

#[test]
fn queries_nearest_vectors() {
    let mut index = AnnIndex::new(AnnConfig::for_dimensions(2)).unwrap();
    index.upsert("north".to_string(), &[1.0, 0.0]).unwrap();
    index.upsert("east".to_string(), &[0.0, 1.0]).unwrap();
    index.upsert("south".to_string(), &[-1.0, 0.0]).unwrap();

    let matches = index
        .query(AnnQuery {
            vector: &[0.9, 0.1],
            top_k: 2,
            ef_search: Some(8),
            filter: None,
        })
        .unwrap();

    assert_eq!(matches[0].id, "north");
    assert!(matches[0].score > matches[1].score);
}

#[test]
fn delete_hides_records_from_results() {
    let mut index = AnnIndex::new(AnnConfig::for_dimensions(2)).unwrap();
    index.upsert("north".to_string(), &[1.0, 0.0]).unwrap();
    index.upsert("east".to_string(), &[0.0, 1.0]).unwrap();

    assert!(index.delete("north"));
    let matches = index
        .query(AnnQuery {
            vector: &[1.0, 0.0],
            top_k: 2,
            ef_search: Some(8),
            filter: None,
        })
        .unwrap();

    assert_eq!(matches.len(), 1);
    assert_eq!(matches[0].id, "east");
}

#[test]
fn upsert_replaces_visible_record() {
    let mut index = AnnIndex::new(AnnConfig::for_dimensions(2)).unwrap();
    index.upsert("item".to_string(), &[1.0, 0.0]).unwrap();
    index.upsert("item".to_string(), &[0.0, 1.0]).unwrap();

    let matches = index
        .query(AnnQuery {
            vector: &[0.0, 1.0],
            top_k: 1,
            ef_search: Some(8),
            filter: None,
        })
        .unwrap();

    assert_eq!(index.len(), 1);
    assert_eq!(matches[0].id, "item");
    assert!(matches[0].score > 0.99);
}

#[test]
fn query_applies_filter_to_results() {
    let mut index = AnnIndex::new(AnnConfig::for_dimensions(2)).unwrap();
    index.upsert("north".to_string(), &[1.0, 0.0]).unwrap();
    index.upsert("north-east".to_string(), &[0.8, 0.2]).unwrap();
    index.upsert("east".to_string(), &[0.0, 1.0]).unwrap();

    let filter = |id: &str| id == "north-east" || id == "east";
    let matches = index
        .query(AnnQuery {
            vector: &[1.0, 0.0],
            top_k: 2,
            ef_search: Some(8),
            filter: Some(&filter),
        })
        .unwrap();

    assert_eq!(matches[0].id, "north-east");
    assert_eq!(matches[1].id, "east");
}

#[test]
fn rebuilds_are_deterministic() {
    let build = || {
        let mut index = AnnIndex::new(AnnConfig::for_dimensions(4)).unwrap();
        for i in 0..200 {
            let angle = i as f32 * 0.031;
            index
                .upsert(
                    format!("record-{i:04}"),
                    &[
                        angle.sin(),
                        angle.cos(),
                        (angle * 2.0).sin(),
                        (angle * 2.0).cos(),
                    ],
                )
                .unwrap();
        }
        index
    };

    let first = build();
    let second = build();

    for i in 0..20 {
        let angle = i as f32 * 0.17;
        let query = AnnQuery {
            vector: &[
                angle.sin(),
                angle.cos(),
                (angle * 2.0).sin(),
                (angle * 2.0).cos(),
            ],
            top_k: 10,
            ef_search: Some(32),
            filter: None,
        };
        let left = first.query(query).unwrap();
        let right = second.query(query).unwrap();
        assert_eq!(
            left, right,
            "identical builds must return identical results"
        );
    }
}