use async_trait::async_trait;
use std::any::Any;
use std::collections::HashMap;
use std::sync::Arc;
use std::sync::Mutex;
use std::sync::atomic::{AtomicUsize, Ordering};
use laurus::Engine;
use laurus::LaurusError;
use laurus::SearchRequestBuilder;
use laurus::storage::memory::MemoryStorage;
use laurus::vector::FlatOption;
use laurus::vector::Vector;
use laurus::{DataValue, Document};
use laurus::{EmbedInput, EmbedInputType, Embedder};
use laurus::{FieldOption, Schema};
#[derive(Debug)]
struct CountingEmbedder {
vectors: Mutex<HashMap<String, Vec<f32>>>,
calls: AtomicUsize,
}
impl CountingEmbedder {
fn new() -> Self {
Self {
vectors: Mutex::new(HashMap::new()),
calls: AtomicUsize::new(0),
}
}
fn add(&self, text: &str, vector: Vec<f32>) {
self.vectors
.lock()
.unwrap()
.insert(text.to_string(), vector);
}
fn call_count(&self) -> usize {
self.calls.load(Ordering::Relaxed)
}
}
#[async_trait]
impl Embedder for CountingEmbedder {
async fn embed(&self, input: &EmbedInput<'_>) -> std::result::Result<Vector, LaurusError> {
match input {
EmbedInput::Text(text) => {
self.calls.fetch_add(1, Ordering::Relaxed);
let map = self.vectors.lock().unwrap();
map.get(*text)
.map(|v| Vector::new(v.clone()))
.ok_or_else(|| {
LaurusError::invalid_argument(format!(
"CountingEmbedder: unknown text '{text}'"
))
})
}
_ => Err(LaurusError::invalid_argument(
"CountingEmbedder only supports text",
)),
}
}
fn supported_input_types(&self) -> Vec<EmbedInputType> {
vec![EmbedInputType::Text]
}
fn supports_text(&self) -> bool {
true
}
fn name(&self) -> &str {
"counting"
}
fn as_any(&self) -> &dyn Any {
self
}
}
fn schema() -> Schema {
Schema::builder()
.add_field(
"embedding",
FieldOption::Flat(FlatOption::default().dimension(3)),
)
.build()
}
async fn index_corpus(engine: &Engine) {
engine
.put_document(
"doc1",
Document::builder()
.add_field("embedding", DataValue::Vector(vec![1.0, 0.0, 0.0]))
.build(),
)
.await
.unwrap();
engine
.put_document(
"doc2",
Document::builder()
.add_field("embedding", DataValue::Vector(vec![0.0, 1.0, 0.0]))
.build(),
)
.await
.unwrap();
engine.commit().await.unwrap();
}
#[tokio::test(flavor = "multi_thread")]
async fn test_cache_disabled_by_default() {
let storage = Arc::new(MemoryStorage::new(Default::default()));
let embedder = Arc::new(CountingEmbedder::new());
embedder.add("apple", vec![1.0, 0.0, 0.0]);
let engine = Engine::builder(storage, schema())
.embedder(embedder.clone())
.build()
.await
.unwrap();
index_corpus(&engine).await;
let base = embedder.call_count();
for _ in 0..3 {
engine
.search(
SearchRequestBuilder::new()
.query_dsl("embedding:\"apple\"")
.build(),
)
.await
.unwrap();
}
assert_eq!(embedder.call_count() - base, 3);
}
#[tokio::test(flavor = "multi_thread")]
async fn test_cache_hit_avoids_reembed() {
let storage = Arc::new(MemoryStorage::new(Default::default()));
let embedder = Arc::new(CountingEmbedder::new());
embedder.add("apple", vec![1.0, 0.0, 0.0]);
let engine = Engine::builder(storage, schema())
.embedder(embedder.clone())
.embedding_cache_capacity(16)
.build()
.await
.unwrap();
index_corpus(&engine).await;
let base = embedder.call_count();
for _ in 0..5 {
engine
.search(
SearchRequestBuilder::new()
.query_dsl("embedding:\"apple\"")
.build(),
)
.await
.unwrap();
}
assert_eq!(embedder.call_count() - base, 1);
}
#[tokio::test(flavor = "multi_thread")]
async fn test_cache_distinguishes_payload() {
let storage = Arc::new(MemoryStorage::new(Default::default()));
let embedder = Arc::new(CountingEmbedder::new());
embedder.add("apple", vec![1.0, 0.0, 0.0]);
embedder.add("banana", vec![0.0, 1.0, 0.0]);
let engine = Engine::builder(storage, schema())
.embedder(embedder.clone())
.embedding_cache_capacity(16)
.build()
.await
.unwrap();
index_corpus(&engine).await;
let base = embedder.call_count();
for _ in 0..2 {
engine
.search(
SearchRequestBuilder::new()
.query_dsl("embedding:\"apple\"")
.build(),
)
.await
.unwrap();
engine
.search(
SearchRequestBuilder::new()
.query_dsl("embedding:\"banana\"")
.build(),
)
.await
.unwrap();
}
assert_eq!(embedder.call_count() - base, 2);
}
#[tokio::test(flavor = "multi_thread")]
async fn test_cache_hit_returns_correct_results() {
let storage = Arc::new(MemoryStorage::new(Default::default()));
let embedder = Arc::new(CountingEmbedder::new());
embedder.add("apple", vec![1.0, 0.0, 0.0]);
let engine = Engine::builder(storage, schema())
.embedder(embedder.clone())
.embedding_cache_capacity(16)
.build()
.await
.unwrap();
index_corpus(&engine).await;
let first = engine
.search(
SearchRequestBuilder::new()
.query_dsl("embedding:\"apple\"")
.build(),
)
.await
.unwrap();
let second = engine
.search(
SearchRequestBuilder::new()
.query_dsl("embedding:\"apple\"")
.build(),
)
.await
.unwrap();
assert!(!first.is_empty());
assert_eq!(first[0].id, "doc1");
assert_eq!(first.len(), second.len());
assert_eq!(first[0].id, second[0].id);
}