workshop private

← all creations

One-Sided

viz · created 2026-09-15

A Bloom filter as a wall of bits with its hash functions drawn as arrows, built so the asymmetry is inescapable — a query that finds a zero is a proof of absence and the viz says so, while a query that finds all ones is only ever circumstantial. Keep inserting and watch the circumstance accumulate: every bit wears the colour of whoever set it, so a word that was never inserted comes back “maybe” as a visible collage of three other words' evidence.

algorithmsinterview-prepcanvas

A Bloom filter is m bits and k hash functions. Insert a word: flip the k bits its hashes point at. Query a word: look at those same k bits. That is all of it, and it buys an asymmetry that no amount of tuning ever removes.

A zero anywhere is a proof. Every word that was inserted set all of its bits, and nothing here ever clears one, so a zero rules the word out — not probably, provably.

All ones is circumstantial. The bits are there; the filter has no idea who put them there.

So a false positive isn’t a bug in the arithmetic. It is a collage: k bits, each of them honestly set by somebody else. This piece is built to make that literal — every set bit wears the colour of the word that set it first, and when a word that was never inserted comes back “maybe”, its three swatches are three other words’ colours, named underneath.

The default run is 512 bits and 3 hashes, which is a filter shaped for 118 words. It is handed 180. At 95 words in, kemezes — never inserted — comes back “maybe” off bits belonging to beefozul, faplegrir and gitruta. By the end the wall is 66% full and 31.7% of never-inserted words get a “maybe”, against 27.7% predicted.

What’s on screen

The reading nobody believes until they see it

k has an optimum, and going past it makes the filter worse. At 512 bits and 180 words the sweep measures 31.5% at k=1, 25.3% at k=2, 27.7% at k=3, then 34.7, 38.6, 52.5 … and 90.3% at k=12. More hash functions means more evidence demanded per query, which should help — but it also means more bits set per insert, and past (m/n)·ln2 the wall fills faster than the extra demand discriminates. The “nine hashes, same wall” preset is the same 512 bits holding the same 180 words at 97% full, wrong three quarters of the time.

The other thing worth sitting with is how close the measurement runs to the formula. (1 − e^(−kn/m))^k assumes independent uniform hashing, which FNV-1a with four salts is not, and the two lines still track each other point for point for the whole run.

The fail state to build toward

Not a crash. The “sized for last quarter’s traffic” preset is a filter with 256 bits and 3 hashes — right for 59 keys — being handed 400. It is 99% full, answers 95.8% of never-inserted words with “maybe”, is still O(k) per query, still never returns a false negative, still honours every promise on the tin, and is now a data structure that says “maybe” to everything. Nothing alerts. The service behind it just quietly stops being protected by its cache filter and starts serving every lookup, because the filter is technically correct and practically an unconditional yes.

Things it turned up

Reuse

src/one-sided.js is framework-free with no rendering in it. positionsFor() is FNV-1a with an avalanche tail and one salt per hash function — independent salts rather than the usual h1 + i·h2 double-hashing trick, which is cheaper and provably good enough but visibly correlates a word’s k positions when you draw them. simulate() returns a frame per operation carrying the k positions, which of them were fresh, who owned them at that moment and the running measured rate, so a different renderer can pick it up unchanged; stateAt() replays the inserts to reconstruct the wall at any frame rather than snapshotting the bit array per frame. sweepK() re-runs the whole stream for every k and probes with a larger held-out set, because the differences between adjacent k are a few points wide and 400 probes cannot resolve them.

node scripts/screenshot-demo.mjs regenerates thumb.png from the run’s first false positive, and fails if that frame stops being the first one.