Bloom Filter em Rust: Estrutura Probabilística

Aprenda Bloom Filter em Rust: conceito probabilístico, múltiplas funções hash, taxa de falsos positivos, zero falsos negativos e implementação completa com exemplos práticos.

O Bloom Filter é uma estrutura de dados probabilística que responde de forma extremamente eficiente à pergunta: “Este elemento pertence ao conjunto?”. A resposta pode ser “definitivamente não” ou “provavelmente sim”. Ele nunca produz falsos negativos, mas pode produzir falsos positivos. Com uso mínimo de memória, é amplamente utilizado em sistemas distribuídos, bancos de dados, web crawlers e verificadores ortográficos.


Conceito e Teoria

Como Funciona?

Um Bloom Filter consiste em um array de bits de tamanho m e k funções hash independentes. Para inserir um elemento, ele é passado por todas as k funções hash, e os bits nas posições resultantes são definidos como 1.

  INSERÇÃO de "rust":

  h1("rust") = 2    h2("rust") = 5    h3("rust") = 9

  Array de bits (m = 12):
  Antes:  [0] [0] [0] [0] [0] [0] [0] [0] [0] [0] [0] [0]
           0   1   2   3   4   5   6   7   8   9  10  11

  Depois: [0] [0] [1] [0] [0] [1] [0] [0] [0] [1] [0] [0]
                   ↑               ↑               ↑
                  h1              h2              h3

Consulta

Para verificar se um elemento pertence ao conjunto, aplicamos as mesmas k funções hash e verificamos se todos os bits correspondentes são 1:

  CONSULTA "rust" -> h1=2, h2=5, h3=9

  [0] [0] [1] [0] [0] [1] [0] [0] [0] [1] [0] [0]
           ↑               ↑               ↑
          bit=1           bit=1           bit=1

  Todos são 1 -> "PROVAVELMENTE SIM" ✓


  CONSULTA "java" -> h1=1, h2=5, h3=7

  [0] [0] [1] [0] [0] [1] [0] [0] [0] [1] [0] [0]
       ↑               ↑           ↑
      bit=0           bit=1       bit=0

  Algum é 0 -> "DEFINITIVAMENTE NÃO" ✗

Falsos Positivos

Quando múltiplos elementos são inseridos, bits podem se sobrepor, causando falsos positivos:

  Inserido: "rust" (bits 2, 5, 9) e "cargo" (bits 1, 5, 11)

  [0] [1] [1] [0] [0] [1] [0] [0] [0] [1] [0] [1]
       ↑   ↑           ↑               ↑       ↑
     cargo rust     ambos            rust    cargo

  Consulta "crate" -> h1=1, h2=2, h3=5

  [0] [1] [1] [0] [0] [1] [0] [0] [0] [1] [0] [1]
       ↑   ↑           ↑
      1!   1!          1!

  Todos são 1 -> "PROVAVELMENTE SIM" (FALSO POSITIVO!)
  "crate" nunca foi inserido, mas os bits coincidem.

Garantias do Bloom Filter

  ┌──────────────────────────────────────────────────┐
  │          GARANTIAS DO BLOOM FILTER                │
  │                                                    │
  │  Se a resposta é NÃO  -> com certeza NÃO está     │
  │                          (ZERO falsos negativos)   │
  │                                                    │
  │  Se a resposta é SIM  -> PROVAVELMENTE está        │
  │                          (possível falso positivo)  │
  │                                                    │
  │  Não suporta remoção  -> bits compartilhados       │
  │                          (Counting Bloom permite)   │
  └──────────────────────────────────────────────────┘

Taxa de Falsos Positivos

A probabilidade de falso positivo é aproximada por:

  p ≈ (1 - e^(-kn/m))^k

  Onde:
    m = tamanho do array de bits
    k = número de funções hash
    n = número de elementos inseridos

  ┌──────────────────────────────────────────┐
  │  Exemplo: 1 milhão de elementos          │
  │                                          │
  │  m = 10M bits (~1.2 MB), k = 7          │
  │  p ≈ 0.82%  (menos de 1% de erro!)      │
  │                                          │
  │  Para comparação:                        │
  │  HashSet<String> para 1M strings ~64 MB  │
  │  Bloom Filter: apenas ~1.2 MB (50x menos)│
  └──────────────────────────────────────────┘

