Introduction
Keyhammer is a typo-tolerant search engine for short strings (words, names, identifiers). Given a dictionary of weighted terms and a possibly misspelled query, it returns the top-k terms of lowest edit cost. Edit costs are keyboard-aware: hitting a neighbouring key on a QWERTY layout, or repeating a letter, costs less than an arbitrary substitution.
The core crate, keyhammer, is no_std (it needs alloc), forbids unsafe
code and has no runtime dependencies.
Status
Keyhammer is an unpublished M0 prototype. Please read the results before relying on it:
- The search is exact: it returns the same top-k as a brute-force oracle, and the test suite checks this.
- It was much faster than the previous engine in the M0 benchmark (one machine; see Results). Ranking quality versus a simple edit-distance-plus-frequency baseline is reported there too. Both are measured on one machine, 300 typo pairs and one corpus, with provisional costs, so read the caveats before drawing conclusions.
- The cost table is provisional and has not been calibrated.
- The project claims no novelty: the ingredients are published work (see prior art).
- The engine currently expects lowercase ASCII (a-z) and compares other bytes verbatim. Unicode and case folding are not implemented yet.
- Nothing is published to crates.io or npm, and the API is unstable.
You can try the engine in your browser on the live demo page, which runs the core compiled to WebAssembly on a small sample dictionary or on terms you paste.
Quick example
This is the example from the crate documentation, which is compiled and run as a doc test.
use keyhammer::cost::CostModel;
use keyhammer::search::{SearchConfig, Searcher};
use keyhammer::trie::Trie;
let trie = Trie::build(&[
("javascript", 10),
("typescript", 10),
("python", 10),
("rust", 10),
("java", 10),
])
.unwrap();
let costs = CostModel::qwerty();
let mut searcher = Searcher::new();
// "javasript" skips the 'c' of "javascript".
let out = searcher
.search(&trie, &costs, b"javasript", &SearchConfig::default())
.unwrap();
let best = &out.hits[0];
assert_eq!(trie.term(best.id), "javascript");
assert_eq!(best.cost, 16);
Costs are fixed point: 16 is one ordinary edit, so the default budget of 32
allows about two edits. The subtree signature bound is on by default (tsb: true;
set tsb: false to turn it off); it prunes work without changing the results (see
How it works).
For higher recall at a latency cost, SearchConfig::high_recall() is the default
with a budget of 48 and the subtree bound on. In the repository's benchmark
(300 Birkbeck typo pairs, one machine, median of three runs) it found the right
word more often. Compared with the default budget (bound on in both), it expanded
about 4.6-5.2 times the nodes and had about 5.5-7.3 times the p95 latency (about
5.2-7.5 times with the bound off); measure it on your own dictionary. The largest accepted budget is 64.
Where to go next
- How it works: the data structure and the search.
- Results (M0 gate): the benchmark, including what failed.
- Prior art: the references checked and what is not novel.
- API reference: generated by rustdoc when the site is built in CI, under
/api/keyhammer/(linked from the navigation bar). - Source code: github.com/Keyhammer/Keyhammer