velesdb-core 1.14.1

High-performance vector database engine written in Rust
Documentation
//! Tests for `AsyncIndexBuilder`.

use super::async_index_builder::{AsyncIndexBuilder, AsyncIndexBuilderConfig};
use crate::distance::DistanceMetric;
use crate::index::hnsw::HnswIndex;

fn default_config() -> AsyncIndexBuilderConfig {
    AsyncIndexBuilderConfig {
        merge_threshold: 100,
        segment_count: Some(2),
    }
}

fn make_index(dim: usize) -> HnswIndex {
    HnswIndex::new(dim, DistanceMetric::Cosine).expect("test index creation")
}

#[test]
fn test_new_creates_empty_builder() {
    let builder = AsyncIndexBuilder::new(default_config());
    assert_eq!(builder.buffer_len(), 0);
    assert!(!builder.is_building());
}

#[test]
fn test_enqueue_adds_vectors() {
    let builder = AsyncIndexBuilder::new(default_config());
    let vectors = vec![(1_u64, vec![1.0_f32, 0.0, 0.0])];
    let triggered = builder.enqueue(vectors);
    assert!(!triggered);
    assert_eq!(builder.buffer_len(), 1);
}

#[test]
fn test_enqueue_returns_true_at_threshold() {
    let config = AsyncIndexBuilderConfig {
        merge_threshold: 3,
        segment_count: Some(1),
    };
    let builder = AsyncIndexBuilder::new(config);

    assert!(!builder.enqueue(vec![(1, vec![1.0])]));
    assert!(!builder.enqueue(vec![(2, vec![2.0])]));
    assert!(builder.enqueue(vec![(3, vec![3.0])]));
}

#[test]
fn test_drain_buffer_returns_all_and_empties() {
    let builder = AsyncIndexBuilder::new(default_config());
    builder.enqueue(vec![(1, vec![1.0, 0.0]), (2, vec![0.0, 1.0])]);
    assert_eq!(builder.buffer_len(), 2);

    let drained = builder.drain_buffer();
    assert_eq!(drained.len(), 2);
    assert_eq!(builder.buffer_len(), 0);
}

#[test]
fn test_search_buffer_finds_vectors() {
    let builder = AsyncIndexBuilder::new(default_config());
    builder.enqueue(vec![
        (1, vec![1.0, 0.0, 0.0]),
        (2, vec![0.0, 1.0, 0.0]),
        (3, vec![0.0, 0.0, 1.0]),
    ]);

    let results = builder.search_buffer(&[1.0, 0.0, 0.0], 2, DistanceMetric::Cosine);
    assert!(!results.is_empty());
    // For cosine, the identical vector should be first
    assert_eq!(results[0].0, 1);
}

#[test]
fn test_search_buffer_empty() {
    let builder = AsyncIndexBuilder::new(default_config());
    let results = builder.search_buffer(&[1.0, 0.0], 5, DistanceMetric::Cosine);
    assert!(results.is_empty());
}

#[test]
fn test_flush_sync_indexes_vectors() {
    let dim = 4;
    let index = make_index(dim);
    let builder = AsyncIndexBuilder::new(default_config());

    // Enqueue some vectors
    let vectors: Vec<(u64, Vec<f32>)> = (0..20)
        .map(|i| {
            let mut v = vec![0.0_f32; dim];
            v[i % dim] = 1.0;
            (i as u64, v)
        })
        .collect();

    builder.enqueue(vectors);
    assert_eq!(builder.buffer_len(), 20);

    // Flush synchronously
    let indexed = builder.flush_sync(&index).unwrap();
    assert_eq!(indexed, 20);
    assert_eq!(builder.buffer_len(), 0);
    assert_eq!(index.len(), 20);
}

#[test]
fn test_flush_sync_empty_buffer() {
    let index = make_index(4);
    let builder = AsyncIndexBuilder::new(default_config());
    let indexed = builder.flush_sync(&index).unwrap();
    assert_eq!(indexed, 0);
}

#[test]
fn test_merge_threshold_accessor() {
    let config = AsyncIndexBuilderConfig {
        merge_threshold: 5000,
        ..default_config()
    };
    let builder = AsyncIndexBuilder::new(config);
    assert_eq!(builder.merge_threshold(), 5000);
}