Número Ótimo de Funções Hash

  k_ótimo = (m / n) * ln(2) ≈ 0.693 * (m / n)

  ┌────────────┬────────────┬──────────────────┐
  │ m/n (bits  │ k ótimo    │ Taxa de falso    │
  │ por elem.) │            │ positivo (p)     │
  ├────────────┼────────────┼──────────────────┤
  │     4      │     3      │    14.7%         │
  │     8      │     6      │     2.2%         │
  │    10      │     7      │     0.82%        │
  │    16      │    11      │     0.046%       │
  │    20      │    14      │     0.0032%      │
  └────────────┴────────────┴──────────────────┘

Complexidade

OperaçãoComplexidadeObservação
inserir(x)O(k)k = número de funções hash
consultar(x)O(k)Verifica k bits
EspaçoO(m)m = tamanho do array de bits (fixo)
ConstruirO(n * k)n elementos, k hashes cada

Nota: Como k é uma constante pequena (tipicamente 3-15), na prática todas as operações são O(1).


Implementação em Rust

Bloom Filter Completo

use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};

/// Bloom Filter: estrutura probabilística para teste de pertinência.
/// Garante ZERO falsos negativos, mas permite falsos positivos.
struct BloomFilter {
    /// Array de bits representado como vetor de booleanos
    bits: Vec<bool>,
    /// Tamanho do array de bits
    tamanho: usize,
    /// Número de funções hash
    num_hashes: usize,
    /// Contador de elementos inseridos
    contagem: usize,
}

impl BloomFilter {
    /// Cria um novo Bloom Filter com o tamanho e número de hashes especificados.
    fn novo(tamanho_bits: usize, num_hashes: usize) -> Self {
        BloomFilter {
            bits: vec![false; tamanho_bits],
            tamanho: tamanho_bits,
            num_hashes,
            contagem: 0,
        }
    }

    /// Cria um Bloom Filter otimizado para a quantidade esperada de elementos
    /// e taxa de falsos positivos desejada.
    fn com_taxa_erro(num_elementos: usize, taxa_falso_positivo: f64) -> Self {
        // m = -n * ln(p) / (ln(2))^2
        let m = (-(num_elementos as f64) * taxa_falso_positivo.ln()
            / (2.0_f64.ln().powi(2)))
        .ceil() as usize;

        // k = (m / n) * ln(2)
        let k = ((m as f64 / num_elementos as f64) * 2.0_f64.ln()).ceil() as usize;

        BloomFilter::novo(m.max(1), k.max(1))
    }

    /// Gera k posições de hash para um dado item.
    /// Usa a técnica de double hashing: h(i) = h1 + i * h2
    fn posicoes_hash<T: Hash>(&self, item: &T) -> Vec<usize> {
        let mut posicoes = Vec::with_capacity(self.num_hashes);

        // Hash primário
        let mut hasher1 = DefaultHasher::new();
        item.hash(&mut hasher1);
        let h1 = hasher1.finish();

        // Hash secundário (usa seed diferente)
        let mut hasher2 = DefaultHasher::new();
        item.hash(&mut hasher2);
        // Perturba com uma constante para obter hash diferente
        (h1.wrapping_mul(6364136223846793005)).hash(&mut hasher2);
        let h2 = hasher2.finish();

        for i in 0..self.num_hashes {
            let pos = h1.wrapping_add((i as u64).wrapping_mul(h2)) % self.tamanho as u64;
            posicoes.push(pos as usize);
        }

        posicoes
    }

    /// Insere um elemento no Bloom Filter.
    fn inserir<T: Hash>(&mut self, item: &T) {
        for pos in self.posicoes_hash(item) {
            self.bits[pos] = true;
        }
        self.contagem += 1;
    }

    /// Consulta se um elemento POSSIVELMENTE pertence ao conjunto.
    /// - Retorna false: DEFINITIVAMENTE não está no conjunto.
    /// - Retorna true: PROVAVELMENTE está no conjunto (pode ser falso positivo).
    fn possivelmente_contem<T: Hash>(&self, item: &T) -> bool {
        self.posicoes_hash(item).iter().all(|&pos| self.bits[pos])
    }

