pub struct BloomFilter {
bits: Vec<u64>,
num_bits: usize,
num_hashes: usize,
item_count: usize,
}
impl BloomFilter {
pub fn new(capacity: usize, false_positive_rate: f64) -> Self {
let capacity = capacity.max(1);
let fp = false_positive_rate.clamp(1e-10, 1.0 - 1e-10);
let ln2 = core::f64::consts::LN_2;
let m = (-(capacity as f64) * fp.ln() / (ln2 * ln2)).ceil() as usize;
let num_bits = m.max(64);
let k = ((num_bits as f64 / capacity as f64) * ln2).round() as usize;
let num_hashes = k.max(1);
let words = (num_bits + 63) / 64;
Self {
bits: vec![0u64; words],
num_bits,
num_hashes,
item_count: 0,
}
}
pub fn insert(&mut self, item: &str) {
let (h1, h2) = Self::double_hash(item, self.num_bits);
for i in 0..self.num_hashes {
let bit = (h1.wrapping_add(i.wrapping_mul(h2))) % self.num_bits;
self.bits[bit / 64] |= 1u64 << (bit % 64);
}
self.item_count += 1;
}
pub fn might_contain(&self, item: &str) -> bool {
let (h1, h2) = Self::double_hash(item, self.num_bits);
for i in 0..self.num_hashes {
let bit = (h1.wrapping_add(i.wrapping_mul(h2))) % self.num_bits;
if self.bits[bit / 64] & (1u64 << (bit % 64)) == 0 {
return false;
}
}
true
}
pub fn item_count(&self) -> usize {
self.item_count
}
pub fn bit_count(&self) -> usize {
self.num_bits
}
pub fn estimated_false_positive_rate(&self) -> f64 {
let k = self.num_hashes as f64;
let n = self.item_count as f64;
let m = self.num_bits as f64;
let fill = (-k * n / m).exp();
(1.0 - fill).powf(k)
}
fn double_hash(s: &str, num_bits: usize) -> (usize, usize) {
const FNV_PRIME_64: u64 = 1_099_511_628_211;
const FNV_OFFSET_64: u64 = 14_695_981_039_346_656_037;
let mut h1 = FNV_OFFSET_64;
let mut h2 = FNV_OFFSET_64 ^ 0xdead_beef_cafe_1234;
for byte in s.bytes() {
h1 ^= byte as u64;
h1 = h1.wrapping_mul(FNV_PRIME_64);
h2 ^= (byte as u64).wrapping_add(7);
h2 = h2.wrapping_mul(FNV_PRIME_64);
}
((h1 as usize) % num_bits, (h2 as usize) % num_bits + 1)
}
}
pub struct TripleBloomIndex {
filter: BloomFilter,
}
impl TripleBloomIndex {
pub fn new(capacity: usize, fp_rate: f64) -> Self {
Self {
filter: BloomFilter::new(capacity, fp_rate),
}
}
pub fn insert(&mut self, subj: &str, pred: &str, obj: &str) {
let key = Self::triple_key(subj, pred, obj);
self.filter.insert(&key);
}
pub fn might_contain(&self, subj: &str, pred: &str, obj: &str) -> bool {
let key = Self::triple_key(subj, pred, obj);
self.filter.might_contain(&key)
}
pub fn item_count(&self) -> usize {
self.filter.item_count()
}
fn triple_key(subj: &str, pred: &str, obj: &str) -> String {
format!("{}\x00{}\x00{}", subj, pred, obj)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_bloom_insert_and_contains() {
let mut bf = BloomFilter::new(100, 0.01);
bf.insert("hello");
assert!(bf.might_contain("hello"));
}
#[test]
fn test_bloom_unknown_item_typically_false() {
let bf = BloomFilter::new(100, 0.01);
assert!(!bf.might_contain("never_inserted"));
}
#[test]
fn test_bloom_item_count_increments() {
let mut bf = BloomFilter::new(100, 0.01);
assert_eq!(bf.item_count(), 0);
bf.insert("a");
assert_eq!(bf.item_count(), 1);
bf.insert("b");
assert_eq!(bf.item_count(), 2);
}
#[test]
fn test_bloom_bit_count_positive() {
let bf = BloomFilter::new(1000, 0.01);
assert!(bf.bit_count() >= 64);
}
#[test]
fn test_bloom_bit_count_larger_capacity_more_bits() {
let bf_small = BloomFilter::new(100, 0.01);
let bf_large = BloomFilter::new(10_000, 0.01);
assert!(bf_large.bit_count() > bf_small.bit_count());
}
#[test]
fn test_bloom_fp_rate_zero_when_empty() {
let bf = BloomFilter::new(100, 0.01);
assert_eq!(bf.estimated_false_positive_rate(), 0.0);
}
#[test]
fn test_bloom_fp_rate_increases_with_fill() {
let mut bf = BloomFilter::new(10, 0.05);
let initial = bf.estimated_false_positive_rate();
for i in 0..10 {
bf.insert(&format!("item_{}", i));
}
let after = bf.estimated_false_positive_rate();
assert!(after > initial);
}
#[test]
fn test_bloom_multiple_inserts_same_key() {
let mut bf = BloomFilter::new(100, 0.01);
bf.insert("dup");
bf.insert("dup");
assert_eq!(bf.item_count(), 2);
assert!(bf.might_contain("dup"));
}
#[test]
fn test_bloom_many_items_all_found() {
let mut bf = BloomFilter::new(1000, 0.01);
let items: Vec<String> = (0..100).map(|i| format!("item_{}", i)).collect();
for item in &items {
bf.insert(item);
}
for item in &items {
assert!(bf.might_contain(item), "item '{}' not found", item);
}
}
#[test]
fn test_bloom_empty_string() {
let mut bf = BloomFilter::new(10, 0.01);
bf.insert("");
assert!(bf.might_contain(""));
assert!(!bf.might_contain("x"));
}
#[test]
fn test_triple_bloom_insert_and_contains() {
let mut idx = TripleBloomIndex::new(100, 0.01);
idx.insert("http://a.org/s", "http://a.org/p", "http://a.org/o");
assert!(idx.might_contain("http://a.org/s", "http://a.org/p", "http://a.org/o"));
}
#[test]
fn test_triple_bloom_unknown_triple_false() {
let idx = TripleBloomIndex::new(100, 0.01);
assert!(!idx.might_contain("http://a.org/s", "http://a.org/p", "http://a.org/o"));
}
#[test]
fn test_triple_bloom_item_count() {
let mut idx = TripleBloomIndex::new(100, 0.01);
assert_eq!(idx.item_count(), 0);
idx.insert("s", "p", "o");
assert_eq!(idx.item_count(), 1);
}
#[test]
fn test_triple_bloom_multiple_triples() {
let mut idx = TripleBloomIndex::new(100, 0.01);
let triples = vec![("s1", "p1", "o1"), ("s2", "p2", "o2"), ("s3", "p3", "o3")];
for (s, p, o) in &triples {
idx.insert(s, p, o);
}
assert_eq!(idx.item_count(), 3);
for (s, p, o) in &triples {
assert!(idx.might_contain(s, p, o));
}
}
#[test]
fn test_triple_bloom_same_subject_different_predicate() {
let mut idx = TripleBloomIndex::new(100, 0.01);
idx.insert("s", "p1", "o");
assert!(idx.might_contain("s", "p1", "o"));
assert!(!idx.might_contain("s", "p2", "o"));
}
#[test]
fn test_triple_bloom_permutations_distinct() {
let mut idx = TripleBloomIndex::new(100, 0.01);
idx.insert("a", "b", "c");
assert!(!idx.might_contain("b", "a", "c"));
}
}