mod error;
mod filter;
pub mod qdrant;
pub use error::VectorError;
pub use filter::{FilterCondition, MatchValue, MatchValues, MemoryFilter, NumericRange};
pub use qdrant::QdrantIndex;
use std::future::Future;
use crate::memory::{KindSelector, Memory, Scope};
#[cfg(test)]
use crate::memory::MemoryKind;
pub trait VectorIndex: Send + Sync + 'static {
fn ensure_collection(&self, vector_dim: usize) -> impl Future<Output = Result<(), VectorError>> + Send;
fn upsert(&self, memory: &Memory, vector: Vec<f32>) -> impl Future<Output = Result<(), VectorError>> + Send;
fn search(
&self,
scope: Scope,
query_embedding: Vec<f32>,
limit: usize,
kinds: KindSelector,
extra_filter: Option<MemoryFilter>,
min_similarity: Option<f32>,
) -> impl Future<Output = Result<Vec<(String, f32)>, VectorError>> + Send;
fn delete_by_pids(&self, pids: &[&str]) -> impl Future<Output = Result<(), VectorError>> + Send;
fn list_pids_in_scope(
&self,
scope: Scope,
page_size: usize,
) -> impl Future<Output = Result<Vec<String>, VectorError>> + Send;
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashMap;
use std::sync::Mutex;
#[derive(Default)]
struct StubIndex {
points: Mutex<HashMap<String, (Scope, MemoryKind, Vec<f32>)>>,
}
impl VectorIndex for StubIndex {
async fn ensure_collection(&self, _vector_dim: usize) -> Result<(), VectorError> {
Ok(())
}
async fn upsert(&self, memory: &Memory, vector: Vec<f32>) -> Result<(), VectorError> {
self.points
.lock()
.unwrap()
.insert(memory.pid.clone(), (memory.scope.clone(), memory.kind, vector));
Ok(())
}
async fn search(
&self,
_scope: Scope,
_query_embedding: Vec<f32>,
limit: usize,
_kinds: KindSelector,
_extra_filter: Option<MemoryFilter>,
_min_similarity: Option<f32>,
) -> Result<Vec<(String, f32)>, VectorError> {
Ok(self
.points
.lock()
.unwrap()
.keys()
.take(limit)
.map(|pid| (pid.clone(), 0.5))
.collect())
}
async fn delete_by_pids(&self, pids: &[&str]) -> Result<(), VectorError> {
let mut points = self.points.lock().unwrap();
for pid in pids {
points.remove(*pid);
}
Ok(())
}
async fn list_pids_in_scope(&self, scope: Scope, _page_size: usize) -> Result<Vec<String>, VectorError> {
Ok(self
.points
.lock()
.unwrap()
.iter()
.filter(|(_, (s, _, _))| s == &scope)
.map(|(pid, _)| pid.clone())
.collect())
}
}
#[tokio::test(flavor = "current_thread")]
async fn should_implement_trait_with_in_test_stub() {
use chrono::Utc;
let index = StubIndex::default();
let scope = Scope {
agent_id: "a".to_string(),
org_id: "o".to_string(),
user_id: "u".to_string(),
};
let now: chrono::DateTime<chrono::FixedOffset> = Utc::now().into();
let memory = Memory {
pid: "pid1".to_string(),
scope: scope.clone(),
content: "hello".to_string(),
metadata: serde_json::json!({}),
kind: MemoryKind::Episodic,
source_pid: None,
supersession: None,
created_at: now,
updated_at: now,
event_at: None,
score: None,
status: crate::store::IndexStatus::Pending,
confidence: crate::memory::Confidence::default(),
category: None,
retirement: None,
};
index.ensure_collection(4).await.unwrap();
index.upsert(&memory, vec![0.1, 0.2, 0.3, 0.4]).await.unwrap();
let hits = index
.search(scope, vec![0.1, 0.2, 0.3, 0.4], 5, KindSelector::default(), None, None)
.await
.unwrap();
assert_eq!(hits.len(), 1);
assert_eq!(hits[0].0, "pid1");
index.delete_by_pids(&["pid1"]).await.unwrap();
}
}