    /// Calcula a taxa teórica de falsos positivos com base no estado atual.
    fn taxa_falso_positivo_estimada(&self) -> f64 {
        let m = self.tamanho as f64;
        let k = self.num_hashes as f64;
        let n = self.contagem as f64;
        (1.0 - (-k * n / m).exp()).powf(k)
    }

    /// Retorna a proporção de bits definidos como 1.
    fn ocupacao(&self) -> f64 {
        let definidos = self.bits.iter().filter(|&&b| b).count();
        definidos as f64 / self.tamanho as f64
    }

    /// Exibe o estado interno do Bloom Filter (para fins didáticos).
    fn exibir_estado(&self) {
        println!("Bloom Filter: {} bits, {} hashes, {} elementos",
            self.tamanho, self.num_hashes, self.contagem);
        println!("Ocupação: {:.1}%", self.ocupacao() * 100.0);
        println!("Taxa estimada de falso positivo: {:.4}%",
            self.taxa_falso_positivo_estimada() * 100.0);

        if self.tamanho <= 64 {
            let visual: String = self.bits
                .iter()
                .map(|&b| if b { '█' } else { '░' })
                .collect();
            println!("Bits: [{}]", visual);
        }
    }
}

Bloom Filter com Bitvec (Otimizado)

/// Versão otimizada que usa bits reais em vez de Vec<bool>
/// (cada bool ocupa 1 byte; com bitvec, cada bit ocupa 1 bit)
struct BloomFilterCompacto {
    bits: Vec<u64>,       // Cada u64 armazena 64 bits
    tamanho_bits: usize,  // Total de bits
    num_hashes: usize,
    contagem: usize,
}

impl BloomFilterCompacto {
    fn novo(tamanho_bits: usize, num_hashes: usize) -> Self {
        let num_palavras = (tamanho_bits + 63) / 64; // Arredonda para cima
        BloomFilterCompacto {
            bits: vec![0u64; num_palavras],
            tamanho_bits,
            num_hashes,
            contagem: 0,
        }
    }

    /// Define o bit na posição especificada
    fn definir_bit(&mut self, pos: usize) {
        let palavra = pos / 64;
        let bit = pos % 64;
        self.bits[palavra] |= 1u64 << bit;
    }

    /// Verifica se o bit na posição especificada está definido
    fn obter_bit(&self, pos: usize) -> bool {
        let palavra = pos / 64;
        let bit = pos % 64;
        (self.bits[palavra] >> bit) & 1 == 1
    }

    fn posicoes_hash<T: Hash>(&self, item: &T) -> Vec<usize> {
        let mut hasher = DefaultHasher::new();
        item.hash(&mut hasher);
        let h1 = hasher.finish();

        let mut hasher2 = DefaultHasher::new();
        h1.wrapping_mul(0x517cc1b727220a95).hash(&mut hasher2);
        let h2 = hasher2.finish();

        (0..self.num_hashes)
            .map(|i| {
                (h1.wrapping_add((i as u64).wrapping_mul(h2)) % self.tamanho_bits as u64) as usize
            })
            .collect()
    }

    fn inserir<T: Hash>(&mut self, item: &T) {
        for pos in self.posicoes_hash(item) {
            self.definir_bit(pos);
        }
        self.contagem += 1;
    }

    fn possivelmente_contem<T: Hash>(&self, item: &T) -> bool {
        self.posicoes_hash(item).iter().all(|&pos| self.obter_bit(pos))
    }
}

Exemplo Prático: Deduplicação de URLs em Web Crawler

use std::collections::hash_map::DefaultHasher;
use std::collections::HashSet;
use std::hash::{Hash, Hasher};

/// Bloom Filter (reutilizando a implementação anterior)
struct BloomFilter {
    bits: Vec<bool>,
    tamanho: usize,
    num_hashes: usize,
    contagem: usize,
}

impl BloomFilter {
    fn novo(tamanho_bits: usize, num_hashes: usize) -> Self {
        BloomFilter {
            bits: vec![false; tamanho_bits],
            tamanho: tamanho_bits,
            num_hashes,
            contagem: 0,
        }
    }

