use std::collections::HashMap;
use std::sync::{Arc, RwLock};
use std::time::{Duration, Instant};
use super::super::types::SearchPath;
use crate::config::CacheConfig as AppConfig;
use crate::document::NodeId;
#[derive(Debug, Clone)]
struct CacheEntry<T> {
value: T,
created_at: Instant,
access_count: usize,
}
impl<T> CacheEntry<T> {
fn new(value: T) -> Self {
Self {
value,
created_at: Instant::now(),
access_count: 0,
}
}
fn access(&mut self) -> &T {
self.access_count += 1;
&self.value
}
}
#[derive(Debug, Clone)]
pub struct CacheConfig {
pub max_entries: usize,
pub ttl: Duration,
pub use_lru: bool,
}
impl Default for CacheConfig {
fn default() -> Self {
Self::from_app_config(&AppConfig::default())
}
}
impl CacheConfig {
pub fn from_app_config(config: &AppConfig) -> Self {
Self {
max_entries: config.max_entries,
ttl: Duration::from_secs(config.ttl_secs),
use_lru: true,
}
}
}
pub struct PathCache {
paths: Arc<RwLock<HashMap<u64, CacheEntry<Vec<SearchPath>>>>>,
scores: Arc<RwLock<HashMap<(u64, NodeId), CacheEntry<f32>>>>,
config: CacheConfig,
}
impl PathCache {
pub fn new() -> Self {
Self {
paths: Arc::new(RwLock::new(HashMap::new())),
scores: Arc::new(RwLock::new(HashMap::new())),
config: CacheConfig::default(),
}
}
pub fn with_config(config: CacheConfig) -> Self {
Self {
paths: Arc::new(RwLock::new(HashMap::new())),
scores: Arc::new(RwLock::new(HashMap::new())),
config,
}
}
pub fn from_app_config(config: &AppConfig) -> Self {
Self::with_config(CacheConfig::from_app_config(config))
}
fn hash_query(query: &str) -> u64 {
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
let mut hasher = DefaultHasher::new();
query.to_lowercase().hash(&mut hasher);
hasher.finish()
}
pub fn get_paths(&self, query: &str) -> Option<Vec<SearchPath>> {
let hash = Self::hash_query(query);
let mut paths = self.paths.write().ok()?;
if let Some(entry) = paths.get_mut(&hash) {
if entry.created_at.elapsed() > self.config.ttl {
paths.remove(&hash);
return None;
}
return Some(entry.access().clone());
}
None
}
pub fn store_paths(&self, query: &str, paths: Vec<SearchPath>) {
let hash = Self::hash_query(query);
if let Ok(mut cache) = self.paths.write() {
if cache.len() >= self.config.max_entries {
self.evict(&mut cache);
}
cache.insert(hash, CacheEntry::new(paths));
}
}
pub fn get_score(&self, query: &str, node_id: NodeId) -> Option<f32> {
let hash = Self::hash_query(query);
let mut scores = self.scores.write().ok()?;
let key = (hash, node_id);
if let Some(entry) = scores.get_mut(&key) {
if entry.created_at.elapsed() > self.config.ttl {
scores.remove(&key);
return None;
}
return Some(*entry.access());
}
None
}
pub fn store_score(&self, query: &str, node_id: NodeId, score: f32) {
let hash = Self::hash_query(query);
if let Ok(mut cache) = self.scores.write() {
let key = (hash, node_id);
if cache.len() >= self.config.max_entries {
self.evict_scores(&mut cache);
}
cache.insert(key, CacheEntry::new(score));
}
}
fn evict<K, V>(&self, cache: &mut HashMap<K, CacheEntry<V>>)
where
K: std::hash::Hash + Eq + Clone,
{
if self.config.use_lru {
if let Some((min_key, _)) = cache.iter().min_by_key(|(_, e)| e.access_count) {
let key = min_key.clone();
cache.remove(&key);
}
} else {
if let Some((oldest_key, _)) = cache.iter().min_by_key(|(_, e)| e.created_at) {
let key = oldest_key.clone();
cache.remove(&key);
}
}
}
fn evict_scores<K, V>(&self, cache: &mut HashMap<K, CacheEntry<V>>)
where
K: std::hash::Hash + Eq + Clone,
{
self.evict(cache)
}
pub fn clear(&self) {
if let Ok(mut paths) = self.paths.write() {
paths.clear();
}
if let Ok(mut scores) = self.scores.write() {
scores.clear();
}
}
pub fn stats(&self) -> CacheStats {
let path_count = self.paths.read().map(|p| p.len()).unwrap_or(0);
let score_count = self.scores.read().map(|s| s.len()).unwrap_or(0);
CacheStats {
path_entries: path_count,
score_entries: score_count,
max_entries: self.config.max_entries,
}
}
}
impl Default for PathCache {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct CacheStats {
pub path_entries: usize,
pub score_entries: usize,
pub max_entries: usize,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_cache_paths() {
let cache = PathCache::new();
let arena = &mut indextree::Arena::new();
let path = SearchPath::from_node(NodeId(arena.new_node(0)), 0.8);
let paths = vec![path];
cache.store_paths("test query", paths.clone());
let cached = cache.get_paths("test query");
assert!(cached.is_some());
assert_eq!(cached.unwrap().len(), 1);
}
#[test]
fn test_cache_case_insensitive() {
let cache = PathCache::new();
let arena = &mut indextree::Arena::new();
let path = SearchPath::from_node(NodeId(arena.new_node(0)), 0.8);
let paths = vec![path];
cache.store_paths("Test Query", paths);
assert!(cache.get_paths("test query").is_some());
assert!(cache.get_paths("TEST QUERY").is_some());
}
}