Bloom Filter

Bloom Filter
Data Structures · Probabilistic · Deep Dive

Bloom Filter

Probabilistic data structure cho phép kiểm tra sự tồn tại của một phần tử trong tập hợp với bộ nhớ cực nhỏ — đánh đổi bằng một tỉ lệ sai số có thể kiểm soát.
Space complexity O(m) — constant bit array
Query time O(k) — số hash functions
Invented Burton H. Bloom, 1970
01

Bài toán gốc rễ

Hãy tưởng tượng mày là kỹ sư tại Google. Hệ thống có 2 tỷ tài khoản Gmail. Mỗi ngày có hàng triệu người thử đăng ký. Với mỗi request, mày cần trả lời một câu hỏi duy nhất:

Câu hỏi cốt lõi

"Email này đã có ai dùng chưa?"

Nghe đơn giản. Nhưng scale mới là vấn đề.

Approach 1 — Query thẳng vào Database

Cách naive nhất: SELECT 1 FROM users WHERE email = ?. Với 2 tỷ record, mỗi query dù có index cũng mất vài milliseconds, cộng network latency. Hàng triệu request/ngày → database trở thành bottleneck ngay.

Vấn đề

DB I/O tốn kém. Disk seek, buffer pool, network round-trip — tất cả cộng lại. Không scale được.

Approach 2 — Cache toàn bộ email vào RAM

Dùng Redis/Memcached lưu tất cả email vào memory. Nhanh hơn nhiều. Nhưng 2 tỷ email, trung bình 25 bytes/email → ~50 GB RAM chỉ để lưu email. Tốn tiền, không thực tế.

Vấn đề

Memory không vô hạn. Dataset tăng tuyến tính → chi phí RAM tăng tuyến tính.

Approach 3 — Bloom Filter

Cùng 2 tỷ email, false positive rate 1% → chỉ tốn ~2.4 GB RAM. Nhanh hơn 20x so với Redis lookup thông thường. Đây chính là vấn đề Bloom Filter sinh ra để giải quyết.

Key insight

Bloom Filter không lưu dữ liệu thực. Nó chỉ lưu dấu vết của dữ liệu dưới dạng bit — đánh đổi một tỉ lệ sai số nhỏ để tiết kiệm bộ nhớ cực lớn.

02

Cơ chế hoạt động

Bloom Filter là một bit array kích thước m, kết hợp với k hash functions độc lập. Ban đầu tất cả các bit đều là 0.

INSERT — Thêm một phần tử

Khi insert item x vào filter:

  1. Tính k hash values: h₁(x), h₂(x), ..., hₖ(x)
  2. Mỗi hash value map sang một vị trí trong bit array (mod m)
  3. Set tất cả k bit tương ứng lên 1
// Insert "hello"
h1("hello") = 3  → set bit[3] = 1
h2("hello") = 7  → set bit[7] = 1
h3("hello") = 12 → set bit[12] = 1

// Insert "world"
h1("world") = 5  → set bit[5] = 1
h2("world") = 7  → bit[7] đã = 1, không sao
h3("world") = 9  → set bit[9] = 1

QUERY — Kiểm tra sự tồn tại

Khi query item x:

  1. Tính tương tự k hash values
  2. Kiểm tra tất cả k bit tương ứng
  3. Nếu bất kỳ bit nào = 0Definitely NOT in set
  4. Nếu tất cả bit đều = 1Probably in set
Tại sao không có false negative?

Khi ta insert x, ta set một số bit lên 1. Những bit đó không bao giờ bị reset về 0. Nên nếu x đã được insert, tất cả k bit của nó đều là 1 — guarantee 100%. Nếu bất kỳ bit nào là 0 thì chắc chắn x chưa được insert.

Tại sao có false positive?

Xét item y chưa được insert. Nhưng qua quá trình insert nhiều items khác, các bit mà h₁(y), h₂(y), h₃(y) trỏ tới đã vô tình bị set bởi những items khác nhau. Bloom Filter không thể phân biệt — nó chỉ thấy "tất cả bit = 1" và trả về "probably yes".

03

Toán học đằng sau

Bloom Filter có nền tảng toán học rất đẹp. Hiểu được sẽ giúp ích lớn trong phỏng vấn system design.

Xác suất false positive

Cho m bits, k hash functions, n items đã insert. Xác suất false positive:

FPR ≈ (1 − e−kn/m)k k = số hash functions · n = số items đã insert · m = số bits

Số hash functions tối ưu

koptimal = (m/n) × ln(2) ≈ 0.693 × (m/n) Tại k này FPR đạt minimum với cùng m và n

Tính số bits cần thiết

Muốn lưu n items với false positive rate p:

m = −n × ln(p) / (ln 2)2 ≈ 1.44 × n × log₂(1/p)

Bảng sizing thực tế

n (items)FPR targetMemoryk
1,000,0001%~1.2 MB7
1,000,0000.1%~1.8 MB10
1,000,000,0001%~1.2 GB7
2,000,000,0001%~2.4 GB7
So sánh với HashSet

2 tỷ email (~25 bytes/email) trong HashSet → ~50 GB. Trong Bloom Filter FPR 1% → chỉ ~2.4 GB. Tiết kiệm hơn 20 lần.