    fn posicoes_hash<T: Hash>(&self, item: &T) -> Vec<usize> {
        let mut hasher = DefaultHasher::new();
        item.hash(&mut hasher);
        let h1 = hasher.finish();
        let mut hasher2 = DefaultHasher::new();
        h1.wrapping_mul(0x517cc1b727220a95).hash(&mut hasher2);
        let h2 = hasher2.finish();
        (0..self.num_hashes)
            .map(|i| (h1.wrapping_add((i as u64).wrapping_mul(h2)) % self.tamanho as u64) as usize)
            .collect()
    }

    fn inserir<T: Hash>(&mut self, item: &T) {
        for pos in self.posicoes_hash(item) {
            self.bits[pos] = true;
        }
        self.contagem += 1;
    }

    fn possivelmente_contem<T: Hash>(&self, item: &T) -> bool {
        self.posicoes_hash(item).iter().all(|&pos| self.bits[pos])
    }
}

/// Simulação de um web crawler com deduplicação por Bloom Filter.
struct WebCrawler {
    /// Bloom Filter para verificação rápida (pode ter falsos positivos)
    filtro: BloomFilter,
    /// Fila de URLs a serem processadas
    fila: Vec<String>,
    /// URLs processadas com sucesso
    processadas: usize,
    /// URLs ignoradas pelo filtro (possivelmente duplicadas)
    ignoradas: usize,
}

impl WebCrawler {
    fn novo(urls_esperadas: usize) -> Self {
        // 10 bits por URL, 7 funções hash -> ~0.82% de falso positivo
        let tamanho = urls_esperadas * 10;
        WebCrawler {
            filtro: BloomFilter::novo(tamanho, 7),
            fila: Vec::new(),
            processadas: 0,
            ignoradas: 0,
        }
    }

    /// Adiciona uma URL à fila se ela provavelmente não foi visitada
    fn adicionar_url(&mut self, url: &str) {
        if self.filtro.possivelmente_contem(&url) {
            self.ignoradas += 1;
            // URL provavelmente já foi visitada (ou falso positivo)
        } else {
            self.filtro.inserir(&url);
            self.fila.push(url.to_string());
        }
    }

    /// Processa a próxima URL da fila
    fn processar_proxima(&mut self) -> Option<String> {
        if let Some(url) = self.fila.pop() {
            self.processadas += 1;
            Some(url)
        } else {
            None
        }
    }

    /// Exibe estatísticas do crawler
    fn estatisticas(&self) {
        println!("╔═══════════════════════════════════════╗");
        println!("║     Estatísticas do Web Crawler       ║");
        println!("╠═══════════════════════════════════════╣");
        println!("║ URLs processadas:   {:>6}             ║", self.processadas);
        println!("║ URLs na fila:       {:>6}             ║", self.fila.len());
        println!("║ URLs ignoradas:     {:>6}             ║", self.ignoradas);
        println!("║ Memória do filtro:  {:>6} bits        ║", self.filtro.tamanho);
        println!("╚═══════════════════════════════════════╝");
    }
}

