use crate::error::{MemoryError, Result};
use futures::future::BoxFuture;
use serde::{Deserialize, Serialize};
use serde_json::Value;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StoreItem {
pub namespace: Vec<String>,
pub key: String,
pub value: Value,
pub created_at: u64,
pub updated_at: u64,
#[serde(default)]
pub score: Option<f32>,
#[serde(default = "default_importance")]
pub importance: f32,
#[serde(default)]
pub last_accessed: Option<u64>,
}
fn default_importance() -> f32 {
5.0
}
impl StoreItem {
pub fn new(namespace: Vec<String>, key: String, value: Value) -> Self {
let now = crate::utils::time::now_secs();
Self {
namespace,
key,
value,
created_at: now,
updated_at: now,
score: None,
importance: 5.0,
last_accessed: None,
}
}
pub fn with_importance(
namespace: Vec<String>,
key: String,
value: Value,
importance: f32,
) -> Self {
let mut item = Self::new(namespace, key, value);
item.importance = importance.clamp(1.0, 10.0);
item
}
pub fn touch(&mut self) {
self.last_accessed = Some(crate::utils::time::now_secs());
}
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub enum SearchMode {
Keyword,
Semantic,
Hybrid {
vector_weight: f32,
},
}
pub const RRF_K: f64 = 60.0;
pub fn rrf_score(vector_rank: usize, keyword_rank: usize, vector_weight: f32) -> f32 {
let vw = vector_weight as f64;
let kw = 1.0 - vw;
let vr = if vector_rank == usize::MAX {
0.0
} else {
vw / (RRF_K + vector_rank as f64)
};
let kr = if keyword_rank == usize::MAX {
0.0
} else {
kw / (RRF_K + keyword_rank as f64)
};
(vr + kr) as f32
}
#[derive(Debug, Clone, Copy)]
pub struct SearchQuery<'a> {
pub text: &'a str,
pub limit: usize,
pub mode: SearchMode,
}
impl<'a> SearchQuery<'a> {
pub fn keyword(text: &'a str, limit: usize) -> Self {
Self {
text,
limit,
mode: SearchMode::Keyword,
}
}
pub fn semantic(text: &'a str, limit: usize) -> Self {
Self {
text,
limit,
mode: SearchMode::Semantic,
}
}
pub fn hybrid(text: &'a str, limit: usize) -> Self {
Self {
text,
limit,
mode: SearchMode::Hybrid { vector_weight: 0.5 },
}
}
pub fn hybrid_weighted(text: &'a str, limit: usize, vector_weight: f32) -> Self {
Self {
text,
limit,
mode: SearchMode::Hybrid { vector_weight },
}
}
}
pub trait Store: Send + Sync {
fn put<'a>(
&'a self,
namespace: &'a [&'a str],
key: &'a str,
value: Value,
) -> BoxFuture<'a, Result<()>>;
fn get<'a>(
&'a self,
namespace: &'a [&'a str],
key: &'a str,
) -> BoxFuture<'a, Result<Option<StoreItem>>>;
fn search<'a>(
&'a self,
namespace: &'a [&'a str],
query: &'a str,
limit: usize,
) -> BoxFuture<'a, Result<Vec<StoreItem>>>;
fn search_with<'a>(
&'a self,
namespace: &'a [&'a str],
query: SearchQuery<'a>,
) -> BoxFuture<'a, Result<Vec<StoreItem>>> {
Box::pin(async move {
match query.mode {
SearchMode::Keyword => self.search(namespace, query.text, query.limit).await,
SearchMode::Semantic => Err(MemoryError::Unsupported(
"semantic search is not supported by this Store".to_string(),
)
.into()),
SearchMode::Hybrid { .. } => Err(MemoryError::Unsupported(
"hybrid search is not supported by this Store".to_string(),
)
.into()),
}
})
}
fn delete<'a>(&'a self, namespace: &'a [&'a str], key: &'a str) -> BoxFuture<'a, Result<bool>>;
fn list_namespaces<'a>(
&'a self,
prefix: Option<&'a [&'a str]>,
) -> BoxFuture<'a, Result<Vec<Vec<String>>>>;
fn list<'a>(&'a self, namespace: &'a [&'a str]) -> BoxFuture<'a, Result<Vec<StoreItem>>>;
}