flashfuzzy 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +52 -0
- package/LICENSE +21 -0
- package/README.md +560 -0
- package/dist/flash-fuzzy.wasm +0 -0
- package/dist/index.d.mts +59 -0
- package/dist/index.d.ts +59 -0
- package/dist/index.js +220 -0
- package/dist/index.mjs +184 -0
- package/logo.png +0 -0
- package/package.json +69 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
### Planned
|
|
11
|
+
- Python binding (PyPI)
|
|
12
|
+
- Java/Kotlin binding (Maven)
|
|
13
|
+
- .NET binding (NuGet)
|
|
14
|
+
- iOS/macOS binding (CocoaPods/SPM)
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## [0.1.0] - 2025-01-XX
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
- Initial release
|
|
22
|
+
- Rust core with Bitap algorithm for fuzzy matching
|
|
23
|
+
- Bloom filter pre-filtering for O(1) rejection
|
|
24
|
+
- WebAssembly build (~3KB gzipped)
|
|
25
|
+
- TypeScript/JavaScript wrapper
|
|
26
|
+
- Zero-copy JS â WASM communication
|
|
27
|
+
- Case-insensitive search
|
|
28
|
+
- Configurable threshold, maxErrors, maxResults
|
|
29
|
+
- Schema support for field weights
|
|
30
|
+
- Record add/remove/reset operations
|
|
31
|
+
- Statistics API (recordCount, stringPoolUsed, availableMemory)
|
|
32
|
+
|
|
33
|
+
### Performance
|
|
34
|
+
- Sub-millisecond search on 10K+ records
|
|
35
|
+
- 80-95% of records rejected by bloom filter before Bitap
|
|
36
|
+
- Zero-allocation memory architecture
|
|
37
|
+
|
|
38
|
+
### Technical
|
|
39
|
+
- `no_std` Rust implementation
|
|
40
|
+
- Static memory pools (4MB string pool, 100K records max)
|
|
41
|
+
- Adaptive maxErrors based on pattern length to prevent false positives
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## Version History
|
|
46
|
+
|
|
47
|
+
| Version | Date | Highlights |
|
|
48
|
+
|---------|------|------------|
|
|
49
|
+
| 0.1.0 | 2025-01 | Initial release with WASM/JS binding |
|
|
50
|
+
|
|
51
|
+
[Unreleased]: https://github.com/RafaCalRob/FlashFuzzy/compare/v0.1.0...HEAD
|
|
52
|
+
[0.1.0]: https://github.com/RafaCalRob/FlashFuzzy/releases/tag/v0.1.0
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Rafael Calderon Robles and Flash-Fuzzy Contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,560 @@
|
|
|
1
|
+
# FlashFuzzy
|
|
2
|
+
|
|
3
|
+
<p align="center">
|
|
4
|
+
<strong>High-performance fuzzy search engine for JavaScript/TypeScript</strong><br>
|
|
5
|
+
<em>Powered by Rust and WebAssembly - Fast, lightweight, zero dependencies</em>
|
|
6
|
+
</p>
|
|
7
|
+
|
|
8
|
+
<p align="center">
|
|
9
|
+
<a href="https://www.npmjs.com/package/flashfuzzy"><img src="https://img.shields.io/npm/v/flashfuzzy.svg" alt="npm version"></a>
|
|
10
|
+
<a href="https://github.com/RafaCalRob/FlashFuzzy/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="license"></a>
|
|
11
|
+
<img src="https://img.shields.io/badge/size-~3KB-brightgreen" alt="bundle size">
|
|
12
|
+
<img src="https://img.shields.io/badge/WASM-yes-orange" alt="WebAssembly">
|
|
13
|
+
</p>
|
|
14
|
+
|
|
15
|
+
<p align="center">
|
|
16
|
+
<a href="https://bdovenbird.com/flash-fuzzy/">ð Documentation</a> âĒ
|
|
17
|
+
<a href="https://bdovenbird.com/flash-fuzzy/playground">ðŪ Live Demo</a> âĒ
|
|
18
|
+
<a href="https://github.com/RafaCalRob/FlashFuzzy">ðŧ GitHub</a>
|
|
19
|
+
</p>
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Why FlashFuzzy?
|
|
24
|
+
|
|
25
|
+
FlashFuzzy is a **blazing-fast 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.
|
|
26
|
+
|
|
27
|
+
### Features
|
|
28
|
+
|
|
29
|
+
- ⥠**Sub-millisecond search** on 100K+ records
|
|
30
|
+
- ðŊ **Typo-tolerant** matching with configurable error distance
|
|
31
|
+
- ðŠķ **Tiny bundle** - ~3KB WASM binary (1.5KB gzipped)
|
|
32
|
+
- ð **Zero dependencies** - Pure Rust core
|
|
33
|
+
- ð§ **Framework agnostic** - Works with React, Vue, Angular, Svelte, vanilla JS
|
|
34
|
+
- ðĶ **Tree-shakeable** - ESM and CommonJS builds
|
|
35
|
+
- ðŠ **TypeScript** support with full type definitions
|
|
36
|
+
- ð **Universal** - Node.js, browsers, Deno, Bun
|
|
37
|
+
|
|
38
|
+
### Performance
|
|
39
|
+
|
|
40
|
+
FlashFuzzy combines two powerful algorithms for maximum speed:
|
|
41
|
+
|
|
42
|
+
1. **Bloom Filter Pre-filtering** - Rejects 80-95% of non-matching records in O(1)
|
|
43
|
+
2. **Bitap Algorithm** - Bit-parallel fuzzy matching
|
|
44
|
+
|
|
45
|
+
**Result:** 10-100x faster than traditional fuzzy search libraries.
|
|
46
|
+
|
|
47
|
+
| Library | Search Time (10K records) | Bundle Size |
|
|
48
|
+
|---------|--------------------------|-------------|
|
|
49
|
+
| **FlashFuzzy** | **0.8ms** | **3KB** |
|
|
50
|
+
| Fuse.js | 145ms | 12KB |
|
|
51
|
+
| fuzzy.js | 89ms | 8KB |
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## Installation
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
npm install flashfuzzy
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Or with other package managers:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
yarn add flashfuzzy
|
|
65
|
+
pnpm add flashfuzzy
|
|
66
|
+
bun add flashfuzzy
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## Quick Start
|
|
72
|
+
|
|
73
|
+
### Basic Usage
|
|
74
|
+
|
|
75
|
+
```javascript
|
|
76
|
+
import { FlashFuzzy } from 'flashfuzzy';
|
|
77
|
+
|
|
78
|
+
// Initialize
|
|
79
|
+
const ff = await FlashFuzzy.init({
|
|
80
|
+
threshold: 0.25, // Lower = stricter matching
|
|
81
|
+
maxResults: 50, // Limit results
|
|
82
|
+
maxErrors: 2 // Max typos allowed
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// Add records
|
|
86
|
+
ff.add([
|
|
87
|
+
{ id: 1, name: "Wireless Headphones" },
|
|
88
|
+
{ id: 2, name: "Mechanical Keyboard" },
|
|
89
|
+
{ id: 3, name: "USB Cable" },
|
|
90
|
+
{ id: 4, name: "Laptop Stand" }
|
|
91
|
+
]);
|
|
92
|
+
|
|
93
|
+
// Search
|
|
94
|
+
const results = ff.search("keyboard");
|
|
95
|
+
// => [{ id: 2, score: 0.95, matches: {...} }]
|
|
96
|
+
|
|
97
|
+
console.log(results);
|
|
98
|
+
// [
|
|
99
|
+
// {
|
|
100
|
+
// id: 2,
|
|
101
|
+
// score: 0.95,
|
|
102
|
+
// matches: {
|
|
103
|
+
// name: {
|
|
104
|
+
// value: "Mechanical Keyboard",
|
|
105
|
+
// ranges: [[11, 19]] // Matched "Keyboard"
|
|
106
|
+
// }
|
|
107
|
+
// }
|
|
108
|
+
// }
|
|
109
|
+
// ]
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### React Example
|
|
113
|
+
|
|
114
|
+
```jsx
|
|
115
|
+
import { useState, useEffect } from 'react';
|
|
116
|
+
import { FlashFuzzy } from 'flashfuzzy';
|
|
117
|
+
|
|
118
|
+
function SearchComponent() {
|
|
119
|
+
const [ff, setFf] = useState(null);
|
|
120
|
+
const [query, setQuery] = useState('');
|
|
121
|
+
const [results, setResults] = useState([]);
|
|
122
|
+
|
|
123
|
+
useEffect(() => {
|
|
124
|
+
FlashFuzzy.init({ threshold: 0.3, maxResults: 10 })
|
|
125
|
+
.then(fuzzy => {
|
|
126
|
+
fuzzy.add([
|
|
127
|
+
{ id: 1, title: "React Documentation" },
|
|
128
|
+
{ id: 2, title: "TypeScript Guide" },
|
|
129
|
+
{ id: 3, title: "WebAssembly Tutorial" }
|
|
130
|
+
]);
|
|
131
|
+
setFf(fuzzy);
|
|
132
|
+
});
|
|
133
|
+
}, []);
|
|
134
|
+
|
|
135
|
+
const handleSearch = (e) => {
|
|
136
|
+
const value = e.target.value;
|
|
137
|
+
setQuery(value);
|
|
138
|
+
|
|
139
|
+
if (ff && value.trim()) {
|
|
140
|
+
setResults(ff.search(value));
|
|
141
|
+
} else {
|
|
142
|
+
setResults([]);
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
return (
|
|
147
|
+
<div>
|
|
148
|
+
<input
|
|
149
|
+
type="text"
|
|
150
|
+
value={query}
|
|
151
|
+
onChange={handleSearch}
|
|
152
|
+
placeholder="Search..."
|
|
153
|
+
/>
|
|
154
|
+
<ul>
|
|
155
|
+
{results.map(result => (
|
|
156
|
+
<li key={result.id}>
|
|
157
|
+
{result.matches.title.value} (score: {result.score})
|
|
158
|
+
</li>
|
|
159
|
+
))}
|
|
160
|
+
</ul>
|
|
161
|
+
</div>
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### Vue 3 Example
|
|
167
|
+
|
|
168
|
+
```vue
|
|
169
|
+
<template>
|
|
170
|
+
<div>
|
|
171
|
+
<input v-model="query" @input="search" placeholder="Search..." />
|
|
172
|
+
<ul>
|
|
173
|
+
<li v-for="result in results" :key="result.id">
|
|
174
|
+
{{ result.matches.name.value }} ({{ result.score }})
|
|
175
|
+
</li>
|
|
176
|
+
</ul>
|
|
177
|
+
</div>
|
|
178
|
+
</template>
|
|
179
|
+
|
|
180
|
+
<script setup>
|
|
181
|
+
import { ref, onMounted } from 'vue';
|
|
182
|
+
import { FlashFuzzy } from 'flashfuzzy';
|
|
183
|
+
|
|
184
|
+
const query = ref('');
|
|
185
|
+
const results = ref([]);
|
|
186
|
+
let ff = null;
|
|
187
|
+
|
|
188
|
+
onMounted(async () => {
|
|
189
|
+
ff = await FlashFuzzy.init({ threshold: 0.3 });
|
|
190
|
+
ff.add([
|
|
191
|
+
{ id: 1, name: 'Product A' },
|
|
192
|
+
{ id: 2, name: 'Product B' }
|
|
193
|
+
]);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
const search = () => {
|
|
197
|
+
results.value = ff ? ff.search(query.value) : [];
|
|
198
|
+
};
|
|
199
|
+
</script>
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
### Next.js Example
|
|
203
|
+
|
|
204
|
+
```tsx
|
|
205
|
+
'use client';
|
|
206
|
+
|
|
207
|
+
import { useEffect, useState } from 'react';
|
|
208
|
+
import { FlashFuzzy } from 'flashfuzzy';
|
|
209
|
+
|
|
210
|
+
export default function SearchPage() {
|
|
211
|
+
const [ff, setFf] = useState<any>(null);
|
|
212
|
+
|
|
213
|
+
useEffect(() => {
|
|
214
|
+
FlashFuzzy.init({ threshold: 0.25 }).then(setFf);
|
|
215
|
+
}, []);
|
|
216
|
+
|
|
217
|
+
// ... rest of component
|
|
218
|
+
}
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
---
|
|
222
|
+
|
|
223
|
+
## Advanced Usage
|
|
224
|
+
|
|
225
|
+
### Schema Support (Multi-field Search)
|
|
226
|
+
|
|
227
|
+
Search across multiple fields with different weights:
|
|
228
|
+
|
|
229
|
+
```javascript
|
|
230
|
+
const ff = await FlashFuzzy.init({
|
|
231
|
+
threshold: 0.25,
|
|
232
|
+
schema: {
|
|
233
|
+
fields: [
|
|
234
|
+
{ name: 'title', weight: 2.0 }, // Title is more important
|
|
235
|
+
{ name: 'description', weight: 1.0 },
|
|
236
|
+
{ name: 'tags', weight: 1.5 }
|
|
237
|
+
]
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
ff.add([
|
|
242
|
+
{
|
|
243
|
+
id: 1,
|
|
244
|
+
title: "MacBook Pro",
|
|
245
|
+
description: "Powerful laptop for developers",
|
|
246
|
+
tags: "apple computer notebook"
|
|
247
|
+
}
|
|
248
|
+
]);
|
|
249
|
+
|
|
250
|
+
const results = ff.search("laptop");
|
|
251
|
+
// Matches in 'description' field
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
### Autocomplete
|
|
255
|
+
|
|
256
|
+
```javascript
|
|
257
|
+
import { FlashFuzzy } from 'flashfuzzy';
|
|
258
|
+
|
|
259
|
+
const autocomplete = await FlashFuzzy.init({
|
|
260
|
+
threshold: 0.2,
|
|
261
|
+
maxResults: 5,
|
|
262
|
+
maxErrors: 1 // Strict for autocomplete
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
autocomplete.add([
|
|
266
|
+
{ id: 1, command: "Open File" },
|
|
267
|
+
{ id: 2, command: "Save File" },
|
|
268
|
+
{ id: 3, command: "Close Window" }
|
|
269
|
+
]);
|
|
270
|
+
|
|
271
|
+
// User types: "opn"
|
|
272
|
+
const suggestions = autocomplete.search("opn");
|
|
273
|
+
// => [{ id: 1, command: "Open File", score: 0.85 }]
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
### Real-time Updates
|
|
277
|
+
|
|
278
|
+
```javascript
|
|
279
|
+
// Add records dynamically
|
|
280
|
+
ff.addRecord({ id: 100, name: "New Product" });
|
|
281
|
+
|
|
282
|
+
// Remove records
|
|
283
|
+
ff.removeRecord(100);
|
|
284
|
+
|
|
285
|
+
// Clear all
|
|
286
|
+
ff.reset();
|
|
287
|
+
|
|
288
|
+
// Get stats
|
|
289
|
+
const stats = ff.getStats();
|
|
290
|
+
console.log(stats);
|
|
291
|
+
// {
|
|
292
|
+
// recordCount: 1000,
|
|
293
|
+
// stringPoolUsed: 45123,
|
|
294
|
+
// availableMemory: 4149877
|
|
295
|
+
// }
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
---
|
|
299
|
+
|
|
300
|
+
## API Reference
|
|
301
|
+
|
|
302
|
+
### `FlashFuzzy.init(options)`
|
|
303
|
+
|
|
304
|
+
Initialize the fuzzy search engine.
|
|
305
|
+
|
|
306
|
+
**Options:**
|
|
307
|
+
|
|
308
|
+
```typescript
|
|
309
|
+
{
|
|
310
|
+
threshold?: number; // 0-1, default: 0.25 (lower = stricter)
|
|
311
|
+
maxResults?: number; // default: 100
|
|
312
|
+
maxErrors?: number; // default: adaptive based on pattern length
|
|
313
|
+
caseSensitive?: boolean; // default: false
|
|
314
|
+
schema?: {
|
|
315
|
+
fields: Array<{
|
|
316
|
+
name: string;
|
|
317
|
+
weight?: number; // default: 1.0
|
|
318
|
+
}>
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
**Returns:** `Promise<FlashFuzzyInstance>`
|
|
324
|
+
|
|
325
|
+
### Instance Methods
|
|
326
|
+
|
|
327
|
+
#### `search(query: string): Result[]`
|
|
328
|
+
|
|
329
|
+
Search for records matching the query.
|
|
330
|
+
|
|
331
|
+
```typescript
|
|
332
|
+
interface Result {
|
|
333
|
+
id: number;
|
|
334
|
+
score: number; // 0-1, higher is better
|
|
335
|
+
matches: {
|
|
336
|
+
[fieldName: string]: {
|
|
337
|
+
value: string;
|
|
338
|
+
ranges: [number, number][]; // Matched character ranges
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
#### `add(records: Record[]): void`
|
|
345
|
+
|
|
346
|
+
Add multiple records at once.
|
|
347
|
+
|
|
348
|
+
```typescript
|
|
349
|
+
interface Record {
|
|
350
|
+
id: number;
|
|
351
|
+
[key: string]: any; // Your searchable fields
|
|
352
|
+
}
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
#### `addRecord(record: Record): void`
|
|
356
|
+
|
|
357
|
+
Add a single record.
|
|
358
|
+
|
|
359
|
+
#### `removeRecord(id: number): void`
|
|
360
|
+
|
|
361
|
+
Remove a record by ID.
|
|
362
|
+
|
|
363
|
+
#### `reset(): void`
|
|
364
|
+
|
|
365
|
+
Clear all records.
|
|
366
|
+
|
|
367
|
+
#### `getStats(): Stats`
|
|
368
|
+
|
|
369
|
+
Get memory and performance statistics.
|
|
370
|
+
|
|
371
|
+
```typescript
|
|
372
|
+
interface Stats {
|
|
373
|
+
recordCount: number;
|
|
374
|
+
stringPoolUsed: number;
|
|
375
|
+
availableMemory: number;
|
|
376
|
+
}
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
---
|
|
380
|
+
|
|
381
|
+
## Use Cases
|
|
382
|
+
|
|
383
|
+
### E-commerce Product Search
|
|
384
|
+
|
|
385
|
+
```javascript
|
|
386
|
+
const productSearch = await FlashFuzzy.init({
|
|
387
|
+
threshold: 0.3,
|
|
388
|
+
schema: {
|
|
389
|
+
fields: [
|
|
390
|
+
{ name: 'name', weight: 2.0 },
|
|
391
|
+
{ name: 'brand', weight: 1.5 },
|
|
392
|
+
{ name: 'category', weight: 1.0 },
|
|
393
|
+
{ name: 'description', weight: 0.5 }
|
|
394
|
+
]
|
|
395
|
+
}
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
productSearch.add(products);
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
### Command Palette (VSCode-style)
|
|
402
|
+
|
|
403
|
+
```javascript
|
|
404
|
+
const commands = await FlashFuzzy.init({
|
|
405
|
+
threshold: 0.2,
|
|
406
|
+
maxResults: 10,
|
|
407
|
+
maxErrors: 2
|
|
408
|
+
});
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
### User Directory Search
|
|
412
|
+
|
|
413
|
+
```javascript
|
|
414
|
+
const users = await FlashFuzzy.init({
|
|
415
|
+
schema: {
|
|
416
|
+
fields: [
|
|
417
|
+
{ name: 'name', weight: 2.0 },
|
|
418
|
+
{ name: 'email', weight: 1.0 },
|
|
419
|
+
{ name: 'department', weight: 0.5 }
|
|
420
|
+
]
|
|
421
|
+
}
|
|
422
|
+
});
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
---
|
|
426
|
+
|
|
427
|
+
## How It Works
|
|
428
|
+
|
|
429
|
+
FlashFuzzy uses a two-phase search algorithm:
|
|
430
|
+
|
|
431
|
+
### Phase 1: Bloom Filter Pre-filtering
|
|
432
|
+
|
|
433
|
+
Before running expensive fuzzy matching, each record is checked using a 64-bit Bloom filter. This rejects 80-95% of records in O(1) time.
|
|
434
|
+
|
|
435
|
+
```
|
|
436
|
+
Record: "Wireless Keyboard"
|
|
437
|
+
Bloom: 01001010 11000101... (64 bits)
|
|
438
|
+
|
|
439
|
+
Query: "keyboard"
|
|
440
|
+
Bloom: 00001010 01000001... (64 bits)
|
|
441
|
+
|
|
442
|
+
Check: (record_bloom & query_bloom) == query_bloom
|
|
443
|
+
â Might match â Run Bitap
|
|
444
|
+
â No match â Skip (most records)
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
### Phase 2: Bitap Algorithm
|
|
448
|
+
|
|
449
|
+
For records that pass the Bloom filter, the Bitap (Shift-Or) algorithm performs bit-parallel fuzzy matching with support for typos, insertions, and deletions.
|
|
450
|
+
|
|
451
|
+
This combination delivers exceptional performance while maintaining high accuracy.
|
|
452
|
+
|
|
453
|
+
---
|
|
454
|
+
|
|
455
|
+
## TypeScript Support
|
|
456
|
+
|
|
457
|
+
FlashFuzzy is written in TypeScript with full type definitions:
|
|
458
|
+
|
|
459
|
+
```typescript
|
|
460
|
+
import { FlashFuzzy, type FlashFuzzyOptions, type SearchResult } from '@bdovenbird/flashfuzzy';
|
|
461
|
+
|
|
462
|
+
const options: FlashFuzzyOptions = {
|
|
463
|
+
threshold: 0.25,
|
|
464
|
+
maxResults: 50
|
|
465
|
+
};
|
|
466
|
+
|
|
467
|
+
const ff = await FlashFuzzy.init(options);
|
|
468
|
+
|
|
469
|
+
const results: SearchResult[] = ff.search("query");
|
|
470
|
+
```
|
|
471
|
+
|
|
472
|
+
---
|
|
473
|
+
|
|
474
|
+
## Browser Support
|
|
475
|
+
|
|
476
|
+
FlashFuzzy works in all modern browsers with WebAssembly support:
|
|
477
|
+
|
|
478
|
+
- â
Chrome/Edge 57+
|
|
479
|
+
- â
Firefox 52+
|
|
480
|
+
- â
Safari 11+
|
|
481
|
+
- â
Opera 44+
|
|
482
|
+
|
|
483
|
+
For older browsers, you'll need a WASM polyfill.
|
|
484
|
+
|
|
485
|
+
---
|
|
486
|
+
|
|
487
|
+
## Node.js Support
|
|
488
|
+
|
|
489
|
+
Requires Node.js 16 or higher.
|
|
490
|
+
|
|
491
|
+
```javascript
|
|
492
|
+
// CommonJS
|
|
493
|
+
const { FlashFuzzy } = require('flashfuzzy');
|
|
494
|
+
|
|
495
|
+
// ESM
|
|
496
|
+
import { FlashFuzzy } from 'flashfuzzy';
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
---
|
|
500
|
+
|
|
501
|
+
## Deno & Bun
|
|
502
|
+
|
|
503
|
+
FlashFuzzy works with Deno and Bun out of the box:
|
|
504
|
+
|
|
505
|
+
```typescript
|
|
506
|
+
// Deno
|
|
507
|
+
import { FlashFuzzy } from "npm:flashfuzzy";
|
|
508
|
+
|
|
509
|
+
// Bun
|
|
510
|
+
import { FlashFuzzy } from "flashfuzzy";
|
|
511
|
+
```
|
|
512
|
+
|
|
513
|
+
---
|
|
514
|
+
|
|
515
|
+
## Benchmarks
|
|
516
|
+
|
|
517
|
+
Run the benchmarks yourself:
|
|
518
|
+
|
|
519
|
+
```bash
|
|
520
|
+
git clone https://github.com/RafaCalRob/FlashFuzzy.git
|
|
521
|
+
cd FlashFuzzy
|
|
522
|
+
npm install
|
|
523
|
+
npm test
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
---
|
|
527
|
+
|
|
528
|
+
## Links
|
|
529
|
+
|
|
530
|
+
- ð **Documentation:** https://bdovenbird.com/flash-fuzzy/
|
|
531
|
+
- ðŪ **Live Demo:** https://bdovenbird.com/flash-fuzzy/playground
|
|
532
|
+
- ðŧ **GitHub:** https://github.com/RafaCalRob/FlashFuzzy
|
|
533
|
+
- ðĶ **npm:** https://www.npmjs.com/package/flashfuzzy
|
|
534
|
+
- ð **Issues:** https://github.com/RafaCalRob/FlashFuzzy/issues
|
|
535
|
+
|
|
536
|
+
---
|
|
537
|
+
|
|
538
|
+
## Contributing
|
|
539
|
+
|
|
540
|
+
Contributions are welcome! See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
|
|
541
|
+
|
|
542
|
+
---
|
|
543
|
+
|
|
544
|
+
## License
|
|
545
|
+
|
|
546
|
+
MIT ÂĐ 2025 [Rafael Calderon Robles](https://www.linkedin.com/in/rafael-c-553545205/)
|
|
547
|
+
|
|
548
|
+
---
|
|
549
|
+
|
|
550
|
+
## Credits
|
|
551
|
+
|
|
552
|
+
Built with:
|
|
553
|
+
- **Rust** - Core implementation
|
|
554
|
+
- **WebAssembly** - Fast, portable execution
|
|
555
|
+
- **Bitap Algorithm** - Efficient fuzzy matching
|
|
556
|
+
- **Bloom Filters** - Fast pre-filtering
|
|
557
|
+
|
|
558
|
+
---
|
|
559
|
+
|
|
560
|
+
**Made with âĪïļ for the JavaScript community**
|
|
Binary file
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Flash-Fuzzy
|
|
3
|
+
* High-performance fuzzy search engine using Rust + WebAssembly
|
|
4
|
+
*/
|
|
5
|
+
interface FlashFuzzyOptions {
|
|
6
|
+
threshold?: number;
|
|
7
|
+
maxErrors?: number;
|
|
8
|
+
maxResults?: number;
|
|
9
|
+
wasmUrl?: string;
|
|
10
|
+
}
|
|
11
|
+
interface FieldSchema {
|
|
12
|
+
name: string;
|
|
13
|
+
weight?: number;
|
|
14
|
+
}
|
|
15
|
+
interface SchemaConfig {
|
|
16
|
+
fields: FieldSchema[];
|
|
17
|
+
}
|
|
18
|
+
interface SearchResult {
|
|
19
|
+
id: number;
|
|
20
|
+
score: number;
|
|
21
|
+
matches: Record<string, [number, number][]>;
|
|
22
|
+
}
|
|
23
|
+
interface IndexStats {
|
|
24
|
+
recordCount: number;
|
|
25
|
+
stringPoolUsed: number;
|
|
26
|
+
availableMemory: number;
|
|
27
|
+
usedMemory: number;
|
|
28
|
+
}
|
|
29
|
+
declare class FlashFuzzy {
|
|
30
|
+
private wasm;
|
|
31
|
+
private memoryBuffer;
|
|
32
|
+
private encoder;
|
|
33
|
+
private schema;
|
|
34
|
+
private initialized;
|
|
35
|
+
private constructor();
|
|
36
|
+
static init(options?: FlashFuzzyOptions): Promise<FlashFuzzy>;
|
|
37
|
+
private initialize;
|
|
38
|
+
private loadWasm;
|
|
39
|
+
private refreshMemory;
|
|
40
|
+
setSchema(config: SchemaConfig): void;
|
|
41
|
+
add<T extends Record<string, unknown>>(records: T | T[]): number;
|
|
42
|
+
addBatch<T extends Record<string, unknown>>(records: T[]): number;
|
|
43
|
+
private extractText;
|
|
44
|
+
search(query: string): SearchResult[];
|
|
45
|
+
remove(id: number): boolean;
|
|
46
|
+
compact(): void;
|
|
47
|
+
reset(): void;
|
|
48
|
+
setThreshold(threshold: number): void;
|
|
49
|
+
setMaxErrors(maxErrors: number): void;
|
|
50
|
+
setMaxResults(maxResults: number): void;
|
|
51
|
+
stats(): IndexStats;
|
|
52
|
+
get count(): number;
|
|
53
|
+
serialize(): Uint8Array | null;
|
|
54
|
+
restore(_data: Uint8Array): boolean;
|
|
55
|
+
setScoring(_config: unknown): void;
|
|
56
|
+
getRecordCount(): number;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export { type FieldSchema, FlashFuzzy, type FlashFuzzyOptions, type IndexStats, type SchemaConfig, type SearchResult, FlashFuzzy as default };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Flash-Fuzzy
|
|
3
|
+
* High-performance fuzzy search engine using Rust + WebAssembly
|
|
4
|
+
*/
|
|
5
|
+
interface FlashFuzzyOptions {
|
|
6
|
+
threshold?: number;
|
|
7
|
+
maxErrors?: number;
|
|
8
|
+
maxResults?: number;
|
|
9
|
+
wasmUrl?: string;
|
|
10
|
+
}
|
|
11
|
+
interface FieldSchema {
|
|
12
|
+
name: string;
|
|
13
|
+
weight?: number;
|
|
14
|
+
}
|
|
15
|
+
interface SchemaConfig {
|
|
16
|
+
fields: FieldSchema[];
|
|
17
|
+
}
|
|
18
|
+
interface SearchResult {
|
|
19
|
+
id: number;
|
|
20
|
+
score: number;
|
|
21
|
+
matches: Record<string, [number, number][]>;
|
|
22
|
+
}
|
|
23
|
+
interface IndexStats {
|
|
24
|
+
recordCount: number;
|
|
25
|
+
stringPoolUsed: number;
|
|
26
|
+
availableMemory: number;
|
|
27
|
+
usedMemory: number;
|
|
28
|
+
}
|
|
29
|
+
declare class FlashFuzzy {
|
|
30
|
+
private wasm;
|
|
31
|
+
private memoryBuffer;
|
|
32
|
+
private encoder;
|
|
33
|
+
private schema;
|
|
34
|
+
private initialized;
|
|
35
|
+
private constructor();
|
|
36
|
+
static init(options?: FlashFuzzyOptions): Promise<FlashFuzzy>;
|
|
37
|
+
private initialize;
|
|
38
|
+
private loadWasm;
|
|
39
|
+
private refreshMemory;
|
|
40
|
+
setSchema(config: SchemaConfig): void;
|
|
41
|
+
add<T extends Record<string, unknown>>(records: T | T[]): number;
|
|
42
|
+
addBatch<T extends Record<string, unknown>>(records: T[]): number;
|
|
43
|
+
private extractText;
|
|
44
|
+
search(query: string): SearchResult[];
|
|
45
|
+
remove(id: number): boolean;
|
|
46
|
+
compact(): void;
|
|
47
|
+
reset(): void;
|
|
48
|
+
setThreshold(threshold: number): void;
|
|
49
|
+
setMaxErrors(maxErrors: number): void;
|
|
50
|
+
setMaxResults(maxResults: number): void;
|
|
51
|
+
stats(): IndexStats;
|
|
52
|
+
get count(): number;
|
|
53
|
+
serialize(): Uint8Array | null;
|
|
54
|
+
restore(_data: Uint8Array): boolean;
|
|
55
|
+
setScoring(_config: unknown): void;
|
|
56
|
+
getRecordCount(): number;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export { type FieldSchema, FlashFuzzy, type FlashFuzzyOptions, type IndexStats, type SchemaConfig, type SearchResult, FlashFuzzy as default };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// js/src/index.ts
|
|
31
|
+
var index_exports = {};
|
|
32
|
+
__export(index_exports, {
|
|
33
|
+
FlashFuzzy: () => FlashFuzzy,
|
|
34
|
+
default: () => index_default
|
|
35
|
+
});
|
|
36
|
+
module.exports = __toCommonJS(index_exports);
|
|
37
|
+
var import_meta = {};
|
|
38
|
+
var FlashFuzzy = class _FlashFuzzy {
|
|
39
|
+
constructor() {
|
|
40
|
+
this.wasm = null;
|
|
41
|
+
this.memoryBuffer = null;
|
|
42
|
+
this.encoder = new TextEncoder();
|
|
43
|
+
this.schema = null;
|
|
44
|
+
this.initialized = false;
|
|
45
|
+
}
|
|
46
|
+
static async init(options = {}) {
|
|
47
|
+
const instance = new _FlashFuzzy();
|
|
48
|
+
await instance.initialize(options);
|
|
49
|
+
return instance;
|
|
50
|
+
}
|
|
51
|
+
async initialize(options) {
|
|
52
|
+
const {
|
|
53
|
+
threshold = 0.25,
|
|
54
|
+
maxErrors = 2,
|
|
55
|
+
maxResults = 50,
|
|
56
|
+
wasmUrl
|
|
57
|
+
} = options;
|
|
58
|
+
const wasmModule = await this.loadWasm(wasmUrl);
|
|
59
|
+
const instance = await WebAssembly.instantiate(wasmModule, {});
|
|
60
|
+
this.wasm = instance.exports;
|
|
61
|
+
this.memoryBuffer = new Uint8Array(this.wasm.memory.buffer);
|
|
62
|
+
this.wasm.init();
|
|
63
|
+
this.wasm.setThreshold(Math.floor(threshold * 1e3));
|
|
64
|
+
this.wasm.setMaxErrors(maxErrors);
|
|
65
|
+
this.wasm.setMaxResults(maxResults);
|
|
66
|
+
this.initialized = true;
|
|
67
|
+
}
|
|
68
|
+
async loadWasm(wasmUrl) {
|
|
69
|
+
const isNode = typeof process !== "undefined" && process.versions != null && process.versions.node != null;
|
|
70
|
+
if (isNode && wasmUrl) {
|
|
71
|
+
try {
|
|
72
|
+
const fs = await import("fs");
|
|
73
|
+
const path = await import("path");
|
|
74
|
+
const resolvedPath = path.resolve(wasmUrl);
|
|
75
|
+
const buffer = fs.readFileSync(resolvedPath);
|
|
76
|
+
return await WebAssembly.compile(buffer);
|
|
77
|
+
} catch (e) {
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
const urls = wasmUrl ? [wasmUrl] : [
|
|
81
|
+
"./flash-fuzzy.wasm",
|
|
82
|
+
"/flash-fuzzy.wasm",
|
|
83
|
+
typeof import_meta !== "undefined" && import_meta.url ? new URL("./flash-fuzzy.wasm", import_meta.url).href : "./flash-fuzzy.wasm"
|
|
84
|
+
];
|
|
85
|
+
let lastError = null;
|
|
86
|
+
for (const url of urls) {
|
|
87
|
+
try {
|
|
88
|
+
const response = await fetch(url);
|
|
89
|
+
if (!response.ok) continue;
|
|
90
|
+
const buffer = await response.arrayBuffer();
|
|
91
|
+
return await WebAssembly.compile(buffer);
|
|
92
|
+
} catch (e) {
|
|
93
|
+
lastError = e;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
throw new Error(`Failed to load WASM: ${lastError?.message}`);
|
|
97
|
+
}
|
|
98
|
+
refreshMemory() {
|
|
99
|
+
if (this.wasm) {
|
|
100
|
+
this.memoryBuffer = new Uint8Array(this.wasm.memory.buffer);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
setSchema(config) {
|
|
104
|
+
this.schema = config;
|
|
105
|
+
}
|
|
106
|
+
add(records) {
|
|
107
|
+
if (!this.wasm || !this.initialized) {
|
|
108
|
+
throw new Error("FlashFuzzy not initialized");
|
|
109
|
+
}
|
|
110
|
+
const items = Array.isArray(records) ? records : [records];
|
|
111
|
+
let added = 0;
|
|
112
|
+
for (const record of items) {
|
|
113
|
+
const id = record.id ?? added;
|
|
114
|
+
const text = this.extractText(record);
|
|
115
|
+
this.refreshMemory();
|
|
116
|
+
const encoded = this.encoder.encode(text);
|
|
117
|
+
const ptr = this.wasm.getWriteBuffer(encoded.length);
|
|
118
|
+
if (ptr === 0) continue;
|
|
119
|
+
this.memoryBuffer.set(encoded, ptr);
|
|
120
|
+
this.wasm.commitWrite(encoded.length);
|
|
121
|
+
if (this.wasm.addRecord(id) === 1) {
|
|
122
|
+
added++;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return added;
|
|
126
|
+
}
|
|
127
|
+
addBatch(records) {
|
|
128
|
+
return this.add(records);
|
|
129
|
+
}
|
|
130
|
+
extractText(record) {
|
|
131
|
+
if (!this.schema) {
|
|
132
|
+
return Object.values(record).filter((v) => typeof v === "string").join(" ");
|
|
133
|
+
}
|
|
134
|
+
const parts = [];
|
|
135
|
+
for (const field of this.schema.fields) {
|
|
136
|
+
const value = record[field.name];
|
|
137
|
+
if (typeof value === "string") {
|
|
138
|
+
parts.push(value);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return parts.join(" ");
|
|
142
|
+
}
|
|
143
|
+
search(query) {
|
|
144
|
+
if (!this.wasm || !this.initialized) {
|
|
145
|
+
throw new Error("FlashFuzzy not initialized");
|
|
146
|
+
}
|
|
147
|
+
if (query.length === 0) return [];
|
|
148
|
+
this.refreshMemory();
|
|
149
|
+
const encoded = this.encoder.encode(query);
|
|
150
|
+
const ptr = this.wasm.getWriteBuffer(encoded.length);
|
|
151
|
+
if (ptr === 0) return [];
|
|
152
|
+
this.memoryBuffer.set(encoded, ptr);
|
|
153
|
+
this.wasm.commitWrite(encoded.length);
|
|
154
|
+
this.wasm.preparePattern();
|
|
155
|
+
const count = this.wasm.search();
|
|
156
|
+
if (count === 0) return [];
|
|
157
|
+
const results = new Array(count);
|
|
158
|
+
for (let i = 0; i < count; i++) {
|
|
159
|
+
results[i] = {
|
|
160
|
+
id: this.wasm.getResultId(i),
|
|
161
|
+
score: this.wasm.getResultScore(i) / 1e3,
|
|
162
|
+
matches: {
|
|
163
|
+
_default: [[this.wasm.getResultStart(i), this.wasm.getResultEnd(i)]]
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
return results;
|
|
168
|
+
}
|
|
169
|
+
remove(id) {
|
|
170
|
+
return this.wasm?.removeRecord(id) === 1;
|
|
171
|
+
}
|
|
172
|
+
compact() {
|
|
173
|
+
this.wasm?.compact();
|
|
174
|
+
}
|
|
175
|
+
reset() {
|
|
176
|
+
this.wasm?.reset();
|
|
177
|
+
}
|
|
178
|
+
setThreshold(threshold) {
|
|
179
|
+
this.wasm?.setThreshold(Math.floor(Math.max(0, Math.min(1, threshold)) * 1e3));
|
|
180
|
+
}
|
|
181
|
+
setMaxErrors(maxErrors) {
|
|
182
|
+
this.wasm?.setMaxErrors(Math.max(0, Math.min(3, maxErrors)));
|
|
183
|
+
}
|
|
184
|
+
setMaxResults(maxResults) {
|
|
185
|
+
this.wasm?.setMaxResults(Math.max(1, Math.min(100, maxResults)));
|
|
186
|
+
}
|
|
187
|
+
stats() {
|
|
188
|
+
if (!this.wasm) {
|
|
189
|
+
return { recordCount: 0, stringPoolUsed: 0, availableMemory: 0, usedMemory: 0 };
|
|
190
|
+
}
|
|
191
|
+
const stringPoolUsed = this.wasm.getStringPoolUsed();
|
|
192
|
+
return {
|
|
193
|
+
recordCount: this.wasm.getRecordCount(),
|
|
194
|
+
stringPoolUsed,
|
|
195
|
+
availableMemory: this.wasm.getAvailableMemory(),
|
|
196
|
+
usedMemory: stringPoolUsed
|
|
197
|
+
// Alias
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
get count() {
|
|
201
|
+
return this.wasm?.getRecordCount() ?? 0;
|
|
202
|
+
}
|
|
203
|
+
// Stubs for API compatibility
|
|
204
|
+
serialize() {
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
restore(_data) {
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
210
|
+
setScoring(_config) {
|
|
211
|
+
}
|
|
212
|
+
getRecordCount() {
|
|
213
|
+
return this.count;
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
var index_default = FlashFuzzy;
|
|
217
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
218
|
+
0 && (module.exports = {
|
|
219
|
+
FlashFuzzy
|
|
220
|
+
});
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
// js/src/index.ts
|
|
2
|
+
var FlashFuzzy = class _FlashFuzzy {
|
|
3
|
+
constructor() {
|
|
4
|
+
this.wasm = null;
|
|
5
|
+
this.memoryBuffer = null;
|
|
6
|
+
this.encoder = new TextEncoder();
|
|
7
|
+
this.schema = null;
|
|
8
|
+
this.initialized = false;
|
|
9
|
+
}
|
|
10
|
+
static async init(options = {}) {
|
|
11
|
+
const instance = new _FlashFuzzy();
|
|
12
|
+
await instance.initialize(options);
|
|
13
|
+
return instance;
|
|
14
|
+
}
|
|
15
|
+
async initialize(options) {
|
|
16
|
+
const {
|
|
17
|
+
threshold = 0.25,
|
|
18
|
+
maxErrors = 2,
|
|
19
|
+
maxResults = 50,
|
|
20
|
+
wasmUrl
|
|
21
|
+
} = options;
|
|
22
|
+
const wasmModule = await this.loadWasm(wasmUrl);
|
|
23
|
+
const instance = await WebAssembly.instantiate(wasmModule, {});
|
|
24
|
+
this.wasm = instance.exports;
|
|
25
|
+
this.memoryBuffer = new Uint8Array(this.wasm.memory.buffer);
|
|
26
|
+
this.wasm.init();
|
|
27
|
+
this.wasm.setThreshold(Math.floor(threshold * 1e3));
|
|
28
|
+
this.wasm.setMaxErrors(maxErrors);
|
|
29
|
+
this.wasm.setMaxResults(maxResults);
|
|
30
|
+
this.initialized = true;
|
|
31
|
+
}
|
|
32
|
+
async loadWasm(wasmUrl) {
|
|
33
|
+
const isNode = typeof process !== "undefined" && process.versions != null && process.versions.node != null;
|
|
34
|
+
if (isNode && wasmUrl) {
|
|
35
|
+
try {
|
|
36
|
+
const fs = await import("fs");
|
|
37
|
+
const path = await import("path");
|
|
38
|
+
const resolvedPath = path.resolve(wasmUrl);
|
|
39
|
+
const buffer = fs.readFileSync(resolvedPath);
|
|
40
|
+
return await WebAssembly.compile(buffer);
|
|
41
|
+
} catch (e) {
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
const urls = wasmUrl ? [wasmUrl] : [
|
|
45
|
+
"./flash-fuzzy.wasm",
|
|
46
|
+
"/flash-fuzzy.wasm",
|
|
47
|
+
typeof import.meta !== "undefined" && import.meta.url ? new URL("./flash-fuzzy.wasm", import.meta.url).href : "./flash-fuzzy.wasm"
|
|
48
|
+
];
|
|
49
|
+
let lastError = null;
|
|
50
|
+
for (const url of urls) {
|
|
51
|
+
try {
|
|
52
|
+
const response = await fetch(url);
|
|
53
|
+
if (!response.ok) continue;
|
|
54
|
+
const buffer = await response.arrayBuffer();
|
|
55
|
+
return await WebAssembly.compile(buffer);
|
|
56
|
+
} catch (e) {
|
|
57
|
+
lastError = e;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
throw new Error(`Failed to load WASM: ${lastError?.message}`);
|
|
61
|
+
}
|
|
62
|
+
refreshMemory() {
|
|
63
|
+
if (this.wasm) {
|
|
64
|
+
this.memoryBuffer = new Uint8Array(this.wasm.memory.buffer);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
setSchema(config) {
|
|
68
|
+
this.schema = config;
|
|
69
|
+
}
|
|
70
|
+
add(records) {
|
|
71
|
+
if (!this.wasm || !this.initialized) {
|
|
72
|
+
throw new Error("FlashFuzzy not initialized");
|
|
73
|
+
}
|
|
74
|
+
const items = Array.isArray(records) ? records : [records];
|
|
75
|
+
let added = 0;
|
|
76
|
+
for (const record of items) {
|
|
77
|
+
const id = record.id ?? added;
|
|
78
|
+
const text = this.extractText(record);
|
|
79
|
+
this.refreshMemory();
|
|
80
|
+
const encoded = this.encoder.encode(text);
|
|
81
|
+
const ptr = this.wasm.getWriteBuffer(encoded.length);
|
|
82
|
+
if (ptr === 0) continue;
|
|
83
|
+
this.memoryBuffer.set(encoded, ptr);
|
|
84
|
+
this.wasm.commitWrite(encoded.length);
|
|
85
|
+
if (this.wasm.addRecord(id) === 1) {
|
|
86
|
+
added++;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return added;
|
|
90
|
+
}
|
|
91
|
+
addBatch(records) {
|
|
92
|
+
return this.add(records);
|
|
93
|
+
}
|
|
94
|
+
extractText(record) {
|
|
95
|
+
if (!this.schema) {
|
|
96
|
+
return Object.values(record).filter((v) => typeof v === "string").join(" ");
|
|
97
|
+
}
|
|
98
|
+
const parts = [];
|
|
99
|
+
for (const field of this.schema.fields) {
|
|
100
|
+
const value = record[field.name];
|
|
101
|
+
if (typeof value === "string") {
|
|
102
|
+
parts.push(value);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return parts.join(" ");
|
|
106
|
+
}
|
|
107
|
+
search(query) {
|
|
108
|
+
if (!this.wasm || !this.initialized) {
|
|
109
|
+
throw new Error("FlashFuzzy not initialized");
|
|
110
|
+
}
|
|
111
|
+
if (query.length === 0) return [];
|
|
112
|
+
this.refreshMemory();
|
|
113
|
+
const encoded = this.encoder.encode(query);
|
|
114
|
+
const ptr = this.wasm.getWriteBuffer(encoded.length);
|
|
115
|
+
if (ptr === 0) return [];
|
|
116
|
+
this.memoryBuffer.set(encoded, ptr);
|
|
117
|
+
this.wasm.commitWrite(encoded.length);
|
|
118
|
+
this.wasm.preparePattern();
|
|
119
|
+
const count = this.wasm.search();
|
|
120
|
+
if (count === 0) return [];
|
|
121
|
+
const results = new Array(count);
|
|
122
|
+
for (let i = 0; i < count; i++) {
|
|
123
|
+
results[i] = {
|
|
124
|
+
id: this.wasm.getResultId(i),
|
|
125
|
+
score: this.wasm.getResultScore(i) / 1e3,
|
|
126
|
+
matches: {
|
|
127
|
+
_default: [[this.wasm.getResultStart(i), this.wasm.getResultEnd(i)]]
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
return results;
|
|
132
|
+
}
|
|
133
|
+
remove(id) {
|
|
134
|
+
return this.wasm?.removeRecord(id) === 1;
|
|
135
|
+
}
|
|
136
|
+
compact() {
|
|
137
|
+
this.wasm?.compact();
|
|
138
|
+
}
|
|
139
|
+
reset() {
|
|
140
|
+
this.wasm?.reset();
|
|
141
|
+
}
|
|
142
|
+
setThreshold(threshold) {
|
|
143
|
+
this.wasm?.setThreshold(Math.floor(Math.max(0, Math.min(1, threshold)) * 1e3));
|
|
144
|
+
}
|
|
145
|
+
setMaxErrors(maxErrors) {
|
|
146
|
+
this.wasm?.setMaxErrors(Math.max(0, Math.min(3, maxErrors)));
|
|
147
|
+
}
|
|
148
|
+
setMaxResults(maxResults) {
|
|
149
|
+
this.wasm?.setMaxResults(Math.max(1, Math.min(100, maxResults)));
|
|
150
|
+
}
|
|
151
|
+
stats() {
|
|
152
|
+
if (!this.wasm) {
|
|
153
|
+
return { recordCount: 0, stringPoolUsed: 0, availableMemory: 0, usedMemory: 0 };
|
|
154
|
+
}
|
|
155
|
+
const stringPoolUsed = this.wasm.getStringPoolUsed();
|
|
156
|
+
return {
|
|
157
|
+
recordCount: this.wasm.getRecordCount(),
|
|
158
|
+
stringPoolUsed,
|
|
159
|
+
availableMemory: this.wasm.getAvailableMemory(),
|
|
160
|
+
usedMemory: stringPoolUsed
|
|
161
|
+
// Alias
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
get count() {
|
|
165
|
+
return this.wasm?.getRecordCount() ?? 0;
|
|
166
|
+
}
|
|
167
|
+
// Stubs for API compatibility
|
|
168
|
+
serialize() {
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
restore(_data) {
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
setScoring(_config) {
|
|
175
|
+
}
|
|
176
|
+
getRecordCount() {
|
|
177
|
+
return this.count;
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
var index_default = FlashFuzzy;
|
|
181
|
+
export {
|
|
182
|
+
FlashFuzzy,
|
|
183
|
+
index_default as default
|
|
184
|
+
};
|
package/logo.png
ADDED
|
Binary file
|
package/package.json
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "flashfuzzy",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "High-performance fuzzy search engine powered by Rust and WebAssembly",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"module": "dist/index.mjs",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.mjs",
|
|
12
|
+
"require": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist",
|
|
17
|
+
"README.md",
|
|
18
|
+
"LICENSE",
|
|
19
|
+
"CHANGELOG.md",
|
|
20
|
+
"logo.png"
|
|
21
|
+
],
|
|
22
|
+
"publishConfig": {
|
|
23
|
+
"access": "public",
|
|
24
|
+
"registry": "https://registry.npmjs.org/"
|
|
25
|
+
},
|
|
26
|
+
"sideEffects": false,
|
|
27
|
+
"scripts": {
|
|
28
|
+
"build:wasm": "cd rust && cargo build --release --target wasm32-unknown-unknown -p flash-fuzzy-wasm && (copy target\\wasm32-unknown-unknown\\release\\flash_fuzzy_wasm.wasm ..\\dist\\flash-fuzzy.wasm || cp target/wasm32-unknown-unknown/release/flash_fuzzy_wasm.wasm ../dist/flash-fuzzy.wasm)",
|
|
29
|
+
"build:js": "tsup js/src/index.ts --format cjs,esm --dts --out-dir dist",
|
|
30
|
+
"build": "npm run build:wasm && npm run build:js",
|
|
31
|
+
"test": "vitest run",
|
|
32
|
+
"test:watch": "vitest",
|
|
33
|
+
"prepublishOnly": "npm run prepare:npm && npm run build",
|
|
34
|
+
"postpublish": "npm run restore:readme",
|
|
35
|
+
"prepare:npm": "(copy README_NPM.md README.md || cp README_NPM.md README.md)",
|
|
36
|
+
"restore:readme": "git checkout README.md"
|
|
37
|
+
},
|
|
38
|
+
"keywords": [
|
|
39
|
+
"fuzzy",
|
|
40
|
+
"search",
|
|
41
|
+
"fuzzy-search",
|
|
42
|
+
"wasm",
|
|
43
|
+
"webassembly",
|
|
44
|
+
"rust",
|
|
45
|
+
"fast",
|
|
46
|
+
"performance",
|
|
47
|
+
"bitap",
|
|
48
|
+
"levenshtein"
|
|
49
|
+
],
|
|
50
|
+
"author": "RafaCalRob",
|
|
51
|
+
"license": "MIT",
|
|
52
|
+
"repository": {
|
|
53
|
+
"type": "git",
|
|
54
|
+
"url": "https://github.com/RafaCalRob/FlashFuzzy.git"
|
|
55
|
+
},
|
|
56
|
+
"homepage": "https://github.com/RafaCalRob/FlashFuzzy#readme",
|
|
57
|
+
"bugs": {
|
|
58
|
+
"url": "https://github.com/RafaCalRob/FlashFuzzy/issues"
|
|
59
|
+
},
|
|
60
|
+
"devDependencies": {
|
|
61
|
+
"@types/node": "^25.0.3",
|
|
62
|
+
"tsup": "^8.0.1",
|
|
63
|
+
"typescript": "^5.3.3",
|
|
64
|
+
"vitest": "^1.1.0"
|
|
65
|
+
},
|
|
66
|
+
"engines": {
|
|
67
|
+
"node": ">=16.0.0"
|
|
68
|
+
}
|
|
69
|
+
}
|