#[test]
fn test_config_serde_roundtrip() {
    let config = AsyncIndexBuilderConfig {
        merge_threshold: 5000,
        segment_count: Some(8),
    };
    let json = serde_json::to_string(&config).expect("serialize");
    let restored: AsyncIndexBuilderConfig = serde_json::from_str(&json).expect("deserialize");
    assert_eq!(restored.merge_threshold, 5000);
    assert_eq!(restored.segment_count, Some(8));
}

#[test]
fn test_config_serde_defaults() {
    let json = "{}";
    let config: AsyncIndexBuilderConfig = serde_json::from_str(json).expect("deserialize empty");
    assert_eq!(config.merge_threshold, 10_000);
    assert!(config.segment_count.is_none());
}

/// Legacy `config.json` files persisted before the removal of
/// `sync_mode` must continue to deserialize. serde ignores unknown
/// fields by default, so the stale value is silently dropped.
#[test]
fn test_config_legacy_sync_mode_field_is_ignored() {
    let legacy_json = r#"{"merge_threshold": 500, "segment_count": 4, "sync_mode": true}"#;
    let config: AsyncIndexBuilderConfig =
        serde_json::from_str(legacy_json).expect("legacy config must still deserialize");
    assert_eq!(config.merge_threshold, 500);
    assert_eq!(config.segment_count, Some(4));
}

#[test]
fn test_trigger_build_async_indexes_in_background() {
    let dim = 4;
    let index = std::sync::Arc::new(make_index(dim));
    let builder = AsyncIndexBuilder::new(default_config());

    // Enqueue vectors
    let vectors: Vec<(u64, Vec<f32>)> = (0..20)
        .map(|i| {
            let mut v = vec![0.0_f32; dim];
            v[i % dim] = 1.0;
            (i as u64, v)
        })
        .collect();

    builder.enqueue(vectors);
    assert_eq!(builder.buffer_len(), 20);

    // Trigger async build
    builder.trigger_build_async(&index);

    // Wait for background thread to finish (poll with timeout)
    let start = std::time::Instant::now();
    while builder.is_building() {
        std::thread::sleep(std::time::Duration::from_millis(10));
        if start.elapsed() > std::time::Duration::from_secs(10) {
            panic!("background build did not complete within 10s");
        }
    }

    // Buffer should be drained and index populated
    assert_eq!(builder.buffer_len(), 0);
    assert_eq!(index.len(), 20);
}

#[test]
fn test_trigger_build_async_noop_when_empty() {
    let dim = 4;
    let index = std::sync::Arc::new(make_index(dim));
    let builder = AsyncIndexBuilder::new(default_config());

    // Trigger on empty buffer — should be a no-op
    builder.trigger_build_async(&index);

    // Should not be building (empty buffer returns immediately)
    assert!(!builder.is_building());
    assert_eq!(index.len(), 0);
}

#[test]
fn test_trigger_build_async_skips_when_already_building() {
    let dim = 4;
    let index = std::sync::Arc::new(make_index(dim));
    let builder = AsyncIndexBuilder::new(default_config());

    // Enqueue vectors
    let vectors: Vec<(u64, Vec<f32>)> = (0..10)
        .map(|i| {
            let mut v = vec![0.0_f32; dim];
            v[i % dim] = 1.0;
            (i as u64, v)
        })
        .collect();
    builder.enqueue(vectors);

    // Trigger first build
    builder.trigger_build_async(&index);

    // Enqueue more while building
    let more: Vec<(u64, Vec<f32>)> = (10..15)
        .map(|i| {
            let mut v = vec![0.0_f32; dim];
            v[i % dim] = 1.0;
            (i as u64, v)
        })
        .collect();
    builder.enqueue(more);

    // Second trigger should be skipped (building flag is set)
    builder.trigger_build_async(&index);

    // Wait for first build to complete
    let start = std::time::Instant::now();
    while builder.is_building() {
        std::thread::sleep(std::time::Duration::from_millis(10));
        if start.elapsed() > std::time::Duration::from_secs(10) {
            panic!("background build did not complete within 10s");
        }
    }

    // First batch indexed, second batch still in buffer
    assert_eq!(index.len(), 10);
    assert_eq!(builder.buffer_len(), 5);
}