04

Interactive Demo

Thử tự tay insert và query để hiểu cơ chế hoạt động. Khi bit array đầy dần, tỉ lệ false positive sẽ tăng lên — mày sẽ thấy rõ điều này.

Bloom Filter — 20 bits · 3 hash functions
Hash positions sẽ hiển thị ở đây
Bit array [20 bits]
Đã insert
Chưa có gì...
Bit array fill rate
Items inserted0
Bits set0 / 20
Est. FPR~0%
False positives seen0

💡 Gợi ý: Insert ~10 từ rồi thử check một từ chưa insert — mày sẽ thấy false positive xảy ra khi nhiều bit bị set.

05

False Positive thực sự là gì?

Đây là phần nhiều người hiểu nhầm nhất. False positive trong Bloom Filter không phải bug — nó là trade-off có thể kiểm soát.

Definitely NOT in set — 100% chính xác

Nếu bất kỳ bit nào trong k bit của item là 0, item này chắc chắn chưa được insert. Không bao giờ sai. Đây là guarantee mạnh nhất của Bloom Filter.

Probably in set — có thể sai ~FPR%

Nếu tất cả k bit đều là 1, item có thể đã được insert — nhưng cũng có thể các bit đó bị set bởi những items khác. Xác suất sai này là false positive rate, thường set ở 1%.

Hệ quả thực tế

Trong production, khi Bloom Filter trả về "probably yes", mày vẫn phải verify với DB. Bloom Filter không thay thế DB — nó là tầng lọc đầu tiên để tránh những lookup DB chắc chắn không cần thiết.

Kết quảÝ nghĩaHành động
Filter: NOT foundChắc chắn không tồn tạiReturn ngay, không query DB
Filter: FOUNDCó thể tồn tại (hoặc false positive)Query DB để confirm

Với FPR = 1%, chỉ 1% request bị false positive và phải query DB không cần thiết. 99% request còn lại được fast-reject không tốn I/O.

06

Trade-off và khi nào dùng

Ưu điểmNhược điểm
Memory cực nhỏ (vài GB cho hàng tỷ items)Có false positive rate
O(k) time — constant, cực nhanhKhông thể delete item (standard)
No false negative — guarantee mạnhKhông retrieve được item gốc
Cache-friendly, CPU-efficientFPR tăng khi dataset vượt dự kiến
Nên dùng khi

Dataset rất lớn (hàng triệu+), câu hỏi là có/không, chấp nhận false positive nhỏ, không cần delete thường xuyên, muốn fast-reject trước khi hit expensive resource (DB, disk, network).

Không nên dùng khi

Cần 100% accuracy (tài chính, y tế), cần delete items thường xuyên, dataset nhỏ (HashSet đủ rồi), cần retrieve giá trị thực của item.

07

Ứng dụng thực tế

🗄️

RocksDB / Cassandra / LevelDB

Bloom Filter per SSTable. Trước khi đọc disk, hỏi filter. "Not found" → skip hoàn toàn. Giảm disk I/O hàng chục lần.

🌐

Chrome Safe Browsing

Lưu danh sách URL độc hại locally. "Clean" → không gọi server. "Maybe bad" → verify với Google API.

📧

Gmail / Google Accounts

Check email đã đăng ký chưa khi tạo tài khoản. Filter fast-reject trước khi query DB chứa hàng tỷ accounts.

🕷️

Web Crawlers

Đã crawl URL này chưa? Hàng tỷ URL không thể HashSet hết vào RAM. Bloom Filter nhớ dấu vết với memory cực nhỏ.

🎬

YouTube / Netflix

User đã xem video này chưa? Số cặp (user, video) lên hàng nghìn tỷ. Tránh recommend nội dung đã xem.

Akamai CDN

Track "lần đầu tiên thấy URL". Chỉ cache vào memory nếu đã thấy lần 2 — tránh cache one-hit wonders.

🔴

Redis RedisBloom

Module chính thức. Dùng để chặn spam, rate limiting theo IP/email mà không cần lưu toàn bộ danh sách.

🔐

Have I Been Pwned

Check password có trong danh sách 500M+ leaked passwords. Dataset quá lớn để HashSet — Bloom Filter là obvious choice.

08

Các biến thể của Bloom Filter

Counting Bloom Filter
Thay bit bằng counter (thường 4-bit). Cho phép delete bằng cách decrement counter. Tốn gấp 4-8x memory so với standard. Dùng khi cần delete.
Scalable Bloom Filter
Tự động grow khi filter đầy bằng cách thêm filter mới với tighter FPR. Dùng khi không biết trước kích thước dataset.
Cuckoo Filter
Hỗ trợ delete, lookup nhanh hơn nhờ cache locality tốt hơn. Thường được prefer hơn Counting BF trong production hiện đại.
Blocked Bloom Filter
Chia filter thành blocks bằng kích thước cache line. Tất cả k hash values map vào một block → cache-friendly hơn nhiều.
Distributed Bloom Filter
Replicate filter qua nhiều nodes. Mỗi node giữ bản local, sync định kỳ. Chấp nhận eventual consistency ngắn để đổi lấy low latency.
09