fn main() {
    // === Web Crawler ===
    println!("=== Simulação de Web Crawler ===\n");

    let mut crawler = WebCrawler::novo(10_000);

    // Simulando descoberta de URLs (com duplicatas)
    let urls = vec![
        "https://rustlang.com.br/",
        "https://rustlang.com.br/tutoriais/",
        "https://rustlang.com.br/blog/",
        "https://rustlang.com.br/tutoriais/", // duplicata
        "https://rustlang.com.br/",           // duplicata
        "https://doc.rust-lang.org/",
        "https://crates.io/",
        "https://rustlang.com.br/blog/",      // duplicata
        "https://docs.rs/",
        "https://crates.io/",                 // duplicata
    ];

    for url in &urls {
        println!("Tentando adicionar: {}", url);
        crawler.adicionar_url(url);
    }

    println!();
    crawler.estatisticas();

    // Processando a fila
    println!("\nProcessando URLs:");
    while let Some(url) = crawler.processar_proxima() {
        println!("  Processando: {}", url);
    }

    // === Verificador de palavras (spell checker) ===
    println!("\n=== Verificador Ortográfico Simples ===\n");

    // Dicionário com 100k palavras -> Bloom Filter com ~120KB
    let mut dicionario = BloomFilter::novo(1_000_000, 7);

    // Simulando inserção de um dicionário
    let palavras_validas = vec![
        "rust", "programação", "linguagem", "sistema",
        "memória", "segurança", "concorrência", "compilador",
        "variável", "função", "struct", "enum", "trait",
    ];

    for palavra in &palavras_validas {
        dicionario.inserir(palavra);
    }

    // Verificando palavras
    let texto_teste = vec![
        "rust", "progrmaação", "linguagem", "sistma",
        "memória", "segurnça", "xyz123",
    ];

    for palavra in &texto_teste {
        let existe = dicionario.possivelmente_contem(palavra);
        let status = if existe { "OK" } else { "ERRO?" };
        println!("  {:20} -> {}", palavra, status);
    }

    // === Comparação de memória ===
    println!("\n=== Comparação de Memória ===");
    println!("Para 1 milhão de strings (média 20 bytes):");
    println!("  HashSet: ~48 MB (string + hash + ponteiros)");
    println!("  Bloom Filter (1% erro): ~1.2 MB (apenas bits)");
    println!("  Economia: ~97.5% de memória!");
}

Exercícios

Exercício 1: Counting Bloom Filter

Implemente um Counting Bloom Filter que suporta remoção. Em vez de bits, use contadores (u8). Incrementar na inserção, decrementar na remoção.

struct CountingBloomFilter {
    contadores: Vec<u8>,
    tamanho: usize,
    num_hashes: usize,
}

impl CountingBloomFilter {
    fn novo(tamanho: usize, num_hashes: usize) -> Self { todo!() }
    fn inserir<T: Hash>(&mut self, item: &T) { todo!() }
    fn remover<T: Hash>(&mut self, item: &T) { todo!() }
    fn possivelmente_contem<T: Hash>(&self, item: &T) -> bool { todo!() }
}

Exercício 2: Taxa de Falsos Positivos Empírica

Escreva um programa que:

  1. Crie um Bloom Filter com m = 1000 bits e k = 7 hashes
  2. Insira n = 100 elementos
  3. Teste 10_000 elementos que NÃO foram inseridos
  4. Calcule a taxa empírica de falsos positivos
  5. Compare com a taxa teórica
fn medir_falsos_positivos() -> (f64, f64) {
    // Retorna (taxa_empirica, taxa_teorica)
    todo!()
}

Exercício 3: Bloom Filter Escalável

Implemente um Scalable Bloom Filter que cria novos filtros internos quando a taxa de erro ultrapassa um limite. Cada novo filtro tem o dobro do tamanho.

struct ScalableBloomFilter {
    filtros: Vec<BloomFilter>,
    taxa_erro_alvo: f64,
    elementos_por_filtro: usize,
}

impl ScalableBloomFilter {
    fn novo(taxa_erro: f64, capacidade_inicial: usize) -> Self { todo!() }
    fn inserir<T: Hash>(&mut self, item: &T) { todo!() }
    fn possivelmente_contem<T: Hash>(&self, item: &T) -> bool { todo!() }
}

Conclusão

O Bloom Filter é uma ferramenta poderosa quando:

  • Memória é escassa – usa ordens de magnitude menos memória que um HashSet
  • Falsos positivos são toleráveis – a taxa pode ser controlada precisamente
  • Operações de escrita são permanentes – Bloom Filters clássicos não suportam remoção
  • Velocidade é crítica – todas as operações são O(k) com k pequeno

Casos de uso reais incluem: bancos de dados (Cassandra, HBase), redes CDN (Akamai), navegadores (Google Chrome para URLs maliciosas), caches distribuídos e sistemas de detecção de spam.

O Bloom Filter é um excelente exemplo de como sacrificar certeza absoluta por eficiência extrema pode ser uma decisão inteligente na engenharia de software.


Conteúdo produzido pela Equipe Rust Brasil para rustlang.com.br