mrkeyoor.com_
Thu 06 Aug 15:42 UTC
npmDataupdated 06 Aug 2026

fuse.js

Fuse.js is client-side fuzzy search with no dependencies and no backend. You hand it an array of objects and a list of keys to search, and it scores every record against the query using the Bitap algorithm, which measures approximate string similarity so that a query of javscript still finds JavaScript. Results come back sorted by a score where 0 is a perfect match and 1 is no match at all, optionally with the exact character ranges that matched so you can highlight them. There is no index server, no tokenizing pipeline, and no analyzer configuration; the whole dataset lives in memory in the browser or in your Node process. Version 7.5 adds two things worth knowing about: token search, which splits a multi-word query into terms, matches each one independently, and ranks with BM25-style weighting so rare words count more; and FuseWorker, which shards the collection across Web Workers so a large collection does not freeze the UI. The package ships full and basic builds, the basic one dropping extended, logical, and token search to save about two kilobytes.

Verdict

The fastest way to put decent typo-tolerant search in front of a few thousand client-side records, with highlighting included and no infrastructure. Plan on an hour of tuning threshold, ignoreLocation, and per-key weights, because the out-of-the-box relevance is loose enough that people assume the library is broken.

API stability5/5The constructor plus search shape has been the same since version 3, and versions 6 and 7 were mostly internals, typing, and packaging. Recent releases add features behind opt-in flags such as useExtendedSearch and useTokenSearch rather than changing defaults, so upgrading has not moved anyone's result ordering
Docs5/5fusejs.io documents every option with a live demo you can type into, explains the scoring theory rather than only listing knobs, and has separate pages for extended search syntax, token search, and Web Workers; the README covers the same ground for people who never leave GitHub
Maintenance4/5Pushed 2026-07-13 with 7.5.0 out the same day, and the tracker sits at 0 open issues out of 1 open issue and PR, which is unusual at 20k stars. The caveat is that it is effectively one author working in bursts, so quiet stretches happen, and an empty tracker partly reflects aggressive closing
Ecosystem4/5About 12.7M downloads a week and it is the default answer for client-side fuzzy search, wired into Docusaurus, VitePress, and Astro search plugins, with an official Swift port. There is no plugin system, so anything the options do not cover you write yourself, and larger datasets push people to a different library entirely

Use it if

  • Your searchable data is small enough to ship to the client, roughly a few thousand records, and you want instant results with no network round trip per keystroke
  • You need typo tolerance rather than substring matching: a command palette, a country picker, a docs sidebar, or a settings search where users half-remember the wording
  • You want match highlighting for free: includeMatches gives you character index ranges per key, which is tedious to compute yourself once fuzziness is involved
  • You are searching structured objects, not strings: nested keys with dot paths, array fields, and per-key weights so a title match outranks a description match
  • You want to add search to a static site with no infrastructure at all, since the index can be precomputed at build time and parsed back with Fuse.parseIndex
Skip it if

Setup reality

npm install fuse.js and there is nothing else to install: zero dependencies, TypeScript types in the package, and an exports map covering ESM and CommonJS. The friction is entirely in configuration. A default Fuse with only keys set will return almost everything for almost any query, because threshold defaults to 0.6 and location scoring penalizes matches later in a field, so the first real task is tuning: set ignoreLocation: true unless you genuinely want position to matter, then lower threshold until the noise stops, then add minMatchCharLength so single characters do not match everything. Two structural gotchas follow. Building a Fuse instance re-indexes the whole collection, so constructing one inside a React render or a keystroke handler is why search feels slow; build it once with useMemo or at module scope and use fuse.add, fuse.remove, or fuse.setCollection for updates. And the Bitap implementation works on 32 character chunks, so very long queries get split into pieces and scored separately, which degrades ranking rather than erroring; useTokenSearch is the intended answer for long multi-word queries. Choose a build deliberately: the default entry is the full build, fuse.js/basic drops extended, logical, and token search, and fuse.js/worker is a separate entry with an async API where function options such as sortFn and getFn cannot be used because functions do not cross the worker boundary.

Patterns

Search an array of objectsbasic-search

import Fuse from "fuse.js";

