Binary Fuse
distillate/fuse ships Binary Fuse 8 and 16, the best static AMQ filter
published and the thing nothing else in JavaScript offers.
- Static. Built once from the complete key set.
- Immutable. There is no
addand nodelete. The filter cannot accept later inserts; to change membership you rebuildfromthe new set. - Takes no
epsilon. The false positive rate is fixed by the fingerprint width, sofromhas no target parameter to give it. - About 1.08 to 1.13 times the floor, roughly 9% overhead, the least in the lineup. Three cache-local probes per query.
Graf and Lemire, JEA 2022 (arXiv 2201.01174). Reference implementation: FastFilter/xorfilter.
Build one
Section titled “Build one”import { BinaryFuse8 } from "distillate/fuse";
const filter = BinaryFuse8.from(["alice", "bob", "carol"]);
filter.has("alice"); // truefilter.has("dave"); // false, or a ~0.39% false positivefilter.size; // 3filter.bitsPerKey; // 64 at this size; see belowfrom is the only constructor. It takes the whole key set and no target rate.
bitsPerKey is the real allocated cost, not the asymptotic figure. A
three-key filter pays 64 bits per key because the fingerprint array has a
fixed minimum; the ~9 figure is what you get once n is large. Use
fuseBitsPerKey(n, width) below to size honestly.
Pick a width, not a rate
Section titled “Pick a width, not a rate”The two widths are the whole tuning surface:
| Class | Fingerprint | FPR | bits/key at n=1M |
|---|---|---|---|
BinaryFuse8 |
8-bit | ~0.39% | ~9.04 |
BinaryFuse16 |
16-bit | ~1/65536 | ~18.09 |
import { BinaryFuse16 } from "distillate/fuse";
// Lower false-positive rate, twice the space.const precise = BinaryFuse16.from(["alice", "bob", "carol"]);precise.has("alice"); // trueObserved at n=100k: about 9.50 bits/key at 0.39% for the 8-bit filter, and
about 19.01 bits/key at roughly 1.3e-5 against the 1/65536 target for the
16-bit. To predict the size before you build, fuseBitsPerKey(n, width):
import { fuseBitsPerKey } from "distillate/fuse";
fuseBitsPerKey(1_000_000, 8); // ~9.04fuseBitsPerKey(1_000_000, 16); // ~18.09If neither rate is what you need, a fuse filter is the wrong structure. Use
Classic or Blocked Bloom, which take a
target epsilon. See sizing and tuning.
How the build works
Section titled “How the build works”Each key is hashed once with hash128 and deduplicated by hash, so a repeated
key costs nothing. The build then peels a 3-hypergraph in split construction
and assigns fingerprints so each key’s XOR of its three lanes equals its
fingerprint. A query recomputes those three lanes and compares.
Peeling can stall. The build retries with a bumped seed, and throws
BinaryFuseBuildError only if it exhausts the retry
budget, which is astronomically unlikely. It never returns a corrupt filter.
Building is the expensive operation: about 39 ms for 100k keys with 8-bit fingerprints. Queries are about 94 ns on a hit and 91 ns on a miss (Apple M5, node v24.14.1). That asymmetry is the whole point of the structure. Build once, query a great many times.
Inspect it
Section titled “Inspect it”import { BinaryFuse8 } from "distillate/fuse";
const filter = BinaryFuse8.from(["alice", "bob", "carol"]);
filter.size; // distinct keysfilter.seed; // the seed the build succeeded onfilter.bitsPerKey; // actual bits per keyThere is no length, no rate(), and no union. The filter is fully
determined at build time, so a fill estimate would say nothing the width does
not already say, and two fuse filters cannot be merged.
Persist it
Section titled “Persist it”import { BinaryFuse8 } from "distillate/fuse";
const filter = BinaryFuse8.from(["alice", "bob", "carol"]);
const bytes = filter.toBytes();const restored = BinaryFuse8.fromBytes(bytes);
restored.equals(filter); // trueFrames are AMQF type 3 for Fuse 8 and type 4 for Fuse 16, and fromBytes
rejects a mismatched type. Fingerprints are a Uint8Array for the 8-bit
filter and a Uint16Array for the 16-bit one, so see
serialization for the byte-order note on the
latter.
Serializing is often the right move: build the filter once in a job, ship the bytes, and let every reader restore it instead of rebuilding.
When to pick it
Section titled “When to pick it”Pick Binary Fuse whenever the key set is known up front and does not change. It is the smallest and the fastest to query, and shipping it as bytes is cheap.
Pick something else when:
- Keys arrive over time. Fuse cannot accept later inserts. Use Blocked Bloom for streaming inserts.
- You need a rate between 0.39% and 1/65536, or below it. Fuse has exactly two
rates. Use Classic Bloom, which takes any
epsilon. - You need
union. Fuse filters cannot be merged; rebuild from the combined key set.