‹ back to the set

FlashFuzzy

rust · wasm · ~3 kB · npm: flashfuzzy

A complete fuzzy search runtime for the browser. Bloom pre-filter, Bitap matching, accent-insensitive, typo-tolerant — zero backend, zero network after load. Open source, MIT, v1.0.0.

source ↗ npm ↗

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 · shell
npm 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:

  1. Drop — feed any array, JSON, or text. No network, no server.
  2. Index — tokenizer + Bloom filter. Unicode fold, 64-bit filter, tokens stored in the BSS pool.
  3. Search — Bloom skips, Bitap finishes. O(1) skip, O(n·k) match.
index.ts · 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");  // 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.

PatternMeaningExample 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 · typescript
import { FlashFuzzy } from "flashfuzzy";

const ff = await FlashFuzzy.create();
console.log("FlashFuzzy ready");

ff.index()

index · typescript
ff.index([
  { id: 1, name: "React Hooks", tags: ["react", "js"] },
  { id: 2, name: "Vue Composition API", tags: ["vue", "js"] },
]);

ff.search()

search · typescript
const hits = ff.search("raect hoks");  // typo-tolerant
for (const h of hits) {
  console.log(h.id, h.score, h.snippet);
}

The Hit type

FieldTypeDescription
idunknownOriginal record identifier as provided to index()
scorenumberRelevance score 0.0–1.0 (higher is better)
snippetstringSurrounding text excerpt with match context

ff.clear()

clear · typescript
ff.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 topology
Input → 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:

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.

performance The Bloom pre-filter is what makes the <1 ms figure possible: most non-matching tokens are skipped in O(1) before Bitap ever runs, which is how FlashFuzzy stays 10–100× faster than pure-JS fuzzy libraries on the same data.

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

note These are the defaults of the static BSS store. Because memory is laid out at compile time (no allocator), the limits are fixed per build rather than growing at runtime.

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 · shell
npm 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 · shell
npm 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 · shell
npm 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 · shell
npm 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

PatternMeaning
wordSingle fuzzy token — up to 2 character edits
a bAll tokens AND — both must appear
"a b"Exact phrase match, no fuzziness applied
a | bEither token — logical OR
"a b" | cPhrase OR single token