use crate::backend::{Backend, IndexDoc};
use crate::document::Document;
use crate::error::Result;
use crate::query::{Hit, Query};
use crate::token::{tokenize, tokenize_value};
use async_trait::async_trait;
use std::collections::HashMap;
use std::sync::RwLock;
const K1: f32 = 1.2;
const B: f32 = 0.75;
struct Stored {
document: Document,
term_freq: HashMap<String, u32>,
len: u32,
}
impl Stored {
fn build(document: Document) -> Self {
let mut tokens = Vec::new();
for (_field, value) in document.iter() {
tokenize_value(value, &mut tokens);
}
let len = tokens.len() as u32;
let mut term_freq = HashMap::new();
for t in tokens {
*term_freq.entry(t).or_insert(0) += 1;
}
Self { document, term_freq, len }
}
}
#[derive(Default)]
struct Index {
docs: HashMap<String, Stored>,
postings: HashMap<String, std::collections::HashSet<String>>,
total_len: u64,
}
impl Index {
fn avgdl(&self) -> f32 {
if self.docs.is_empty() {
0.0
} else {
self.total_len as f32 / self.docs.len() as f32
}
}
fn remove(&mut self, id: &str) {
if let Some(old) = self.docs.remove(id) {
self.total_len -= old.len as u64;
for term in old.term_freq.keys() {
if let Some(set) = self.postings.get_mut(term) {
set.remove(id);
if set.is_empty() {
self.postings.remove(term);
}
}
}
}
}
fn insert(&mut self, id: String, document: Document) {
self.remove(&id);
let stored = Stored::build(document);
self.total_len += stored.len as u64;
for term in stored.term_freq.keys() {
self.postings.entry(term.clone()).or_default().insert(id.clone());
}
self.docs.insert(id, stored);
}
}
#[derive(Default)]
pub struct MemoryBackend {
indexes: RwLock<HashMap<String, Index>>,
}
impl MemoryBackend {
pub fn new() -> Self {
Self::default()
}
}
fn passes_filters(document: &Document, query: &Query) -> bool {
query
.filters
.iter()
.all(|f| document.get(&f.field) == Some(&f.value))
}
#[async_trait]
impl Backend for MemoryBackend {
async fn upsert(&self, index: &str, docs: Vec<IndexDoc>) -> Result<()> {
let mut map = self.indexes.write().unwrap();
let idx = map.entry(index.to_string()).or_default();
for d in docs {
idx.insert(d.id, d.document);
}
Ok(())
}
async fn delete(&self, index: &str, ids: &[String]) -> Result<()> {
let mut map = self.indexes.write().unwrap();
if let Some(idx) = map.get_mut(index) {
for id in ids {
idx.remove(id);
}
}
Ok(())
}
async fn search(&self, index: &str, query: &Query) -> Result<Vec<Hit>> {
let map = self.indexes.read().unwrap();
let Some(idx) = map.get(index) else {
return Ok(Vec::new());
};
let query_terms: Vec<String> = query
.text
.as_deref()
.map(tokenize)
.unwrap_or_default();
let n = idx.docs.len() as f32;
let avgdl = idx.avgdl();
let mut idf: HashMap<&str, f32> = HashMap::new();
for term in &query_terms {
if idf.contains_key(term.as_str()) {
continue;
}
let df = idx.postings.get(term).map(|s| s.len()).unwrap_or(0) as f32;
let value = (1.0 + (n - df + 0.5) / (df + 0.5)).ln();
idf.insert(term.as_str(), value);
}
let mut hits: Vec<Hit> = Vec::new();
for (id, stored) in &idx.docs {
if !passes_filters(&stored.document, query) {
continue;
}
let score = if query_terms.is_empty() {
0.0
} else {
let mut s = 0.0f32;
for term in &query_terms {
let tf = *stored.term_freq.get(term).unwrap_or(&0) as f32;
if tf == 0.0 {
continue;
}
let norm = tf * (K1 + 1.0)
/ (tf + K1 * (1.0 - B + B * stored.len as f32 / avgdl.max(1.0)));
s += idf[term.as_str()] * norm;
}
s
};
if query_terms.is_empty() || score > 0.0 {
hits.push(Hit {
id: id.clone(),
score,
document: stored.document.clone(),
});
}
}
hits.sort_by(|a, b| {
b.score
.partial_cmp(&a.score)
.unwrap_or(std::cmp::Ordering::Equal)
.then_with(|| a.id.cmp(&b.id))
});
let limit = query.effective_limit();
Ok(hits.into_iter().skip(query.offset).take(limit).collect())
}
async fn clear(&self, index: &str) -> Result<()> {
self.indexes.write().unwrap().remove(index);
Ok(())
}
async fn count(&self, index: &str) -> Result<usize> {
Ok(self
.indexes
.read()
.unwrap()
.get(index)
.map(|i| i.docs.len())
.unwrap_or(0))
}
}