#850·Fuse

[Feature] Optional native backend? Bit-identical drop-in acceleration of Fuse.js (8.8–38.7x warm) via a Mojo Bitap kernel

Author: 0xamlabCreated Sep 19, 2026Updated Sep 19, 2026
  • I have searched the existing issues and pull requests

Summary

We've built fuse-mojo, a drop-in faster replacement for Fuse.js 7.1.0: same API, bit-identical scores, powered by a clean-room Bitap/Shift-And kernel written in Mojo, with the vendored Fuse.js basic build as an automatic fallback on platforms without a native build. Repo (public, Apache-2.0): https://github.com/thyn-ai/mojo-kernels — package, benchmark, and full details in typescript/fuse-mojo.

This issue asks one question: is an optional native backend interesting to Fuse.js upstream, or would you prefer it remain a standalone drop-in package?

Feature description — why this exists

A V8 CPU profile (node --cpu-prof) of Fuse.js 7.1.0 searching a 100k-document corpus shows ~94% of query CPU is the Bitap bitmask DP executed in JavaScript, per document, per query (self time by function):

self time function what it is
48.1% search (dist/fuse.cjs:713) the Bitap bitmask DP itself
45.2% chunks.forEach closure (dist/fuse.cjs:988) BitapSearch.searchIn driving the DP per document (inlined DP work attributed here)
1.8% (garbage collector) allocation churn
1.1% searchIn (dist/fuse.cjs:957) per-document dispatch, toLowerCase
<1% everything else scoring, sort, format

fuse-mojo moves exactly that loop into a compiled kernel (word-parallel Shift-And, 32 text positions per 32-bit word, plus deterministic thread fan-out across documents) and leaves everything else — key weights, field-norm scoring, sort, format — in JS, bit-compatibly with the reference's operation order.

Measured results

Environment: Apple M4 Max (16 threads), macOS arm64, Node v23.10.0, fuse.js 7.1.0 (npm), Mojo 1.1.0, measured 2026-09-19. Corpora generated from fixed seeds (10k/50k/100k documents of 5–15 word-like tokens), 15 patterns per cell, median of 5 runs; a correctness gate asserting identical refIndex order and scores within 1e-9 runs before every timing pass. Reproduce with benchmarks/bench_fuse.mjs in the repo.

Warm steady state (median of 5 runs)

corpus pattern len Fuse.js ms/query fuse-mojo ms/query Fuse.js q/s fuse-mojo q/s speedup
10,000 3 14.813 1.629 67.5 613.9 9.1x
10,000 8 34.502 1.584 29.0 631.2 21.8x
10,000 16 75.916 3.080 13.2 324.7 24.6x
50,000 3 72.545 8.270 13.8 120.9 8.8x
50,000 8 184.715 5.731 5.4 174.5 32.2x
50,000 16 414.061 13.077 2.4 76.5 31.7x
100,000 3 168.540 18.851 5.9 53.0 8.9x
100,000 8 419.301 16.232 2.4 61.6 25.8x
100,000 16 1,239.508 32.003 0.8 31.2 38.7x

Cold first call (fresh instance: index build + first query, median of 5 runs × 3 patterns)

corpus pattern len Fuse.js cold (ms) fuse-mojo cold (ms) speedup
10,000 3 18.9 6.6 2.9x
10,000 8 37.5 6.1 6.2x
10,000 16 75.3 6.7 11.2x
50,000 3 84.8 29.5 2.9x
50,000 8 195.7 27.3 7.2x
50,000 16 402.6 30.4 13.3x
100,000 3 171.4 55.5 3.1x
100,000 8 417.7 60.5 6.9x
100,000 16 945.4 70.3 13.4x

Index construction is one-time and ~1.4–1.7x slower than Fuse.js's (we lowercase and copy every searchable string into the native index once, instead of re-lowercasing every document on every query); it pays for itself within the first couple of queries at these corpus sizes.

Parity proof

  • Scores are bit-identical, not approximations: the maximum measured difference vs Fuse.js 7.1.0 across the benchmark corpus is exactly 0 (the differential gate tolerates ≤1e-9; measured agreement is 0).
  • A differential suite runs against the published fuse.js 7.1.0 package: ~200 generated patterns (exact tokens, typo'd tokens, substrings, multi-token spans, absent tokens, unicode/emoji, >32-code-unit chunked patterns) across 20 option cells (threshold, location/distance, ignoreLocation, findAllMatches, minMatchCharLength, ignoreFieldNorm, fieldNormWeight, case sensitivity, string lists and object lists with single/weighted/nested/array keys), asserting identical refIndex order, scores, and match spans. 76 tests pass: 39 on the native backend + 37 on the forced-fallback backend (2 native-only cells skip there by design).
  • The fallback is the vendored Fuse.js 7.1.0 basic build, so behavior on unsupported platforms (e.g. Windows, where no Mojo toolchain exists) is identical by construction.

Desired solution — the question

Fuse.js already has precedent for pluggable/modular pieces — the Fuse.use() plugin mechanism (e.g. #822) and the basic/full build split — so an optional native backend (per-platform optionalDependencies, always silently falling back to the pure-JS path) doesn't seem architecturally foreign. Concretely:

  1. Would you consider an optional native backend inside Fuse.js itself (same API, pure-JS fallback always intact)?
  2. Or would you rather it remain a standalone drop-in (import Fuse from '@fuse-mojo/core')?

Either answer is fine. If (1) is interesting, we're happy to discuss what an upstreamable, plugin-shaped integration would look like. If (2), no action needed — a docs pointer someday would be appreciated but is entirely your call.

Alternatives considered

Remaining a standalone drop-in package — this works today: npm install @fuse-mojo/core, prebuilt darwin-arm64 / linux-x64 native libraries via optionalDependencies, ABI-version handshake, transparent fallback to the vendored Fuse.js on any load failure or unsupported platform, no Mojo toolchain required at install time.

Honest gaps (unsupported options)

The constructor throws an explicit UnsupportedOptionError (identical on both backends) for:

  • extended search (useExtendedSearch: true and logical $and/$or query objects)
  • ignoreDiacritics
  • custom getFn (including key-level getFn) and custom sortFn
  • external indices (Fuse.createIndex / Fuse.parseIndex / constructor index argument)
  • non-integer location / minMatchCharLength, non-finite threshold

(Your own fuse.basic build makes essentially the same extended-search trade-off.) Extended search is the main gap — if upstream interest exists, closing it would be the first thing we'd take on.

One packaging note for completeness: the platform packages self-contain the Mojo runtime (load paths rewritten with delocate/patchelf); redistribution terms for Modular's runtime binaries would need confirming with Modular before any npm publish.

Disclosure

I work on this as part of thyn-ai's Mojo Kernel Acceleration Program. The kernel is a clean-room implementation of the textbook Bitap/Shift-And algorithm — no Fuse.js code was read or copied into it; Fuse.js is vendored as the fallback backend and used as the test oracle under its own Apache-2.0 license (see NOTICE). fuse-mojo itself is Apache-2.0, © Algenta.