Fuse.js is a zero-dependency JavaScript library that adds fuzzy search to any dataset that fits in memory. It handles typos, partial matches, and relevance ranking without a server or backend service. For datasets under 10,000 small objects, it is one of the fastest ways to add high-quality search to a web application.
This guide walks through setup, configuration, and the key options that determine how well search results match user intent. For context on the broader search-as-you-type implementation including debounce and request cancellation, the 137Foundry guide on search-as-you-type covers the complete approach. 137Foundry builds custom web applications that incorporate search as a core feature.
Step 1: Install Fuse.js
Install via npm for module-based projects:
npm install fuse.js
For browser-only projects without a build step, Fuse.js is also available via CDN -- check the Fuse.js releases page for the current version and include it as a script tag pointing to the jsdelivr or unpkg CDN.
Step 2: Prepare Your Dataset
Fuse.js works best with an array of objects. The library indexes the fields you specify and searches across them. A simple product catalog might look like:
const products = [
{ id: 1, name: 'Wireless Keyboard', category: 'Electronics', description: 'Compact Bluetooth keyboard' },
{ id: 2, name: 'USB-C Hub', category: 'Electronics', description: 'Multi-port USB hub for laptops' },
{ id: 3, name: 'Desk Lamp', category: 'Home Office', description: 'LED lamp with adjustable brightness' },
// ... more items
];
The dataset can come from a JSON file, an API call at page load, or be embedded in the HTML. Fuse.js indexes it in memory; updates to the original array require re-indexing.
Step 3: Create the Fuse Instance
import Fuse from 'fuse.js';
const options = {
keys: ['name', 'category', 'description'],
threshold: 0.3,
includeScore: true,
};
const fuse = new Fuse(products, options);
The keys array tells Fuse which fields to search. The threshold controls how fuzzy the matching is. A threshold of 0.0 requires exact matches; 1.0 matches everything. A value of 0.3 is a good starting point: it catches typos and minor misspellings without returning too many irrelevant results.
includeScore: true adds a relevance score to each result (0.0 is a perfect match, higher values are worse matches). This is useful for debugging and for sorting results that pass the threshold. Enable it during development even if scores are not displayed in the UI -- logging scores for real user queries is the fastest way to determine whether the threshold needs adjustment for your specific dataset.
Step 4: Search and Render Results
function search(query) {
if (query.length < 2) return [];
return fuse.search(query);
}
// Each result looks like:
// { item: { id: 1, name: 'Wireless Keyboard', ... }, score: 0.12, refIndex: 0 }
fuse.search(query) returns results sorted by score (best match first). Each result wraps the original item with item, score, and refIndex properties. Access the original object through result.item.
Step 5: Wire It to a Search Input With Debounce
Combining Fuse.js with debounce gives you instant, typo-tolerant search without any server requests:
const searchInput = document.querySelector('#search-input');
const resultsContainer = document.querySelector('#results');
function debounce(fn, delay) {
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => fn(...args), delay);
};
}
const handleSearch = debounce((query) => {
const results = search(query.trim());
renderResults(results);
}, 200);
searchInput.addEventListener('input', (e) => {
handleSearch(e.target.value);
});
function renderResults(results) {
if (results.length === 0) {
resultsContainer.innerHTML = '<p>No results found.</p>';
return;
}
resultsContainer.innerHTML = results
.map(({ item }) => `<div class="result">${item.name}</div>`)
.join('');
}
Because Fuse.js searches in memory, there are no race conditions and no AbortController needed. The debounce delay can be shorter (150-200ms) than a server-based search because the search itself is instant.
Configuring Field Weights
Not all fields are equally important. Matching a query in the product name is more relevant than matching it in the description. Fuse.js supports per-key weights:
const options = {
keys: [
{ name: 'name', weight: 3 },
{ name: 'category', weight: 1.5 },
{ name: 'description', weight: 1 },
],
threshold: 0.3,
};
Higher weight values mean matches in that field boost the result's score more. A product whose name matches the query will rank significantly above one where only the description matches.
Getting weights right usually takes experimentation. Start with name weighted 3x description and adjust based on what kinds of queries your users actually run.
Handling Nested Objects
Fuse.js can search nested fields using dot notation:
const data = [
{ id: 1, title: 'Article A', author: { name: 'Alice', role: 'Editor' } },
// ...
];
const options = {
keys: ['title', 'author.name'],
};
Arrays of strings within an object are also supported:
const data = [
{ id: 1, name: 'Product', tags: ['wireless', 'bluetooth', 'compact'] },
// ...
];
const options = {
keys: ['name', 'tags'],
};
Fuse.js searches each element of the array and treats a match in any element as a match for that field.
Limiting the Result Set
By default, Fuse.js returns all results above the threshold. For a search UI, this can be too many. Limit results with the limit option:
fuse.search(query, { limit: 10 });
Or slice the results array:
const results = fuse.search(query).slice(0, 10);
Limiting to 10-20 results is appropriate for most search UIs. Showing more than that without pagination creates a list that users do not scroll through anyway.
When Fuse.js Is Not the Right Tool
Fuse.js has limits. It does not support full-text search features like stemming (treating "running" and "runs" as the same term), phrase search, or proximity search. It loads the full dataset into memory, which is fine for small datasets but impractical for large ones. Memory use scales with object count and field size: ten thousand small objects might use 3-4 MB of browser memory, while one hundred thousand records with longer text fields can push past 20 MB and cause noticeable slowdowns on lower-end mobile hardware.
For datasets over 10,000 items, consider Lunr.js (pre-built inverted index), a server-side search API, or a dedicated search service like Algolia or Typesense. For the backend implementation and the server-side patterns that apply when the dataset is too large for client-side search, the 137Foundry guide on search-as-you-type covers the server-side approach including AbortController for managing in-flight requests.
A Complete Minimal Example
import Fuse from 'fuse.js';
const data = [
{ id: 1, name: 'Wireless Keyboard', category: 'Electronics' },
{ id: 2, name: 'USB Hub', category: 'Electronics' },
{ id: 3, name: 'Monitor Stand', category: 'Furniture' },
];
const fuse = new Fuse(data, {
keys: [{ name: 'name', weight: 2 }, { name: 'category', weight: 1 }],
threshold: 0.3,
includeScore: true,
});
const input = document.querySelector('#search');
const results = document.querySelector('#results');
let timer;
input.addEventListener('input', (e) => {
clearTimeout(timer);
timer = setTimeout(() => {
const query = e.target.value.trim();
if (query.length < 2) {
results.innerHTML = '';
return;
}
const hits = fuse.search(query, { limit: 5 });
results.innerHTML = hits
.map(({ item }) => `<div>${item.name} (${item.category})</div>`)
.join('');
}, 200);
});
This is production-ready for small datasets. Add weight tuning, empty state messaging, and ARIA attributes for the complete implementation. 137Foundry handles the full stack when search is a core requirement, from library selection through relevance tuning and ongoing optimization.
Top comments (0)