Algorithms/Bloom Filter

Bloom Filter

Three hashes set three bits per word. A 0 bit is a definite no, a full row of 1s only a maybe.

Difficulty
Medium
Time
O(k)
Space
O(m)
Problems
5

Fig 01 · Bloom filter

query “lime”00010213140516170809010011012113114115

Query “lime”. Three hash functions pick bits 4, 14, and 7. Every one must read 1 for a maybe. A single 0 ends it.

m=16 · k=3 · bits set 7/16 · words 3

In the filter

Type a word and query or add it (max 8 words). Click a word chip to query it.

01 / 05
Bloom Filter Visualizer

A Bloom filter answers set membership with two answers instead of one: a confident definitely not, or an uncommitted maybe. It never says a plain yes. In exchange for the uncertainty it stores almost nothing: no words, only a row of bits.

The figure above runs one with sixteen bits and three hash functions. Three words are already in: apple, banana, and cherry. Press play and the figure queries lime, a word nobody added. Watch what the filter gets wrong, then read on to see why that wrong answer is the point.

Adding a word sets three bits

To add a word, the filter hashes it three times. Hash functions h₁, h₂, and h₃ each map the word to one position in the bit array, and the filter sets those bits to 1. That is the entire insert. The word itself is never stored, which is why the filter stays small, and also why it can never list its contents.

Try it in either figure. Type kiwi and press Add. The badges read h₁ → 11, h₂ → 14, and h₃ → 2, and a line drops from each badge to its cell. Bit 14 was already 1, because banana set it earlier. Shared bits are normal, and they are where the maybe comes from.

A query reads the same three bits

A query runs the same three hashes and reads the bits instead of writing them. If any checked bit is 0, the word was never added. Had the word been added, that bit would be 1, and bits are never cleared. This answer is exact. If every checked bit is 1, the answer is maybe. The word might be in, or other words might have covered its bits.

Query mango in the figure. The first check reads bit 3 and finds a 1, set by apple. The second check reads bit 2 and finds a 0, and the filter stops there. One 0 is proof, so the third bit is never read.

The other direction is guaranteed. Add kiwi, then click its chip under the array. All three checks read 1, because the add set those bits and nothing ever clears a bit. A member always reads maybe. Bloom filters have false positives, but never false negatives.

The whole data structure fits in one class. The hash functions differ from the figure's, but the shape is the same. The implementation panel below carries this class in Python and JavaScript:

class BloomFilter:
	def __init__(self, m=16, seeds=(1, 2, 3)):
		self.bits = [0] * m
		self.m = m
		self.seeds = seeds

	def _positions(self, word):
		for seed in self.seeds:
			h = seed
			for ch in word:
				h = (h * 131 + ord(ch)) % (1 << 32)
			yield h % self.m

	def add(self, word):
		for pos in self._positions(word):
			self.bits[pos] = 1

	def __contains__(self, word):
		return all(self.bits[pos] for pos in self._positions(word))

Where the maybe comes from

Bits are shared, and a bit does not record who set it. So a word that was never added can find all of its bits on, when other words happen to cover them. That is a false positive: a maybe for a word that was never added.

The figure above opens on exactly that case. lime hashes to bits 4, 14, and 7:

  1. h₁ maps lime to bit 4. It reads 1, set by apple and cherry.

  2. h₂ maps lime to bit 14. It reads 1, set by banana.

  3. h₃ maps lime to bit 7. It reads 1, set by banana again.

Three 1s for a word nobody added, so the filter answers maybe. The figure below replays the query. Then try to fool the filter yourself. Add words and watch the array fill: the fuller the array, the more often a maybe lies.

Fig 02 · The lime query, replayed

query “lime”00010213140516170809010011012113114115

Query “lime”. Three hash functions pick bits 4, 14, and 7. Every one must read 1 for a maybe. A single 0 ends it.

m=16 · k=3 · bits set 7/16 · words 3

In the filter

Type a word and query or add it (max 8 words). Click a word chip to query it.

01 / 05
Bloom Filter Visualizer

Sizing the filter

The maybe rate is a dial, not fate. Three numbers set it: m the number of bits, n the number of words you plan to add, and k the number of hash functions. The false-positive rate is approximately (1 − e^(−kn/m))^k, where kn/m measures how full the array is.

The knobs behave the way you would hope. More bits per word push the rate down. More words into the same array push it up. The hash count has a sweet spot. Too few, and one shared bit can fake a match. Too many, and every query needs a crowded row of 1s. For a given m and n, the best k is (m/n) ln 2. The figure below plots the rate against k for the current m and n, so drag the sliders and watch the sweet spot move.

Fig 03 · Sizing the filter

false-positive rate vs k100%10%1%0.1%0.01%123456789101112kbest k = 6

With 96 bits and 12 words, k = 6 gives the lowest maybe rate, about 2.2%.

False-positive rate3.1%at k = 3
Best k6for m = 96, n = 12
Bits per word8.0memory budget
Bloom Filter Tuning

With k at its best value, the rate falls off fast:

Bits per word

False-positive rate

6

5.6%

8

2.1%

10

0.8%

12

0.3%

Ten bits per word buys a wrong maybe about once in 125 queries. The demo filter is small on purpose. Sixteen bits for three words with k = 3 gives a maybe rate near 8%, so false positives show up within a few tries instead of once in a hundred.

What a Bloom filter cannot do

Three limits follow straight from the design:

  • Delete. Clearing a word's bits can clear bits that other words share, so removal is unsafe. When deletes matter, a counting Bloom filter swaps each bit for a small counter.

  • List. The words were never stored, so there is nothing to enumerate.

  • Confirm. The filter has no plain yes. When an exact answer matters, a maybe must be checked against the real data.

Where it earns its keep

A Bloom filter earns its keep as a screen in front of an expensive check. The cheap filter answers most queries with a fast definitely not, and only the maybes pay for the slow lookup. Keep the filter in memory while the real data sits on disk, and almost every miss skips the disk.

  • Storage engines such as RocksDB and Cassandra keep a Bloom filter per data file, so a read for a missing key never opens the file.

  • CDNs screen one-hit wonders: a URL is cached only after the filter says it was requested before.

  • Breach-password checks answer against huge lists that never ship to the device.

Keep the two answers straight and the practice problems below get easier: a 0 bit is a proof, and a 1 bit is only a rumor. Each one is membership in disguise. Solve it with a hash set first, then ask what a bit array would trade.

Implementation

/** Probabilistic membership set: answers "definitely not" or "maybe". */class BloomFilter {  constructor(m = 1024, seeds = [1, 2, 3]) {    this.bits = new Uint8Array(m);    this.m = m;    this.seeds = seeds;  }   *positions(word) {    for (const seed of this.seeds) {      let h = seed >>> 0;      for (const ch of word) {        h = (Math.imul(h, 131) + ch.codePointAt(0)) >>> 0;      }      yield h % this.m;    }  }   add(word) {    for (const pos of this.positions(word)) {      this.bits[pos] = 1;    }  }   has(word) {    for (const pos of this.positions(word)) {      if (this.bits[pos] === 0) return false;    }    return true;  }}

Newsletter

One sharp idea, every week

System design and interview prep — short enough to finish.

No spam. Unsubscribe anytime.

Practice

Related problems

5 problems use Bloom Filter