const books = [
  { title: "Old Man's War", author: "John Scalzi" },
  { 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', ... }, refIndex: 1 }]

fuse.search("crockford", { limit: 5 });

Results are wrapped: item is your original object, refIndex is its position in the array you passed in. The constructor indexes the whole collection, so create it once and reuse it, not per keystroke. limit is a second argument to search, not an option on the constructor, and it truncates after sorting so it does not save any scanning work.

Fix the defaults before blaming the resultstune-relevance

const fuse = new Fuse(items, {
  keys: ["title", "body"],
  threshold: 0.3,          // default 0.6 is very loose
  ignoreLocation: true,    // default false: matches far from index 0 score badly
  minMatchCharLength: 2,   // default 1: single letters match everything
  includeScore: true,
});

// only if position really matters (matching an ID prefix, say)
new Fuse(items, { keys: ["sku"], location: 0, distance: 20, threshold: 0.2 });

threshold is the maximum score allowed through, where 0 demands a perfect match and 1 lets everything through. location and distance define a window around the start of the field, and a match outside that window is penalized even if it is exact, which is why searching for a word in the middle of a long description returns nothing until you set ignoreLocation: true. These three are the source of most Fuse.js issue reports.

Weight fields and reach into nested dataweighted-and-nested-keys

const fuse = new Fuse(docs, {
  keys: [
    { name: "title", weight: 3 },
    { name: "tags", weight: 2 },              // array of strings works
    { name: ["author", "lastName"], weight: 1 },   // nested path
    "body",                                    // implicit weight 1
    {
      name: "fullName",                        // computed field
      getFn: (doc) => `${doc.first} ${doc.last}`,
    },
  ],
  ignoreFieldNorm: false,
});

Weights are relative and get normalized, so 3/2/1 and 0.6/0.4/0.2 behave the same. Nested keys can be written as 'author.lastName' or as an array, and the array form is the one that survives a field name containing a dot. Field norm divides the score by the square root of the field length, so a hit in a two-word title beats the same hit in a long paragraph; set ignoreFieldNorm: true if you want length not to matter.

Get the character ranges and render themhighlight-matches

const fuse = new Fuse(items, {
  keys: ["title"],
  includeMatches: true,
  includeScore: true,
  minMatchCharLength: 2,
});

const [hit] = fuse.search("javscript");
hit.matches[0].indices;   // [[0, 9]]  inclusive start and end

function highlight(text, indices) {
  let out = "", last = 0;
  for (const [s, e] of indices) {
    out += escapeHtml(text.slice(last, s)) + "<mark>" +
           escapeHtml(text.slice(s, e + 1)) + "</mark>";
    last = e + 1;
  }
  return out + escapeHtml(text.slice(last));
}

Indices are inclusive on both ends, so the slice needs e + 1; off-by-one here is the usual reason highlights come out one character short. Fuzzy matching produces many tiny disjoint ranges, so raise minMatchCharLength or the output looks like confetti. Always escape the surrounding text before inserting the mark tags, since this is user-controlled content going into innerHTML.

Give power users query syntaxextended-search-operators

const fuse = new Fuse(list, { keys: ["title", "lang"], useExtendedSearch: true });

fuse.search("jscript");     // fuzzy, the default behaviour
fuse.search("=scheme");     // exact whole-field match
fuse.search("'python");     // includes this exact substring
fuse.search("!ruby");       // does NOT include
fuse.search("^java");       // field starts with
fuse.search(".js$");        // field ends with
fuse.search("!^java");      // does not start with
fuse.search("^core go$");   // space means AND
fuse.search("^java | ^python");   // pipe means OR

Space is AND and the pipe is OR, so a user typing a two-word phrase gets an AND query, not a phrase search. Quote a term to include spaces in it, as in ='exact phrase' style with double quotes after the operator. These operators are not available in the basic build, and they are not sanitized: a query that is entirely operators returns whatever the parser makes of it, so validate before showing results to end users.

Multi-word queries with per-word typo tolerancetoken-search

const fuse = new Fuse(docs, {
  useTokenSearch: true,
  keys: ["title", "body"],
  tokenMatch: "any",          // 'all' requires every term to match
});

fuse.search("javascrpt paterns");
// finds "JavaScript Patterns" despite two typos in two words

// custom tokenizer for terms with internal punctuation, or CJK
new Fuse(docs, {
  useTokenSearch: true,
  keys: ["title"],
  tokenize: /[\w.+#-]+/g,
});

Each term is fuzzy-matched independently and ranked with IDF weighting, so rare words count more than common ones and word order stops mattering. This is the right mode for a search box where people type sentences, and it also sidesteps the 32 character chunking that degrades long single-pattern queries. It is full-build only, it cannot be used with Fuse.match, and tokenMatch: 'all' turns the search into a filter that returns nothing when one word is misspelled beyond the threshold.

Do not rebuild the index on every renderbuild-instance-once

import { useMemo, useState } from "react";
import Fuse from "fuse.js";

function Search({ items }) {
  const [q, setQ] = useState("");

  const fuse = useMemo(
    () => new Fuse(items, { keys: ["title"], threshold: 0.3, ignoreLocation: true }),
    [items],
  );

  const results = useMemo(
    () => (q ? fuse.search(q, { limit: 20 }) : items.slice(0, 20).map((item) => ({ item }))),
    [q, fuse, items],
  );
  // ...
}

new Fuse(...) walks the entire collection and builds the index, so putting it in the render body means re-indexing on every keystroke and is the number one cause of slow Fuse.js. Note the options object must also be stable, since an inline literal is a new reference each render and would defeat the memo if you listed it as a dependency. An empty query returns an empty array rather than everything, so handle that case yourself.

Add and remove documents without rebuildingmutate-collection

const fuse = new Fuse(items, { keys: ["title"] });

fuse.add({ title: "New Book", author: "New Author" });
fuse.remove((doc) => doc.title === "Old Book");   // returns removed docs
fuse.removeAt(3);

fuse.setCollection(nextItems);        // wholesale replace, re-indexes
fuse.getIndex().size();

add and remove update the index incrementally, which is much cheaper than constructing a new instance when a websocket pushes one row. remove takes a predicate and runs it over every document, so it is a linear scan; batching several deletions into one predicate call is worth it. setCollection re-indexes everything, so it is a rebuild by another name and belongs outside the hot path.

Build the index at build time, parse it at runtimeprecomputed-index

// build step (Node)
import Fuse from "fuse.js";
import fs from "node:fs";

const keys = ["title", "body"];
const index = Fuse.createIndex(keys, docs);
fs.writeFileSync("search-index.json", JSON.stringify(index.toJSON()));

// runtime (browser)
const raw = await fetch("/search-index.json").then((r) => r.json());
const index = Fuse.parseIndex(raw);
const fuse = new Fuse(docs, { keys }, index);

This moves index construction off the client, which matters for a static site with a few thousand pages where the build already has the data. The keys array must be identical on both sides or matching silently misbehaves. You still have to ship the documents themselves, so this saves CPU on load, not bytes; if payload size is the problem, ship only the fields you display and look the rest up by id.

Search off the main threadweb-worker

import { FuseWorker } from "fuse.js/worker";

const fuse = new FuseWorker(docs, {
  keys: ["title", "author", "description"],
  threshold: 0.3,
  ignoreLocation: true,
});

const results = await fuse.search("query");

// clean up when the component unmounts
fuse.terminate();

Same options and same result shape as Fuse, but every method is async because the data is sharded across workers. Function-valued options do not survive the structured clone, so sortFn, getFn, and per-key getFn are unavailable; anything computed has to be a real field on the document. Call terminate() or the workers leak, and remember the documents are copied into each worker, so memory use is roughly multiplied by the shard count.

Compare one string without building an indexmatch-single-string

import Fuse from "fuse.js";

Fuse.match("javscript", "JavaScript: The Good Parts");
// { isMatch: true, score: 0.04, indices: [[0, 9]] }

Fuse.match("xyz", "JavaScript", { threshold: 0.2 });
// { isMatch: false, ... }

// filter an array yourself when you only need a boolean
const hits = names.filter((n) => Fuse.match(q, n, { threshold: 0.3 }).isMatch);

Useful for a one-off comparison, a custom filter, or validating a guess, with no index build cost. It cannot do token search, because that needs corpus-level statistics, and passing useTokenSearch: true throws an explicit error rather than degrading. Calling this in a loop over a big array is slower than one Fuse instance, since you lose the precomputed field norms.

Pick the basic build when you only need fuzzysmaller-build

// full build: fuzzy + extended + logical + token search
import Fuse from "fuse.js";

// fuzzy only, roughly two kilobytes smaller
import Fuse from "fuse.js/basic";

// pre-minified entries, for direct browser loading
import Fuse from "fuse.js/min";
import Fuse from "fuse.js/min-basic";

The basic build silently ignores useExtendedSearch, useTokenSearch, and logical query objects, so a query that worked in development against the full build can quietly stop filtering in production if the import changed. Prefer the plain entries and let your bundler minify; the /min entries exist for script tags and CDN use where nothing else will.

Alternatives

PackageRegistryPick it when
minisearchnpmYou want a real inverted index in the browser with prefix search, stemming hooks, and better scaling past ten thousand documents
flexsearchnpmRaw query speed on larger in-memory collections matters more than a friendly API or predictable defaults
@orama/oramanpmYou want typo tolerance plus filters, facets, and optional vector search from one library that also runs on the server or an edge runtime
@leeoniya/ufuzzynpmYou are fuzzy-filtering a flat list of strings and want the smallest, fastest option with ordering you can reason about