use tempfile::TempDir;
use laurus::Document;
use laurus::Engine;
use laurus::SearchRequestBuilder;
use laurus::lexical::TermQuery;
use laurus::lexical::TextOption;
use laurus::storage::file::FileStorageConfig;
use laurus::storage::{StorageConfig, StorageFactory};
use laurus::vector::HnswOption;
use laurus::vector::Vector;
use laurus::{FieldOption, LexicalSearchQuery, QueryVector, Schema, VectorSearchQuery};
#[tokio::test(flavor = "multi_thread")]
async fn test_engine_unified_deletion() -> laurus::Result<()> {
let temp_dir = TempDir::new().unwrap();
let storage_config = StorageConfig::File(FileStorageConfig::new(temp_dir.path()));
let storage = StorageFactory::create(storage_config)?;
let config = Schema::builder()
.add_field("title", FieldOption::Text(TextOption::default()))
.add_field("embedding", FieldOption::Hnsw(HnswOption::default()))
.build();
let engine = Engine::new(storage.clone(), config).await?;
let doc1 = Document::builder()
.add_field("title", "Hello Laurus")
.add_field("embedding", vec![0.1; 128])
.build();
engine.put_document("doc1", doc1).await?;
engine.commit().await?;
let req_lexical = SearchRequestBuilder::new()
.lexical_query(LexicalSearchQuery::Obj(Box::new(TermQuery::new(
"title", "hello",
))))
.build();
let res_lexical = engine.search(req_lexical).await?;
assert_eq!(res_lexical.len(), 1, "Should be found lexically");
let req_vector = SearchRequestBuilder::new()
.vector_query(VectorSearchQuery::Vectors(vec![QueryVector {
vector: Vector::new(vec![0.1; 128]),
weight: 1.0,
fields: Some(vec!["embedding".into()]),
}]))
.build();
let res_vector = engine.search(req_vector).await?;
assert_eq!(res_vector.len(), 1, "Should be found via vector");
engine.delete_documents("doc1").await?;
engine.commit().await?;
let res_lexical_after = engine
.search(
SearchRequestBuilder::new()
.lexical_query(LexicalSearchQuery::Obj(Box::new(TermQuery::new(
"title", "hello",
))))
.build(),
)
.await?;
assert_eq!(res_lexical_after.len(), 0, "Should be deleted lexically");
let res_vector_after = engine
.search(
SearchRequestBuilder::new()
.vector_query(VectorSearchQuery::Vectors(vec![QueryVector {
vector: Vector::new(vec![0.1; 128]),
weight: 1.0,
fields: Some(vec!["embedding".into()]),
}]))
.build(),
)
.await?;
assert_eq!(res_vector_after.len(), 0, "Should be deleted via vector");
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn test_engine_upsert() -> laurus::Result<()> {
let temp_dir = TempDir::new().unwrap();
let storage_config = StorageConfig::File(FileStorageConfig::new(temp_dir.path()));
let storage = StorageFactory::create(storage_config)?;
use laurus::vector::FlatOption;
let config = Schema::builder()
.add_field("title", FieldOption::Text(TextOption::default()))
.add_field(
"embedding",
FieldOption::Flat(FlatOption {
dimension: 2,
..Default::default()
}),
)
.build();
let engine = Engine::new(storage.clone(), config).await?;
let doc1 = Document::builder()
.add_field("title", "Initial Version")
.add_field("embedding", vec![1.0, 0.0])
.build();
engine.put_document("doc1", doc1).await?;
engine.commit().await?;
let res = engine
.search(
SearchRequestBuilder::new()
.lexical_query(LexicalSearchQuery::Obj(Box::new(TermQuery::new(
"title", "initial",
))))
.build(),
)
.await?;
assert_eq!(res.len(), 1);
let doc1_v2 = Document::builder()
.add_field("title", "Updated Version")
.add_field("embedding", vec![0.0, 1.0])
.build();
engine.put_document("doc1", doc1_v2).await?;
engine.commit().await?;
let res_old = engine
.search(
SearchRequestBuilder::new()
.lexical_query(LexicalSearchQuery::Obj(Box::new(TermQuery::new(
"title", "initial",
))))
.build(),
)
.await?;
assert_eq!(res_old.len(), 0, "Old version should be replaced");
let res_new = engine
.search(
SearchRequestBuilder::new()
.lexical_query(LexicalSearchQuery::Obj(Box::new(TermQuery::new(
"title", "updated",
))))
.build(),
)
.await?;
assert_eq!(res_new.len(), 1, "New version should be found");
let res_vec = engine
.search(
SearchRequestBuilder::new()
.vector_query(VectorSearchQuery::Vectors(vec![QueryVector {
vector: Vector::new(vec![0.0, 1.0]),
weight: 1.0,
fields: Some(vec!["embedding".into()]),
}]))
.build(),
)
.await?;
assert_eq!(res_vec.len(), 1, "New vector should be found");
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn test_engine_delete_nonexistent() -> laurus::Result<()> {
let temp_dir = TempDir::new().unwrap();
let storage_config = StorageConfig::File(FileStorageConfig::new(temp_dir.path()));
let storage = StorageFactory::create(storage_config)?;
use laurus::vector::FlatOption;
let config = Schema::builder()
.add_field("title", FieldOption::Text(TextOption::default()))
.add_field(
"embedding",
FieldOption::Flat(FlatOption {
dimension: 2,
..Default::default()
}),
)
.build();
let engine = Engine::new(storage, config).await?;
let result = engine.delete_documents("nonexistent_doc").await;
assert!(
result.is_ok(),
"Deleting non-existent document should succeed silently"
);
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn test_engine_double_delete() -> laurus::Result<()> {
let temp_dir = TempDir::new().unwrap();
let storage_config = StorageConfig::File(FileStorageConfig::new(temp_dir.path()));
let storage = StorageFactory::create(storage_config)?;
use laurus::vector::FlatOption;
let config = Schema::builder()
.add_field("title", FieldOption::Text(TextOption::default()))
.add_field(
"embedding",
FieldOption::Flat(FlatOption {
dimension: 2,
..Default::default()
}),
)
.build();
let engine = Engine::new(storage, config).await?;
engine
.put_document(
"doc1",
Document::builder()
.add_field("title", "Hello")
.add_field("embedding", vec![1.0, 0.0])
.build(),
)
.await?;
engine.commit().await?;
engine.delete_documents("doc1").await?;
engine.commit().await?;
let result = engine.delete_documents("doc1").await;
assert!(result.is_ok(), "Double delete should succeed silently");
engine.commit().await?;
let res = engine
.search(
SearchRequestBuilder::new()
.lexical_query(LexicalSearchQuery::Obj(Box::new(TermQuery::new(
"title", "hello",
))))
.build(),
)
.await?;
assert_eq!(res.len(), 0, "Document should remain deleted");
Ok(())
}