rapid-fuzzy 0.5.0 → 0.6.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.
Files changed (2) hide show
  1. package/README.md +113 -70
  2. package/package.json +11 -10
package/README.md CHANGED
@@ -8,18 +8,36 @@
8
8
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
9
9
  [![Node.js](https://img.shields.io/badge/node-%3E%3D20.0.0-brightgreen)](https://nodejs.org/)
10
10
 
11
- Rust-powered fuzzy search and string distance for JavaScript/TypeScript.
11
+ Blazing-fast fuzzy search for JavaScript powered by Rust, works everywhere.
12
12
 
13
- > **Status**: Early release (v0.x). API may change between minor versions.
13
+ <img src=".github/assets/demo.svg" alt="rapid-fuzzy demo fuzzy search, query syntax, FuzzyIndex, and string distance" width="580" />
14
14
 
15
15
  ## Features
16
16
 
17
- - **Fast**: Up to 40x faster than fuse.js for large datasets (Rust + napi-rs)
17
+ - **Fast**: Up to 7,000x faster than fuse.js with FuzzyIndex (Rust + napi-rs)
18
18
  - **Universal**: Works in Node.js (native), browsers (WASM), Deno, and Bun
19
19
  - **Zero JS dependencies**: Pure Rust core with napi-rs bindings
20
20
  - **Type-safe**: Full TypeScript support with auto-generated type definitions
21
21
  - **Drop-in**: API compatible with popular fuzzy search libraries
22
22
 
23
+ ## Quick Start
24
+
25
+ ```typescript
26
+ import { search } from 'rapid-fuzzy';
27
+
28
+ const results = search('typscript', ['TypeScript', 'JavaScript', 'Python']);
29
+ // → [{ item: 'TypeScript', score: 0.85, index: 0 }, ...]
30
+ ```
31
+
32
+ For repeated searches, use `FuzzyIndex` for up to 182x faster lookups:
33
+
34
+ ```typescript
35
+ import { FuzzyIndex } from 'rapid-fuzzy';
36
+
37
+ const index = new FuzzyIndex(['TypeScript', 'JavaScript', 'Python', ...]);
38
+ index.search('typscript'); // sub-millisecond with incremental cache
39
+ ```
40
+
23
41
  ## Installation
24
42
 
25
43
  ```bash
@@ -33,17 +51,7 @@ pnpm add rapid-fuzzy
33
51
  - **Node.js** (>=20): Uses native bindings via napi-rs for best performance.
34
52
  - **Browser / Deno / Bun**: Falls back to a WASM build automatically.
35
53
 
36
- ## Usage
37
-
38
- ### String Distance
39
-
40
- ```typescript
41
- import { levenshtein, jaroWinkler, sorensenDice } from 'rapid-fuzzy';
42
-
43
- levenshtein('kitten', 'sitting'); // 3
44
- jaroWinkler('MARTHA', 'MARHTA'); // 0.961
45
- sorensenDice('night', 'nacht'); // 0.25
46
- ```
54
+ > **Note**: rapid-fuzzy is pre-1.0 — the API is stable but minor versions may include additions.
47
55
 
48
56
  ### Fuzzy Search
49
57
 
@@ -81,6 +89,32 @@ closest('xyz', items, 0.5);
81
89
  // → null
82
90
  ```
83
91
 
92
+ ### String Distance
93
+
94
+ ```typescript
95
+ import { levenshtein, jaroWinkler, sorensenDice } from 'rapid-fuzzy';
96
+
97
+ levenshtein('kitten', 'sitting'); // 3
98
+ jaroWinkler('MARTHA', 'MARHTA'); // 0.961
99
+ sorensenDice('night', 'nacht'); // 0.25
100
+ ```
101
+
102
+ ### Query Syntax
103
+
104
+ Queries support extended syntax powered by the [nucleo](https://github.com/helix-editor/nucleo) pattern parser:
105
+
106
+ | Pattern | Match type | Example |
107
+ |---|---|---|
108
+ | `foo bar` | AND (order-independent) | `john smith` matches "Smith, John" |
109
+ | `!term` | Exclude | `apple !pie` excludes "apple pie" |
110
+ | `^term` | Starts with | `^app` matches "apple" but not "pineapple" |
111
+ | `term$` | Ends with | `pie$` matches "apple pie" |
112
+ | `'term` | Exact substring | `'pie` matches "pie" literally |
113
+
114
+ Diacritics are handled automatically — `cafe` matches `café`, `uber` matches `über`, and `naive` matches `naïve` with no configuration needed.
115
+
116
+ > **Note**: These patterns apply to all search functions: `search()`, `closest()`, `FuzzyIndex.search()`, `FuzzyObjectIndex.search()`, and `searchObjects()`. They do **not** apply to distance functions (`levenshtein`, `jaro`, etc.).
117
+
84
118
  ### Object Search
85
119
 
86
120
  Search across object properties with weighted keys — a drop-in replacement for fuse.js's `keys` option:
@@ -119,7 +153,7 @@ For applications that search the same dataset repeatedly (autocomplete, file fin
119
153
  ```typescript
120
154
  import { FuzzyIndex, FuzzyObjectIndex } from 'rapid-fuzzy';
121
155
 
122
- // String search index — up to 5x faster than standalone search()
156
+ // String search index — up to 182x faster than standalone search()
123
157
  const index = new FuzzyIndex(['TypeScript', 'JavaScript', 'Python', ...]);
124
158
 
125
159
  index.search('typscript', { maxResults: 5 });
@@ -215,6 +249,27 @@ levenshteinMany('kitten', ['sitting', 'kittens', 'kitchen']);
215
249
 
216
250
  > **Tip**: Prefer batch/many variants over calling single-pair functions in a loop — they are significantly faster for multiple comparisons.
217
251
 
252
+ ## Choosing an Algorithm
253
+
254
+ | Use case | Recommended | Why |
255
+ |---|---|---|
256
+ | Typo detection / spell check | `levenshtein`, `damerauLevenshtein` | Counts edits; Damerau adds transposition support |
257
+ | Name / address matching | `jaroWinkler`, `tokenSortRatio` | Prefix-weighted or order-independent matching |
258
+ | Document / text similarity | `sorensenDice` | Bigram-based; handles longer text well |
259
+ | Normalized comparison (0–1) | `normalizedLevenshtein` | Length-independent similarity score |
260
+ | Reordered words / messy data | `tokenSortRatio`, `tokenSetRatio` | Handles word order differences and extra tokens |
261
+ | Substring / abbreviation matching | `partialRatio` | Finds best partial match within longer strings |
262
+ | Best-effort similarity | `weightedRatio` | Picks the best score across all methods automatically |
263
+ | Interactive fuzzy search | `search`, `closest` | Nucleo algorithm (same as Helix editor) |
264
+ | Repeated search on same data | `FuzzyIndex`, `FuzzyObjectIndex` | Persistent Rust-side index with incremental cache, up to 182x faster |
265
+
266
+ **Return types:**
267
+
268
+ - `levenshtein`, `damerauLevenshtein` → integer (edit count)
269
+ - `jaro`, `jaroWinkler`, `sorensenDice`, `normalizedLevenshtein` → float between 0.0 (no match) and 1.0 (identical)
270
+ - `tokenSortRatio`, `tokenSetRatio`, `partialRatio`, `weightedRatio` → float between 0.0 and 1.0
271
+ - `search` → array of `{ item, score, index, positions }` sorted by relevance (score: 0.0–1.0)
272
+
218
273
  ## Benchmarks
219
274
 
220
275
  Measured on Apple M-series with Node.js v22 using [Vitest bench](https://vitest.dev/guide/features.html#benchmarking). Each benchmark processes 6 realistic string pairs of varying length and similarity.
@@ -228,46 +283,50 @@ Measured on Apple M-series with Node.js v22 using [Vitest bench](https://vitest.
228
283
 
229
284
  | Function | rapid-fuzzy | fastest-levenshtein | leven | string-similarity |
230
285
  |---|---:|---:|---:|---:|
231
- | Levenshtein | 528,195 ops/s | **739,107 ops/s** | 221,817 ops/s | — |
232
- | Normalized Levenshtein | **534,231 ops/s** | — | — | — |
233
- | Sorensen-Dice | **149,567 ops/s** | — | — | 82,908 ops/s |
234
- | Jaro-Winkler | **278,554 ops/s** | — | — | — |
235
- | Damerau-Levenshtein | **112,370 ops/s** | — | — | — |
286
+ | Levenshtein | 562,063 ops/s | **794,298 ops/s** | 228,688 ops/s | — |
287
+ | Normalized Levenshtein | **546,107 ops/s** | — | — | — |
288
+ | Sorensen-Dice | **147,850 ops/s** | — | — | 84,308 ops/s |
289
+ | Jaro-Winkler | **293,403 ops/s** | — | — | — |
290
+ | Damerau-Levenshtein | **116,153 ops/s** | — | — | — |
236
291
 
237
292
  </details>
238
293
 
239
- > **Note**: For single-pair Levenshtein, fastest-levenshtein is ~1.4x faster due to its optimized pure-JS implementation that avoids FFI overhead. rapid-fuzzy is **2.4x faster** than leven, and provides broader algorithm coverage plus batch / search scenarios.
294
+ > **Note**: For single-pair Levenshtein, fastest-levenshtein is ~1.4x faster due to its optimized pure-JS implementation that avoids FFI overhead. rapid-fuzzy is **2.5x faster** than leven, and provides broader algorithm coverage plus batch / search scenarios.
240
295
 
241
296
  ### Search Performance
242
297
 
243
- <img src=".github/assets/bench-search.svg" alt="Search performance chart — rapid-fuzzy vs fuse.js vs fuzzysort" width="680" />
298
+ <img src=".github/assets/bench-search.svg" alt="Search performance chart — rapid-fuzzy vs fuse.js vs fuzzysort vs uFuzzy" width="680" />
299
+
300
+ > Both `rapid-fuzzy` columns below show the same library: standalone `search()` vs `FuzzyIndex` (indexed mode for repeated searches).
244
301
 
245
302
  <details>
246
303
  <summary>Raw numbers</summary>
247
304
 
248
- | Dataset size | rapid-fuzzy | FuzzyIndex | fuse.js | fuzzysort |
249
- |---|---:|---:|---:|---:|
250
- | Small (20 items) | 174,812 ops/s | 406,269 ops/s | 127,167 ops/s | **2,502,299 ops/s** |
251
- | Medium (1K items) | 6,531 ops/s | 22,014 ops/s | 395 ops/s | **62,845 ops/s** |
252
- | Large (10K items) | 794 ops/s | 3,985 ops/s | 20 ops/s | **28,846 ops/s** |
305
+ | Dataset size | rapid-fuzzy | rapid-fuzzy (indexed) | fuse.js | fuzzysort | uFuzzy |
306
+ |---|---:|---:|---:|---:|---:|
307
+ | Small (20 items) | 303,982 ops/s | 405,604 ops/s | 105,568 ops/s | **2,606,394 ops/s** | 923,069 ops/s |
308
+ | Medium (1K items) | 6,787 ops/s | **80,579 ops/s** | 367 ops/s | 64,372 ops/s | 28,953 ops/s |
309
+ | Large (10K items) | 751 ops/s | **136,528 ops/s** | 19 ops/s | 26,112 ops/s | 6,393 ops/s |
310
+ | XL (50K items) | — | **31,903 ops/s** | — | 5,916 ops/s | 1,292 ops/s |
253
311
 
254
312
  </details>
255
313
 
256
314
  ### Closest Match (Levenshtein-based)
257
315
 
258
- | Dataset size | rapid-fuzzy | FuzzyIndex | fastest-levenshtein |
316
+ | Dataset size | rapid-fuzzy | rapid-fuzzy (indexed) | fastest-levenshtein |
259
317
  |---|---:|---:|---:|
260
- | Medium (1K items) | 8,304 ops/s | **60,469 ops/s** | 8,946 ops/s |
261
- | Large (10K items) | 757 ops/s | **4,103 ops/s** | 604 ops/s |
318
+ | Medium (1K items) | 8,611 ops/s | **989,095 ops/s** | 6,797 ops/s |
319
+ | Large (10K items) | 924 ops/s | **156,014 ops/s** | 658 ops/s |
262
320
 
263
- > With `FuzzyIndex`, rapid-fuzzy is up to **6.8x faster** than fastest-levenshtein for closest-match lookups.
321
+ > In indexed mode (`FuzzyIndex`), rapid-fuzzy is up to **237x faster** than fastest-levenshtein for closest-match lookups.
264
322
 
265
323
  ### Why these numbers matter
266
324
 
267
- - **vs fuse.js**: rapid-fuzzy is **17x faster** on medium datasets and **40x faster** on large datasets for fuzzy search.
268
- - **FuzzyIndex**: Pre-computing string data on the Rust side gives an additional **3–5x speedup** over standalone `search()`, making it the recommended approach for repeated searches.
269
- - **vs fastest-levenshtein**: With `FuzzyIndex`, closest-match is **6.8x faster** at scale. Even standalone `closest()` wins on large datasets.
270
- - **fuzzysort** uses a different (substring-based) matching algorithm that is extremely fast but produces different ranking results. Choose based on your matching needs.
325
+ - **vs fuse.js**: `FuzzyIndex` is **219x faster** on medium datasets and **6,869x faster** on large datasets. Even standalone `search()` is 18x / 40x faster.
326
+ - **Indexed mode**: `FuzzyIndex` keeps data on the Rust side with an incremental search cache, delivering sub-millisecond autocomplete. On large datasets this is **182x faster** than standalone `search()`.
327
+ - **vs fuzzysort**: `FuzzyIndex` now **outperforms fuzzysort** on medium-and-above datasets — 1.25x faster at 1K, 5.2x at 10K, and 5.4x at 50K.
328
+ - **vs uFuzzy**: `FuzzyIndex` is **2.8x faster** at medium and **21x faster** at large datasets.
329
+ - **vs fastest-levenshtein**: With `FuzzyIndex`, closest-match is **145x faster** at 1K and **237x faster** at 10K.
271
330
 
272
331
  Run benchmarks yourself:
273
332
 
@@ -276,50 +335,34 @@ pnpm run bench # JavaScript benchmarks
276
335
  cargo bench # Rust internal benchmarks
277
336
  ```
278
337
 
279
- ## Choosing an Algorithm
280
-
281
- | Use case | Recommended | Why |
282
- |---|---|---|
283
- | Typo detection / spell check | `levenshtein`, `damerauLevenshtein` | Counts edits; Damerau adds transposition support |
284
- | Name / address matching | `jaroWinkler`, `tokenSortRatio` | Prefix-weighted or order-independent matching |
285
- | Document / text similarity | `sorensenDice` | Bigram-based; handles longer text well |
286
- | Normalized comparison (0–1) | `normalizedLevenshtein` | Length-independent similarity score |
287
- | Reordered words / messy data | `tokenSortRatio`, `tokenSetRatio` | Handles word order differences and extra tokens |
288
- | Substring / abbreviation matching | `partialRatio` | Finds best partial match within longer strings |
289
- | Best-effort similarity | `weightedRatio` | Picks the best score across all methods automatically |
290
- | Interactive fuzzy search | `search`, `closest` | Nucleo algorithm (same as Helix editor) |
291
- | Repeated search on same data | `FuzzyIndex`, `FuzzyObjectIndex` | Persistent Rust-side index, 3–5x faster than standalone |
292
-
293
- **Return types:**
294
-
295
- - `levenshtein`, `damerauLevenshtein` → integer (edit count)
296
- - `jaro`, `jaroWinkler`, `sorensenDice`, `normalizedLevenshtein` → float between 0.0 (no match) and 1.0 (identical)
297
- - `tokenSortRatio`, `tokenSetRatio`, `partialRatio`, `weightedRatio` → float between 0.0 and 1.0
298
- - `search` → array of `{ item, score, index, positions }` sorted by relevance (score: 0.0–1.0)
299
-
300
338
  ## Why rapid-fuzzy?
301
339
 
302
- | | rapid-fuzzy | fuse.js | fastest-levenshtein | fuzzysort |
303
- |---|:---:|:---:|:---:|:---:|
304
- | **Algorithms** | 9 (Levenshtein, Jaro, Dice, …) | Bitap | Levenshtein | Substring |
305
- | **Runtime** | Rust native + WASM | Pure JS | Pure JS | Pure JS |
306
- | **Object search** | ✅ weighted keys | ✅ | — | ✅ |
307
- | **Persistent index** | ✅ FuzzyIndex / FuzzyObjectIndex | — | — | ✅ prepared targets |
308
- | **Score threshold** | ✅ | ✅ | — | |
309
- | **Match positions** | ✅ | | — | ✅ |
310
- | **Highlight utility** | ✅ | | — | ✅ |
311
- | **Batch API** | ✅ | | — | — |
312
- | **Node.js native** | ✅ napi-rs | — | | |
313
- | **Browser** | ✅ WASM | | ✅ | ✅ |
314
- | **TypeScript** | ✅ full | full | | |
340
+ | | rapid-fuzzy | fuse.js | fastest-levenshtein | fuzzysort | uFuzzy |
341
+ |---|:---:|:---:|:---:|:---:|:---:|
342
+ | **Algorithms** | 9 (Levenshtein, Jaro, Dice, …) | Bitap | Levenshtein | Substring | Regex-based |
343
+ | **Runtime** | Rust native + WASM | Pure JS | Pure JS | Pure JS | Pure JS |
344
+ | **Object search** | ✅ weighted keys | ✅ | — | ✅ | — |
345
+ | **Persistent index** | ✅ FuzzyIndex / FuzzyObjectIndex | — | — | ✅ prepared targets | — |
346
+ | **Query syntax** | ✅ exclude, prefix, suffix, exact | ✅ extended search | — | | partial (`-` only) |
347
+ | **Out-of-order matching** | ✅ automatic | | — | — | with option |
348
+ | **Diacritics** | ✅ automatic | option | — | ✅ auto | ✅ `latinize()` |
349
+ | **Score threshold** | ✅ | | — | ✅ | — |
350
+ | **Match positions** | ✅ | | — | | |
351
+ | **Highlight utility** | ✅ | | | ✅ | ✅ |
352
+ | **Batch API** | ✅ | | | | |
353
+ | **Node.js native** | ✅ napi-rs | — | — | — | — |
354
+ | **Browser** | ✅ WASM | ✅ | ✅ | ✅ | ✅ |
355
+ | **TypeScript** | ✅ full | ✅ full | ✅ | ✅ | ✅ |
315
356
 
316
357
  ## Migration Guides
317
358
 
318
359
  Switching from another library? These guides provide API mapping tables, code examples, and performance comparisons:
319
360
 
320
361
  - [**From string-similarity**](docs/migration/from-string-similarity.md) — Same Dice coefficient algorithm, now maintained and faster
321
- - [**From fuse.js**](docs/migration/from-fuse-js.md) — 17–40x faster fuzzy search with a simpler API
362
+ - [**From fuse.js**](docs/migration/from-fuse-js.md) — Up to 7,000x faster fuzzy search with FuzzyIndex
322
363
  - [**From leven / fastest-levenshtein**](docs/migration/from-leven.md) — Multi-algorithm upgrade with batch APIs
364
+ - [**From fuzzysort**](docs/migration/from-fuzzysort.md) — Richer matching with query syntax and 9 distance algorithms
365
+ - [**From uFuzzy**](docs/migration/from-ufuzzy.md) — Weighted object search, batch APIs, and persistent indexes
323
366
 
324
367
  ## License
325
368
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rapid-fuzzy",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "Rust-powered fuzzy search and string distance for JavaScript/TypeScript. 10-50x faster than fuse.js/leven.",
5
5
  "license": "MIT",
6
6
  "author": "derodero24",
@@ -107,6 +107,7 @@
107
107
  "@commitlint/config-conventional": "^20.4.4",
108
108
  "@emnapi/core": "^1.9.0",
109
109
  "@emnapi/runtime": "^1.9.0",
110
+ "@leeoniya/ufuzzy": "^1.0.19",
110
111
  "@napi-rs/cli": "^3.5.1",
111
112
  "@napi-rs/wasm-runtime": "^1.1.1",
112
113
  "@playwright/test": "^1.58.2",
@@ -131,14 +132,14 @@
131
132
  ]
132
133
  },
133
134
  "optionalDependencies": {
134
- "rapid-fuzzy-darwin-x64": "0.5.0",
135
- "rapid-fuzzy-darwin-arm64": "0.5.0",
136
- "rapid-fuzzy-linux-x64-gnu": "0.5.0",
137
- "rapid-fuzzy-linux-x64-musl": "0.5.0",
138
- "rapid-fuzzy-linux-arm64-gnu": "0.5.0",
139
- "rapid-fuzzy-linux-arm64-musl": "0.5.0",
140
- "rapid-fuzzy-win32-x64-msvc": "0.5.0",
141
- "rapid-fuzzy-win32-arm64-msvc": "0.5.0",
142
- "rapid-fuzzy-wasm32-wasi": "0.5.0"
135
+ "rapid-fuzzy-darwin-x64": "0.6.0",
136
+ "rapid-fuzzy-darwin-arm64": "0.6.0",
137
+ "rapid-fuzzy-linux-x64-gnu": "0.6.0",
138
+ "rapid-fuzzy-linux-x64-musl": "0.6.0",
139
+ "rapid-fuzzy-linux-arm64-gnu": "0.6.0",
140
+ "rapid-fuzzy-linux-arm64-musl": "0.6.0",
141
+ "rapid-fuzzy-win32-x64-msvc": "0.6.0",
142
+ "rapid-fuzzy-win32-arm64-msvc": "0.6.0",
143
+ "rapid-fuzzy-wasm32-wasi": "0.6.0"
143
144
  }
144
145
  }