Navigation: ← Back to Algorithms | Next Layer: Automata →
Note: The dictionary implementations have been extracted to the libdictenstein crate for independent use. For comprehensive dictionary documentation, see:
- libdictenstein/docs/algorithms/ - Implementation guides
- libdictenstein/docs/theory/ - SCDAWG and disk-trie theory
This document focuses on how liblevenshtein integrates dictionaries with Levenshtein transducers.
The Dictionary Layer forms the foundation of liblevenshtein's fuzzy matching capabilities. It provides pluggable backend implementations for storing and traversing collections of terms, optimized for efficient character-by-character navigation required by Levenshtein automata.
This layer abstracts over different data structures (tries, DAWGs, double-array tries) through common traits, allowing you to choose the best backend for your specific use case while maintaining a consistent API.
Backend taxonomy: how the available dictionary implementations relate to one another.
Dictionary trait hierarchy: the common interface every backend implements.
┌─────────────────────────────────────────────────────────────────┐
│ Dictionary Layer API │
│ ┌────────────────┐ ┌──────────────────┐ ┌─────────────────┐ │
│ │ Dictionary │ │ MappedDictionary │ │ DictionaryNode │ │
│ │ (Trait) │ │ (Trait) │ │ (Trait) │ │
│ └────────────────┘ └──────────────────┘ └─────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
│
┌───────────────────┼───────────────────┐
▼ ▼ ▼
┌──────────┐ ┌──────────────┐ ┌────────────┐
│ Trie │ │ DAWG │ │ Suffix │
│ Backends │ │ Backends │ │ Automaton │
└──────────┘ └──────────────┘ └────────────┘
│ │ │
┌────┴────┐ ┌────┴────┐ │
│ DAT │ │ Dynamic │ │
│ (rec) │ │ DAWG │ │
└─────────┘ └─────────┘ │
│ │
┌────┴────┐ │
│ DAT-Char│ │
│ (UTF-8) │ │
└─────────┘ │
Legend: (rec) = recommended default
The Dictionary trait defines the minimal interface for any dictionary backend:
pub trait Dictionary {
type Node: DictionaryNode;
fn root(&self) -> Self::Node;
fn contains(&self, term: &str) -> bool;
fn len(&self) -> Option<usize>;
fn is_empty(&self) -> bool;
}
Key Features:
Nodes represent positions in the dictionary graph:
pub trait DictionaryNode: Clone + Send + Sync {
type Unit: CharUnit; // u8 or char
fn is_final(&self) -> bool;
fn transition(&self, label: Self::Unit) -> Option<Self>;
fn edges(&self) -> Box<dyn Iterator<Item = (Self::Unit, Self)> + '_>;
}
Key Features:
Extensions for dictionaries that associate values with terms:
pub trait MappedDictionary: Dictionary {
type Value: DictionaryValue;
fn get_value(&self, term: &str) -> Option<Self::Value>;
fn contains_with_value<F>(&self, term: &str, predicate: F) -> bool
where F: Fn(&Self::Value) -> bool;
}
Performance Impact: Filtering during traversal provides 10-100x speedup compared to post-filtering.
See Value Storage for detailed documentation.
The library supports two modes for handling text:
| Mode | Type | Best For | Correctness |
|---|---|---|---|
| Byte-level | u8 | ASCII/Latin-1, Speed | Edit distances on byte sequences |
| Character-level | char | Unicode text | Proper Unicode code point distances |
Example:
// Byte-level: "café" = ['c', 'a', 'f', 0xC3, 0xA9] (5 bytes)
let dict_bytes = DoubleArrayTrie::from_terms(vec!["café"]);
// Character-level: "café" = ['c', 'a', 'f', 'é'] (4 chars)
let dict_chars = DoubleArrayTrieChar::from_terms(vec!["café"]);
// Different Levenshtein distances:
// "cafe" → "café": distance 1 (char-level), distance 2 (byte-level)
Best for: General-purpose applications
use libdictenstein::double_array_trie::DoubleArrayTrie;
let mut dict = DoubleArrayTrie::from_terms(vec![
"algorithm", "approximate", "automaton"
]);
dict.insert("analysis"); // Supports runtime insertions
Characteristics:
Best for: Multi-language applications with proper Unicode handling
use libdictenstein::double_array_trie::DoubleArrayTrieChar;
let mut dict = DoubleArrayTrieChar::from_terms(vec![
"café", "naïve", "中文", "🎉"
]);
dict.insert("新しい");
Characteristics:
Best for: Applications requiring both insert and remove operations
use libdictenstein::dynamic_dawg::DynamicDawg;
let dict = DynamicDawg::from_terms(vec!["initial", "terms"]);
dict.insert("new_term"); // ✅ Thread-safe
dict.remove("old_term"); // ✅ Supports removal
Characteristics:
Best for: Unicode applications with full dynamic updates
use libdictenstein::dynamic_dawg::DynamicDawgChar;
let dict = DynamicDawgChar::from_terms(vec!["café", "中文"]);
dict.insert("新しい"); // ✅ Unicode + thread-safe
dict.remove("café"); // ✅ Full removal support
Characteristics:
Best for: Substring/infix search within text
use libdictenstein::suffix_automaton::SuffixAutomaton;
let dict = SuffixAutomaton::from_source_text("the quick brown fox");
// Finds "quick" even though it's not a prefix
Characteristics:
pathmap-backend)Best for: Frequent updates with simpler structure
#[cfg(feature = "pathmap-backend")]
use libdictenstein::pathmap::PathMapDictionary;
let dict = PathMapDictionary::from_terms(vec!["test"]);
dict.insert("new"); // Simpler internal structure
Characteristics:
The classic static DawgDictionary and the arena-optimized OptimizedDawg were
removed in the 0.9.x line, when the dictionary backends were extracted into the
libdictenstein crate. Their roles are now
served by current backends:
DynamicDawg / DynamicDawgChar — a minimized DAWG that additionally supports
runtime insertions and removals (with SIMD and Bloom-filter acceleration) — for the
directed-acyclic-word-graph structure itself; andDoubleArrayTrie / DoubleArrayTrieChar — for static, read-optimized
dictionaries where the term set is fixed after construction.Code that imported either removed type should migrate to one of these; the decision guide below selects the right one for each workload.
Start: What do you need?
│
├─ Need to remove terms? ──Yes──> Unicode? ─Yes─> DynamicDawgChar
│ │ │
│ No └─No──> DynamicDawg
│ │
├─ Unicode text? ──Yes──> DoubleArrayTrieChar
│ │
│ No
│ │
└─> DoubleArrayTrie (recommended default)
| Feature | DAT | DAT-Char | DynDAWG | DynDAWG-Char | PathMap | SuffixAuto |
|---|---|---|---|---|---|---|
| Query Speed | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐ | ⭐⭐⭐ |
| Memory | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐ | ⭐⭐ |
| Construction | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐ |
| Insert | ✅ Append | ✅ Append | ✅ Full | ✅ Full | ✅ Full | ✅ Full |
| Remove | ❌ | ❌ | ✅ | ✅ | ✅ | ✅ |
| Union | ❌ | ❌ | ✅ | ✅ | ✅ | ❌ |
| Clone Cost | $\mathcal{O}(n)$ | $\mathcal{O}(n)$ | $\mathcal{O}(1)$ | $\mathcal{O}(1)$ | $\mathcal{O}(1)$ | N/A |
| Clone Sharing | ❌ Deep | ❌ Deep | ✅ Arc | ✅ Arc | ✅ Arc×2 | N/A |
| Unicode | Byte | ✅ Char | Byte | ✅ Char | Byte | Byte |
| Thread-Safe | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Use Case | General | Unicode | Dynamic | Dyn+Unicode | Simple | Substring |
Based on 10,000-word dictionary:
DoubleArrayTrie: 3.2ms
DoubleArrayTrieChar: 3.4ms (+6%)
PathMapDictionary: 3.5ms (+9%)
DynamicDawg: 4.1ms (+28%)
DoubleArrayTrie: 6.6µs
DoubleArrayTrieChar: 6.9µs (+5%)
PathMapDictionary: 71.1µs (+977%)
DoubleArrayTrie: 0.22µs per check
DoubleArrayTrieChar: 0.23µs (+5%)
PathMapDictionary: 132µs (+59900%)
DoubleArrayTrie: 16.3µs
DoubleArrayTrieChar: 17.1µs (+5%)
PathMapDictionary: 5,919µs (+36200%)
Key Takeaway: DoubleArrayTrie variants are consistently 3-30x faster than alternatives for fuzzy matching workloads.
DoubleArrayTrie: 8 bytes/state
DoubleArrayTrieChar: 12 bytes/state (char labels = 4x u8)
DynamicDawg: 24 bytes/state (Arc overhead)
PathMapDictionary: 32 bytes/state (HashMap overhead)
SuffixAutomaton: 48 bytes/state (suffix links)
DoubleArrayTrie: ~800 KB
DoubleArrayTrieChar: ~1.2 MB
DynamicDawg: ~2.4 MB
PathMapDictionary: ~3.2 MB
Recommendation: DoubleArrayTrie or DoubleArrayTrieChar
use libdictenstein::double_array_trie::DoubleArrayTrie;
use liblevenshtein::levenshtein::Algorithm;
use liblevenshtein::levenshtein_automaton::LevenshteinAutomaton;
// Initialize once at startup
let dict = DoubleArrayTrie::from_terms(load_product_names());
// Per-request fuzzy search
fn autocomplete(query: &str, max_distance: usize) -> Vec<String> {
let automaton = LevenshteinAutomaton::new(query, max_distance, Algorithm::Standard);
automaton.query(&dict).collect()
}
Why: Fast queries (microseconds), low memory, append-only updates for new products.
Recommendation: DoubleArrayTrieChar
use libdictenstein::double_array_trie::DoubleArrayTrieChar;
let dict = DoubleArrayTrieChar::from_terms(vec![
// English
"color", "colour",
// Spanish
"niño", "año",
// Chinese
"你好", "世界",
// Emoji
"😀", "🎉"
]);
// Correct Levenshtein distances for all languages
Why: Character-level distances handle accents, CJK, emoji correctly.
Recommendation: DynamicDawg or DynamicDawgChar
use libdictenstein::dynamic_dawg::DynamicDawg;
let dict = DynamicDawg::new();
// User adds word to personal dictionary
dict.insert("refactoring");
// User removes word
dict.remove("typo");
// Active autocomplete queries see changes immediately
Why: Thread-safe insert/remove, queries reflect updates instantly.
Recommendation: DoubleArrayTrie<u32> with values
use libdictenstein::double_array_trie::DoubleArrayTrie;
let dict = DoubleArrayTrie::from_terms_with_values(vec![
("println", 1), // Global scope
("format", 1), // Global scope
("my_var", 42), // Local scope 42
("temp", 42), // Local scope 42
]);
// Query only local scope (10-100x faster than post-filtering)
let results = query_with_filter(&dict, "temp", 2, |scope| *scope == 42);
Why: Value filtering during traversal is dramatically faster. See Value Storage.
Recommendation: SuffixAutomaton
use libdictenstein::suffix_automaton::SuffixAutomaton;
let doc = "The quick brown fox jumps over the lazy dog";
let dict = SuffixAutomaton::from_source_text(doc);
// Find "quick" even though it's not at the beginning
let results = fuzzy_search(&dict, "quik", 1); // Finds "quick"
Why: Matches substrings anywhere in text, not just prefixes.
Recommendation: DynamicDawg or PathMapDictionary with values
use libdictenstein::dynamic_dawg::DynamicDawg;
use libdictenstein::MutableMappedDictionary;
// System-wide default frequencies
let system_dict: DynamicDawg<u32> = DynamicDawg::new();
system_dict.insert_with_value("algorithm", 1000);
system_dict.insert_with_value("database", 800);
// User-specific word frequencies
let user_dict: DynamicDawg<u32> = DynamicDawg::new();
user_dict.insert_with_value("algorithm", 50); // User types this often
user_dict.insert_with_value("refactoring", 30); // User-specific term
// Merge: prioritize user frequencies but include system terms
system_dict.union_with(&user_dict, |system_freq, user_freq| {
// Boost user terms by 10x for better autocomplete ranking
user_freq * 10 + system_freq
});
// Result: "algorithm" = 1500 (50*10 + 1000)
// "refactoring" = 300 (30*10 + 0)
// "database" = 800 (unchanged)
Why: Union operations enable personalized autocomplete by combining user patterns with system defaults, custom merge logic for ranking.
Alternative with Configuration Layers:
use libdictenstein::pathmap::PathMapDictionary;
use libdictenstein::MutableMappedDictionary;
// Default application settings
let defaults: PathMapDictionary<String> = PathMapDictionary::new();
defaults.insert_with_value("theme", "light".to_string());
defaults.insert_with_value("language", "en".to_string());
// User preferences
let user_prefs: PathMapDictionary<String> = PathMapDictionary::new();
user_prefs.insert_with_value("theme", "dark".to_string()); // Override
// Merge: user preferences override defaults (last-writer-wins)
defaults.union_replace(&user_prefs);
// Effective config: theme=dark, language=en
Why: PathMapDictionary's structural sharing makes it ideal for configuration layers with frequent snapshots.
The Dictionary Layer is designed to work seamlessly with Layer 2 (Automata):
use libdictenstein::double_array_trie::DoubleArrayTrie;
use liblevenshtein::levenshtein::Algorithm;
use liblevenshtein::levenshtein_automaton::LevenshteinAutomaton;
// Step 1: Create dictionary
let dict = DoubleArrayTrie::from_terms(vec!["test", "testing", "tested"]);
// Step 2: Create automaton for query
let automaton = LevenshteinAutomaton::new("tset", 1, Algorithm::Standard);
// Step 3: Query dictionary with automaton
let results: Vec<String> = automaton.query(&dict).collect();
// Results: ["test"] (distance 1: swap 's' and 'e')
The automaton traverses the dictionary graph using DictionaryNode::transition() to explore only paths within the distance threshold.
See Automata Layer for details.
All dictionary implementations in this library are thread-safe for concurrent reads:
use std::sync::Arc;
use std::thread;
let dict = Arc::new(DoubleArrayTrie::from_terms(vec!["test"]));
// Multiple threads can query simultaneously
let handles: Vec<_> = (0..4).map(|_| {
let dict = Arc::clone(&dict);
thread::spawn(move || {
dict.contains("test") // ✅ Safe
})
}).collect();
For concurrent writes, dictionaries have different strategies:
| Dictionary | Strategy | Writes | Notes |
|---|---|---|---|
| DoubleArrayTrie | Persistent | Rebuild + atomic swap | Append-only via builder |
| DynamicDawg | InternalSync | Direct mutation | Lock-free (ArcSwap reads; CAS writes) |
| PathMapDictionary | InternalSync | Direct mutation | Lock-free (ArcSwap<PathMapState> swap) |
To implement a custom backend:
use libdictenstein::{Dictionary, DictionaryNode, CharUnit};
#[derive(Clone)]
struct MyNode {
// Your node structure
}
impl DictionaryNode for MyNode {
type Unit = u8;
fn is_final(&self) -> bool {
// Check if this node marks end of term
}
fn transition(&self, label: Self::Unit) -> Option<Self> {
// Follow edge labeled with 'label'
}
fn edges(&self) -> Box<dyn Iterator<Item = (Self::Unit, Self)> + '_> {
// Return all outgoing edges
}
}
struct MyDictionary {
// Your dictionary structure
}
impl Dictionary for MyDictionary {
type Node = MyNode;
fn root(&self) -> Self::Node {
// Return root node
}
fn len(&self) -> Option<usize> {
Some(/* term count */)
}
}
Dictionaries can be serialized for persistence:
use libdictenstein::double_array_trie::DoubleArrayTrie;
use libdictenstein::serialization::{BincodeSerializer, DictionarySerializer};
let dict = DoubleArrayTrie::from_terms(vec!["test"]);
// Serialize (`Vec<u8>` implements `Write`)
let mut bytes = Vec::new();
BincodeSerializer::serialize(&dict, &mut bytes)?;
std::fs::write("dict.bin", &bytes)?;
// Deserialize (`&[u8]` implements `Read`)
let bytes = std::fs::read("dict.bin")?;
let dict: DoubleArrayTrie = BincodeSerializer::deserialize(&bytes[..])?;
See Serialization Guide for details.
Aoe, J. (1989). "An Efficient Digital Search Algorithm by Using a Double-Array Structure"
Yata, S., Oono, M., Morita, K., Fuketa, M., Sumitomo, T., & Aoe, J. (2007). "A compact static double-array keeping character codes"
Blumer, A., Blumer, J., Haussler, D., McConnell, R., & Ehrenfeucht, A. (1987). "Complete inverted files for efficient text retrieval and analysis"
Navigation: ← Back to Algorithms | Next Layer: Automata →
Can you improve this documentation?Edit on GitHub
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |