use std::collections::HashMap;
use std::sync::{Arc, RwLock};
use super::{Hit, SearchIndex, SearchOptions, Searchable};
use crate::core::item::AnyItem;
pub trait DynSearchIndex: Send + Sync + 'static {
fn as_any(&self) -> &dyn std::any::Any;
fn entity_type(&self) -> &'static str;
fn insert_dyn(&self, item: &dyn AnyItem);
fn remove_dyn(&self, id: &str);
fn search_arc_str(&self, query: &str, opts: SearchOptions) -> Vec<Hit<Arc<str>>>;
fn len(&self) -> usize;
fn is_empty(&self) -> bool {
self.len() == 0
}
}
pub struct TypedShim<T: Searchable + AnyItem> {
entity_type: &'static str,
inner: RwLock<SearchIndex<T>>,
}
impl<T: Searchable + AnyItem> TypedShim<T> {
pub fn new(entity_type: &'static str) -> Self {
Self {
entity_type,
inner: RwLock::new(SearchIndex::new()),
}
}
pub fn inner(&self) -> &RwLock<SearchIndex<T>> {
&self.inner
}
}
impl<T> DynSearchIndex for TypedShim<T>
where
T: Searchable + AnyItem,
T::Id: From<Arc<str>>,
Arc<str>: From<T::Id>,
{
fn as_any(&self) -> &dyn std::any::Any {
self
}
fn entity_type(&self) -> &'static str {
self.entity_type
}
fn insert_dyn(&self, item: &dyn AnyItem) {
let Some(t) = item.as_any().downcast_ref::<T>() else {
log::warn!(
"SearchRegistry: type mismatch routing {} into {}",
item.entity_type(),
self.entity_type
);
return;
};
if let Ok(mut idx) = self.inner.write() {
idx.insert(t);
}
}
fn remove_dyn(&self, id: &str) {
let arc: Arc<str> = <Arc<str> as From<&str>>::from(id);
let typed: T::Id = arc.into();
if let Ok(mut idx) = self.inner.write() {
idx.remove(&typed);
}
}
fn search_arc_str(&self, query: &str, opts: SearchOptions) -> Vec<Hit<Arc<str>>> {
let Ok(guard) = self.inner.read() else {
return Vec::new();
};
guard
.search(query, opts)
.into_iter()
.map(|h| Hit {
id: Arc::<str>::from(h.id),
score: h.score,
matched_field: h.matched_field,
})
.collect()
}
fn len(&self) -> usize {
self.inner.read().map(|g| g.len()).unwrap_or(0)
}
}
pub struct SearchRegistry {
indexes: HashMap<&'static str, Arc<dyn DynSearchIndex>>,
}
impl SearchRegistry {
pub fn new() -> Self {
Self {
indexes: HashMap::new(),
}
}
pub fn register<T>(&mut self, entity_type: &'static str)
where
T: Searchable + AnyItem,
T::Id: From<Arc<str>>,
Arc<str>: From<T::Id>,
{
let shim: Arc<dyn DynSearchIndex> = Arc::new(TypedShim::<T>::new(entity_type));
self.indexes.insert(entity_type, shim);
}
pub fn insert(&self, item: &dyn AnyItem) {
let Some(shim) = self.indexes.get(item.entity_type()) else {
return;
};
shim.insert_dyn(item);
}
pub fn remove(&self, entity_type: &str, id: &str) {
if let Some(shim) = self.indexes.get(entity_type) {
shim.remove_dyn(id);
}
}
pub fn search(
&self,
entity_type: &str,
query: &str,
opts: SearchOptions,
) -> Vec<Hit<Arc<str>>> {
let Some(shim) = self.indexes.get(entity_type) else {
return Vec::new();
};
shim.search_arc_str(query, opts)
}
pub fn with_typed<T, R>(
&self,
entity_type: &str,
f: impl FnOnce(&RwLock<SearchIndex<T>>) -> R,
) -> Option<R>
where
T: Searchable + AnyItem + 'static,
{
let shim = self.indexes.get(entity_type)?;
let typed = shim.as_any().downcast_ref::<TypedShim<T>>()?;
Some(f(&typed.inner))
}
pub fn len(&self) -> usize {
self.indexes.len()
}
pub fn is_empty(&self) -> bool {
self.indexes.is_empty()
}
pub fn entity_types(&self) -> impl Iterator<Item = &'static str> + '_ {
self.indexes.keys().copied()
}
}
impl Default for SearchRegistry {
fn default() -> Self {
Self::new()
}
}
#[cfg(all(test, feature = "bench"))]
mod tests {
use super::*;
use crate::bench_entities::BenchItem;
fn item(id: &str, name: &str, category: &str) -> BenchItem {
BenchItem {
id: id.into(),
name: name.to_string(),
category: category.to_string(),
value: 0,
}
}
#[test]
fn registry_routes_inserts_and_searches() {
let mut registry = SearchRegistry::new();
registry.register::<BenchItem>("BenchItem");
let i1 = item("1", "audio mixer", "hardware");
let i2 = item("2", "video camera", "hardware");
registry.insert(&i1);
registry.insert(&i2);
let hits = registry.search("BenchItem", "mixer", SearchOptions::default());
assert_eq!(hits.len(), 1);
assert_eq!(hits[0].id.as_ref(), "1");
}
#[test]
fn registry_remove_via_string_id() {
let mut registry = SearchRegistry::new();
registry.register::<BenchItem>("BenchItem");
registry.insert(&item("1", "audio mixer", "hardware"));
registry.remove("BenchItem", "1");
let hits = registry.search("BenchItem", "mixer", SearchOptions::default());
assert!(hits.is_empty());
}
#[test]
fn registry_search_unknown_entity_type_returns_empty() {
let registry = SearchRegistry::new();
let hits = registry.search("Unknown", "anything", SearchOptions::default());
assert!(hits.is_empty());
}
#[test]
fn registry_insert_wrong_type_is_noop_for_other_indexes() {
let mut registry = SearchRegistry::new();
registry.register::<BenchItem>("BenchItem");
registry.insert(&item("1", "x", "y"));
assert_eq!(
registry
.search("BenchItem", "x", SearchOptions::default())
.len(),
1
);
}
#[test]
fn typed_handle_round_trips_via_with_typed() {
let mut registry = SearchRegistry::new();
registry.register::<BenchItem>("BenchItem");
registry.insert(&item("1", "audio mixer", "hardware"));
let hits = registry
.with_typed::<BenchItem, _>("BenchItem", |lock| {
lock.read()
.unwrap()
.search("mixer", SearchOptions::default())
})
.unwrap();
assert_eq!(hits.len(), 1);
assert_eq!(hits[0].id.0.as_ref(), "1");
}
}