Skip to content

didrod205/fuzzrank

fuzzrank

Fuzzy matching that actually ranks well — fzf-quality scoring with match-position highlighting, in ~1 KB. Zero dependencies.

npm version bundle size CI types license

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"

Why fuzzrank?

  • 🧠 Quality scoring, not just filtering. A real O(n·m) alignment with fzf-style boundary / camelCase / consecutive bonuses. Prefixes beat mid-string hits; gc finds getController over genericClass.
  • 🖍️ 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.

Install

npm install fuzzrank
# or: pnpm add fuzzrank  /  yarn add fuzzrank  /  bun add fuzzrank

Ships ESM and CommonJS:

import { filter } from "fuzzrank";        // ESM / TypeScript
const { filter } = require("fuzzrank");   // CommonJS

Usage

filter — rank a list

The 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 — score one pair

match("fb", "foo-bar");  // { score: 55, positions: [0, 4] }
match("xyz", "foo-bar"); // null  (not a subsequence)
match("", "anything");   // { score: 0, positions: [] }

highlight — mark the match in your UI

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>.

Case sensitivity

match("fb", "FooBar");                       // matches (smart case: lowercase query)
match("FB", "foobar");                       // null    (smart case: query has uppercase)
match("FB", "foobar", { caseSensitive: false }); // matches (forced)

How the scoring works

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 (aB) 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.

Comparison

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

Contributing

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 test

💖 Sponsor

fuzzrank 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! 🙏

License

MIT © fuzzrank contributors

About

Tiny zero-dependency fuzzy matcher with fzf-quality scoring and match-position highlighting — for command palettes, quick-open & autocomplete. ~1KB.

Topics

Resources

License

Code of conduct

Contributing

Stars

Watchers

Forks

Packages

 
 
 

Contributors