Fuzzy string matching via Levenshtein edit distance. Used for typo-tolerant full-text search. When an exact term match isn't found in the inverted index, fuzzy matching finds terms within a configurable edit distance. Distance thresholds are adaptive based on token length: - 1-3 chars: exact only (no fuzzy — too many false positives) - 4-6 chars: max distance 1 - 7+ chars: max distance 2 Compute
(a: &str, b: &str)
| 18 | /// |
| 19 | /// Uses the two-row DP: O(min(a,b)) space, O(a*b) time. |
| 20 | pub fn levenshtein(a: &str, b: &str) -> usize { |
| 21 | let a_chars: Vec<char> = a.chars().collect(); |
| 22 | let b_chars: Vec<char> = b.chars().collect(); |
| 23 | let a_len = a_chars.len(); |
| 24 | let b_len = b_chars.len(); |
| 25 | |
| 26 | if a_len == 0 { |
| 27 | return b_len; |
| 28 | } |
| 29 | if b_len == 0 { |
| 30 | return a_len; |
| 31 | } |
| 32 | |
| 33 | // Iterate the shorter string as the inner dimension. |
| 34 | let (short, long) = if a_len <= b_len { |
| 35 | (&a_chars[..], &b_chars[..]) |
| 36 | } else { |
| 37 | (&b_chars[..], &a_chars[..]) |
| 38 | }; |
| 39 | let s_len = short.len(); |
| 40 | |
| 41 | let mut prev_row: Vec<usize> = (0..=s_len).collect(); |
| 42 | let mut curr_row: Vec<usize> = vec![0; s_len + 1]; |
| 43 | |
| 44 | for (j, l_ch) in long.iter().enumerate() { |
| 45 | curr_row[0] = j + 1; |
| 46 | for (i, s_ch) in short.iter().enumerate() { |
| 47 | let cost = if s_ch == l_ch { 0 } else { 1 }; |
| 48 | curr_row[i + 1] = (prev_row[i + 1] + 1) |
| 49 | .min(curr_row[i] + 1) |
| 50 | .min(prev_row[i] + cost); |
| 51 | } |
| 52 | std::mem::swap(&mut prev_row, &mut curr_row); |
| 53 | } |
| 54 | |
| 55 | prev_row[s_len] |
| 56 | } |
| 57 | |
| 58 | /// Maximum allowed edit distance for a given token length. |
| 59 | /// |
no test coverage detected