# uFuzzy: a zero-dependency fuzzy matcher for client-side list filtering

> uFuzzy is a small JavaScript library that filters and ranks a short search phrase against a large list of strings without building an index. It suits autocomplete, typeahead and filename or title search, and it is a poor fit for natural-language search or non-Latin scripts at full speed.

**leeoniya/uFuzzy** — A tiny, efficient fuzzy search that doesn't suck

- Repository: https://github.com/leeoniya/uFuzzy
- Stars: 3,033 · Forks: 58
- Language: JavaScript
- License: MIT
- Published: 2026-09-24 · Updated: 2026-09-24 · Language: en
- Canonical page: https://hysenlabs.com/projects/leeoniya-ufuzzy

## The problem uFuzzy solves: forgiving substring matching over a list

uFuzzy targets one shape of problem. A short needle is matched against a large list of short-to-medium haystack phrases. The README describes it as a more forgiving String.includes(), and the intended uses are list filtering, autocomplete, typeahead, and searches over titles, names, descriptions, filenames and function names. It is not a document search engine and it does not tokenize prose.

The design decision that follows from that scope is the absence of an index. The README states there is no index to build, so startup is below 1ms with near-zero memory overhead, and searching a three-term phrase in a 162,000 phrase dataset takes 5ms with out-of-order terms. Those numbers come from the project's own documentation, not from independent measurement. The trade-off is that cost scales with haystack size at query time rather than being paid once at build time, which is why the author caps ranked results at 1,000 items in the example.

Who it is for: front-end engineers who already hold an array of strings in memory and need to narrow it as the user types. If your data lives in a database, or your corpus is paragraphs rather than phrases, the library's matching rules will fight you.

## How matching works: MultiInsert, SingleError and the three-stage pipeline

In the default MultiInsert mode, each match must contain all alphanumeric characters from the needle in the same sequence. That is the core rule, and it explains why results stay predictable: there is no scoring model to tune and no relevance cutoff to guess. In SingleError mode, single typos are tolerated per term, with a Damerau-Levenshtein distance of 1.

The API is a pipeline rather than a single call. uf.filter() returns an array of indices into the haystack, or null when the needle has no alphanumeric characters and is therefore non-searchable. uf.info() takes those indices and may reduce them further based on prefix and suffix rules. uf.sort() returns an order array, which the README describes as a double-indirection array: a re-order of the passed-in idxs, so that corresponding info can be grabbed directly by index.

The separation matters for performance. The README's example ranks only when the filtered set is at or below 1,000 items and otherwise renders the pre-filtered but unordered matches. So the first stage is cheap and the ranking stage is bounded. Sorting is a plain Array.sort() with access to each match's stats and counters, not a black-box composite score. That is the most distinctive part of the design: you can reason about why a result ranked where it did. The cost is that you write the comparator yourself.

The search API also handles out-of-order terms, multiple substring exclusions such as fruit -green -melon, and exact terms containing non-alphanumeric characters such as "C++", "$100" and "#hashtag". The README notes that, held just right, it can match against multiple object properties. That phrasing is doing real work: multi-property matching is a technique you arrange, not a documented feature with a stable interface.

## Installing uFuzzy from npm and running a first search

The README gives two installation paths. For Node and bundlers, the package is published as @leeoniya/ufuzzy. The package.json declares main as ./dist/uFuzzy.cjs, module as ./dist/uFuzzy.mjs and types as ./dist/uFuzzy.d.ts, so both CommonJS and ES module consumers are covered and TypeScript declarations ship with the package. The published files list is limited to package.json, README.md, LICENSE and the dist directory.

```bash
npm i @leeoniya/ufuzzy
```

In a CommonJS context the README shows a require call:

```js
const uFuzzy = require('@leeoniya/ufuzzy');
```

For a plain browser page with no bundler, the README loads the IIFE build directly. The README states the minified IIFE build is currently about 7.5KB, and there are zero dependencies.

```js
<script src="./dist/uFuzzy.iife.min.js"></script>
```

A first real use follows the pipeline described earlier. Construct an instance, filter, then rank only if the filtered set is small enough. The snippet below is adapted from the README example, with the same option object, the same threshold value and the same method names.

```js
let haystack = ['puzzle', 'Super Awesome Thing (now with stuff!)', 'FileName.js', '/feeding/the/catPic.jpg'];
let needle = 'feed cat';
let uf = new uFuzzy({});

let idxs = uf.filter(haystack, needle);
if (idxs != null && idxs.length > 0) {
  let infoThresh = 1e3;
  if (idxs.length <= infoThresh) {
    let info = uf.info(idxs, haystack, needle);
    let order = uf.sort(info, haystack, needle);
    for (let i = 0; i < order.length; i++) console.log(haystack[info.idx[order[i]]]);
  }
}
```

What you should see for the needle feed cat is the entry /feeding/the/catPic.jpg, because it contains the characters f-e-e-d and c-a-t in sequence. The other three strings do not satisfy the MultiInsert rule and are dropped at the filter stage. If you pass a needle with no alphanumeric characters, filter() returns null rather than an empty array, and the README's example checks for that case explicitly.

## Where uFuzzy breaks down: Latin-only regexps and the 1,000-item ranking ceiling

The first limitation is stated plainly in the README. uFuzzy is optimized for the Latin/Roman alphabet and relies internally on non-unicode regular expressions. Support for other languages works either by augmenting the built-in Latin regexps with additional characters or by using the universal {unicode: true} variant, which the README says is 50% to 75% slower. There is a simpler but less flexible {alpha: "..."} option that replaces the A-Z and a-z parts of the built-in regexps with characters of your choice, with letter case handled automatically during replacement.

