Overview
Fuzzy search, shipped as 3 KB of WebAssembly. FlashFuzzy is a complete fuzzy search runtime that runs entirely in the browser — no server, no API calls, no network traffic after load. It combines a Bloom pre-filter with Bitap matching to deliver typo-tolerant, accent-insensitive search over any array, object, or JSON dataset.
FlashFuzzy exposes a single class, FlashFuzzy, that wraps the WebAssembly module. All search logic runs in the WASM sandbox; the JS surface is intentionally thin. Built with Rust, targeting wasm32-unknown-unknown.
3 KB
Main WASM binary
<1 ms
Query on 100k records
10–100×
Faster than JS fuzzy libs
0 deps
Core + search crates
Built for the browser edge
Zero backend
Runs entirely in the browser. No server, no API calls. 3 KB · MIT.
Typo-tolerant
Bitap allows up to 2 edit operations. Finds "javascript" from "javsacript". bitap · adaptive.
Accent-insensitive
café matches cafe. Full Latin Extended fold (U+00C0 – U+00FF).
Multi-field
Search across name, description, tags simultaneously.
Any data
Arrays of strings, objects, JSON — no schema required.
Zero allocator
BSS static memory. No heap, no GC pressure. no_std · no_alloc.
Install
Ship fuzzy search today. No backend required — the runtime is delivered as an npm package and runs entirely in the browser. Pick your package manager:
install · shellnpm install flashfuzzy
pnpm add flashfuzzy
yarn add flashfuzzy
cargo add flashfuzzy
pip install flashfuzzy
Quick start
Up in under a minute. Three steps, no server:
- Drop — feed any array, JSON, or text. No network, no server.
- Index — tokenizer + Bloom filter. Unicode fold, 64-bit filter, tokens stored in the BSS pool.
- Search — Bloom skips, Bitap finishes. O(1) skip, O(n·k) match.
import { FlashFuzzy } from "flashfuzzy";
const ff = await FlashFuzzy.create();
ff.index([
{ id: 1, name: "React Hooks", tags: ["react", "js"] },
{ id: 2, name: "Vue Composition API", tags: ["vue", "js"] },
]);
const hits = ff.search("raect hoks"); // typo-tolerant
for (const h of hits) {
console.log(h.id, h.score, h.snippet);
}
Query syntax
The query language supports fuzzy single tokens, AND, exact phrases, and OR — composable in a single string.
| Pattern | Meaning | Example match |
|---|---|---|
word |
Fuzzy single token — up to 2 character edits | flashfuzzy → matches flashfuzzy, flashfuzy |
a b |
All tokens AND — both must appear | react hooks → both must appear |
"a b" |
Exact phrase match, no fuzziness applied | "hello world" |
a | b |
Either token — logical OR | typescript | javascript |
"a b" | c |
Phrase OR single token | "fuzzy search" | search |
API reference
The whole surface is one class and four calls.
static FlashFuzzy.create() → Promise<FlashFuzzy>
Initialises the WebAssembly module and returns a ready FlashFuzzy instance. Must be awaited before any other call. Safe to call once per page load.
ff.index(records: T[]) → void
Tokenises each record and builds the Bloom filter index. Accepts arrays of strings, objects, or JSON. Fields are concatenated for multi-field search.
ff.search(query: string) → Hit[]
Runs the Bloom → Bitap → Score → Rank pipeline and returns hits sorted by score descending. Typo-tolerant up to 2 edits. Supports phrase, AND, and OR queries.
ff.clear() → void
Clears all indexed records and resets the BSS store. The instance is immediately ready to index a new dataset.
FlashFuzzy.create()
create · typescriptimport { FlashFuzzy } from "flashfuzzy";
const ff = await FlashFuzzy.create();
console.log("FlashFuzzy ready");
ff.index()
index · typescriptff.index([
{ id: 1, name: "React Hooks", tags: ["react", "js"] },
{ id: 2, name: "Vue Composition API", tags: ["vue", "js"] },
]);
ff.search()
search · typescriptconst hits = ff.search("raect hoks"); // typo-tolerant
for (const h of hits) {
console.log(h.id, h.score, h.snippet);
}
The Hit type
| Field | Type | Description |
|---|---|---|
id | unknown | Original record identifier as provided to index() |
score | number | Relevance score 0.0–1.0 (higher is better) |
snippet | string | Surrounding text excerpt with match context |
ff.clear()
clear · typescriptff.clear();
console.log("Index cleared, ready for new records");
Architecture
The runtime topology is seven nodes and six edges, split across an ingest lane and a search lane. A query traverses the whole graph in under a millisecond:
runtime topologyInput → Tokenizer → Bloom Builder → Index → Store → Pipeline → Results
Ingest lane
01 · Input
JSON · arrays. Any array, object or JSON input.
02 · Tokenizer
Unicode fold. Unicode-aware tokenizer with accent folding.
03 · Bloom builder
64-bit filter. Probabilistic set membership filter.
04 · Index
BSS pool. Static memory index, zero fragmentation.
05 · Store
3 KB · 50k tokens. Compact BSS-backed token store.
Search lane
06 · Pipeline
bloom ▸ bitap / score ▸ rank. The full search pipeline.
07 · Results
< 1 ms. Sorted Hit[] returned to JS.
Memory layout
All state lives in static BSS memory — no allocator, no heap, no GC pressure. The store is split into four segments:
- BSS pool — 3 KB text pool
- Tokens — 50k token chunks
- Bloom bits — the 64-bit filter scratch
- Doc index — record name table
Search pipeline
Four stages, one pass. Every query runs Bloom → Bitap → Score → Rank in a single traversal of the index.
01 · Bloom filter
O(1) — probabilistic skip. The 64-bit filter rejects tokens that cannot match before any expensive work happens.
02 · Bitap shift-or
O(n·k) — up to 2 edits. Bitwise approximate matching finishes what Bloom lets through.
03 · Score
O(1) — edit-dist, position, freq. Each candidate is scored by edit distance (1.0), match position (0.7), and token frequency (0.5) weights.
04 · Rank
O(log k) — min-heap top-K. A bounded min-heap keeps only the best K hits, sorted by score descending.
Formats & capacity
What goes in
arrays
JavaScript arrays — native JS array input.
objects
Plain JS objects — any object shape.
JSON
Serialised JSON — parsed automatically.
CSV
CSV rows — parsed as row objects.
text
Raw strings — string array input.
Default limits
3 KB
WASM binary — total runtime size
50k
Token capacity — tokens stored in BSS pool
100
Max documents — concurrent indexed records
32 chars
Query limit — max query string length
Guides
From install to first fuzzy hit in under 60 seconds — the same recipe in plain ESM/TypeScript, React, Vue, and Svelte.
ESM / TypeScript
01 · install · shellnpm install flashfuzzy
# or
pnpm add flashfuzzy
02 · index + search · typescript
import { FlashFuzzy } from "flashfuzzy";
const ff = await FlashFuzzy.create();
ff.index([
{ id: 1, name: "React Hooks", tags: ["react", "js"] },
{ id: 2, name: "Vue Composition API", tags: ["vue", "js"] },
]);
const hits = ff.search("raect hoks");
for (const h of hits) {
console.log(h.id, h.score, h.snippet);
}
03 · advanced · typescript
// Clear and re-index
ff.clear();
ff.index(newRecords);
// OR query
const results = ff.search("typescript | javascript");
// Phrase query
const exact = ff.search('"fuzzy search"');
// Typo-tolerant single token
const fuzzy = ff.search("javsacript"); // matches javascript
React hook
01 · install · shellnpm install flashfuzzy
02 · index + search · jsx
import { useEffect, useRef, useState } from "react";
import { FlashFuzzy } from "flashfuzzy";
export function FuzzySearch({ records }) {
const ffRef = useRef(null);
const [hits, setHits] = useState([]);
useEffect(() => {
FlashFuzzy.create().then(ff => {
ff.index(records);
ffRef.current = ff;
});
}, [records]);
function handleSearch(q) {
setHits(ffRef.current?.search(q) ?? []);
}
return (
<div>
<input onChange={e => handleSearch(e.target.value)} />
{hits.map(h => <div key={h.id}>{h.snippet}</div>)}
</div>
);
}
Vue composable
01 · install · shellnpm install flashfuzzy
02 · index + search · javascript
import { ref, onMounted } from "vue";
import { FlashFuzzy } from "flashfuzzy";
export function useFuzzySearch(records) {
const ff = ref(null);
const hits = ref([]);
onMounted(async () => {
ff.value = await FlashFuzzy.create();
ff.value.index(records);
});
function search(query) {
hits.value = ff.value?.search(query) ?? [];
}
return { hits, search };
}
Svelte store
01 · install · shellnpm install flashfuzzy
02 · index + search · javascript
import { writable } from "svelte/store";
import { FlashFuzzy } from "flashfuzzy";
export function createFuzzyStore(records) {
const hits = writable([]);
let ff = null;
FlashFuzzy.create().then(instance => {
ff = instance;
ff.index(records);
});
function search(query) {
hits.set(ff?.search(query) ?? []);
}
return { hits, search };
}
Query syntax quick reference
| Pattern | Meaning |
|---|---|
word | Single fuzzy token — up to 2 character edits |
a b | All tokens AND — both must appear |
"a b" | Exact phrase match, no fuzziness applied |
a | b | Either token — logical OR |
"a b" | c | Phrase OR single token |