Implement từ scratch

TypeScript

class BloomFilter {
  private bits: Uint8Array;
  private m: number;
  private k: number;
  private count = 0;

  constructor(expectedItems: number, fpr: number) {
    // m = -n * ln(p) / (ln2)²
    this.m = Math.ceil(
      -expectedItems * Math.log(fpr) / (Math.LN2 ** 2)
    );
    // k = (m/n) * ln2
    this.k = Math.ceil((this.m / expectedItems) * Math.LN2);
    this.bits = new Uint8Array(Math.ceil(this.m / 8));
  }

  add(item: string): void {
    for (const pos of this.positions(item)) {
      this.bits[Math.floor(pos / 8)] |= (1 << (pos % 8));
    }
    this.count++;
  }

  has(item: string): boolean {
    return this.positions(item).every(pos =>
      ((this.bits[Math.floor(pos / 8)] >> (pos % 8)) & 1) === 1
    );
  }

  private positions(item: string): number[] {
    // Double hashing: h(i) = h1(x) + i * h2(x)
    let h1 = 5381, h2 = 52711;
    for (const c of item) {
      const code = c.charCodeAt(0);
      h1 = ((h1 << 5) + h1) ^ code;
      h2 = ((h2 << 5) - h2) ^ code;
    }
    return Array.from({length: this.k}, (_, i) =>
      Math.abs((h1 + i * h2) >>> 0) % this.m
    );
  }

  estimatedFPR(): number {
    return (1 - Math.exp(-this.k * this.count / this.m)) ** this.k;
  }
}

// Usage
const filter = new BloomFilter(1_000_000, 0.01); // 1M items, FPR 1%
filter.add("user@gmail.com");
filter.has("user@gmail.com");   // probably true
filter.has("other@gmail.com");  // definitely false OR ~1% false positive

Go

package bloom

import (
    "math"
    "encoding/binary"
    "crypto/sha256"
)

type BloomFilter struct {
    bits []byte
    m, k uint
}

func New(n uint, fpr float64) *BloomFilter {
    m := uint(-float64(n) * math.Log(fpr) / math.Ln2 / math.Ln2)
    k := uint(float64(m) / float64(n) * math.Ln2)
    return &BloomFilter{bits: make([]byte, (m+7)/8), m: m, k: k}
}

func (bf *BloomFilter) Add(item []byte) {
    h1, h2 := bf.hashes(item)
    for i := uint(0); i < bf.k; i++ {
        pos := (h1 + i*h2) % bf.m
        bf.bits[pos/8] |= 1 << (pos % 8)
    }
}

func (bf *BloomFilter) Has(item []byte) bool {
    h1, h2 := bf.hashes(item)
    for i := uint(0); i < bf.k; i++ {
        pos := (h1 + i*h2) % bf.m
        if bf.bits[pos/8] & (1 << (pos % 8)) == 0 {
            return false
        }
    }
    return true
}

func (bf *BloomFilter) hashes(data []byte) (uint, uint) {
    h := sha256.Sum256(data)
    return uint(binary.BigEndian.Uint64(h[:8])),
           uint(binary.BigEndian.Uint64(h[8:16]))
}
10

Bloom Filter trong phỏng vấn

Bloom Filter là một trong những câu trả lời gây "wow effect" tốt nhất trong system design interview vì ít người junior nghĩ tới.

Pattern nhận diện — khi nào mention

3 dấu hiệu trong đề bài

Dataset rất lớn (triệu+) · Câu hỏi có/không · Muốn fast-reject trước khi tốn I/O · Chấp nhận sai số nhỏ. Thấy đủ 3 dấu hiệu này → propose Bloom Filter ngay.

Các bài system design hay trigger

✉️

Design Gmail registration

Email đã tồn tại chưa? → Bloom Filter fast-reject trước DB.

🔗

Design URL shortener

Short code đã tồn tại? → Filter trước INSERT.

🕷️

Design web crawler

URL đã crawl chưa? → Tỷ URL, chỉ Bloom Filter mới vừa RAM.

🗃️

Design key-value store

Key có trong SSTable không? → Per-SSTable Bloom Filter.

🎬

Design recommendation

User đã xem chưa? → Hàng nghìn tỷ cặp user-item.

🛡️

Design spam filter

IP/email trong blacklist? → Local filter, không cần call DB mỗi request.

Follow-up questions phải biết

Interviewer sẽ drill thêm

"FPR bao nhiêu là acceptable?" — Tuỳ context. Email registration: 0.1–1% ổn vì vẫn verify với DB. Spam filter: có thể chấp nhận 5%.

"User xoá account thì filter làm gì?" — Standard filter không shrink. Giải pháp: Counting BF, hoặc rebuild định kỳ (daily/weekly).

"Multi-node thì sync filter kiểu gì?" — Mỗi node giữ bản local, accept eventual consistency ngắn. Khi cần consistent mạnh: shared Redis BloomFilter.

"Cuckoo Filter khác gì?" — Hỗ trợ delete, lookup nhanh hơn một chút nhờ cache locality tốt hơn.