flashfuzzy
v0.2.0
Published
High-performance fuzzy search engine powered by Rust and WebAssembly
Downloads
14,445
Maintainers
Readme
FlashFuzzy
Overview
FlashFuzzy is a high-performance fuzzy search library for JavaScript/TypeScript applications. Built with Rust and compiled to WebAssembly, it delivers exceptional performance for autocomplete, command palettes, product search, and more.
Key Features
- Sub-millisecond search on 100K+ records
- Typo-tolerant matching with configurable error distance
- Tiny bundle size: ~3KB WASM binary (1.5KB gzipped)
- Zero dependencies
- Framework agnostic: React, Vue, Angular, Svelte, vanilla JS
- Tree-shakeable ESM and CommonJS builds
- Full TypeScript support with type definitions
- Universal: Node.js, browsers, Deno, Bun
Performance
FlashFuzzy combines Bloom Filter pre-filtering with the Bitap algorithm for maximum speed. The Bloom filter rejects 80-95% of non-matching records in O(1) time before running fuzzy matching.
Result: 10-100x faster than traditional fuzzy search libraries.
| Library | Search Time (100K records) | Bundle Size | |---------|---------------------------|-------------| | FlashFuzzy | 0.8ms | 3KB | | Alternative A | 145ms | 12KB |
Installation
npm install flashfuzzyOr with other package managers:
yarn add flashfuzzy
pnpm add flashfuzzy
bun add flashfuzzyQuick Start
import { FlashFuzzy } from 'flashfuzzy';
const searchEngine = await FlashFuzzy.init({
threshold: 0.25,
maxResults: 50,
maxErrors: 2
});
searchEngine.add([
{ id: 1, name: "Wireless Headphones" },
{ id: 2, name: "Mechanical Keyboard" },
{ id: 3, name: "USB Cable" },
{ id: 4, name: "Laptop Stand" }
]);
const results = searchEngine.search("keyboard");
console.log(results);
// [
// {
// id: 2,
// score: 0.95,
// match: [11, 19] // character offsets of the best match
// }
// ]
// Results carry only id/score/match; look the original text up by id.React Integration
Custom Hook Pattern
import { useState, useEffect, useCallback, useMemo } from 'react';
import { FlashFuzzy } from 'flashfuzzy';
interface SearchItem {
id: number;
title: string;
description: string;
}
const useFlashFuzzy = <T extends SearchItem>(
items: T[],
config = { threshold: 0.3, maxResults: 10 }
) => {
const [engine, setEngine] = useState<any>(null);
const [isReady, setIsReady] = useState(false);
useEffect(() => {
FlashFuzzy.init(config).then(instance => {
instance.add(items);
setEngine(instance);
setIsReady(true);
});
}, []);
const search = useCallback(
(query: string) => (engine && query.trim() ? engine.search(query) : []),
[engine]
);
const addItem = useCallback(
(item: T) => engine?.add(item),
[engine]
);
const removeItem = useCallback(
(id: number) => engine?.remove(id),
[engine]
);
return { search, addItem, removeItem, isReady };
};
// Usage
export default function SearchableList() {
const [query, setQuery] = useState('');
const items = [
{ id: 1, title: "React Patterns", description: "Advanced patterns" },
{ id: 2, title: "TypeScript Guide", description: "Type safety" }
];
const byId = useMemo(
() => Object.fromEntries(items.map(i => [i.id, i])),
[]
);
const { search, isReady } = useFlashFuzzy(items);
const results = useMemo(() => search(query), [query, search]);
if (!isReady) return <div>Loading...</div>;
return (
<div>
<input
type="text"
value={query}
onChange={e => setQuery(e.target.value)}
placeholder="Search..."
/>
{results.map(({ id, score }) => (
<div key={id}>{byId[id]?.title} ({score.toFixed(2)})</div>
))}
</div>
);
}Higher-Order Component Pattern
import { Component, ComponentType } from 'react';
import { FlashFuzzy } from 'flashfuzzy';
interface WithSearchProps {
searchEngine: any;
isSearchReady: boolean;
}
const withFlashFuzzy = <P extends object>(
WrappedComponent: ComponentType<P & WithSearchProps>,
items: any[],
config = {}
) => {
return class extends Component<P, { engine: any; ready: boolean }> {
state = { engine: null, ready: false };
async componentDidMount() {
const engine = await FlashFuzzy.init(config);
engine.add(items);
this.setState({ engine, ready: true });
}
render() {
return (
<WrappedComponent
{...(this.props as P)}
searchEngine={this.state.engine}
isSearchReady={this.state.ready}
/>
);
}
};
};Vue 3 Composition API
import { ref, computed, onMounted } from 'vue';
import { FlashFuzzy } from 'flashfuzzy';
export const useSearch = (items: any[], options = {}) => {
const engine = ref(null);
const query = ref('');
const isReady = ref(false);
onMounted(async () => {
engine.value = await FlashFuzzy.init(options);
engine.value.add(items);
isReady.value = true;
});
const results = computed(() =>
engine.value && query.value.trim()
? engine.value.search(query.value)
: []
);
const addRecord = (item: any) => engine.value?.add(item);
const removeRecord = (id: number) => engine.value?.remove(id);
const reset = () => engine.value?.reset();
return {
query,
results,
isReady,
addRecord,
removeRecord,
reset
};
};
// Component usage
export default {
setup() {
const items = [
{ id: 1, name: 'Product Alpha' },
{ id: 2, name: 'Product Beta' }
];
const { query, results, isReady } = useSearch(items, {
threshold: 0.25,
maxResults: 20
});
return { query, results, isReady };
}
};Advanced Usage
Multi-field Search
import { FlashFuzzy } from 'flashfuzzy';
interface Product {
id: number;
title: string;
brand: string;
category: string;
description: string;
}
const createProductSearch = async (products: Product[]) => {
const engine = await FlashFuzzy.init({ threshold: 0.25 });
engine.setSchema({
fields: [
{ name: 'title' },
{ name: 'brand' },
{ name: 'category' },
{ name: 'description' }
]
});
engine.add(products);
return {
search: (query: string) => engine.search(query),
addProduct: (product: Product) => engine.add(product),
removeProduct: (id: number) => engine.remove(id),
getStats: () => engine.stats()
};
};
// Usage
const productSearch = await createProductSearch([
{
id: 1,
title: "MacBook Pro",
brand: "Apple",
category: "Laptops",
description: "High-performance laptop for professionals"
}
]);
const results = productSearch.search("laptop");Command Palette Implementation
import { FlashFuzzy } from 'flashfuzzy';
interface Command {
id: number;
command: string;
shortcut: string;
category: string;
action: () => void;
}
class CommandPalette {
private engine: any;
private commands: Map<number, Command>;
constructor() {
this.commands = new Map();
}
async initialize(commands: Command[]) {
this.engine = await FlashFuzzy.init({
threshold: 0.2,
maxResults: 10,
maxErrors: 2
});
this.engine.setSchema({
fields: [{ name: 'command' }, { name: 'category' }]
});
commands.forEach(cmd => {
this.commands.set(cmd.id, cmd);
this.engine.add({
id: cmd.id,
command: cmd.command,
category: cmd.category
});
});
}
search(query: string) {
return this.engine
.search(query)
.map(result => ({
...this.commands.get(result.id),
score: result.score
}));
}
execute(commandId: number) {
const command = this.commands.get(commandId);
command?.action();
}
}
// Usage
const palette = new CommandPalette();
await palette.initialize([
{
id: 1,
command: "Open File",
shortcut: "Ctrl+O",
category: "File",
action: () => console.log("Opening file...")
},
{
id: 2,
command: "Save File",
shortcut: "Ctrl+S",
category: "File",
action: () => console.log("Saving file...")
}
]);
const matches = palette.search("open");Reactive Search with RxJS
import { fromEvent, debounceTime, map, switchMap, distinctUntilChanged } from 'rxjs';
import { FlashFuzzy } from 'flashfuzzy';
const createReactiveSearch = async (items: any[], inputElement: HTMLInputElement) => {
const engine = await FlashFuzzy.init({ threshold: 0.3 });
engine.add(items);
return fromEvent(inputElement, 'input').pipe(
map(event => (event.target as HTMLInputElement).value),
debounceTime(300),
distinctUntilChanged(),
map(query => engine.search(query))
);
};
// Usage
const searchResults$ = await createReactiveSearch(
[{ id: 1, name: "Item 1" }],
document.querySelector('input')
);
searchResults$.subscribe(results => {
console.log('Search results:', results);
});API Reference
FlashFuzzy.init(options)
Initialize the fuzzy search engine.
Options:
interface FlashFuzzyOptions {
threshold?: number; // 0-1, default: 0.25 (HIGHER = stricter)
maxResults?: number; // default: 50 (max 100)
maxErrors?: number; // 0-3, default: 2 (adaptive for short patterns)
wasmUrl?: string; // explicit path/URL to flash-fuzzy.wasm
}Matching is always case-insensitive and accent-aware (México matches méxico).
Returns: Promise<FlashFuzzy>
Instance Methods
search(query: string): SearchResult[]
Search for records matching the query.
interface SearchResult {
id: number;
score: number; // 0-1, higher is better
match: [number, number]; // [start, end) CHARACTER offsets of the best match
}Results carry no field text; look the record up by id.
add(records: T | T[]): number
Add one or many records; returns how many were added. setSchema (optional)
restricts/orders the indexed fields (no per-field weights).
remove(id: number): boolean
Remove a record by ID.
compact(): void
Physically drop removed records, reclaiming memory.
reset(): void
Clear all records.
stats(): IndexStats
Get index statistics.
interface IndexStats {
recordCount: number;
stringPoolUsed: number;
usedMemory: number; // alias for stringPoolUsed
}Use Cases
E-commerce Product Search
const productSearch = await FlashFuzzy.init({ threshold: 0.3 });
productSearch.setSchema({
fields: [
{ name: 'name' },
{ name: 'brand' },
{ name: 'category' },
{ name: 'description' }
]
});
productSearch.add(products);User Directory Search
const userSearch = await FlashFuzzy.init();
userSearch.setSchema({
fields: [{ name: 'name' }, { name: 'email' }, { name: 'department' }]
});Autocomplete System
const autocomplete = await FlashFuzzy.init({
threshold: 0.2,
maxResults: 5,
maxErrors: 1
});How It Works
FlashFuzzy uses a two-phase search algorithm:
Phase 1: Bloom Filter Pre-filtering
Each record is checked using a 64-bit Bloom filter before running expensive fuzzy matching. This rejects 80-95% of non-matching records in O(1) time.
Record: "Wireless Keyboard"
Bloom: 01001010 11000101... (64 bits)
Query: "keyboard"
Bloom: 00001010 01000001... (64 bits)
Check: (record_bloom & query_bloom) == query_bloom
Pass → Run Bitap
Fail → SkipPhase 2: Bitap Algorithm
For records that pass the Bloom filter, the Bitap (Shift-Or) algorithm performs bit-parallel fuzzy matching with support for typos, insertions, and deletions.
TypeScript Support
Full TypeScript definitions included:
import { FlashFuzzy, type FlashFuzzyOptions, type SearchResult } from 'flashfuzzy';
const options: FlashFuzzyOptions = {
threshold: 0.25,
maxResults: 50
};
const engine = await FlashFuzzy.init(options);
const results: SearchResult[] = engine.search("query");Platform Support
Browser Compatibility
Works in all modern browsers with WebAssembly support:
- Chrome/Edge 57+
- Firefox 52+
- Safari 11+
- Opera 44+
Node.js
Requires Node.js 16 or higher.
// CommonJS
const { FlashFuzzy } = require('flashfuzzy');
// ESM
import { FlashFuzzy } from 'flashfuzzy';Deno & Bun
// Deno
import { FlashFuzzy } from "npm:flashfuzzy";
// Bun
import { FlashFuzzy } from "flashfuzzy";Benchmarks
Run benchmarks yourself:
git clone https://github.com/RafaCalRob/FlashFuzzy.git
cd FlashFuzzy
npm install
npm testLinks
- Documentation: https://bdovenbird.com/flash-fuzzy/
- Live Demo: https://bdovenbird.com/flash-fuzzy/playground
- GitHub: https://github.com/RafaCalRob/FlashFuzzy
- npm: https://www.npmjs.com/package/flashfuzzy
- Issues: https://github.com/RafaCalRob/FlashFuzzy/issues
Contributing
Contributions are welcome! See CONTRIBUTING.md for guidelines.
License
MIT © 2025 Rafael Calderon Robles
Credits
Built with Rust, WebAssembly, Bitap Algorithm, and Bloom Filters.
