Fuse.js: Client-Side Fuzzy Search Without a Search Backend
Lightweight fuzzy-search, in JavaScript
At a glance
- What is it?
- Fuse.js is a zero-dependency JavaScript fuzzy-search library built on the Bitap algorithm. It fits small-to-medium datasets that live in the browser or on the server, and it stops being the right answer once you need an index that survives a page reload.
- Who is it for?
- Adopt Fuse.js when the corpus is small or medium, already in memory, and you want typo-tolerant matching without running a search service; the package is Apache-2.0 and ships as ESM, CJS and minified builds. Do not adopt it when you need persistence, index rollback, or server-side relevance tuning, because the README does not document rollback and the index is built from the array you hand it.
- Can I use it commercially?
- Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 52 days ago.
- What is it written in?
- Mainly JavaScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on September 29, 2026, and from our analysis. They are not legal advice.
Editorial analysis
What Fuse.js Is For, and What It Is Not
Fuse.js targets a specific situation: you have an array of records already loaded in the page or process, and you want users to find entries despite typos. The README frames it as a library "designed for searching small-to-medium datasets on the client side where you can't rely on a dedicated search backend." That sentence is the whole scope. If your data lives in Postgres, Elasticsearch or a hosted search API, Fuse.js is not competing with those systems; it is competing with the naive `Array.prototype.filter` call you would otherwise write.
The audience follows from that. A React or Vue app with a few thousand catalogue rows, a documentation site with a client-side index, a desktop tool built on Electron, or a Node script that needs to rank a JSON file. All of those share a property: the corpus fits in memory, and rebuilding the search structure is cheap enough to do at startup. The library has no persistence layer, no HTTP surface, and no schema. You construct a `Fuse` instance from an array, and you search it.
The trade-off is explicit rather than hidden. Because there is no backend, there is no shared index, no incremental update across clients, and no way for two users to see different result sets derived from the same server-side corpus. Each `Fuse` instance is local to the process or tab that created it.
Bitap Matching, Weighted Keys and the Ranking Pipeline
The core matcher is the Bitap algorithm, which the README links to and describes as handling "typos, misspellings, and partial matches out of the box." Bitap works by representing the pattern as a bitmask and shifting it across the text, so approximate matching falls out of bitwise operations rather than edit-distance computation. That is why the library can stay dependency-free and small: the matching loop is arithmetic, not a graph traversal.
On top of that sit several layers. Weighted keys let one field outrank another. The README gives this configuration, where a title match counts twice as much as a description match:
const fuse = new Fuse(docs, {
keys: [
{ name: 'title', weight: 2 },
{ name: 'description', weight: 1 }
]
})Extended search adds operators, but the more interesting design choice is the object query syntax, which the README presents as a structured alternative to the magic characters. It needs no `useExtendedSearch` flag because the operators are unambiguous, it autocompletes in TypeScript, and it avoids quoting and escaping. The mapping is direct: `$fuzzy`, `$eq`, `$contains`, `$startsWith`, `$endsWith`, with `$not` wrapping exactly one of the last three, and `$and` / `$or` available field-locally. The README states that object queries return identical results to the equivalent string query, and that they "validate strictly: unknown operators, empty or non-string values, and illegal nesting throw instead of silently degrading to a fuzzy search." That strictness is the point. A typo in a query string silently becomes a fuzzy term; a typo in an object key throws.
Token search is the newer layer. It splits a multi-word query into terms, fuzzy-matches each independently, and ranks with BM25-style IDF weighting, so rare words count more than common ones. Word order becomes irrelevant, and the README notes there is "no query length limit" because each term is searched separately. The `tokenMatch` option switches between the default `'any'` and `'all'`, the latter behaving as a filter. A custom tokenizer via `tokenize` handles tokens with internal punctuation such as `node.js` or `c++`, or `Intl.Segmenter` for CJK and Thai segmentation. Token search and logical search are both listed as available in the full build only.
Installing Fuse.js and Running a First Search
Installation is a single npm command, and the README also lists a yarn equivalent and a CDN script tag pointing at `https://cdn.jsdelivr.net/npm/fuse.js/dist/fuse.min.mjs`.
npm install fuse.jsThe package is ESM-first (`"type": "module"`) but ships both module systems. The `exports` map exposes `.` for the full build, `./min`, `./basic`, `./min-basic`, `./worker` and `./worker-script`, each with an `import` and a `require` condition, plus separate `.d.ts` and `.d.cts` type entries. Check which subpath you import: pulling `fuse.js/basic` gets you a smaller build without the token and logical search features, and the README does not enumerate exactly what the basic build omits.
A first real use is the books example from the README. You pass the array and the keys to search, then call `search` with a misspelled term.
import Fuse from 'fuse.js'
const books = [
{ title: "Old Man's War", author: 'John Scalzi' },
{ title: 'The Lock Artist', author: 'Steve Hamilton' },
{ title: 'HTML5', author: 'Remy Sharp' },
{ title: 'JavaScript: The Good Parts', author: 'Douglas Crockford' }
]
const fuse = new Fuse(books, {
keys: ['title', 'author']
})
fuse.search('javscript')
// → [{ item: { title: 'JavaScript: The Good Parts', ... }, ... }]The result is an array of wrapper objects, not the raw records. Each entry carries the matched `item` alongside match metadata, so rendering a list means reading `result.item`, not `result`. That indirection is easy to miss on the first integration and shows up as a table full of `undefined`.
For larger corpora, the README offers a worker wrapper. `FuseWorker` splits data across multiple Web Workers and searches in parallel, and the README claims roughly 5x faster on 100K documents. It is imported from a separate entry point and returns a promise.
import { FuseWorker } from 'fuse.js/worker'
const fuse = new FuseWorker(docs, {
keys: ['title', 'author', 'description']
})
const results = await fuse.search('query')The README states that `FuseWorker` takes the same options and returns the same results as `Fuse`, with one exception: function-valued options (`sortFn`, `getFn`, `keys[].getFn`) are not supported, because functions cannot be transferred to a worker. It also documents `fuse.terminate()`, which you call to release the workers. Skipping that call leaves workers alive for the lifetime of the page.
Where Fuse.js Breaks Down
The first limitation is structural: there is no index to persist. Every `Fuse` instance is built from an array in memory, so a page reload rebuilds it, and a server restart does the same. There is no documented way to serialize the index, ship it as a static artifact, or restore it. For a documentation site with a few thousand pages, rebuilding at startup is fine. For a corpus that takes seconds to prepare, it is not.
The second is the worker constraint. Because functions cannot cross the worker boundary, `sortFn`, `getFn` and `keys[].getFn` are unavailable under `FuseWorker`. If your ranking depends on a custom accessor that normalizes fields, you either give up the parallel path or move that normalization into the data before constructing the instance. The README states the restriction but does not describe a workaround.
The third is scope creep in the other direction. Fuse.js is not a query language and not a relevance platform. Token search adds BM25-style IDF weighting, which is a real ranking signal, but there is no documented mechanism for boosting by recency, popularity, or any signal outside the text itself. If your product needs those, the library will not grow into them.
Finally, the version situation deserves attention. The most recent release listed is `v7.6.0-beta.0` from 2026-08-09, following stable `v7.5.0` on 2026-07-13 and `v7.4.2` on 2026-06-05. Token search, object query syntax, logical search and Web Workers are all documented as part of the current line, and the beta tag on the newest release means the newest features may still shift. The README does not document rollback, so pinning a version in `package.json` is the only stated control you have.
Fuse.js Against a Real Search Backend
The honest alternative is a server-side search engine such as Elasticsearch or Meilisearch, and the difference is not speed. It is where the index lives and who owns ranking.
Fuse.js builds its structure inside your process from an array you already have. A search backend builds and persists an inverted index on a server, exposes it over HTTP, and lets you tune relevance, add filters, paginate, and share one index across every client. That means a query from a phone and a query from a desktop hit the same corpus with the same configuration. With Fuse.js, each client holds its own copy, and the corpus has to be delivered to the client in the first place.
That delivery cost is the dividing line. Fuse.js is cheaper when the data is already being sent to the client for rendering, because the search structure costs nothing extra in network terms. It is more expensive when the corpus is large enough that shipping it to the browser is the dominant cost, at which point a backend that returns only matching rows wins on bandwidth regardless of matching speed.
There is a middle option worth naming: keeping Fuse.js on the server in Node, where the corpus is loaded once per process rather than once per tab. That avoids the delivery problem while keeping the zero-dependency, no-service property. It still gives up persistence and cross-process index sharing, so it is a partial answer.
Maintenance, Licensing and the Cost of Upgrading
The repository is not archived, and the last push was on 2026-08-09. Releases have come steadily through 2026: `v7.4.2` on 2026-06-05, `v7.5.0` on 2026-07-13, and `v7.6.0-beta.0` on 2026-08-09. The project is maintained, and the beta suffix on the newest tag indicates the release channel is active rather than frozen.
Licensing is Apache-2.0, which permits commercial use and modification and includes an explicit patent grant. That matters for teams that have to clear dependencies formally, and it is a permissive licence rather than a copyleft one, so it does not impose source-disclosure obligations on your application. This is a description of the licence identifier, not legal advice; route anything unusual through your own counsel.
Upgrade cost is where the design shows its age in a good way. The package has zero runtime dependencies, so a version bump cannot drag in a transitive tree. The `exports` map is the piece to watch: the full, minified, basic, min-basic, worker and worker-script entries are separate build targets, and the README documents that token search and logical search are only in the full build. An upgrade that changes which subpath carries a feature is a breaking change for anyone importing the basic variant, and the README does not publish a per-build feature matrix. Read `CHANGELOG.md` in the repository before bumping, and pin the version if your import path matters.
Editorial conclusion
Adopt Fuse.js when the corpus is small or medium, already in memory, and you want typo-tolerant matching without running a search service; the package is Apache-2.0 and ships as ESM, CJS and minified builds. Do not adopt it when you need persistence, index rollback, or server-side relevance tuning, because the README does not document rollback and the index is built from the array you hand it. Before committing, verify which build subpath you import from, whether your bundler resolves the exports map, and whether the function-valued options you rely on survive the move to FuseWorker, since the README states those cannot be transferred to a worker.
Frequently asked questions
How do I install Fuse.js?
Run npm install fuse.js, or yarn add fuse.js. The README also shows a CDN option that loads https://cdn.jsdelivr.net/npm/fuse.js/dist/fuse.min.mjs directly in a script tag.
Does Fuse.js work with React?
The README does not mention React specifically, but it states the library works in the browser and on the server and is designed for client-side searching of small-to-medium datasets. That is the same environment a React component runs in.
What is the difference between the full build and the basic build of Fuse.js?
The package exports separate entries for the full build and for fuse.js/basic. The README states that token search and logical search are available in the full build, which implies the basic build omits them, but it does not publish a complete per-build feature list.
Why do function options not work with FuseWorker in Fuse.js?
The README states that sortFn, getFn and keys[].getFn are not supported under FuseWorker because functions cannot be transferred to a Web Worker. Everything else carries over, and the results are the same as the synchronous Fuse class.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/krisk-fuse)