Fuzzy matching that actually ranks well — fzf-quality scoring with match-position highlighting, in ~1 KB. Zero dependencies.
Most "fuzzy" libraries just test whether your query is a subsequence, then sort
by string length. The results feel random. fuzzrank ports the scoring model
that makes fzf feel telepathic — a
dynamic-programming aligner that rewards matches at word boundaries,
camelCase humps and consecutive runs, and penalizes gaps — and it gives
you back the exact matched character indices so you can highlight them.
import { filter, match, highlight } from "fuzzrank";
// Rank a list, best-first
filter(["src/index.ts", "src/utils.ts", "test/index.test.ts"], "sidx");
// → [{ item: "src/index.ts", score: …, positions: [0,4,6,7] }, …]
// Score a single pair + highlight the match
const { positions } = match("gc", "getController")!; // { score, positions: [0, 3] }
highlight("getController", positions, (s) => `<b>${s}</b>`);
// → "<b>g</b>et<b>C</b>ontroller"- 🧠 Quality scoring, not just filtering. A real
O(n·m)alignment withfzf-style boundary / camelCase / consecutive bonuses. Prefixes beat mid-string hits;gcfindsgetControllerovergenericClass. - 🖍️ Highlight positions included. Every match returns the exact indices — drop them straight into your UI. Most libraries make you re-find them.
- 🪶 ~1 KB gzipped, zero dependencies. No index to build, no 30 KB download for a command palette.
- 🔡 Smart case. Lowercase query → case-insensitive; add an uppercase letter → it gets strict. (Override any time.)
- 🌍 Runs everywhere. Node 18+, Deno, Bun, Cloudflare Workers and the browser. Pure functions, no DOM.
- 🛡️ Type-safe & tested. Written in TypeScript; ranking invariants are covered by tests.
npm install fuzzrank
# or: pnpm add fuzzrank / yarn add fuzzrank / bun add fuzzrankShips ESM and CommonJS:
import { filter } from "fuzzrank"; // ESM / TypeScript
const { filter } = require("fuzzrank"); // CommonJSThe function you'll reach for most: pass items and a query, get matches sorted best-first (non-matches dropped).
filter(["readme.md", "src/app.ts", "src/app.test.ts"], "appts");
// Objects? Provide a key. Add a limit for big lists.
filter(users, "jo", { key: (u) => u.name, limit: 10 });
// → [{ item: { name: "Joanna", … }, score, positions }, …]match("fb", "foo-bar"); // { score: 55, positions: [0, 4] }
match("xyz", "foo-bar"); // null (not a subsequence)
match("", "anything"); // { score: 0, positions: [] }const res = match("idx", "src/index.ts")!;
highlight("src/index.ts", res.positions, (s) => `<mark>${s}</mark>`);
// → "src/<mark>i</mark>n<mark>d</mark>e<mark>x</mark>.ts"highlight merges consecutive matched characters into a single wrapped run, so
you get <b>idx</b> rather than <b>i</b><b>d</b><b>x</b>.
match("fb", "FooBar"); // matches (smart case: lowercase query)
match("FB", "foobar"); // null (smart case: query has uppercase)
match("FB", "foobar", { caseSensitive: false }); // matches (forced)For a query of length m and target of length n, fuzzrank fills two
m × n matrices in O(n·m) time:
- H — the best achievable score for aligning the query prefix with the target up to each position.
- C — the length of the current consecutive-match run, used to reward unbroken sequences.
Each matched character earns a base score plus a bonus that depends on the character before it in the target:
| Situation | Bonus |
|---|---|
| Start of string / after whitespace | high |
After a delimiter (/ , : ; |) |
high |
After other punctuation (_ - . …) |
medium |
camelCase hump (a → B) or letter → digit |
medium |
| Consecutive with the previous match | small |
Gaps incur a start penalty plus a per-character extension penalty. A back-trace
through C recovers the exact matched indices. This is the same family of
heuristics popularized by fzf and fzy, which is why short, sloppy queries
still land on the result you meant.
fuzzrank |
naive includes/subsequence |
full-text engines | |
|---|---|---|---|
| Boundary / camelCase scoring | ✅ | ❌ | |
| Match positions for highlight | ✅ | ❌ | |
| Zero dependencies | ✅ | ✅ | |
| ~1 KB, no index to build | ✅ | ✅ | ❌ |
| Best for | palettes, quick-open, autocomplete | trivial filters | large document search |
Contributions are very welcome! Please read CONTRIBUTING.md and our Code of Conduct.
git clone https://github.com/didrod205/fuzzrank.git
cd fuzzrank
npm install
npm testfuzzrank is free and MIT-licensed, built and maintained in spare time. If it
made your search box feel smart, please consider supporting it — every bit helps
keep the project healthy.
- ⭐ Star this repo — the simplest, free way to help others discover it.
- 🍋 Sponsor via Lemon Squeezy — one-time or recurring support.
Sponsoring? Open an issue and we'll add your name/logo here. Thank you! 🙏
MIT © fuzzrank contributors