Fuse.js vs FlexSearch vs Orama: Client-Side Search in 2026
TL;DR
For fuzzy "find as you type" matching on small datasets (under 10K items), Fuse.js is the zero-config default — it just works. For high-performance full-text search at scale, FlexSearch is dramatically faster. For a modern batteries-included search experience with vector similarity, typo tolerance, and facets, Orama (formerly Lyra) is the most compelling new entrant. The right choice depends entirely on whether you need fuzzy matching, full-text relevance, or both.
Key Takeaways
- Fuse.js: ~1.1M weekly downloads — simple fuzzy matching, zero config, small (4KB), slow at scale
- FlexSearch: ~600K weekly downloads — fastest full-text search library in JS, excellent for 100K+ documents
- Orama: ~150K weekly downloads — batteries-included with typo tolerance, facets, vectors, and cloud sync
- Search type matters: fuzzy matching ≠ full-text search ≠ vector similarity
- Fuse.js is not a full-text search engine — it's approximate string matching
- Orama replaced Lyra and now offers both embedded + cloud-hosted search
Search Types Explained
Before comparing libraries, understand what you actually need:
| Type | Best For | Example | Library |
|---|---|---|---|
| Fuzzy matching | Typo-tolerant name/title search | "jvscript" → "javascript" | Fuse.js |
| Full-text search | Keyword search across document bodies | BM25 relevance ranking | FlexSearch, Orama |
| Prefix search | Autocomplete as you type | "java" → "javascript", "java" | FlexSearch, Orama |
| Vector/semantic | Meaning-based similarity | "fast cars" → "automobiles" | Orama |
Download Trends
| Package | Weekly Downloads | Stars | Bundle Size |
|---|---|---|---|
fuse.js | ~1.1M | 18K | ~4KB |
flexsearch | ~600K | 12K | ~6KB (web bundle) |
orama | ~150K | 9K | ~22KB |
Fuse.js
Fuse.js implements the Bitap algorithm for approximate string matching. It's not a full-text search engine — it's a fuzzy string matcher that finds approximate matches within strings.
import Fuse from "fuse.js"
const packages = [
{ name: "react", description: "A JavaScript library for building user interfaces" },
{ name: "vue", description: "The Progressive JavaScript Framework" },
{ name: "angular", description: "Platform for building mobile and desktop web applications" },
{ name: "svelte", description: "Cybernetically enhanced web apps" },
]
const fuse = new Fuse(packages, {
keys: ["name", "description"],
threshold: 0.3, // 0 = perfect match, 1 = match anything
includeScore: true, // Include relevance score in results
includeMatches: true, // Include matched indices for highlighting
minMatchCharLength: 2,
})
const results = fuse.search("recat") // Typo for "react"
// Returns: [{ item: { name: "react", ... }, score: 0.002, matches: [...] }]
Fuse.js strengths:
- Dead simple API — create index, search
- Built-in typo tolerance (the Bitap algorithm handles transpositions, insertions, deletions)
- Works on nested objects with dot-notation keys
- Weighted fields —
keys: [{ name: "name", weight: 2 }, { name: "description", weight: 1 }] - Returns match indices for text highlighting
Fuse.js limitations:
// Performance problem: Fuse.js is O(n) — scans ALL items for EVERY query
const fuse = new Fuse(largeArray, { keys: ["title"] })
fuse.search("query") // Fine at 1K items. Slow at 100K. Unusable at 1M.
// No full-text indexing — searching in document body is slow
// No stemming — "running" won't match "run"
// No ranking by document frequency — all matches weighted equally
When Fuse.js is right:
- Dataset under 5,000-10,000 items
- Search box on a navigation menu, command palette, or contact list
- You need typo tolerance for names/identifiers (not full document bodies)
- Zero configuration priority
FlexSearch
FlexSearch is built for one thing: maximum search throughput. It outperforms every other JavaScript search library on benchmarks by significant margins.
import { Document } from "flexsearch"
// Document index (for structured data):
const index = new Document({
document: {
id: "id",
index: ["title", "description", "body"],
store: ["title", "description"], // Fields to return with results
},
tokenize: "forward", // forward = prefix search; full = contains; reverse = suffix
language: "en", // Enable English stemming + stopwords
cache: 100, // Cache last 100 searches
})
// Add documents:
await index.addAsync(1, {
id: 1,
title: "React vs Vue",
description: "Comparing the two most popular JavaScript frameworks",
body: "React and Vue are both excellent choices for building..."
})
// Search:
const results = await index.searchAsync("javascript framework", {
limit: 10,
suggest: true, // Return partial matches if no exact match
enrich: true, // Include stored field data in results
})
// Returns: [{ field: "title", result: [{ id: 1, doc: { title: "...", description: "..." } }] }]
FlexSearch's architecture:
FlexSearch uses an inverted index with configurable tokenization:
"forward"— prefix search ("java" matches "javascript")"reverse"— suffix search ("script" matches "javascript")"full"— substring search ("vasc" matches "javascript") — slowest"strict"— exact word match only — fastest
// Performance benchmark (searching 100K documents):
// Fuse.js: ~2000ms
// FlexSearch: ~15ms
// This is the main reason to choose FlexSearch over Fuse.js at scale
FlexSearch limitations:
- No built-in fuzzy/typo matching (Fuse.js does this better)
- API is more complex than Fuse.js
- "Relevance" ranking is simpler than BM25
- Documentation is sparse in places
Orama
Orama (formerly Lyra) is the most ambitious of the three — a full-featured search engine that runs anywhere JavaScript runs, with a cloud sync option:
import { create, insert, search, remove } from "@orama/orama"
// Create schema-first index:
const db = await create({
schema: {
name: "string",
description: "string",
downloads: "number",
tags: "string[]",
published: "boolean",
},
})
// Insert documents:
await insert(db, {
name: "react",
description: "A JavaScript library for building user interfaces",
downloads: 25000000,
tags: ["ui", "components", "frontend"],
published: true,
})
// Search with typo tolerance, facets, and filters:
const results = await search(db, {
term: "javascript ui library",
properties: ["name", "description"],
tolerance: 1, // Allow 1 typo per term
limit: 10,
offset: 0,
facets: {
tags: { limit: 5 } // Get tag distribution in results
},
where: {
downloads: { gte: 1000000 },
published: true
},
sortBy: {
property: "downloads",
order: "DESC"
}
})
Orama's unique features:
// Vector/embedding search (semantic similarity):
import { createIndex, create, insert, search } from "@orama/orama"
import { openai } from "@orama/plugin-embeddings"
const db = await create({
schema: {
text: "string",
embedding: "vector[1536]", // OpenAI embedding dimensions
},
plugins: [openai({ apiKey: process.env.OPENAI_KEY })]
})
// Auto-generate embeddings on insert:
await insert(db, {
text: "Fast cars and racing vehicles",
// embedding auto-generated from text
})
// Semantic search — finds "automobiles" even if that word isn't in text:
const results = await search(db, {
mode: "vector",
vector: { value: queryEmbedding, property: "embedding" }
})
Orama Cloud:
// Sync with Orama Cloud for distributed search:
import { CloudManager } from "@oramacloud/client"
const manager = new CloudManager({ api_key: process.env.ORAMA_KEY })
await manager.deployIndex(db, "my-search-index")
// Client-side — search against cloud index:
import { OramaClient } from "@oramacloud/client"
const client = new OramaClient({
endpoint: "https://cloud.orama.run/v1/indexes/my-index",
api_key: process.env.ORAMA_PUBLIC_KEY,
})
const results = await client.search({ term: "javascript", limit: 5 })
Comparison Table
| Feature | Fuse.js | FlexSearch | Orama |
|---|---|---|---|
| Fuzzy/typo matching | ✅ Excellent | ❌ None built-in | ✅ tolerance param |
| Full-text indexing | ❌ No index | ✅ Inverted index | ✅ Inverted index |
| Prefix search | ✅ | ✅ | ✅ |
| Semantic/vector search | ❌ | ❌ | ✅ Plugin |
| Faceted search | ❌ | ❌ | ✅ Built-in |
| Filtering/where clauses | ❌ | ❌ | ✅ Built-in |
| Sort results | ❌ | ❌ | ✅ Built-in |
| Stemming | ❌ | ✅ (with language) | ✅ |
| Bundle size | ~4KB | ~6KB | ~22KB |
| Performance at 100K docs | ❌ Slow | ✅ Excellent | ✅ Good |
| TypeScript | ✅ | ⚠️ Partial | ✅ First-class |
| Cloud sync option | ❌ | ❌ | ✅ Orama Cloud |
| Schema validation | ❌ | ❌ | ✅ |
Performance at Scale
For 10,000 documents with body text of ~500 words each:
| Query | Fuse.js | FlexSearch | Orama |
|---|---|---|---|
| Exact term | ~800ms | ~5ms | ~8ms |
| Prefix ("java") | ~800ms | ~3ms | ~4ms |
| Fuzzy ("javasript") | ~800ms | ❌ No typo support | ~6ms |
| Full-text multi-term | ~800ms | ~12ms | ~15ms |
Fuse.js is unsuitable beyond ~10K documents.
Real-World Implementation Patterns
Command Palette (Fuse.js)
// Perfect Fuse.js use case: small curated list of commands/navigation items
const commands = getAppCommands() // < 500 items
const fuse = new Fuse(commands, {
keys: [{ name: "title", weight: 3 }, { name: "description", weight: 1 }],
threshold: 0.4,
includeScore: true,
})
function CommandPalette({ query }: { query: string }) {
const results = useMemo(
() => query ? fuse.search(query).slice(0, 10) : commands.slice(0, 10),
[query]
)
return <ResultsList results={results} />
}
Documentation Search (FlexSearch)
// FlexSearch for documentation: index at build time, ship with bundle
const docIndex = new Document({
document: { id: "slug", index: ["title", "headings", "content"] },
tokenize: "forward",
language: "en",
})
// At build time: index all MDX files
for (const doc of allDocs) {
await docIndex.addAsync(doc.slug, {
slug: doc.slug,
title: doc.title,
headings: doc.headings.join(" "),
content: doc.content,
})
}
// Export index to JSON for client-side use
const indexData = docIndex.export()
E-commerce Product Search (Orama)
// Orama's faceting and filtering make it ideal for product search
const productDb = await create({
schema: {
name: "string",
description: "string",
category: "string",
price: "number",
rating: "number",
inStock: "boolean",
}
})
const searchProducts = (query: string, filters: SearchFilters) =>
search(productDb, {
term: query,
tolerance: 1,
facets: { category: { limit: 10 } },
where: {
price: { lte: filters.maxPrice },
inStock: true,
},
sortBy: { property: "rating", order: "DESC" },
})
When to Use Each
Choose Fuse.js if:
- Small dataset (< 10,000 items)
- Need typo tolerance for names/identifiers (not document bodies)
- Zero configuration is the priority
- Building a command palette, contact search, or nav search
Choose FlexSearch if:
- Large dataset (10,000+ documents)
- Need maximum search throughput
- Building documentation search or indexing article bodies
- Prefix/autocomplete search is the primary use case
Choose Orama if:
- Need faceted search, filters, and sorting in one library
- TypeScript-first API is important
- Planning to add semantic/vector search later
- Want the option to sync to Orama Cloud for distributed search
Methodology
Download data from npm registry (weekly average, February 2026). Performance benchmarks are approximate based on library documentation and community benchmarks with 10K text documents. Bundle sizes from bundlephobia.