Client-side fuzzy search without an external search service
Fuse.js is a lightweight JavaScript library for client-side fuzzy search that pairs well with the statically generated content index of Nuxt Content, without needing to run or pay for an external search service such as Algolia. For a project with a manageable number of content pages, it makes a typo-tolerant search possible entirely in the browser, including weighting titles higher than body text, with no additional server infrastructure required.
Table of Contents
- 1. Why Fuse.js fits client-side search in Nuxt Content
- 2. Building a search index from the generated content
- 3. Field weighting: prioritizing titles over body text
- 4. Tuning threshold and fuzzy tolerance correctly
- 5. Keeping an eye on performance as the page count grows
- 6. Debouncing and result presentation for a smooth experience
- 7. An alternative to an external search service such as Algolia
- 8. Accounting for multilingual content in search
- 9. Keeping the search index current with new or changed content
- 10. Summary
- 11. FAQ
1. Why Fuse.js fits client-side search in Nuxt Content
Fuse.js implements fuzzy search logic that still surfaces relevant results even with slightly different spelling, typos, or incomplete search terms, unlike a simple substring search that only recognizes exact partial matches. The entire search runs inside the user's browser without sending a request to any server, which fits statically generated Nuxt Content pages particularly well, since the full content is already known at build time anyway.
This combination works best for projects with a manageable, but not trivial, number of pages, such as a company blog, documentation, or a knowledge base with a few hundred entries. For projects that size, running a dedicated search service is rarely worth it, while a plain substring search quickly hits its limits once users do not type the exact term that appears in the text.
2. Building a search index from the generated content
The first step is generating a flat JSON array from every Nuxt Content page, containing exactly the fields that should later be searchable, typically a title, a short description, and an excerpt from the body text. This index can either be generated at build time through a dedicated Nitro plugin or fetched directly through Nuxt Content's query API and cached on the client.
It matters to keep the index deliberately lean and avoid stuffing the full Markdown content of every page into it, since this index is shipped in full to the client and its size directly affects how quickly the search feature becomes usable. An excerpt of a few hundred characters from the body text is usually enough to surface relevant results without needlessly bloating the index.
// composables/useSearchIndex.ts
import Fuse from 'fuse.js'
interface SearchEntry {
title: string
description: string
excerpt: string
path: string
}
export function useSearchIndex() {
const { data: entries } = useAsyncData<SearchEntry[]>('search-index', () =>
queryContent('/blog').only(['title', 'description', 'excerpt', '_path']).find()
)
const fuse = computed(() => new Fuse(entries.value ?? [], {
keys: [
{ name: 'title', weight: 0.6 },
{ name: 'description', weight: 0.3 },
{ name: 'excerpt', weight: 0.1 },
],
threshold: 0.35,
ignoreLocation: true,
}))
return { fuse }
}
3. Field weighting: prioritizing titles over body text
Fuse.js lets every searched field be assigned its own weight between 0 and 1 through the keys configuration, with higher values counting more heavily toward the computed relevance score. In practice, weighting the title noticeably higher than the description and the body excerpt tends to work well, because a match in the title is usually more topically relevant than a random occurrence of a search term somewhere in the running text.
A typical split might use 0.6 for the title, 0.3 for the description, and 0.1 for the body excerpt, though the exact values are best calibrated by trying real search queries from your own audience. Without deliberate weighting, Fuse.js treats every field equally, which can let a random match buried in body text outrank a much more relevant title match in the results list.
4. Tuning threshold and fuzzy tolerance correctly
The threshold parameter controls how tolerant Fuse.js is toward differences between the search term and a match, ranging from 0 (exact match only) to 1 (nearly anything matches). A value set too low returns almost nothing when there is a typo, while a value set too high floods the result list with barely relevant matches, which makes the search feel confusing rather than helpful to users.
A starting value between 0.3 and 0.4 has proven to be a reasonable compromise across many projects, but it should be verified against real user queries once enough data is available. The ignoreLocation option additionally helps surface matches regardless of where in the text the search term appears, which matters particularly for longer body-text excerpts.
5. Keeping an eye on performance as the page count grows
As long as the search index covers a few hundred to a couple thousand entries, Fuse.js stays comfortably fast even on average hardware, since the fuzzy search runs entirely in the browser's memory. As the entry count grows, though, both the time needed to build the Fuse index and the time needed for each individual search increase, because Fuse.js internally scores every entry against the search term.
Beyond a few tens of thousands of entries, this becomes noticeable, especially on lower-end mobile devices, where a delay of several hundred milliseconds per keystroke already feels sluggish. At that scale, it is worth either adding a debounce strategy that only triggers the search after a brief pause in typing, or moving to a server-side search solution that no longer needs to compute results in the client.
6. Debouncing and result presentation for a smooth experience
Without debouncing, every single keystroke would trigger a new Fuse.js search, which can produce noticeable input lag on larger indexes because the search briefly blocks the browser's main thread. A delay of 150 to 250 milliseconds between the last keystroke and the actual search execution is usually enough to avoid that effect without making the search feel slow to users.
When rendering results, it also helps to sort matches by the relevance score Fuse.js already provides by default, and optionally to highlight the matched portion of the text, so users immediately understand why a given result showed up. These small details often matter more to perceived search quality than the exact fuzzy logic running underneath.
7. An alternative to an external search service such as Algolia
Algolia and similar hosted search services offer considerably more sophisticated relevance algorithms, typo-tolerant search backed by machine learning, faceted filtering, and practically unlimited scalability, but they come with ongoing costs and an additional external dependency that has to be maintained and kept in sync separately. For smaller to mid-sized projects, that extra effort often does not pay off relative to the actual benefit.
Fuse.js, by contrast, costs nothing, needs no external service, and runs entirely within the existing Nuxt deployment, which considerably simplifies both maintenance and privacy, since no search queries are ever sent to a third party. The trade-off lies in more limited scalability and simpler relevance algorithms, which for most content sites in the low to mid four-digit entry range is not a practical constraint at all.
8. Accounting for multilingual content in search
If a site runs in multiple languages, the search index should be built separately per language, since a shared fuzzy search across content in different languages rarely produces meaningful results and users usually only want to search within their current language version anyway. In practice, that means either generating the index separately per language or filtering it by the current locale before handing it to Fuse.js.
This separation also prevents a German search term from accidentally matching an English content entry with a similar character sequence, which can happen without a clean split, since Fuse.js optimizes purely for character similarity and has no notion of linguistic meaning. Thinking about language separation from the start saves a considerable amount of rework to the search logic later on.
9. Keeping the search index current with new or changed content
Since the search index is typically generated at build time from the current content, it automatically reflects the state of the last deployment without needing any separate synchronization step, unlike what is often required with an external search service maintaining its own index. After every new build, the updated index becomes available as soon as the site is redeployed.
For projects with very frequent content changes outside of regular deployments, for example through a connected CMS with instant publishing, it can make sense to additionally regenerate the index at runtime through a dedicated API route, instead of only relying on the next build. That decision depends heavily on how time-critical it is for new content to become findable through search.
| Criterion | Fuse.js (client-side) | Algolia (hosted) | Plain substring search |
|---|---|---|---|
| Cost | Free, no service needed | Ongoing usage costs | Free |
| Typo tolerance | Yes, configurable fuzzy logic | Yes, very mature | No, exact partial matches only |
| Scalability | Good up to a few thousand entries | Practically unlimited | Good, but not very relevant |
| External dependency | None, runs client-side | Yes, hosted third-party service | None |
| Setup effort | Low to moderate | Moderate to high | Very low |
Mironsoft
Vue architecture, Composition API, and Nuxt performance
Vue applications that don't get more complicated with every feature?
We review existing Vue and Nuxt projects for unstructured composables, unnecessary reactivity, and bloated bundles, then build an architecture that absorbs new features without making the codebase harder to follow.
Architecture Review
Checking composables, state management, and component structure for maintainability.
Performance Audit
Systematically optimizing reactivity overhead, bundle size, and Nuxt rendering strategy.
Nuxt Integration
Building robust, type-safe SSR/SSG setup and API integration.
10. Summary
Fuse.js search in Nuxt Content: the essentials at a glance
Core idea
Fuzzy search running entirely in the browser over a build-time generated content index.
Weighting
Weight the title considerably higher than the description and body-text excerpt.
Limit
Beyond a few tens of thousands of entries, a server-side solution becomes more sensible.
Alternative
Algolia offers more scalability, but with ongoing costs and an external dependency.