use crate::transducer::{Algorithm, Transducer};
use libdictenstein::{DictionaryValue, MappedDictionary, MutableMappedDictionary};
use std::collections::{BTreeSet, HashSet};
use std::hash::Hash;
pub trait CollectionAggregate: Sized {
fn aggregate<I>(values: I) -> Self
where
I: Iterator<Item = Self>;
}
impl<T> CollectionAggregate for HashSet<T>
where
T: Eq + Hash + Clone,
{
fn aggregate<I>(values: I) -> Self
where
I: Iterator<Item = Self>,
{
let mut values = values.peekable();
let initial_capacity = values
.peek()
.map(|first_set| first_set.len() * 2) .unwrap_or(0);
let mut acc = HashSet::with_capacity(initial_capacity);
for set in values {
if acc.len() + set.len() > acc.capacity() {
acc.reserve(set.len());
}
acc.extend(set);
}
acc
}
}
impl<T> CollectionAggregate for BTreeSet<T>
where
T: Ord + Clone,
{
fn aggregate<I>(values: I) -> Self
where
I: Iterator<Item = Self>,
{
values.fold(BTreeSet::new(), |mut acc, set| {
acc.extend(set);
acc
})
}
}
impl<T> CollectionAggregate for Vec<T>
where
T: Clone,
{
fn aggregate<I>(values: I) -> Self
where
I: Iterator<Item = Self>,
{
let mut values = values.peekable();
let initial_capacity = values
.peek()
.map(|first_vec| first_vec.len() * 2) .unwrap_or(0);
let mut acc = Vec::with_capacity(initial_capacity);
for vec in values {
acc.reserve(vec.len());
acc.extend(vec);
}
acc
}
}
pub struct FuzzyMultiMap<C, D>
where
D: crate::dictionary::Dictionary,
{
dictionary: D,
transducer: Transducer<D>,
_phantom: std::marker::PhantomData<C>,
}
impl<C, D> FuzzyMultiMap<C, D>
where
C: CollectionAggregate + DictionaryValue,
D: MappedDictionary<Value = C> + crate::dictionary::Dictionary + Clone,
{
pub fn new(dictionary: D, algorithm: Algorithm) -> Self {
let transducer = Transducer::new(dictionary.clone(), algorithm);
Self {
dictionary,
transducer,
_phantom: std::marker::PhantomData,
}
}
pub fn query(&self, query_term: &str, max_distance: usize) -> Option<C> {
let mut values = self
.transducer
.query(query_term, max_distance)
.filter_map(|term| self.dictionary.get_value(&term))
.peekable();
values.peek()?;
Some(C::aggregate(values))
}
pub fn dictionary(&self) -> &D {
&self.dictionary
}
pub fn algorithm(&self) -> Algorithm {
self.transducer.algorithm()
}
pub fn query_with_distance(
&self,
query_term: &str,
max_distance: usize,
) -> Vec<(String, usize, C)> {
self.transducer
.query_with_distance(query_term, max_distance)
.filter_map(|candidate| {
self.dictionary
.get_value(&candidate.term)
.map(|value| (candidate.term, candidate.distance, value))
})
.collect()
}
}
impl<C, D> FuzzyMultiMap<C, D>
where
C: CollectionAggregate + DictionaryValue,
D: MutableMappedDictionary<Value = C> + crate::dictionary::Dictionary + Clone,
{
pub fn insert(&self, term: &str, value: C) -> bool {
self.dictionary.insert_with_value(term, value)
}
pub fn update_or_insert<F>(&self, term: &str, default_value: C, update_fn: F) -> bool
where
F: Fn(&mut C),
{
self.dictionary
.update_or_insert(term, default_value, update_fn)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashSet;
#[cfg(feature = "pathmap-backend")]
use libdictenstein::pathmap::PathMapDictionary;
#[test]
#[cfg(feature = "pathmap-backend")]
fn test_fuzzy_multimap_hashset_union() {
let dict = PathMapDictionary::from_terms_with_values([
("foo", HashSet::from([1, 2])),
("bar", HashSet::from([3])),
("baz", HashSet::from([4, 5])),
]);
let fuzzy = FuzzyMultiMap::new(dict, Algorithm::Standard);
let result = fuzzy.query("bat", 1).expect("expected Some result in test");
assert_eq!(result, HashSet::from([3, 4, 5]));
}
#[test]
#[cfg(feature = "pathmap-backend")]
fn test_fuzzy_multimap_vec_concatenation() {
let dict = PathMapDictionary::from_terms_with_values([
("foo", vec![1, 2]),
("fob", vec![3]),
("fog", vec![4, 5]),
]);
let fuzzy = FuzzyMultiMap::new(dict, Algorithm::Standard);
let result = fuzzy.query("foo", 0).expect("expected Some result in test");
assert_eq!(result, vec![1, 2]);
let result = fuzzy.query("fox", 1).expect("expected Some result in test");
assert!(result.contains(&3));
assert!(result.contains(&4));
assert!(result.contains(&5));
}
#[test]
#[cfg(feature = "pathmap-backend")]
fn test_fuzzy_multimap_no_matches() {
let dict = PathMapDictionary::from_terms_with_values([
("foo", HashSet::from([1])),
("bar", HashSet::from([2])),
]);
let fuzzy = FuzzyMultiMap::new(dict, Algorithm::Standard);
let result = fuzzy.query("xyz", 1);
assert!(result.is_none());
}
#[test]
#[cfg(feature = "pathmap-backend")]
fn test_fuzzy_multimap_exact_match() {
let dict = PathMapDictionary::from_terms_with_values([("hello", HashSet::from([1, 2, 3]))]);
let fuzzy = FuzzyMultiMap::new(dict, Algorithm::Standard);
let result = fuzzy
.query("hello", 0)
.expect("expected Some result in test");
assert_eq!(result, HashSet::from([1, 2, 3]));
}
#[test]
#[cfg(feature = "pathmap-backend")]
fn test_fuzzy_multimap_overlapping_values() {
let dict = PathMapDictionary::from_terms_with_values([
("foo", HashSet::from([1, 2])),
("foe", HashSet::from([2, 3])),
("fog", HashSet::from([3, 4])),
]);
let fuzzy = FuzzyMultiMap::new(dict, Algorithm::Standard);
let result = fuzzy.query("fox", 1).expect("expected Some result in test");
assert_eq!(result, HashSet::from([1, 2, 3, 4]));
}
#[test]
#[cfg(feature = "pathmap-backend")]
fn test_fuzzy_multimap_with_transposition() {
let dict = PathMapDictionary::from_terms_with_values([
("hello", HashSet::from([1])),
("ehllo", HashSet::from([2])), ]);
let fuzzy = FuzzyMultiMap::new(dict, Algorithm::Transposition);
let result = fuzzy
.query("hello", 2)
.expect("expected Some result in test");
assert!(result.contains(&1));
}
#[test]
#[cfg(feature = "pathmap-backend")]
fn test_fuzzy_multimap_query_with_distance() {
let dict = PathMapDictionary::from_terms_with_values([
("foo", vec!["original_foo".to_string()]),
("bar", vec!["original_bar".to_string()]),
("baz", vec!["original_baz".to_string()]),
]);
let fuzzy = FuzzyMultiMap::new(dict, Algorithm::Standard);
let results = fuzzy.query_with_distance("bat", 1);
assert_eq!(results.len(), 2);
let bar_result = results.iter().find(|(key, _, _)| key == "bar");
assert!(bar_result.is_some());
let (_, distance, values) = bar_result.expect("expected Some bar_result in test");
assert_eq!(*distance, 1);
assert_eq!(values, &vec!["original_bar".to_string()]);
let baz_result = results.iter().find(|(key, _, _)| key == "baz");
assert!(baz_result.is_some());
let (_, distance, values) = baz_result.expect("expected Some baz_result in test");
assert_eq!(*distance, 1);
assert_eq!(values, &vec!["original_baz".to_string()]);
}
#[test]
#[cfg(feature = "pathmap-backend")]
fn test_fuzzy_multimap_query_with_distance_exact() {
let dict = PathMapDictionary::from_terms_with_values([
("test", vec!["exact_match".to_string()]),
("tost", vec!["near_match".to_string()]),
]);
let fuzzy = FuzzyMultiMap::new(dict, Algorithm::Standard);
let results = fuzzy.query_with_distance("test", 1);
let exact = results.iter().find(|(key, _, _)| key == "test");
assert!(exact.is_some());
let (_, distance, _) = exact.expect("expected Some exact in test");
assert_eq!(*distance, 0);
let near = results.iter().find(|(key, _, _)| key == "tost");
assert!(near.is_some());
let (_, distance, _) = near.expect("expected Some near in test");
assert_eq!(*distance, 1);
}
}