use fastbloom::BloomFilter;
use rphonetic::{Encoder, Metaphone};
use std::collections::HashMap;
use std::hash::Hash;
#[derive(Default, Debug)]
pub struct FullTextIndex<V>
where
V: Eq + Hash + Copy,
{
filters: HashMap<V, BloomFilter>,
metaphone: Metaphone,
}
impl<V> FullTextIndex<V>
where
V: Eq + Hash + Copy,
{
pub(crate) fn insert(&mut self, key: V, text: &str) {
let word_count = text.split_whitespace().count();
let mut filter = BloomFilter::with_false_pos(0.001).expected_items(word_count);
for word in text.split_whitespace() {
let encoded = self.metaphone.encode(word);
filter.insert(&encoded);
}
self.filters.insert(key, filter);
}
pub(crate) fn search<'a>(&'a self, search: &str) -> impl Iterator<Item = V> + 'a {
let search = search
.split_whitespace()
.map(|w| self.metaphone.encode(w))
.collect::<Vec<_>>();
self.filters.iter().filter_map(move |(v, filter)| {
for word in &search {
if !filter.contains(&word) {
return None;
}
}
Some(*v)
})
}
pub(crate) fn remove(&mut self, key: &V) {
self.filters.remove(key);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_basic_insert_and_search() {
let mut index = FullTextIndex::default();
index.insert(1, "hello world");
let results: Vec<_> = index.search("hello").collect();
assert_eq!(results, vec![1]);
let results: Vec<_> = index.search("world").collect();
assert_eq!(results, vec![1]);
}
#[test]
fn test_phonetic_matching() {
let mut index = FullTextIndex::default();
index.insert(1, "phone");
let results: Vec<_> = index.search("fone").collect();
assert_eq!(results, vec![1]);
}
#[test]
fn test_multiple_entries() {
let mut index = FullTextIndex::default();
index.insert(1, "hello world");
index.insert(2, "hello there");
index.insert(3, "goodbye world");
let results: Vec<_> = index.search("hello").collect();
assert_eq!(results.len(), 2);
assert!(results.contains(&1));
assert!(results.contains(&2));
}
#[test]
fn test_remove() {
let mut index = FullTextIndex::default();
index.insert(1, "hello world");
index.remove(&1);
let results: Vec<_> = index.search("hello").collect();
assert!(results.is_empty());
}
#[test]
fn test_multi_word_search() {
let mut index = FullTextIndex::default();
index.insert(1, "hello beautiful world");
index.insert(2, "hello ugly world");
let results: Vec<_> = index.search("hello world").collect();
assert_eq!(results.len(), 2);
let results: Vec<_> = index.search("beautiful world").collect();
assert_eq!(results, vec![1]);
}
#[test]
fn test_empty_cases() {
let mut index = FullTextIndex::<i32>::default();
let results: Vec<_> = index.search("anything").collect();
assert!(results.is_empty());
index.insert(1, "");
let results: Vec<_> = index.search("").collect();
assert_eq!(results, vec![1]);
}
}