fuse.js review
Fuse.js 7.5.0 is an in-process fuzzy matcher for arrays of strings or objects. It builds an index in the browser or Node, ranks approximate matches across weighted and nested fields, and can return character ranges for highlighting. Full builds also understand extended operators, typed object queries, logical expressions, and multi-token searches with corpus-aware weighting. It does not store documents or call a search service. Our complete browser import was 26 KB minified and 9.4 KB gzipped, with bundled TypeScript types and no dependencies. Version 7.5.0 leaves method signatures alone but corrects four scoring faults, so field normalization, key weights, tied limits, and short exact matches can produce a different order than 7.4.
Fuse.js 7.5.0 added one package and 1 MB in 0.6 seconds in our sandbox, while its full browser import measured 9.4 KB gzipped with no audit findings. That is a fair cost for typo-tolerant local search, but server-owned data or language-aware retrieval needs a real search backend.
We installed it
| Install | ✓ · 0.6s | 1 package on disk · 1 MB |
| Import | ✓ | ESM import works · require() works · ESM package with exports map |
| Browser | 9.4 KB | gzipped (26 KB minified), bundled with esbuild |
| Types | ✓ | TypeScript types bundled |
| Known vulns | 0 | 0 critical · 0 high · 0 moderate · 0 low (npm audit) |
Answers from our run
Does fuse.js install cleanly?
Yes. In a fresh container with an empty cache, npm install fuse.js finished in 0.6s, leaving 1 package and 1 MB on disk. npm audit reported no known vulnerabilities.
How much does fuse.js add to a browser bundle?
9.4 KB gzipped (26 KB minified) when the whole package is bundled for the browser with esbuild. Importing only part of it is usually smaller.
Does fuse.js work with both ESM and CommonJS?
Yes. Both import 'fuse.js' and require('fuse.js') worked in Node 22 in our run. The package is published as ESM with an exports map.
Does fuse.js include TypeScript types?
Yes, type declarations ship inside the package, so no @types install is needed.
fuse.js or minisearch: which should you use?
minisearch: Pick it for a browser-side inverted index with prefix matching and document-oriented search. Fuse.js 7.5.0 added one package and 1 MB in 0.6 seconds in our sandbox, while its full browser import measured 9.4 KB gzipped with no audit findings.
When should you not use fuse.js?
The searchable records are private or too large to send to every browser. Fuse runs beside the data and provides no remote index.
Use it if
- A picker, command menu, documentation panel, or settings screen needs typo tolerance over records already present in the client.
- Several object fields should contribute different weights to one ranked result list.
- The interface needs inclusive match ranges so it can mark the characters that matched.
- You can construct or restore one index and update it when the local collection changes.
- The searchable records are private or too large to send to every browser. Fuse runs beside the data and provides no remote index.
- Search needs stemming, synonyms, language analyzers, facets, access control, or durable storage. Those are search-engine jobs outside Fuse's documented API.
- A release must preserve exact relevance scores and ordering. Version 7.5.0 deliberately changes results where 7.4 miscounted field words, mishandled weights, or chose the wrong tied result.
- Queries over a large collection cannot spend time on the UI thread, and an async Web Worker boundary does not fit the calling code.
- The list only needs exact prefix or substring checks. Native `startsWith()` or `includes()` avoids the 9.4 KB gzipped bundle we measured and is easier to reason about.
Setup reality
We installed fuse.js 7.5.0 in a fresh Node 22 Bookworm sandbox. npm finished in 0.6 seconds and left one package using 1 MB. The package has 0 direct dependencies and 0 peer dependencies; npm audit returned 0 findings at every severity. It is 452 KB unpacked, includes TypeScript declarations, and declares Node 10 or later. Both require() and ESM import worked despite type: module.
There are no accounts, secrets, or configuration files. Relevance settings are the setup work. includeScore exposes a value where 0 is best, while threshold decides which distances survive. Location penalties can suppress a valid word late in a long field, so test ignoreLocation on your corpus. Version 7.5.0 normalizes field weights correctly and counts tabs and line breaks as word boundaries; update ordering snapshots after upgrading.
Index construction happens when new Fuse(records, options) runs. Do that outside render and input handlers. Use add() and remove() for small mutations, or setCollection() for replacement. A build can serialize Fuse.createIndex() and the browser can restore it through Fuse.parseIndex(). That saves index-building time at startup, though the browser still needs the source records associated with each result. An empty query returns an empty list.
FuseWorker sends work to Web Workers and makes search() asynchronous. Call terminate() when its owner is disposed. Functions such as getFn, sortFn, and key-level getters cannot be transferred to a worker. Token search is also unavailable there because it relies on corpus statistics that the worker path does not support in 7.5.0. The basic export omits token, logical, and extended search, so test the same export that ships.
Patterns
Fuzzy-search titles and authors search-object-fields
import Fuse from 'fuse.js'
const books = [
{ title: 'The Lock Artist', author: 'Steve Hamilton' },
{ title: 'JavaScript: The Good Parts', author: 'Douglas Crockford' },
]
const fuse = new Fuse(books, { keys: ['title', 'author'] })
const results = fuse.search('javscript', { limit: 5 })A result carries the original object in `item` and its collection position in `refIndex`.
Expose scores and narrow fuzzy matches calibrate-scoring
const fuse = new Fuse(records, {
keys: ['title', 'summary'],
includeScore: true,
threshold: 0.3,
ignoreLocation: true,
minMatchCharLength: 2,
})A score of 0 is the closest match. Calibrate the 0.3 threshold with expected typos and known irrelevant queries from your data.
Give title matches more influence weight-indexed-fields
const fuse = new Fuse(records, {
keys: [
{ name: 'title', weight: 2 },
{ name: 'tags', weight: 1.5 },
{ name: 'body', weight: 1 },
],
})Fuse 7.5.0 fixes weight normalization for object and keyless logical queries, which can reorder results produced by 7.4.
Read a nested name and string array index-nested-values
const fuse = new Fuse(people, {
keys: [
['profile', 'displayName'],
'skills',
],
})
const results = fuse.search('database')An array path avoids ambiguity when a property name contains a dot. Arrays of strings are indexed as values of that field.
Collect ranges for safe highlighting render-match-ranges
const fuse = new Fuse(records, { keys: ['title'], includeMatches: true })
const [hit] = fuse.search('javscript')
for (const match of hit?.matches ?? []) {
console.log(match.key, match.indices)
}Each range includes both endpoints. Escape the original string before inserting highlight markup into HTML.
Use typed object search operators query-with-operators
const results = fuse.search({
title: {
$startsWith: 'old',
$not: { $contains: 'draft' },
},
})Object operators work in the full build without `useExtendedSearch`. Unknown operators and illegal nesting throw instead of falling back to fuzzy search.
Match every word independently search-query-tokens
const fuse = new Fuse(articles, {
keys: ['title', 'body'],
useTokenSearch: true,
tokenMatch: 'all',
})
const results = fuse.search('express midleware')`tokenMatch: 'all'` requires all query tokens to match. Token search is absent from the basic and worker paths in 7.5.0.
Join field conditions with boolean logic combine-boolean-queries
const results = fuse.search({
$and: [
{ title: 'javascript' },
{ $or: [{ author: 'crockford' }, { tags: 'language' }] },
],
})Logical conditions address keys in the index configuration and require the full package export.
Keep an existing index in sync mutate-live-index
fuse.add({ id: 42, title: 'New handbook' })
const removed = fuse.remove((item) => item.id === 12)
if (replacedEverything) fuse.setCollection(nextRecords)`add()` and `remove()` change the current index. `setCollection()` replaces the data and rebuilds its index.
Restore an index generated during the build restore-built-index
const keys = ['title', 'body']
const raw = await fetch('/search-index.json').then((r) => r.json())
const parsed = Fuse.parseIndex(raw)
const fuse = new Fuse(documents, { keys }, parsed)The key configuration and document order must be the same ones supplied to `Fuse.createIndex()` at build time.
Move expensive searches off the main thread run-in-web-worker
import { FuseWorker } from 'fuse.js/worker'
const searcher = new FuseWorker(records, { keys: ['title', 'body'] })
const results = await searcher.search('release notes')
searcher.terminate()Worker searches return promises. Function options cannot cross the structured-clone boundary, and the worker must be terminated when unused.
Test one pattern against one value match-single-string
import Fuse from 'fuse.js'
const result = Fuse.match('javscript', 'JavaScript handbook', {
threshold: 0.3,
})
if (result.isMatch) console.log(result.score, result.indices)`Fuse.match()` avoids constructing a corpus index. It rejects token search because one string has no document-frequency statistics.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| minisearch | npm | Pick it for a browser-side inverted index with prefix matching and document-oriented search. |
| flexsearch | npm | Pick it when query throughput over a larger memory-resident index is the main concern. |
| @orama/orama | npm | Pick it when local search also needs filters, facets, or vector search features. |
| @leeoniya/ufuzzy | npm | Pick it for fuzzy matching over a flat string list with a narrower API. |
More data guides
numpy · fsspec · pandas · sqlalchemy · pyarrow · lxml · the whole shelf →
How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.

