sqlitegraph 3.9.0

Embedded graph database with explicit SQLite, native-v3, and combined backend modes; HNSW vector search; and graph algorithms
Documentation
use super::*;
use tempfile::TempDir;

#[cfg(feature = "turbovec")]
#[test]
fn test_hnsw_vector_search_normalizes_ids_and_supports_exact_override() {
    let temp_dir = TempDir::new().unwrap();
    let db_path = temp_dir.path().join("hnsw.graph");
    let backend = V3Backend::create(&db_path).unwrap();
    let dimension = 128;

    backend.create_hnsw_index("vectors", dimension).unwrap();

    let make_vector = |seed: usize| -> Vec<f32> {
        (0..dimension)
            .map(|j| {
                let mut x = ((seed as u64) << 32) ^ j as u64 ^ 0x9E37_79B9_7F4A_7C15;
                x ^= x >> 30;
                x = x.wrapping_mul(0xBF58_476D_1CE4_E5B9);
                x ^= x >> 27;
                x = x.wrapping_mul(0x94D0_49BB_1331_11EB);
                x ^= x >> 31;
                if x & 1 == 0 { -1.0 } else { 1.0 }
            })
            .collect()
    };

    for i in 0..100 {
        let node_id = 10_000 + i as i64;
        backend
            .insert_hnsw_vector(
                "vectors",
                &make_vector(i),
                Some(serde_json::json!({"node_id": node_id})),
            )
            .unwrap();
    }

    let small_result = backend
        .hnsw_vector_search("vectors", &make_vector(42), 1)
        .unwrap();
    assert!(
        (10_000..11_002).contains(&small_result[0].0),
        "expected normalized node_id, got {}",
        small_result[0].0
    );

    for i in 100..1002 {
        let node_id = 10_000 + i as i64;
        backend
            .insert_hnsw_vector(
                "vectors",
                &make_vector(i),
                Some(serde_json::json!({"node_id": node_id})),
            )
            .unwrap();
    }

    assert_eq!(backend.hnsw_embedding_count("vectors").unwrap(), 1002);
    assert!(backend.hnsw_turbovec_ready("vectors").unwrap());

    {
        let metadata = backend.hnsw_indexes.read();
        let index = metadata.get("vectors").unwrap();
        let mut turbovec = index.turbovec_index.lock().unwrap();
        *turbovec = None;
    }
    assert!(!backend.hnsw_turbovec_ready("vectors").unwrap());

    let exact_result = backend
        .hnsw_vector_search_with_config(
            "vectors",
            &make_vector(1001),
            1,
            HnswSearchConfig {
                force_exact: true,
                ef_search_override: Some(100),
            },
        )
        .unwrap();
    assert!(
        (10_000..11_002).contains(&exact_result[0].0),
        "expected normalized node_id, got {}",
        exact_result[0].0
    );
    assert!(
        !backend.hnsw_turbovec_ready("vectors").unwrap(),
        "force_exact should not rebuild the turbovec cache"
    );

    let turbovec_result = backend
        .hnsw_vector_search("vectors", &make_vector(1001), 1)
        .unwrap();
    assert!(
        (10_000..11_002).contains(&turbovec_result[0].0),
        "expected normalized node_id, got {}",
        turbovec_result[0].0
    );
    assert!(
        backend.hnsw_turbovec_ready("vectors").unwrap(),
        "default search should rebuild the turbovec cache when eligible"
    );
}

#[test]
fn test_hnsw_vector_search_rejects_invalid_ef_override() {
    let temp_dir = TempDir::new().unwrap();
    let db_path = temp_dir.path().join("invalid_ef.graph");
    let backend = V3Backend::create(&db_path).unwrap();

    backend.create_hnsw_index("vectors", 64).unwrap();

    let error = backend
        .hnsw_vector_search_with_config(
            "vectors",
            &[0.0; 64],
            1,
            HnswSearchConfig {
                force_exact: true,
                ef_search_override: Some(0),
            },
        )
        .unwrap_err();
    assert!(
        error.to_string().contains("Invalid ef_search_override"),
        "unexpected error: {error}"
    );
}