use wasm_bindgen::prelude::*;
use crate::edit::*;
use crate::token::*;
#[wasm_bindgen]
#[derive(Clone, Copy, Debug)]
pub enum Algorithm {
Levenshtein,
Jaro,
JaroWinkler,
Hamming,
Sift4,
LcsSeq,
LcsStr,
Ratcliff,
SmithWaterman,
NeedlemanWunsch,
Gotoh,
BagDistance,
Mra,
Jaccard,
Cosine,
Sorensen,
Tversky,
Overlap,
JaccardBigram,
CosineBigram,
}
fn apply_algorithm(algo: Algorithm, a: &str, b: &str) -> f64 {
match algo {
Algorithm::Levenshtein => levenshtein_normalized(a, b),
Algorithm::Jaro => jaro(a, b),
Algorithm::JaroWinkler => jaro_winkler(a, b),
Algorithm::Hamming => hamming_normalized(a, b),
Algorithm::Sift4 => sift4_simple_normalized(a, b),
Algorithm::LcsSeq => lcs_seq_normalized(a, b),
Algorithm::LcsStr => lcs_str_normalized(a, b),
Algorithm::Ratcliff => ratcliff_obershelp(a, b),
Algorithm::SmithWaterman => smith_waterman_normalized(a, b),
Algorithm::NeedlemanWunsch => needleman_wunsch_normalized(a, b),
Algorithm::Gotoh => gotoh_normalized(a, b),
Algorithm::BagDistance => bag_distance_normalized(a, b),
Algorithm::Mra => mra_normalized(a, b),
Algorithm::Jaccard => jaccard(a, b),
Algorithm::Cosine => cosine(a, b),
Algorithm::Sorensen => sorensen(a, b),
Algorithm::Tversky => tversky(a, b),
Algorithm::Overlap => overlap(a, b),
Algorithm::JaccardBigram => jaccard_bigram(a, b),
Algorithm::CosineBigram => cosine_bigram(a, b),
}
}
#[wasm_bindgen]
pub struct SearchResult {
index: u32,
score: f64,
}
#[wasm_bindgen]
impl SearchResult {
#[wasm_bindgen(getter)]
pub fn index(&self) -> u32 { self.index }
#[wasm_bindgen(getter)]
pub fn score(&self) -> f64 { self.score }
}
#[wasm_bindgen]
pub struct FuzzySearch {
items: Vec<String>,
algo: Algorithm,
threshold: f64,
case_sensitive: bool,
}
#[wasm_bindgen]
impl FuzzySearch {
#[wasm_bindgen(constructor)]
pub fn new(items: Vec<String>, algo: Algorithm, threshold: f64, case_sensitive: bool) -> Self {
FuzzySearch { items, algo, threshold, case_sensitive }
}
#[wasm_bindgen]
pub fn search(&self, query: &str, limit: Option<u32>) -> Vec<SearchResult> {
let q = if self.case_sensitive { query.to_string() } else { query.to_lowercase() };
let effective_limit = limit.unwrap_or(u32::MAX) as usize;
let mut results: Vec<SearchResult> = Vec::new();
for (i, item) in self.items.iter().enumerate() {
let item_str = if self.case_sensitive { item.clone() } else { item.to_lowercase() };
let score = apply_algorithm(self.algo, &q, &item_str);
if score >= self.threshold {
results.push(SearchResult { index: i as u32, score });
}
}
results.sort_by(|a, b| b.score.partial_cmp(&a.score).unwrap_or(std::cmp::Ordering::Equal));
results.truncate(effective_limit);
results
}
#[wasm_bindgen(getter)]
pub fn size(&self) -> u32 { self.items.len() as u32 }
#[wasm_bindgen]
pub fn set_items(&mut self, items: Vec<String>) { self.items = items; }
#[wasm_bindgen]
pub fn add(&mut self, item: String) { self.items.push(item); }
#[wasm_bindgen]
pub fn clear(&mut self) { self.items.clear(); }
}
#[wasm_bindgen]
pub struct MultiKeySearchResult {
index: u32,
score: f64,
key_scores: Vec<f64>,
}
#[wasm_bindgen]
impl MultiKeySearchResult {
#[wasm_bindgen(getter)]
pub fn index(&self) -> u32 { self.index }
#[wasm_bindgen(getter)]
pub fn score(&self) -> f64 { self.score }
#[wasm_bindgen]
pub fn key_scores(&self) -> Vec<f64> { self.key_scores.clone() }
}
#[wasm_bindgen]
pub struct MultiKeyFuzzySearch {
key_values: Vec<String>,
num_keys: usize,
num_items: usize,
weights: Vec<f64>,
algo: Algorithm,
threshold: f64,
case_sensitive: bool,
}
#[wasm_bindgen]
impl MultiKeyFuzzySearch {
#[wasm_bindgen(constructor)]
pub fn new(
key_values: Vec<String>,
num_keys: u32,
weights: Vec<f64>,
algo: Algorithm,
threshold: f64,
case_sensitive: bool,
) -> Self {
let nk = num_keys as usize;
let ni = if nk > 0 { key_values.len() / nk } else { 0 };
let total: f64 = weights.iter().sum();
let normalized: Vec<f64> = if total > 0.0 {
weights.iter().map(|w| w / total).collect()
} else {
vec![1.0 / nk as f64; nk]
};
MultiKeyFuzzySearch {
key_values,
num_keys: nk,
num_items: ni,
weights: normalized,
algo,
threshold,
case_sensitive,
}
}
#[wasm_bindgen]
pub fn search(&self, query: &str, limit: Option<u32>) -> Vec<MultiKeySearchResult> {
let q = if self.case_sensitive { query.to_string() } else { query.to_lowercase() };
let effective_limit = limit.unwrap_or(u32::MAX) as usize;
let mut results: Vec<MultiKeySearchResult> = Vec::new();
for i in 0..self.num_items {
let mut score = 0.0;
let mut key_scores = Vec::with_capacity(self.num_keys);
for k in 0..self.num_keys {
let val = &self.key_values[i * self.num_keys + k];
let item_str = if self.case_sensitive { val.clone() } else { val.to_lowercase() };
let s = apply_algorithm(self.algo, &q, &item_str);
key_scores.push(s);
score += self.weights[k] * s;
}
if score >= self.threshold {
results.push(MultiKeySearchResult { index: i as u32, score, key_scores });
}
}
results.sort_by(|a, b| b.score.partial_cmp(&a.score).unwrap_or(std::cmp::Ordering::Equal));
results.truncate(effective_limit);
results
}
#[wasm_bindgen(getter)]
pub fn size(&self) -> u32 { self.num_items as u32 }
}
#[wasm_bindgen]
pub fn find_best_match(
query: &str,
items: Vec<String>,
algo: Algorithm,
threshold: f64,
case_sensitive: bool,
) -> SearchResult {
let q = if case_sensitive { query.to_string() } else { query.to_lowercase() };
let mut best_idx: u32 = u32::MAX;
let mut best_score = 0.0f64;
for (i, item) in items.iter().enumerate() {
let item_str = if case_sensitive { item.clone() } else { item.to_lowercase() };
let score = apply_algorithm(algo, &q, &item_str);
if score >= threshold && score > best_score {
best_score = score;
best_idx = i as u32;
}
}
SearchResult { index: best_idx, score: best_score }
}