use crate::dsl::{Document, SchemaBuilder};
use crate::index::{Index, IndexConfig, IndexWriter};
use crate::query::{BooleanQuery, RangeQuery, SparseVectorQuery, TermQuery};
async fn create_range_test_index() -> (
Index<crate::directories::MmapDirectory>,
crate::dsl::Field, // title
crate::dsl::Field, // price (u64)
crate::dsl::Field, // temp (i64)
crate::dsl::Field, // weight (f64)
) {
use crate::directories::MmapDirectory;
let tmp_dir = tempfile::tempdir().unwrap();
let dir = MmapDirectory::new(tmp_dir.path());
let mut sb = SchemaBuilder::default();
let title = sb.add_text_field("title", true, true);
let price = sb.add_u64_field("price", false, true);
sb.set_fast(price, true);
let temp = sb.add_i64_field("temperature", false, true);
sb.set_fast(temp, true);
let weight = sb.add_f64_field("weight", false, true);
sb.set_fast(weight, true);
let schema = sb.build();
let config = IndexConfig {
max_indexing_memory_bytes: 4096,
..Default::default()
};
let mut writer = IndexWriter::create(dir.clone(), schema, config.clone())
.await
.unwrap();
for i in 0u64..25 {
let mut doc = Document::new();
doc.add_text(title, format!("product_{} electronics", i));
doc.add_u64(price, 100 + i * 10); doc.add_i64(temp, i as i64 * 10 - 50); doc.add_f64(weight, 0.5 + i as f64 * 0.25); writer.add_document(doc).unwrap();
}
writer.commit().await.unwrap();
for i in 25u64..50 {
let mut doc = Document::new();
doc.add_text(title, format!("product_{} clothing", i));
doc.add_u64(price, 100 + i * 10); doc.add_i64(temp, i as i64 * 10 - 50); doc.add_f64(weight, 0.5 + i as f64 * 0.25); writer.add_document(doc).unwrap();
}
writer.commit().await.unwrap();
writer.force_merge().await.unwrap();
let index = Index::open(dir, config).await.unwrap();
assert_eq!(index.num_docs().await.unwrap(), 50);
(index, title, price, temp, weight)
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_range_u64_exact() {
let (index, _, price, _, _) = create_range_test_index().await;
let q = RangeQuery::u64(price, Some(200), Some(300));
let results = index.search(&q, 100).await.unwrap();
assert_eq!(
results.hits.len(),
11,
"U64 range [200,300] should match 11 docs"
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_range_u64_open_min() {
let (index, _, price, _, _) = create_range_test_index().await;
let q = RangeQuery::u64(price, None, Some(150));
let results = index.search(&q, 100).await.unwrap();
assert_eq!(
results.hits.len(),
6,
"U64 range [_,150] should match 6 docs"
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_range_u64_open_max() {
let (index, _, price, _, _) = create_range_test_index().await;
let q = RangeQuery::u64(price, Some(550), None);
let results = index.search(&q, 100).await.unwrap();
assert_eq!(
results.hits.len(),
5,
"U64 range [550,_] should match 5 docs"
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_range_i64_crossing_zero() {
let (index, _, _, temp, _) = create_range_test_index().await;
let q = RangeQuery::i64(temp, Some(-20), Some(20));
let results = index.search(&q, 100).await.unwrap();
assert_eq!(
results.hits.len(),
5,
"I64 range [-20,20] should match 5 docs"
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_range_i64_all_negative() {
let (index, _, _, temp, _) = create_range_test_index().await;
let q = RangeQuery::i64(temp, Some(-50), Some(-10));
let results = index.search(&q, 100).await.unwrap();
assert_eq!(
results.hits.len(),
5,
"I64 range [-50,-10] should match 5 docs"
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_range_f64() {
let (index, _, _, _, weight) = create_range_test_index().await;
let q = RangeQuery::f64(weight, Some(1.0), Some(3.0));
let results = index.search(&q, 100).await.unwrap();
assert_eq!(
results.hits.len(),
9,
"F64 range [1.0,3.0] should match 9 docs"
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_range_no_matches() {
let (index, _, price, _, _) = create_range_test_index().await;
let q = RangeQuery::u64(price, Some(9000), Some(9999));
let results = index.search(&q, 100).await.unwrap();
assert_eq!(results.hits.len(), 0, "Out-of-range should match 0 docs");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_range_in_boolean_must() {
let (index, title, price, _, _) = create_range_test_index().await;
let q = BooleanQuery::new()
.must(TermQuery::new(title, b"electronics".to_vec()))
.must(RangeQuery::u64(price, Some(200), Some(300)));
let results = index.search(&q, 100).await.unwrap();
assert_eq!(
results.hits.len(),
11,
"Boolean MUST [text + u64 range] should match 11 docs"
);
for hit in &results.hits {
assert!(
hit.score > 0.0,
"Hits should have positive scores from text query"
);
}
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_range_two_ranges_in_boolean() {
let (index, _, price, temp, _) = create_range_test_index().await;
let q = BooleanQuery::new()
.must(RangeQuery::u64(price, Some(200), Some(400)))
.must(RangeQuery::i64(temp, Some(0), Some(100)));
let results = index.search(&q, 100).await.unwrap();
assert_eq!(
results.hits.len(),
6,
"Two range MUST should match 6 docs (intersection)"
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_must_range_should_sparse() {
use crate::directories::MmapDirectory;
let tmp_dir = tempfile::tempdir().unwrap();
let dir = MmapDirectory::new(tmp_dir.path());
let mut sb = SchemaBuilder::default();
let content = sb.add_text_field("content", true, true);
let timestamp = sb.add_u64_field("timestamp", false, true);
sb.set_fast(timestamp, true);
let embedding = sb.add_sparse_vector_field("embedding", true, true);
let schema = sb.build();
let config = IndexConfig {
max_indexing_memory_bytes: 8192,
..Default::default()
};
let mut writer = IndexWriter::create(dir.clone(), schema, config.clone())
.await
.unwrap();
for i in 0u64..100 {
let mut doc = Document::new();
doc.add_text(content, format!("document_{}", i));
doc.add_u64(timestamp, 1000 + i * 100);
let mut entries: Vec<(u32, f32)> = vec![(0, 0.5), (1, 0.3)];
entries.push((i as u32, 0.8));
doc.add_sparse_vector(embedding, entries);
writer.add_document(doc).unwrap();
}
writer.commit().await.unwrap();
writer.force_merge().await.unwrap();
let index = Index::open(dir, config).await.unwrap();
assert_eq!(index.num_docs().await.unwrap(), 100);
let q = BooleanQuery::new()
.must(RangeQuery::u64(timestamp, Some(5000), Some(7000)))
.should(SparseVectorQuery::new(
embedding,
vec![(50, 1.0), (51, 1.0)],
));
let results = index.search(&q, 100).await.unwrap();
assert_eq!(
results.hits.len(),
2,
"Only SHOULD-matching docs in range are returned, got {}",
results.hits.len()
);
let doc_ids: Vec<u32> = results.hits.iter().map(|h| h.address.doc_id).collect();
assert!(
doc_ids.contains(&50) && doc_ids.contains(&51),
"Results should be docs 50 and 51, got {:?}",
doc_ids,
);
for hit in &results.hits {
assert!(
hit.score > 0.0,
"SHOULD-matching docs should have positive score, got {}",
hit.score,
);
}
}