chronix_engine/segment/
bloom.rs1use std::collections::HashSet;
19
20const FNV_OFFSET: u64 = 14_695_981_039_346_656_037;
22const FNV_OFFSET_2: u64 = 0x517c_c1b7_2722_0a95;
24const FNV_PRIME: u64 = 1_099_511_628_211;
26
27const K: u32 = 7;
30
31fn fnv1a_seeded(data: &[u8], offset: u64) -> u64 {
33 let mut hash = offset;
34 for &byte in data {
35 hash ^= u64::from(byte);
36 hash = hash.wrapping_mul(FNV_PRIME);
37 }
38 hash
39}
40
41pub fn bloom_filter_build(values: &[&str], false_positive_rate: f64) -> Vec<u8> {
59 if values.is_empty() {
60 return Vec::new();
61 }
62
63 let unique: HashSet<&str> = values.iter().copied().collect();
65 let n = unique.len().max(1);
66
67 let m_f = -(n as f64) * false_positive_rate.ln() / (std::f64::consts::LN_2.powi(2));
69 let m = (m_f as usize).max(8);
70 let m_bytes = m.div_ceil(8);
71 let m_bits = (m_bytes * 8) as u64;
72
73 let mut bits = vec![0u8; m_bytes];
74
75 for &val in &unique {
76 let h1 = fnv1a_seeded(val.as_bytes(), FNV_OFFSET);
77 let h2 = fnv1a_seeded(val.as_bytes(), FNV_OFFSET_2);
78 for i in 0..K {
79 let idx = h1.wrapping_add(u64::from(i).wrapping_mul(h2)) % m_bits;
80 bits[(idx / 8) as usize] |= 1 << (idx % 8);
81 }
82 }
83
84 bits
85}
86
87pub fn bloom_filter_contains(filter: &[u8], value: &str) -> bool {
98 if filter.is_empty() {
99 return false;
100 }
101
102 let m_bits = (filter.len() * 8) as u64;
103 let h1 = fnv1a_seeded(value.as_bytes(), FNV_OFFSET);
104 let h2 = fnv1a_seeded(value.as_bytes(), FNV_OFFSET_2);
105
106 for i in 0..K {
107 let idx = h1.wrapping_add(u64::from(i).wrapping_mul(h2)) % m_bits;
108 if filter[(idx / 8) as usize] & (1 << (idx % 8)) == 0 {
109 return false;
110 }
111 }
112
113 true
114}
115
116#[cfg(test)]
117mod tests {
118 use super::*;
119
120 #[test]
121 fn build_contains_known_values() {
122 let values = vec!["server-1", "server-2", "server-3", "us-east-1", "us-west-2"];
123 let filter = bloom_filter_build(&values, 0.01);
124
125 for &v in &values {
127 assert!(
128 bloom_filter_contains(&filter, v),
129 "bloom filter should contain '{v}'"
130 );
131 }
132 }
133
134 #[test]
135 fn rejects_unknown_values() {
136 let owned: Vec<String> = (0..100).map(|i| format!("host-{i}")).collect();
138 let values: Vec<&str> = owned.iter().map(String::as_str).collect();
139 let filter = bloom_filter_build(&values, 0.01);
140
141 let mut false_positives = 0;
144 for i in 1000..2000 {
145 let test_val = format!("unknown-{i}");
146 if bloom_filter_contains(&filter, &test_val) {
147 false_positives += 1;
148 }
149 }
150
151 assert!(
153 false_positives < 50,
154 "too many false positives: {false_positives}/1000 (expected <50)"
155 );
156 }
157
158 #[test]
159 fn empty_filter() {
160 let filter = bloom_filter_build(&[], 0.01);
161 assert!(filter.is_empty());
162 assert!(!bloom_filter_contains(&filter, "anything"));
163 }
164
165 #[test]
166 fn single_value() {
167 let filter = bloom_filter_build(&["hello"], 0.01);
168 assert!(bloom_filter_contains(&filter, "hello"));
169 assert!(!bloom_filter_contains(&filter, "world"));
170 }
171
172 #[test]
173 fn duplicate_values_handled() {
174 let values = vec!["a", "a", "b", "b", "c", "c"];
175 let filter = bloom_filter_build(&values, 0.01);
176 assert!(bloom_filter_contains(&filter, "a"));
177 assert!(bloom_filter_contains(&filter, "b"));
178 assert!(bloom_filter_contains(&filter, "c"));
179 }
180}