So a Russian or Norwegian haystack is not unsupported, but it costs you either a hand-written regexp set or a measurable performance penalty. The README provides full option objects for Latin plus Norwegian and Latin plus Russian, which is more guidance than most libraries give, but it also means the correct configuration is your responsibility. A separate utility, uFuzzy.latinize(), strips common accents and diacritics from the haystack and needle before searching, which is a different strategy: normalize the data rather than widen the alphabet.

The second limitation is case sensitivity. The README states that all searches are currently case-insensitive and that it is not possible to do a case-sensitive search. For filename search on case-sensitive filesystems, that is a real constraint, not a nuance.

The third is the ranking threshold. The README's own example ranks only when the filtered set is at or below 1,000 items; above that it renders pre-filtered but unordered matches. The library does not hide this, but a reader skimming the feature list may miss it. If your workflow requires relevance ordering over tens of thousands of matches, uFuzzy's documented pattern does not give it to you.

Finally, the package.json test script is echo "Error: no test specified" && exit 1. There is no test suite in the published package. For a matching library, where correctness is judged by edge cases, that is worth weighing before you depend on it.

## uFuzzy compared with Fuse.js and fuzzysort

The README's demo page boots uFuzzy alongside fuzzysort, QuickScore and Fuse.js on the same dataset, and the project links a section titled a biased appraisal of similar work. The author is explicit that the comparison is biased, which is a reasonable thing to say and also a reason to run the demo yourself rather than take the ordering on faith.

The difference in approach is the ranking model. Fuse.js is commonly described as a weighted scoring search, where you configure keys, weights and a score threshold and the library returns a composite relevance number. uFuzzy has no composite score. Its README says there is no composite, black box score to understand, and that sorting is done with a plain Array.sort() that receives each match's stats and counters. That is the real fork in the road: Fuse.js asks you to tune parameters until the output looks right; uFuzzy asks you to write the comparator that expresses what right means.

fuzzysort occupies similar ground to uFuzzy as a fast matcher, and the README groups it in the same demo comparison. Since the project does not describe fuzzysort's internals, the honest statement is that the two are presented as alternatives on the same benchmark page and the project invites you to compare them there.

One more practical difference: uFuzzy has zero dependencies and a minified IIFE build the README states is about 7.5KB, with no index to build. If your constraint is a small bundle and instant startup on a list already in memory, that is the axis on which uFuzzy is designed to win. If your constraint is matching keys across nested objects with configurable weights, Fuse.js is built for that and uFuzzy is not.

## Maintenance status, licence and upgrade cost

The repository is not archived. The last push was on 2026-09-08, which is recent. The most recent release listed is 1.0.19 from 2025-08-22, preceded by 1.0.18 in January 2025 and 1.0.17 in November 2024. The version numbers have stayed in the 1.0.x line, and the package.json version matches 1.0.19, so the published artifact and the repository are in step.

Upgrade cost is low by construction. The dependency list is empty at runtime; devDependencies are rollup and @rollup/plugin-terser, used only for the build. The build script is rollup -c. There is no index format to migrate, no schema, and no persisted state, so a version bump cannot invalidate stored data. The main upgrade risk is behavioural: the matching rules and the option names are the entire interface, and a change to the built-in regexps or to the sort contract would show up as different result ordering rather than as an error.

The licence is MIT, declared in package.json and present as a LICENSE file at the repository root. MIT permits commercial use, modification and redistribution provided the copyright notice and permission notice are retained. That is a factual statement about the licence text, not legal advice; if you redistribute the library inside a product, have your own counsel confirm how the notice is carried in your distribution.

## Conclusion

Adopt uFuzzy when you filter a few thousand short strings in the browser and want ranking rules you can read. Do not adopt it for full-text search, non-Latin scripts at full speed, or result sets far above the 1,000-item ranking threshold. Verify first that your haystack fits the phrase shape the library targets, and that the default MultiInsert mode tolerates the typos your users actually make.

## FAQ

### How does fuzzy matching in uFuzzy work?

In the default MultiInsert mode, every match must contain all alphanumeric characters from the needle in the same sequence, which the README describes as a more forgiving String.includes(). A SingleError mode tolerates one typo per term at a Damerau-Levenshtein distance of 1.

### What does fuzzy match mean in uFuzzy?

It means the needle does not have to appear as a contiguous substring. The characters must appear in order, so a needle like feed cat can match a path such as /feeding/the/catPic.jpg, and out-of-order terms are supported as well.

### Can uFuzzy do a case-sensitive search?

No. The README states that all searches are currently case-insensitive and that it is not possible to do a case-sensitive search.

### Does uFuzzy build an index?

No. The README states there is no index to build, so startup is below 1ms with near-zero memory overhead, and it gives a figure of 5ms for a three-term phrase searched against a 162,000 phrase dataset.

### What is the size of the uFuzzy bundle and does it have dependencies?

The README states the library is micro with zero dependencies and that the minified IIFE build is currently about 7.5KB. The package.json lists only rollup and @rollup/plugin-terser as devDependencies.

### Does uFuzzy work with non-Latin alphabets such as Russian?

It works, but with a cost. The README says uFuzzy is optimized for the Latin alphabet and relies on non-unicode regular expressions, so other languages need either augmented regexps or the universal {unicode: true} variant, which the README says is 50% to 75% slower.

## Sources

- [Issues](https://github.com/leeoniya/uFuzzy/issues)
- [leeoniya/uFuzzy on GitHub](https://github.com/leeoniya/uFuzzy)
- [License: MIT](https://github.com/leeoniya/uFuzzy/blob/main/LICENSE)
- [README](https://github.com/leeoniya/uFuzzy/blob/main/README.md)
- [Releases](https://github.com/leeoniya/uFuzzy/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/leeoniya-ufuzzy
