use super::*;
use crate::distance::DistanceMetric;
use crate::point::Point;
use serde_json::json;
use tempfile::tempdir;
#[test]
fn test_collection_create() {
let dir = tempdir().unwrap();
let path = dir.path().join("test_collection");
let collection = Collection::create(path, 3, DistanceMetric::Cosine).unwrap();
let config = collection.config();
assert_eq!(config.dimension, 3);
assert_eq!(config.metric, DistanceMetric::Cosine);
assert_eq!(config.point_count, 0);
}
#[test]
fn test_collection_upsert_and_search() {
let dir = tempdir().unwrap();
let path = dir.path().join("test_collection");
let collection = Collection::create(path, 3, DistanceMetric::Cosine).unwrap();
let points = vec![
Point::without_payload(1, vec![1.0, 0.0, 0.0]),
Point::without_payload(2, vec![0.0, 1.0, 0.0]),
Point::without_payload(3, vec![0.0, 0.0, 1.0]),
];
collection.upsert(points).unwrap();
assert_eq!(collection.len(), 3);
let query = vec![1.0, 0.0, 0.0];
let results = collection.search(&query, 2).unwrap();
assert_eq!(results.len(), 2);
assert_eq!(results[0].point.id, 1); }
#[test]
fn test_dimension_mismatch() {
let dir = tempdir().unwrap();
let path = dir.path().join("test_collection");
let collection = Collection::create(path, 3, DistanceMetric::Cosine).unwrap();
let points = vec![Point::without_payload(1, vec![1.0, 0.0])];
let result = collection.upsert(points);
assert!(result.is_err());
}
#[test]
fn test_collection_open_existing() {
let dir = tempdir().unwrap();
let path = dir.path().join("test_collection");
{
let collection = Collection::create(path.clone(), 3, DistanceMetric::Euclidean).unwrap();
let points = vec![
Point::without_payload(1, vec![1.0, 2.0, 3.0]),
Point::without_payload(2, vec![4.0, 5.0, 6.0]),
];
collection.upsert(points).unwrap();
collection.flush().unwrap();
}
let collection = Collection::open(path).unwrap();
let config = collection.config();
assert_eq!(config.dimension, 3);
assert_eq!(config.metric, DistanceMetric::Euclidean);
assert_eq!(collection.len(), 2);
}
#[test]
fn test_collection_get_points() {
let dir = tempdir().unwrap();
let path = dir.path().join("test_collection");
let collection = Collection::create(path, 3, DistanceMetric::Cosine).unwrap();
let points = vec![
Point::without_payload(1, vec![1.0, 0.0, 0.0]),
Point::without_payload(2, vec![0.0, 1.0, 0.0]),
];
collection.upsert(points).unwrap();
let retrieved = collection.get(&[1, 2, 999]);
assert!(retrieved[0].is_some());
assert_eq!(retrieved[0].as_ref().unwrap().id, 1);
assert!(retrieved[1].is_some());
assert_eq!(retrieved[1].as_ref().unwrap().id, 2);
assert!(retrieved[2].is_none()); }
#[test]
fn test_collection_delete_points() {
let dir = tempdir().unwrap();
let path = dir.path().join("test_collection");
let collection = Collection::create(path, 3, DistanceMetric::Cosine).unwrap();
let points = vec![
Point::without_payload(1, vec![1.0, 0.0, 0.0]),
Point::without_payload(2, vec![0.0, 1.0, 0.0]),
Point::without_payload(3, vec![0.0, 0.0, 1.0]),
];
collection.upsert(points).unwrap();
assert_eq!(collection.len(), 3);
collection.delete(&[2]).unwrap();
assert_eq!(collection.len(), 2);
let retrieved = collection.get(&[2]);
assert!(retrieved[0].is_none());
}
#[test]
fn test_collection_is_empty() {
let dir = tempdir().unwrap();
let path = dir.path().join("test_collection");
let collection = Collection::create(path, 3, DistanceMetric::Cosine).unwrap();
assert!(collection.is_empty());
collection
.upsert(vec![Point::without_payload(1, vec![1.0, 0.0, 0.0])])
.unwrap();
assert!(!collection.is_empty());
}
#[test]
fn test_collection_with_payload() {
let dir = tempdir().unwrap();
let path = dir.path().join("test_collection");
let collection = Collection::create(path, 3, DistanceMetric::Cosine).unwrap();
let points = vec![Point::new(
1,
vec![1.0, 0.0, 0.0],
Some(json!({"title": "Test Document", "category": "tech"})),
)];
collection.upsert(points).unwrap();
let retrieved = collection.get(&[1]);
assert!(retrieved[0].is_some());
let point = retrieved[0].as_ref().unwrap();
assert!(point.payload.is_some());
assert_eq!(point.payload.as_ref().unwrap()["title"], "Test Document");
}
#[test]
fn test_collection_search_dimension_mismatch() {
let dir = tempdir().unwrap();
let path = dir.path().join("test_collection");
let collection = Collection::create(path, 3, DistanceMetric::Cosine).unwrap();
collection
.upsert(vec![Point::without_payload(1, vec![1.0, 0.0, 0.0])])
.unwrap();
let result = collection.search(&[1.0, 0.0], 5);
assert!(result.is_err());
}
#[test]
fn test_collection_search_ids_fast() {
let dir = tempdir().unwrap();
let path = dir.path().join("test_collection");
let collection = Collection::create(path, 3, DistanceMetric::Cosine).unwrap();
collection
.upsert(vec![
Point::without_payload(1, vec![1.0, 0.0, 0.0]),
Point::without_payload(2, vec![0.9, 0.1, 0.0]),
Point::without_payload(3, vec![0.0, 1.0, 0.0]),
])
.unwrap();
let results = collection.search_ids(&[1.0, 0.0, 0.0], 2).unwrap();
assert_eq!(results.len(), 2);
assert_eq!(results[0].id, 1); assert!(results[0].score > results[1].score); }
#[test]
fn test_collection_upsert_replaces_payload() {
let dir = tempdir().unwrap();
let path = dir.path().join("test_collection");
let collection = Collection::create(path, 3, DistanceMetric::Cosine).unwrap();
collection
.upsert(vec![Point::new(
1,
vec![1.0, 0.0, 0.0],
Some(json!({"version": 1})),
)])
.unwrap();
collection
.upsert(vec![Point::without_payload(1, vec![1.0, 0.0, 0.0])])
.unwrap();
let retrieved = collection.get(&[1]);
let point = retrieved[0].as_ref().unwrap();
assert!(point.payload.is_none());
}
#[test]
fn test_collection_flush() {
let dir = tempdir().unwrap();
let path = dir.path().join("test_collection");
let collection = Collection::create(path, 3, DistanceMetric::Cosine).unwrap();
collection
.upsert(vec![Point::without_payload(1, vec![1.0, 0.0, 0.0])])
.unwrap();
let result = collection.flush();
assert!(result.is_ok());
}
#[test]
fn test_collection_euclidean_metric() {
let dir = tempdir().unwrap();
let path = dir.path().join("test_collection");
let collection = Collection::create(path, 3, DistanceMetric::Euclidean).unwrap();
let points = vec![
Point::without_payload(1, vec![0.0, 0.0, 0.0]),
Point::without_payload(2, vec![1.0, 0.0, 0.0]),
Point::without_payload(3, vec![10.0, 0.0, 0.0]),
];
collection.upsert(points).unwrap();
let query = vec![0.5, 0.0, 0.0];
let results = collection.search(&query, 3).unwrap();
assert!(results[0].point.id == 1 || results[0].point.id == 2);
}
#[test]
fn test_collection_text_search() {
let dir = tempdir().unwrap();
let path = dir.path().join("test_collection");
let collection = Collection::create(path, 3, DistanceMetric::Cosine).unwrap();
let points = vec![
Point::new(
1,
vec![1.0, 0.0, 0.0],
Some(json!({"title": "Rust Programming", "content": "Learn Rust language"})),
),
Point::new(
2,
vec![0.0, 1.0, 0.0],
Some(json!({"title": "Python Tutorial", "content": "Python is great"})),
),
Point::new(
3,
vec![0.0, 0.0, 1.0],
Some(json!({"title": "Rust Performance", "content": "Rust is fast"})),
),
];
collection.upsert(points).unwrap();
let results = collection.text_search("rust", 10).unwrap();
assert_eq!(results.len(), 2);
let ids: Vec<u64> = results.iter().map(|r| r.point.id).collect();
assert!(ids.contains(&1));
assert!(ids.contains(&3));
}
#[test]
fn test_collection_hybrid_search() {
let dir = tempdir().unwrap();
let path = dir.path().join("test_collection");
let collection = Collection::create(path, 3, DistanceMetric::Cosine).unwrap();
let points = vec![
Point::new(
1,
vec![1.0, 0.0, 0.0],
Some(json!({"title": "Rust Programming"})),
),
Point::new(
2,
vec![0.9, 0.1, 0.0], Some(json!({"title": "Python Programming"})),
),
Point::new(
3,
vec![0.0, 1.0, 0.0],
Some(json!({"title": "Rust Performance"})),
),
];
collection.upsert(points).unwrap();
let query = vec![1.0, 0.0, 0.0];
let results = collection
.hybrid_search(&query, "rust", 3, Some(0.5), None)
.unwrap();
assert!(!results.is_empty());
assert_eq!(results[0].point.id, 1);
}
#[test]
fn test_extract_text_from_payload() {
let payload = json!({
"title": "Hello",
"meta": {
"author": "World",
"tags": ["rust", "fast"]
}
});
let text = Collection::extract_text_from_payload(&payload);
assert!(text.contains("Hello"));
assert!(text.contains("World"));
assert!(text.contains("rust"));
assert!(text.contains("fast"));
}
#[test]
fn test_text_search_empty_query() {
let dir = tempdir().unwrap();
let path = dir.path().join("test_collection");
let collection = Collection::create(path, 3, DistanceMetric::Cosine).unwrap();
let points = vec![Point::new(
1,
vec![1.0, 0.0, 0.0],
Some(json!({"content": "test document"})),
)];
collection.upsert(points).unwrap();
let results = collection.text_search("", 10).unwrap();
assert!(results.is_empty());
}
#[test]
fn test_text_search_no_payload() {
let dir = tempdir().unwrap();
let path = dir.path().join("test_collection");
let collection = Collection::create(path, 3, DistanceMetric::Cosine).unwrap();
let points = vec![
Point::new(1, vec![1.0, 0.0, 0.0], None),
Point::new(2, vec![0.0, 1.0, 0.0], None),
];
collection.upsert(points).unwrap();
let results = collection.text_search("test", 10).unwrap();
assert!(results.is_empty());
}
#[test]
fn test_hybrid_search_text_weight_zero() {
let dir = tempdir().unwrap();
let path = dir.path().join("test_collection");
let collection = Collection::create(path, 3, DistanceMetric::Cosine).unwrap();
let points = vec![
Point::new(1, vec![1.0, 0.0, 0.0], Some(json!({"title": "Rust"}))),
Point::new(2, vec![0.9, 0.1, 0.0], Some(json!({"title": "Python"}))),
];
collection.upsert(points).unwrap();
let query = vec![0.9, 0.1, 0.0];
let results = collection
.hybrid_search(&query, "rust", 2, Some(1.0), None)
.unwrap();
assert_eq!(results[0].point.id, 2);
}
#[test]
fn test_hybrid_search_vector_weight_zero() {
let dir = tempdir().unwrap();
let path = dir.path().join("test_collection");
let collection = Collection::create(path, 3, DistanceMetric::Cosine).unwrap();
let points = vec![
Point::new(
1,
vec![1.0, 0.0, 0.0],
Some(json!({"title": "Rust programming language"})),
),
Point::new(
2,
vec![0.99, 0.01, 0.0], Some(json!({"title": "Python programming"})),
),
];
collection.upsert(points).unwrap();
let query = vec![0.99, 0.01, 0.0];
let results = collection
.hybrid_search(&query, "rust", 2, Some(0.0), None)
.unwrap();
assert_eq!(results[0].point.id, 1);
}
#[test]
fn test_bm25_update_document() {
let dir = tempdir().unwrap();
let path = dir.path().join("test_collection");
let collection = Collection::create(path, 3, DistanceMetric::Cosine).unwrap();
let points = vec![Point::new(
1,
vec![1.0, 0.0, 0.0],
Some(json!({"content": "rust programming"})),
)];
collection.upsert(points).unwrap();
let results = collection.text_search("rust", 10).unwrap();
assert_eq!(results.len(), 1);
let points = vec![Point::new(
1,
vec![1.0, 0.0, 0.0],
Some(json!({"content": "python programming"})),
)];
collection.upsert(points).unwrap();
let results = collection.text_search("rust", 10).unwrap();
assert!(results.is_empty());
let results = collection.text_search("python", 10).unwrap();
assert_eq!(results.len(), 1);
}
#[test]
fn test_bm25_large_dataset() {
let dir = tempdir().unwrap();
let path = dir.path().join("test_collection");
let collection = Collection::create(path, 4, DistanceMetric::Cosine).unwrap();
let points: Vec<Point> = (0..100)
.map(|i| {
let content = if i % 10 == 0 {
format!("rust document number {i}")
} else {
format!("other document number {i}")
};
Point::new(
i,
vec![0.1, 0.2, 0.3, 0.4],
Some(json!({"content": content})),
)
})
.collect();
collection.upsert(points).unwrap();
let results = collection.text_search("rust", 100).unwrap();
assert_eq!(results.len(), 10);
for result in &results {
assert_eq!(result.point.id % 10, 0);
}
}
#[test]
fn test_bm25_persistence_on_reopen() {
let dir = tempdir().unwrap();
let path = dir.path().join("test_collection");
{
let collection = Collection::create(path.clone(), 4, DistanceMetric::Cosine).unwrap();
let points = vec![
Point::new(
1,
vec![1.0, 0.0, 0.0, 0.0],
Some(json!({"content": "Rust programming language"})),
),
Point::new(
2,
vec![0.0, 1.0, 0.0, 0.0],
Some(json!({"content": "Python tutorial"})),
),
Point::new(
3,
vec![0.0, 0.0, 1.0, 0.0],
Some(json!({"content": "Rust is fast and safe"})),
),
];
collection.upsert(points).unwrap();
let results = collection.text_search("rust", 10).unwrap();
assert_eq!(results.len(), 2);
}
{
let collection = Collection::open(path).unwrap();
let results = collection.text_search("rust", 10).unwrap();
assert_eq!(results.len(), 2);
let ids: Vec<u64> = results.iter().map(|r| r.point.id).collect();
assert!(ids.contains(&1));
assert!(ids.contains(&3));
}
}
#[test]
fn test_upsert_bulk_basic() {
let dir = tempdir().unwrap();
let path = dir.path().join("test_collection");
let collection = Collection::create(path, 3, DistanceMetric::Cosine).unwrap();
let points = vec![
Point::new(1, vec![1.0, 0.0, 0.0], None),
Point::new(2, vec![0.0, 1.0, 0.0], None),
Point::new(3, vec![0.0, 0.0, 1.0], None),
];
let inserted = collection.upsert_bulk(&points).unwrap();
assert_eq!(inserted, 3);
assert_eq!(collection.len(), 3);
}
#[test]
fn test_upsert_bulk_with_payload() {
let dir = tempdir().unwrap();
let path = dir.path().join("test_collection");
let collection = Collection::create(path, 3, DistanceMetric::Cosine).unwrap();
let points = vec![
Point::new(1, vec![1.0, 0.0, 0.0], Some(json!({"title": "Doc 1"}))),
Point::new(2, vec![0.0, 1.0, 0.0], Some(json!({"title": "Doc 2"}))),
];
collection.upsert_bulk(&points).unwrap();
let retrieved = collection.get(&[1, 2]);
assert_eq!(retrieved.len(), 2);
assert!(retrieved[0].as_ref().unwrap().payload.is_some());
}
#[test]
fn test_upsert_bulk_empty() {
let dir = tempdir().unwrap();
let path = dir.path().join("test_collection");
let collection = Collection::create(path, 3, DistanceMetric::Cosine).unwrap();
let points: Vec<Point> = vec![];
let inserted = collection.upsert_bulk(&points).unwrap();
assert_eq!(inserted, 0);
}
#[test]
fn test_upsert_bulk_dimension_mismatch() {
let dir = tempdir().unwrap();
let path = dir.path().join("test_collection");
let collection = Collection::create(path, 3, DistanceMetric::Cosine).unwrap();
let points = vec![
Point::new(1, vec![1.0, 0.0, 0.0], None),
Point::new(2, vec![0.0, 1.0], None), ];
let result = collection.upsert_bulk(&points);
assert!(result.is_err());
}
#[test]
#[allow(clippy::cast_precision_loss)]
fn test_upsert_bulk_large_batch() {
let dir = tempdir().unwrap();
let path = dir.path().join("test_collection");
let collection = Collection::create(path, 32, DistanceMetric::Cosine).unwrap();
let points: Vec<Point> = (0_u64..100)
.map(|i| {
let vector: Vec<f32> = (0_u64..32)
.map(|j| ((i + j) % 100) as f32 / 100.0)
.collect();
Point::new(i, vector, None)
})
.collect();
let inserted = collection.upsert_bulk(&points).unwrap();
assert_eq!(inserted, 100);
assert_eq!(collection.len(), 100);
}
#[test]
fn test_upsert_bulk_search_works() {
let dir = tempdir().unwrap();
let path = dir.path().join("test_collection");
let collection = Collection::create(path, 3, DistanceMetric::Cosine).unwrap();
let points = vec![
Point::new(1, vec![1.0, 0.0, 0.0], None),
Point::new(2, vec![0.0, 1.0, 0.0], None),
Point::new(3, vec![0.0, 0.0, 1.0], None),
];
collection.upsert_bulk(&points).unwrap();
let query = vec![1.0, 0.0, 0.0];
let results = collection.search(&query, 3).unwrap();
assert!(!results.is_empty());
assert_eq!(results[0].point.id, 1);
}
#[test]
fn test_upsert_bulk_bm25_indexing() {
let dir = tempdir().unwrap();
let path = dir.path().join("test_collection");
let collection = Collection::create(path, 3, DistanceMetric::Cosine).unwrap();
let points = vec![
Point::new(
1,
vec![1.0, 0.0, 0.0],
Some(json!({"content": "Rust lang"})),
),
Point::new(2, vec![0.0, 1.0, 0.0], Some(json!({"content": "Python"}))),
Point::new(
3,
vec![0.0, 0.0, 1.0],
Some(json!({"content": "Rust fast"})),
),
];
collection.upsert_bulk(&points).unwrap();
let results = collection.text_search("rust", 10).unwrap();
assert_eq!(results.len(), 2);
}
#[test]
fn test_search_with_filter_basic_equality() {
let dir = tempdir().unwrap();
let path = dir.path().join("test_collection");
let collection = Collection::create(path, 3, DistanceMetric::Cosine).unwrap();
let mut points = vec![
Point::new(
1,
vec![1.0, 0.0, 0.0],
Some(json!({"category": "tech", "price": 100})),
),
Point::new(
2,
vec![0.9, 0.1, 0.0],
Some(json!({"category": "science", "price": 200})),
),
Point::new(
3,
vec![0.95, 0.05, 0.0],
Some(json!({"category": "tech", "price": 150})),
),
Point::new(
4,
vec![0.7, 0.3, 0.0],
Some(json!({"category": "science", "price": 120})),
),
Point::new(
5,
vec![0.85, 0.15, 0.0],
Some(json!({"category": "tech", "price": 80})),
),
];
for i in 6u64..20 {
let cat = if i % 2 == 0 { "other" } else { "misc" };
#[allow(clippy::cast_precision_loss)]
let v = vec![0.5 + (i as f32 * 0.02), 0.3, 0.2];
points.push(Point::new(i, v, Some(json!({"category": cat}))));
}
collection.upsert(points).unwrap();
let filter = crate::filter::Filter::new(crate::filter::Condition::eq("category", "tech"));
let query = vec![1.0, 0.0, 0.0];
let results = collection.search_with_filter(&query, 10, &filter).unwrap();
assert!(!results.is_empty(), "Expected tech results, got none");
let ids: Vec<u64> = results.iter().map(|r| r.point.id).collect();
assert!(
ids.iter().all(|id| *id == 1 || *id == 3 || *id == 5),
"Expected only tech IDs (1, 3, 5), got {ids:?}"
);
}
#[test]
fn test_search_with_filter_range() {
let dir = tempdir().unwrap();
let path = dir.path().join("test_collection");
let collection = Collection::create(path, 3, DistanceMetric::Cosine).unwrap();
let points = vec![
Point::new(1, vec![1.0, 0.0, 0.0], Some(json!({"price": 50}))),
Point::new(2, vec![0.95, 0.05, 0.0], Some(json!({"price": 150}))),
Point::new(3, vec![0.9, 0.1, 0.0], Some(json!({"price": 250}))),
Point::new(4, vec![0.5, 0.5, 0.0], Some(json!({"price": 75}))),
Point::new(5, vec![0.3, 0.7, 0.0], Some(json!({"price": 200}))),
];
collection.upsert(points).unwrap();
let filter = crate::filter::Filter::new(crate::filter::Condition::gt("price", 100));
let query = vec![1.0, 0.0, 0.0];
let results = collection.search_with_filter(&query, 10, &filter).unwrap();
assert!(
results.len() >= 2,
"Expected at least 2 results, got {}",
results.len()
);
let ids: Vec<u64> = results.iter().map(|r| r.point.id).collect();
let matching_ids: Vec<u64> = ids
.iter()
.filter(|id| [2, 3, 5].contains(id))
.copied()
.collect();
assert!(
!matching_ids.is_empty(),
"Expected matching IDs from [2, 3, 5], got {ids:?}"
);
}
#[test]
fn test_search_batch_with_filters() {
let dir = tempdir().unwrap();
let path = dir.path().join("test_collection");
let collection = Collection::create(path, 3, DistanceMetric::Cosine).unwrap();
let points = vec![
Point::new(
1,
vec![1.0, 0.0, 0.0],
Some(json!({"category": "tech", "price": 50})),
),
Point::new(
2,
vec![0.9, 0.1, 0.0],
Some(json!({"category": "science", "price": 150})),
),
Point::new(
3,
vec![0.8, 0.2, 0.0],
Some(json!({"category": "tech", "price": 250})),
),
];
collection.upsert(points).unwrap();
let query1 = vec![1.0, 0.0, 0.0];
let query2 = vec![1.0, 0.0, 0.0];
let queries = vec![query1.as_slice(), query2.as_slice()];
let category_filter = Some(crate::filter::Filter::new(crate::filter::Condition::eq(
"category", "science",
)));
let price_filter = Some(crate::filter::Filter::new(crate::filter::Condition::gt(
"price", 200,
)));
let batch_filters = vec![category_filter, price_filter];
let all_results = collection
.search_batch_with_filters(&queries, 10, &batch_filters)
.unwrap();
assert_eq!(all_results.len(), 2);
assert!(
!all_results[0].is_empty(),
"Expected at least 1 science result"
);
assert!(all_results[0].iter().all(|r| r.point.id == 2));
assert!(
!all_results[1].is_empty(),
"Expected at least 1 high-price result"
);
assert!(all_results[1].iter().all(|r| r.point.id == 3));
}
#[test]
fn test_search_batch_with_some_filters_none() {
let dir = tempdir().unwrap();
let path = dir.path().join("test_collection");
let collection = Collection::create(path, 3, DistanceMetric::Cosine).unwrap();
let points = vec![
Point::new(1, vec![1.0, 0.0, 0.0], Some(json!({"category": "tech"}))),
Point::new(2, vec![0.9, 0.1, 0.0], Some(json!({"category": "science"}))),
Point::new(3, vec![0.8, 0.2, 0.0], Some(json!({"category": "tech"}))),
Point::new(4, vec![0.7, 0.3, 0.0], Some(json!({"category": "science"}))),
Point::new(5, vec![0.6, 0.4, 0.0], Some(json!({"category": "tech"}))),
];
collection.upsert(points).unwrap();
let query = vec![1.0, 0.0, 0.0];
let queries = vec![query.as_slice(), query.as_slice()];
let tech_filter = Some(crate::filter::Filter::new(crate::filter::Condition::eq(
"category", "tech",
)));
let mixed_filters = vec![tech_filter, None];
let all_results = collection
.search_batch_with_filters(&queries, 10, &mixed_filters)
.unwrap();
assert_eq!(all_results.len(), 2);
assert!(!all_results[0].is_empty());
assert!(all_results[0]
.iter()
.all(|r| r.point.id == 1 || r.point.id == 3 || r.point.id == 5));
assert!(
all_results[1].len() >= 2,
"Expected at least 2 results, got {}",
all_results[1].len()
);
}
#[test]
fn test_search_with_filter_combined_and() {
let dir = tempdir().unwrap();
let path = dir.path().join("test_collection");
let collection = Collection::create(path, 3, DistanceMetric::Cosine).unwrap();
let points = vec![
Point::new(
1,
vec![1.0, 0.0, 0.0],
Some(json!({"category": "tech", "price": 50})),
),
Point::new(
2,
vec![0.9, 0.1, 0.0],
Some(json!({"category": "tech", "price": 150})),
),
Point::new(
3,
vec![0.8, 0.2, 0.0],
Some(json!({"category": "science", "price": 150})),
),
];
collection.upsert(points).unwrap();
let filter = crate::filter::Filter::new(crate::filter::Condition::and(vec![
crate::filter::Condition::eq("category", "tech"),
crate::filter::Condition::gt("price", 100),
]));
let query = vec![1.0, 0.0, 0.0];
let results = collection.search_with_filter(&query, 10, &filter).unwrap();
assert_eq!(results.len(), 1);
assert_eq!(results[0].point.id, 2);
}
#[test]
fn test_search_with_filter_no_matches() {
let dir = tempdir().unwrap();
let path = dir.path().join("test_collection");
let collection = Collection::create(path, 3, DistanceMetric::Cosine).unwrap();
let points = vec![
Point::new(1, vec![1.0, 0.0, 0.0], Some(json!({"category": "tech"}))),
Point::new(2, vec![0.9, 0.1, 0.0], Some(json!({"category": "science"}))),
];
collection.upsert(points).unwrap();
let filter = crate::filter::Filter::new(crate::filter::Condition::eq("category", "art"));
let query = vec![1.0, 0.0, 0.0];
let results = collection.search_with_filter(&query, 10, &filter).unwrap();
assert!(results.is_empty());
}
#[test]
fn test_search_with_filter_nested_field() {
let dir = tempdir().unwrap();
let path = dir.path().join("test_collection");
let collection = Collection::create(path, 3, DistanceMetric::Cosine).unwrap();
let points = vec![
Point::new(
1,
vec![1.0, 0.0, 0.0],
Some(json!({"metadata": {"author": "Alice"}})),
),
Point::new(
2,
vec![0.9, 0.1, 0.0],
Some(json!({"metadata": {"author": "Bob"}})),
),
];
collection.upsert(points).unwrap();
let filter =
crate::filter::Filter::new(crate::filter::Condition::eq("metadata.author", "Alice"));
let query = vec![1.0, 0.0, 0.0];
let results = collection.search_with_filter(&query, 10, &filter).unwrap();
assert_eq!(results.len(), 1);
assert_eq!(results[0].point.id, 1);
}
#[test]
#[allow(clippy::cast_precision_loss)]
fn test_search_with_filter_respects_k_limit() {
let dir = tempdir().unwrap();
let path = dir.path().join("test_collection");
let collection = Collection::create(path, 3, DistanceMetric::Cosine).unwrap();
let points: Vec<Point> = (1..=10)
.map(|i| {
Point::new(
i,
vec![1.0 - (i as f32 * 0.05), i as f32 * 0.05, 0.0],
Some(json!({"category": "tech"})),
)
})
.collect();
collection.upsert(points).unwrap();
let filter = crate::filter::Filter::new(crate::filter::Condition::eq("category", "tech"));
let query = vec![1.0, 0.0, 0.0];
let results = collection.search_with_filter(&query, 3, &filter).unwrap();
assert_eq!(results.len(), 3);
}
#[test]
fn test_search_with_filter_in_condition() {
let dir = tempdir().unwrap();
let path = dir.path().join("test_collection");
let collection = Collection::create(path, 3, DistanceMetric::Cosine).unwrap();
let points = vec![
Point::new(1, vec![1.0, 0.0, 0.0], Some(json!({"status": "active"}))),
Point::new(2, vec![0.9, 0.1, 0.0], Some(json!({"status": "pending"}))),
Point::new(3, vec![0.8, 0.2, 0.0], Some(json!({"status": "deleted"}))),
];
collection.upsert(points).unwrap();
let filter = crate::filter::Filter::new(crate::filter::Condition::is_in(
"status",
vec![json!("active"), json!("pending")],
));
let query = vec![1.0, 0.0, 0.0];
let results = collection.search_with_filter(&query, 10, &filter).unwrap();
assert_eq!(results.len(), 2);
let ids: Vec<u64> = results.iter().map(|r| r.point.id).collect();
assert!(ids.contains(&1));
assert!(ids.contains(&2));
}
#[test]
fn test_search_with_filter_dimension_mismatch() {
let dir = tempdir().unwrap();
let path = dir.path().join("test_collection");
let collection = Collection::create(path, 3, DistanceMetric::Cosine).unwrap();
collection
.upsert(vec![Point::new(
1,
vec![1.0, 0.0, 0.0],
Some(json!({"category": "tech"})),
)])
.unwrap();
let filter = crate::filter::Filter::new(crate::filter::Condition::eq("category", "tech"));
let result = collection.search_with_filter(&[1.0, 0.0], 10, &filter);
assert!(result.is_err());
}
#[test]
fn test_hybrid_search_with_filter() {
let dir = tempdir().unwrap();
let path = dir.path().join("test_collection");
let collection = Collection::create(path, 3, DistanceMetric::Cosine).unwrap();
let points = vec![
Point::new(
1,
vec![1.0, 0.0, 0.0],
Some(json!({"title": "Rust Programming", "category": "tech"})),
),
Point::new(
2,
vec![0.9, 0.1, 0.0],
Some(json!({"title": "Rust Tutorial", "category": "education"})),
),
Point::new(
3,
vec![0.8, 0.2, 0.0],
Some(json!({"title": "Python Guide", "category": "tech"})),
),
];
collection.upsert(points).unwrap();
let filter = crate::filter::Filter::new(crate::filter::Condition::eq("category", "tech"));
let query = vec![1.0, 0.0, 0.0];
let results = collection
.hybrid_search_with_filter(&query, "rust", 10, Some(0.5), &filter, None)
.unwrap();
assert!(!results.is_empty());
assert_eq!(results[0].point.id, 1);
for r in &results {
assert!(
r.point.id == 1 || r.point.id == 3,
"unexpected id: {}",
r.point.id
);
}
}
#[test]
fn test_text_search_with_filter() {
let dir = tempdir().unwrap();
let path = dir.path().join("test_collection");
let collection = Collection::create(path, 3, DistanceMetric::Cosine).unwrap();
let points = vec![
Point::new(
1,
vec![1.0, 0.0, 0.0],
Some(json!({"title": "Rust Programming", "category": "tech"})),
),
Point::new(
2,
vec![0.9, 0.1, 0.0],
Some(json!({"title": "Rust Tutorial", "category": "education"})),
),
Point::new(
3,
vec![0.8, 0.2, 0.0],
Some(json!({"title": "Rust Guide", "category": "tech"})),
),
];
collection.upsert(points).unwrap();
let filter = crate::filter::Filter::new(crate::filter::Condition::eq("category", "tech"));
let results = collection
.text_search_with_filter("rust", 10, &filter)
.unwrap();
assert_eq!(results.len(), 2);
let ids: Vec<u64> = results.iter().map(|r| r.point.id).collect();
assert!(ids.contains(&1));
assert!(ids.contains(&3));
}
#[test]
fn test_multi_query_search_basic() {
let dir = tempdir().unwrap();
let path = dir.path().join("test_collection");
let collection = Collection::create(path, 3, DistanceMetric::Cosine).unwrap();
let points = vec![
Point::without_payload(1, vec![1.0, 0.0, 0.0]),
Point::without_payload(2, vec![0.0, 1.0, 0.0]),
Point::without_payload(3, vec![0.0, 0.0, 1.0]),
Point::without_payload(4, vec![0.7, 0.7, 0.0]),
Point::without_payload(5, vec![0.5, 0.5, 0.5]),
];
collection.upsert(points).unwrap();
let q1 = vec![1.0, 0.0, 0.0];
let q2 = vec![0.0, 1.0, 0.0];
let results = collection
.multi_query_search(&[&q1, &q2], 3, crate::fusion::FusionStrategy::Average, None)
.unwrap();
assert!(!results.is_empty());
assert!(results.len() <= 3);
let ids: Vec<u64> = results.iter().map(|r| r.point.id).collect();
assert!(ids.contains(&4), "Doc 4 should be in results: {ids:?}");
}
#[test]
fn test_multi_query_search_with_rrf() {
let dir = tempdir().unwrap();
let path = dir.path().join("test_collection");
let collection = Collection::create(path, 3, DistanceMetric::Cosine).unwrap();
let points = vec![
Point::without_payload(1, vec![1.0, 0.0, 0.0]),
Point::without_payload(2, vec![0.9, 0.1, 0.0]),
Point::without_payload(3, vec![0.0, 1.0, 0.0]),
Point::without_payload(4, vec![0.1, 0.9, 0.0]),
];
collection.upsert(points).unwrap();
let q1 = vec![1.0, 0.0, 0.0];
let q2 = vec![0.0, 1.0, 0.0];
let results = collection
.multi_query_search(
&[&q1, &q2],
4,
crate::fusion::FusionStrategy::RRF { k: 60 },
None,
)
.unwrap();
assert_eq!(results.len(), 4);
let ids: Vec<u64> = results.iter().map(|r| r.point.id).collect();
assert!(ids.contains(&1));
assert!(ids.contains(&2));
assert!(ids.contains(&3));
assert!(ids.contains(&4));
}
#[test]
fn test_multi_query_search_with_weighted() {
let dir = tempdir().unwrap();
let path = dir.path().join("test_collection");
let collection = Collection::create(path, 3, DistanceMetric::Cosine).unwrap();
let points = vec![
Point::without_payload(1, vec![1.0, 0.0, 0.0]),
Point::without_payload(2, vec![0.0, 1.0, 0.0]),
Point::without_payload(3, vec![0.5, 0.5, 0.0]),
];
collection.upsert(points).unwrap();
let q1 = vec![1.0, 0.0, 0.0];
let q2 = vec![0.0, 1.0, 0.0];
let q3 = vec![0.5, 0.5, 0.0];
let results = collection
.multi_query_search(
&[&q1, &q2, &q3],
3,
crate::fusion::FusionStrategy::Weighted {
avg_weight: 0.6,
max_weight: 0.3,
hit_weight: 0.1,
},
None,
)
.unwrap();
assert_eq!(results.len(), 3);
}
#[test]
fn test_multi_query_search_single_vector() {
let dir = tempdir().unwrap();
let path = dir.path().join("test_collection");
let collection = Collection::create(path, 3, DistanceMetric::Cosine).unwrap();
let points = vec![
Point::without_payload(1, vec![1.0, 0.0, 0.0]),
Point::without_payload(2, vec![0.9, 0.1, 0.0]),
];
collection.upsert(points).unwrap();
let q1 = vec![1.0, 0.0, 0.0];
let results = collection
.multi_query_search(&[&q1], 2, crate::fusion::FusionStrategy::Average, None)
.unwrap();
assert_eq!(results.len(), 2);
assert_eq!(results[0].point.id, 1);
}
#[test]
fn test_multi_query_search_empty_vectors_error() {
let dir = tempdir().unwrap();
let path = dir.path().join("test_collection");
let collection = Collection::create(path, 3, DistanceMetric::Cosine).unwrap();
let empty: &[&[f32]] = &[];
let result =
collection.multi_query_search(empty, 10, crate::fusion::FusionStrategy::Average, None);
assert!(result.is_err());
}
#[test]
fn test_multi_query_search_dimension_mismatch() {
let dir = tempdir().unwrap();
let path = dir.path().join("test_collection");
let collection = Collection::create(path, 3, DistanceMetric::Cosine).unwrap();
let q1 = vec![1.0, 0.0, 0.0];
let q2 = vec![0.0, 1.0];
let result = collection.multi_query_search(
&[&q1, &q2],
10,
crate::fusion::FusionStrategy::Average,
None,
);
assert!(result.is_err());
}
#[test]
fn test_multi_query_search_max_vectors_limit() {
let dir = tempdir().unwrap();
let path = dir.path().join("test_collection");
let collection = Collection::create(path, 3, DistanceMetric::Cosine).unwrap();
let vectors: Vec<Vec<f32>> = (0..11).map(|_| vec![1.0, 0.0, 0.0]).collect();
let refs: Vec<&[f32]> = vectors.iter().map(Vec::as_slice).collect();
let result =
collection.multi_query_search(&refs, 10, crate::fusion::FusionStrategy::Average, None);
assert!(result.is_err());
}
#[test]
fn test_multi_query_search_with_filter() {
let dir = tempdir().unwrap();
let path = dir.path().join("test_collection");
let collection = Collection::create(path, 3, DistanceMetric::Cosine).unwrap();
let points = vec![
Point::new(1, vec![1.0, 0.0, 0.0], Some(json!({"category": "A"}))),
Point::new(2, vec![0.9, 0.1, 0.0], Some(json!({"category": "B"}))),
Point::new(3, vec![0.0, 1.0, 0.0], Some(json!({"category": "A"}))),
Point::new(4, vec![0.1, 0.9, 0.0], Some(json!({"category": "B"}))),
];
collection.upsert(points).unwrap();
let q1 = vec![1.0, 0.0, 0.0];
let q2 = vec![0.0, 1.0, 0.0];
let filter = crate::filter::Filter::new(crate::filter::Condition::eq("category", "A"));
let results = collection
.multi_query_search(
&[&q1, &q2],
10,
crate::fusion::FusionStrategy::RRF { k: 60 },
Some(&filter),
)
.unwrap();
assert!(!results.is_empty());
for r in &results {
let ids = [1u64, 3u64];
assert!(ids.contains(&r.point.id), "Only A category docs expected");
}
}
#[test]
fn test_multi_query_search_ids() {
let dir = tempdir().unwrap();
let path = dir.path().join("test_collection");
let collection = Collection::create(path, 3, DistanceMetric::Cosine).unwrap();
let points = vec![
Point::without_payload(1, vec![1.0, 0.0, 0.0]),
Point::without_payload(2, vec![0.0, 1.0, 0.0]),
Point::without_payload(3, vec![0.5, 0.5, 0.0]),
];
collection.upsert(points).unwrap();
let q1 = vec![1.0, 0.0, 0.0];
let q2 = vec![0.0, 1.0, 0.0];
let results = collection
.multi_query_search_ids(&[&q1, &q2], 3, crate::fusion::FusionStrategy::Average)
.unwrap();
assert_eq!(results.len(), 3);
let ids: Vec<u64> = results.iter().map(|(id, _)| *id).collect();
assert!(ids.contains(&1));
assert!(ids.contains(&2));
assert!(ids.contains(&3));
}
#[test]
fn test_traverse_bfs_config_respects_min_depth() {
let dir = tempdir().unwrap();
let path = dir.path().join("graph_collection");
let collection = Collection::create_graph_collection(
path,
"graph_collection",
crate::collection::graph::GraphSchema::schemaless(),
None,
DistanceMetric::Cosine,
)
.unwrap();
collection
.add_edge(crate::collection::graph::GraphEdge::new(100, 1, 2, "KNOWS").unwrap())
.unwrap();
collection
.add_edge(crate::collection::graph::GraphEdge::new(101, 2, 3, "KNOWS").unwrap())
.unwrap();
let cfg = crate::collection::graph::TraversalConfig::with_range(2, 3).with_limit(10);
let results = collection.traverse_bfs_config(1, &cfg);
assert!(!results.is_empty());
assert!(results.iter().all(|r| r.depth >= 2));
assert!(results.iter().any(|r| r.target_id == 3 && r.depth == 2));
}
#[test]
fn test_legacy_traverse_paths_use_edge_ids() {
let dir = tempdir().unwrap();
let path = dir.path().join("graph_collection");
let collection = Collection::create_graph_collection(
path,
"graph_collection",
crate::collection::graph::GraphSchema::schemaless(),
None,
DistanceMetric::Cosine,
)
.unwrap();
collection
.add_edge(crate::collection::graph::GraphEdge::new(100, 1, 2, "KNOWS").unwrap())
.unwrap();
collection
.add_edge(crate::collection::graph::GraphEdge::new(101, 2, 3, "KNOWS").unwrap())
.unwrap();
let bfs = collection.traverse_bfs(1, 3, None, 10).unwrap();
let bfs_to_3 = bfs
.iter()
.find(|r| r.target_id == 3 && r.depth == 2)
.expect("BFS must reach node 3 at depth 2");
assert_eq!(bfs_to_3.path.as_slice(), &[100, 101]);
let dfs = collection.traverse_dfs(1, 3, None, 10).unwrap();
let dfs_to_3 = dfs
.iter()
.find(|r| r.target_id == 3 && r.depth == 2)
.expect("DFS must reach node 3 at depth 2");
assert_eq!(dfs_to_3.path.as_slice(), &[100, 101]);
}