rskit-vectorstore 0.2.0-alpha.1

Vector store abstraction with in-memory default and opt-in adapter backends
Documentation
#![allow(missing_docs)]

use rskit_errors::ErrorCode;
use rskit_vectorstore::{
    InMemoryVectorStore, PayloadValue, PointPayload, SearchFilter, SimilarityMetric, VectorStore,
    VectorStoreConfig, VectorStoreLimits,
};
use serde_json::json;

#[test]
fn payload_values_are_typed_scalars_with_validation_limits() {
    assert_eq!(PayloadValue::from("doc").as_str(), Some("doc"));
    assert_eq!(
        serde_json::to_value(PayloadValue::from(7_i32)).unwrap(),
        json!(7)
    );
    assert_eq!(
        serde_json::from_value::<PayloadValue>(json!(true)).unwrap(),
        PayloadValue::Bool(true)
    );
    assert!(
        serde_json::from_value::<PayloadValue>(serde_json::Value::Number(
            serde_json::Number::from(u64::MAX)
        ))
        .is_err()
    );

    let limits = VectorStoreLimits::new()
        .with_max_payload_fields(1)
        .with_max_payload_bytes(9);
    assert!(
        PointPayload::new()
            .with_field("a", 1_i32)
            .validate_limits(&limits)
            .is_ok()
    );
    assert_eq!(
        PointPayload::new()
            .with_field("a", 1_i32)
            .with_field("b", 2_i32)
            .validate_limits(&limits)
            .unwrap_err()
            .code(),
        ErrorCode::InvalidInput
    );
    assert_eq!(
        PointPayload::new()
            .with_field("long-key", "long-value")
            .validate_limits(&limits)
            .unwrap_err()
            .code(),
        ErrorCode::InvalidInput
    );
    assert_eq!(
        PointPayload::new()
            .with_field("x", f64::NAN)
            .validate_limits(&VectorStoreLimits::new())
            .unwrap_err()
            .code(),
        ErrorCode::InvalidInput
    );
}

#[test]
fn payload_value_conversions_cover_all_scalar_shapes() {
    assert_eq!(
        PayloadValue::from(String::from("owned")),
        PayloadValue::String("owned".into())
    );
    assert_eq!(PayloadValue::from(-7_i64), PayloadValue::Integer(-7));
    assert_eq!(PayloadValue::from(7_u32), PayloadValue::Integer(7));
    assert_eq!(PayloadValue::from(1.25_f64), PayloadValue::Float(1.25));
    assert_eq!(PayloadValue::from(1.5_f32), PayloadValue::Float(1.5));
    assert_eq!(PayloadValue::from(false), PayloadValue::Bool(false));
    assert_eq!(
        serde_json::from_value::<PayloadValue>(json!(-5)).unwrap(),
        PayloadValue::Integer(-5)
    );
    assert_eq!(
        serde_json::from_value::<PayloadValue>(json!(5_u64)).unwrap(),
        PayloadValue::Integer(5)
    );
}

#[test]
fn filters_and_limits_reject_unbounded_search_inputs() {
    let limits = VectorStoreLimits::new()
        .with_max_search_limit(2)
        .with_max_filter_conditions(1)
        .with_max_payload_bytes(8);
    assert!(limits.validate_dimensions(1).is_ok());
    assert_eq!(
        limits.validate_dimensions(0).unwrap_err().code(),
        ErrorCode::InvalidInput
    );
    assert_eq!(
        limits.validate_dimensions(33_000).unwrap_err().code(),
        ErrorCode::InvalidInput
    );
    assert_eq!(
        limits.validate_search_limit(0).unwrap_err().code(),
        ErrorCode::InvalidInput
    );
    assert_eq!(
        limits.validate_search_limit(3).unwrap_err().code(),
        ErrorCode::InvalidInput
    );

    let too_many = SearchFilter::new()
        .must_match("a", 1_i32)
        .must_match("b", 2_i32);
    assert_eq!(
        too_many.validate_limits(&limits).unwrap_err().code(),
        ErrorCode::InvalidInput
    );
    let too_large = SearchFilter::new().must_match("long-key", "long-value");
    assert_eq!(
        too_large.validate_limits(&limits).unwrap_err().code(),
        ErrorCode::InvalidInput
    );
    let non_finite = SearchFilter::new().must_match("x", f64::INFINITY);
    assert_eq!(
        non_finite
            .validate_limits(&VectorStoreLimits::new())
            .unwrap_err()
            .code(),
        ErrorCode::InvalidInput
    );
}

#[test]
fn configs_deserialize_defaults_and_metric_names() {
    let config: VectorStoreConfig =
        serde_json::from_value(json!({"memory":{"metric":"dot"}})).unwrap();
    assert_eq!(config.backend, "memory");
    assert_eq!(config.memory.metric, SimilarityMetric::Dot);
    assert_eq!(
        serde_json::to_value(SimilarityMetric::L2).unwrap(),
        json!("l2")
    );
}

#[tokio::test]
async fn in_memory_store_searches_filters_updates_deletes_and_reports_errors() {
    let store = InMemoryVectorStore::with_metric(SimilarityMetric::Dot);
    store.ensure_collection("docs", 2).await.unwrap();
    store
        .upsert(
            "docs",
            "a",
            vec![1.0, 0.0],
            PointPayload::new().with_field("kind", "guide"),
        )
        .await
        .unwrap();
    store
        .upsert(
            "docs",
            "b",
            vec![0.0, 2.0],
            PointPayload::new().with_field("kind", "api"),
        )
        .await
        .unwrap();
    store
        .upsert(
            "docs",
            "a",
            vec![0.0, 3.0],
            PointPayload::new().with_field("kind", "guide"),
        )
        .await
        .unwrap();

    let filtered = store
        .search(
            "docs",
            vec![0.0, 1.0],
            10,
            Some(SearchFilter::new().must_match("kind", "guide")),
        )
        .await
        .unwrap();
    assert_eq!(filtered.len(), 1);
    assert_eq!(filtered[0].id, "a");
    assert!(filtered[0].score > 2.9);

    store.delete("docs", "a").await.unwrap();
    assert!(
        store
            .search("docs", vec![0.0, 1.0], 10, None)
            .await
            .unwrap()
            .iter()
            .all(|r| r.id != "a")
    );
    assert_eq!(
        store
            .upsert("missing", "x", vec![1.0, 0.0], PointPayload::new())
            .await
            .unwrap_err()
            .code(),
        ErrorCode::NotFound
    );
    assert_eq!(
        store
            .search("docs", vec![1.0], 1, None)
            .await
            .unwrap_err()
            .code(),
        ErrorCode::InvalidInput
    );
    assert_eq!(
        store.delete("missing", "x").await.unwrap_err().code(),
        ErrorCode::NotFound
    );
}