#![allow(clippy::module_name_repetitions)]
use crate::core::filter::{BloomFilter, MutableBloomFilter};
use crate::error::{BloomCraftError, Result};
use crate::filters::standard::StandardBloomFilter;
use crate::hash::{BloomHasher, StdHasher};
use std::fmt;
use std::hash::Hash;
use std::marker::PhantomData;
use std::time::{SystemTime, UNIX_EPOCH};
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
pub const MAX_FILTERS: usize = 64;
pub(crate) const MIN_FPR: f64 = 1e-15;
pub(crate) const DEFAULT_FILL_THRESHOLD: f64 = 0.5;
pub(crate) const CAPACITY_WARNING_THRESHOLD: usize = 5;
const HLL_PRECISION: u8 = 14;
const HLL_REGISTER_COUNT: usize = 1 << HLL_PRECISION;
const HLL_REGISTER_MASK: u64 = (HLL_REGISTER_COUNT - 1) as u64;
const ALPHA_INF: f64 = 0.7213 / (1.0 + 1.079 / HLL_REGISTER_COUNT as f64);
const SMALL_RANGE_THRESHOLD: f64 = (5.0 / 2.0) * HLL_REGISTER_COUNT as f64;
const LARGE_RANGE_THRESHOLD: f64 = (1u64 << 32) as f64 / 30.0;
const MAX_GROWTH_HISTORY: usize = 128;
pub(crate) struct InternalHasher(u64);
impl InternalHasher {
const OFFSET_BASIS: u64 = 0xcbf2_9ce4_8422_2325;
const PRIME: u64 = 0x0000_0100_0000_01B3;
#[inline]
fn new() -> Self {
Self(Self::OFFSET_BASIS)
}
#[inline]
pub(crate) fn hash_one<T: Hash>(item: &T) -> u64 {
let mut h = Self::new();
item.hash(&mut h);
Self::fmix64(h.0)
}
#[inline]
fn fmix64(mut k: u64) -> u64 {
k ^= k >> 33;
k = k.wrapping_mul(0xff51afd7ed558ccd);
k ^= k >> 33;
k = k.wrapping_mul(0xc4ceb9fe1a85ec53);
k ^= k >> 33;
k
}
}
impl std::hash::Hasher for InternalHasher {
#[inline]
fn finish(&self) -> u64 {
self.0
}
#[inline]
fn write(&mut self, bytes: &[u8]) {
for &b in bytes {
self.0 ^= b as u64;
self.0 = self.0.wrapping_mul(Self::PRIME);
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum GrowthStrategy {
Constant,
Geometric(f64),
Adaptive {
initial_ratio: f64,
min_ratio: f64,
max_ratio: f64,
},
Bounded {
scale: f64,
max_filter_size: usize,
},
}
impl Default for GrowthStrategy {
fn default() -> Self {
Self::Geometric(2.0)
}
}
impl GrowthStrategy {
pub(crate) fn validate(&self) -> Result<()> {
match self {
GrowthStrategy::Geometric(scale) => {
if *scale <= 1.0 {
return Err(BloomCraftError::invalid_parameters(format!(
"Geometric growth scale must be > 1.0, got {}",
scale
)));
}
if !scale.is_finite() {
return Err(BloomCraftError::invalid_parameters(
"Geometric growth scale must be finite",
));
}
Ok(())
}
GrowthStrategy::Bounded { scale, .. } => {
if *scale <= 0.0 {
return Err(BloomCraftError::invalid_parameters(format!(
"Bounded growth scale must be > 0.0, got {}",
scale
)));
}
if !scale.is_finite() {
return Err(BloomCraftError::invalid_parameters(
"Bounded growth scale must be finite",
));
}
Ok(())
}
_ => Ok(()),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Default)]
pub enum CapacityExhaustedBehavior {
#[default]
Silent,
Error,
#[cfg(debug_assertions)]
Panic,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Default)]
pub enum QueryStrategy {
Forward,
#[default]
Reverse,
}
#[derive(Debug, Clone)]
pub struct HyperLogLog {
registers: Box<[u8; HLL_REGISTER_COUNT]>,
sparse: Option<std::collections::HashMap<u16, u8>>,
sparse_threshold: usize,
}
impl HyperLogLog {
#[must_use]
pub fn new() -> Self {
Self {
registers: Box::new([0; HLL_REGISTER_COUNT]),
sparse: Some(std::collections::HashMap::new()),
sparse_threshold: HLL_REGISTER_COUNT / 4,
}
}
pub fn add<T: Hash>(&mut self, item: &T) {
let hash = InternalHasher::hash_one(item);
let register_idx = (hash & HLL_REGISTER_MASK) as usize;
let remaining = hash >> HLL_PRECISION;
let leading_zeros: u8 = if remaining == 0 {
(64u8 - HLL_PRECISION) + 1
} else {
remaining.leading_zeros() as u8 - HLL_PRECISION + 1
};
if let Some(ref mut sparse) = self.sparse {
let current = sparse.get(&(register_idx as u16)).copied().unwrap_or(0);
if leading_zeros > current {
sparse.insert(register_idx as u16, leading_zeros);
}
if sparse.len() > self.sparse_threshold {
self.convert_to_dense();
}
} else if leading_zeros > self.registers[register_idx] {
self.registers[register_idx] = leading_zeros;
}
}
#[must_use]
pub fn estimate(&self) -> usize {
if let Some(ref sparse) = self.sparse {
return self.estimate_sparse(sparse);
}
self.estimate_dense()
}
pub fn merge(&mut self, other: &Self) {
match (&mut self.sparse, &other.sparse) {
(Some(self_sparse), Some(other_sparse)) => {
for (&idx, &val) in other_sparse.iter() {
let entry = self_sparse.entry(idx).or_insert(0);
*entry = (*entry).max(val);
}
if self_sparse.len() > self.sparse_threshold {
self.convert_to_dense();
}
}
(Some(_), None) => {
self.convert_to_dense();
for i in 0..HLL_REGISTER_COUNT {
self.registers[i] = self.registers[i].max(other.registers[i]);
}
}
(None, Some(other_sparse)) => {
for (&idx, &val) in other_sparse.iter() {
let i = idx as usize;
self.registers[i] = self.registers[i].max(val);
}
}
(None, None) => {
for i in 0..HLL_REGISTER_COUNT {
self.registers[i] = self.registers[i].max(other.registers[i]);
}
}
}
}
fn convert_to_dense(&mut self) {
if let Some(sparse) = self.sparse.take() {
for (idx, val) in sparse.iter() {
self.registers[*idx as usize] = *val;
}
}
}
fn estimate_sparse(&self, sparse: &std::collections::HashMap<u16, u8>) -> usize {
if sparse.is_empty() {
return 0;
}
if sparse.len() < 50 {
return sparse.len();
}
let mut sum = 0.0;
let mut zero_count = HLL_REGISTER_COUNT;
for i in 0..HLL_REGISTER_COUNT {
let val = sparse.get(&(i as u16)).copied().unwrap_or(0);
if val > 0 {
zero_count -= 1;
}
sum += 2f64.powi(-(val as i32));
}
let raw_estimate = ALPHA_INF * (HLL_REGISTER_COUNT as f64).powi(2) / sum;
if raw_estimate <= SMALL_RANGE_THRESHOLD && zero_count > 0 {
(HLL_REGISTER_COUNT as f64 * (HLL_REGISTER_COUNT as f64 / zero_count as f64).ln())
as usize
} else {
raw_estimate as usize
}
}
fn estimate_dense(&self) -> usize {
let mut sum = 0.0;
let mut zero_count = 0;
for ®ister in self.registers.iter() {
if register == 0 {
zero_count += 1;
}
sum += 2f64.powi(-(register as i32));
}
let raw_estimate = ALPHA_INF * (HLL_REGISTER_COUNT as f64).powi(2) / sum;
if raw_estimate <= SMALL_RANGE_THRESHOLD {
if zero_count > 0 {
(HLL_REGISTER_COUNT as f64 * (HLL_REGISTER_COUNT as f64 / zero_count as f64).ln())
as usize
} else {
raw_estimate as usize
}
} else if raw_estimate <= LARGE_RANGE_THRESHOLD {
raw_estimate as usize
} else {
let corrected =
-((1u64 << 32) as f64) * (1.0 - raw_estimate / (1u64 << 32) as f64).ln();
corrected as usize
}
}
#[must_use]
pub fn memory_usage(&self) -> usize {
std::mem::size_of::<Self>()
+ HLL_REGISTER_COUNT
+ self.sparse.as_ref().map(|s| s.capacity() * 3).unwrap_or(0)
}
}
impl Default for HyperLogLog {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
#[allow(dead_code)]
struct GrowthEvent {
timestamp: u64, filter_index: usize, capacity: usize, fpr: f64, total_items: usize, }
#[derive(Debug, Clone, PartialEq)]
pub struct ScalableHealthMetrics {
pub filter_count: usize,
pub total_capacity: usize,
pub total_items: usize,
pub estimated_fpr: f64,
pub max_fpr: f64,
pub target_fpr: f64,
pub current_error_ratio: f64,
pub current_fill_rate: f64,
pub avg_fill_rate: f64,
pub memory_bytes: usize,
pub remaining_growth: usize,
pub growth_events: usize,
pub query_strategy: QueryStrategy,
}
impl fmt::Display for ScalableHealthMetrics {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(f, "ScalableBloomFilter Health Metrics")?;
writeln!(f, "==================================")?;
writeln!(f, "Filters: {}", self.filter_count)?;
writeln!(f, "Total capacity: {}", self.total_capacity)?;
writeln!(f, "Total items: {}", self.total_items)?;
writeln!(f, "Estimated FPR: {:.4}%", self.estimated_fpr * 100.0)?;
writeln!(f, "Max FPR (bound): {:.4}%", self.max_fpr * 100.0)?;
writeln!(f, "Target FPR: {:.4}%", self.target_fpr * 100.0)?;
writeln!(f, "Error ratio: {:.3}", self.current_error_ratio)?;
writeln!(
f,
"Current fill: {:.1}%",
self.current_fill_rate * 100.0
)?;
writeln!(f, "Avg fill: {:.1}%", self.avg_fill_rate * 100.0)?;
writeln!(f, "Memory usage: {} bytes", self.memory_bytes)?;
writeln!(f, "Remaining growth: {} filters", self.remaining_growth)?;
writeln!(f, "Growth events: {}", self.growth_events)?;
writeln!(f, "Query strategy: {:?}", self.query_strategy)?;
Ok(())
}
}
#[cfg(feature = "trace")]
pub mod trace {
use std::time::{Duration, Instant};
#[derive(Debug, Clone)]
pub struct QueryTrace {
pub total_duration: Duration,
pub filter_traces: Vec<FilterTrace>,
pub early_terminated: bool,
pub matched_filter: Option<usize>,
pub total_bits_checked: usize,
pub strategy: String,
}
#[derive(Debug, Clone)]
pub struct FilterTrace {
pub index: usize,
pub duration: Duration,
pub matched: bool,
pub hashes_checked: usize,
pub bits_checked: usize,
pub fill_rate: f64,
}
impl QueryTrace {
#[must_use]
pub fn new() -> Self {
Self {
total_duration: Duration::ZERO,
filter_traces: Vec::new(),
early_terminated: false,
matched_filter: None,
total_bits_checked: 0,
strategy: String::from("unknown"),
}
}
#[must_use]
pub fn format_detailed(&self) -> String {
let mut output = String::new();
output.push_str(&format!("Query Trace ({})\n", self.strategy));
output.push_str(&format!("Total duration: {:?}\n", self.total_duration));
output.push_str(&format!("Early terminated: {}\n", self.early_terminated));
output.push_str(&format!("Matched filter: {:?}\n", self.matched_filter));
output.push_str(&format!(
"Total bits checked: {}\n",
self.total_bits_checked
));
output.push_str("Filters checked:\n");
for ft in &self.filter_traces {
output.push_str(&format!(
" [{}] {:?} | matched: {} | fill: {:.1}% | bits: {}\n",
ft.index,
ft.duration,
ft.matched,
ft.fill_rate * 100.0,
ft.bits_checked
));
}
output
}
}
impl Default for QueryTrace {
fn default() -> Self {
Self::new()
}
}
pub struct QueryTraceBuilder {
trace: QueryTrace,
start_time: Instant,
}
impl QueryTraceBuilder {
#[must_use]
pub fn new(strategy: &str) -> Self {
let mut trace = QueryTrace::new();
trace.strategy = strategy.to_string();
Self {
trace,
start_time: Instant::now(),
}
}
pub fn record_filter(
&mut self,
index: usize,
matched: bool,
hashes_checked: usize,
bits_checked: usize,
fill_rate: f64,
start: Instant,
) {
self.trace.filter_traces.push(FilterTrace {
index,
duration: start.elapsed(),
matched,
hashes_checked,
bits_checked,
fill_rate,
});
self.trace.total_bits_checked += bits_checked;
if matched {
self.trace.matched_filter = Some(index);
}
}
#[must_use]
pub fn finish(mut self) -> QueryTrace {
self.trace.total_duration = self.start_time.elapsed();
self.trace
}
}
}
#[cfg(feature = "trace")]
pub use trace::{QueryTrace, QueryTraceBuilder};
#[derive(Debug)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(
feature = "serde",
serde(bound(
serialize = "H: BloomHasher + Clone + Default",
deserialize = "H: BloomHasher + Clone + Default"
))
)]
pub struct ScalableBloomFilter<T, H = StdHasher>
where
T: Hash,
H: BloomHasher + Clone + Default,
{
filters: Vec<StandardBloomFilter<T, H>>,
#[cfg_attr(feature = "serde", serde(skip))]
filter_nonempty: Vec<bool>,
initial_capacity: usize,
target_fpr: f64,
error_ratio: f64,
growth: GrowthStrategy,
fill_threshold: f64,
#[cfg_attr(feature = "serde", serde(skip))]
hasher: H,
total_items: usize,
#[cfg_attr(feature = "serde", serde(skip))]
items_in_current_filter: usize,
#[cfg_attr(feature = "serde", serde(skip))]
current_filter_threshold: usize,
capacity_behavior: CapacityExhaustedBehavior,
query_strategy: QueryStrategy,
#[cfg_attr(feature = "serde", serde(skip))]
growth_history: std::collections::VecDeque<GrowthEvent>,
#[cfg_attr(feature = "serde", serde(skip))]
cardinality_sketches: Vec<HyperLogLog>,
track_cardinality: bool,
_phantom: PhantomData<T>,
}
impl<T, H> Clone for ScalableBloomFilter<T, H>
where
T: Hash,
H: BloomHasher + Clone + Default,
{
fn clone(&self) -> Self {
Self {
filters: self.filters.clone(),
filter_nonempty: self.filter_nonempty.clone(),
initial_capacity: self.initial_capacity,
target_fpr: self.target_fpr,
error_ratio: self.error_ratio,
growth: self.growth,
fill_threshold: self.fill_threshold,
hasher: self.hasher.clone(),
total_items: self.total_items,
items_in_current_filter: self.items_in_current_filter,
current_filter_threshold: self.current_filter_threshold,
capacity_behavior: self.capacity_behavior,
query_strategy: self.query_strategy,
growth_history: self.growth_history.clone(),
cardinality_sketches: self.cardinality_sketches.clone(),
track_cardinality: self.track_cardinality,
_phantom: PhantomData,
}
}
}
impl<T> ScalableBloomFilter<T, StdHasher>
where
T: Hash,
{
pub fn new(initial_capacity: usize, target_fpr: f64) -> Result<Self> {
Self::with_hasher(initial_capacity, target_fpr, StdHasher::new())
}
pub fn with_strategy(
initial_capacity: usize,
target_fpr: f64,
error_ratio: f64,
growth: GrowthStrategy,
) -> Result<Self> {
Self::with_strategy_and_hasher(
initial_capacity,
target_fpr,
error_ratio,
growth,
StdHasher::new(),
)
}
}
impl<T, H> ScalableBloomFilter<T, H>
where
T: Hash,
H: BloomHasher + Clone + Default,
{
pub fn with_hasher(initial_capacity: usize, target_fpr: f64, hasher: H) -> Result<Self> {
Self::with_strategy_and_hasher(
initial_capacity,
target_fpr,
0.5,
GrowthStrategy::default(),
hasher,
)
}
pub fn with_strategy_and_hasher(
initial_capacity: usize,
target_fpr: f64,
error_ratio: f64,
growth: GrowthStrategy,
hasher: H,
) -> Result<Self> {
growth.validate()?;
if initial_capacity == 0 {
return Err(BloomCraftError::invalid_item_count(initial_capacity));
}
if target_fpr <= 0.0 || target_fpr >= 1.0 {
return Err(BloomCraftError::fp_rate_out_of_bounds(target_fpr));
}
match growth {
GrowthStrategy::Adaptive {
initial_ratio,
min_ratio,
max_ratio,
} => {
if min_ratio <= 0.0 || min_ratio >= 1.0 {
return Err(BloomCraftError::invalid_parameters(format!(
"Adaptive growth min_ratio must be in (0.0, 1.0), got {}",
min_ratio
)));
}
if max_ratio <= 0.0 || max_ratio >= 1.0 {
return Err(BloomCraftError::invalid_parameters(format!(
"Adaptive growth max_ratio must be in (0.0, 1.0), got {}",
max_ratio
)));
}
if min_ratio > initial_ratio || initial_ratio > max_ratio {
return Err(BloomCraftError::invalid_parameters(format!(
"Adaptive growth requires min_ratio ({}) ≤ initial_ratio ({}) ≤ max_ratio ({})",
min_ratio, initial_ratio, max_ratio
)));
}
}
_ => {
if error_ratio <= 0.0 || error_ratio >= 1.0 {
return Err(BloomCraftError::invalid_parameters(format!(
"error_ratio must be in (0.0, 1.0), got {}",
error_ratio
)));
}
}
}
let mut filter = Self {
filters: Vec::new(),
filter_nonempty: Vec::new(),
initial_capacity,
target_fpr,
error_ratio,
growth,
fill_threshold: DEFAULT_FILL_THRESHOLD,
hasher: hasher.clone(),
total_items: 0,
items_in_current_filter: 0,
current_filter_threshold: 0,
capacity_behavior: CapacityExhaustedBehavior::default(),
query_strategy: QueryStrategy::default(),
growth_history: std::collections::VecDeque::new(),
cardinality_sketches: Vec::new(),
track_cardinality: false,
_phantom: PhantomData,
};
filter.try_add_filter()?;
Ok(filter)
}
#[must_use]
pub fn with_capacity_behavior(mut self, behavior: CapacityExhaustedBehavior) -> Self {
self.capacity_behavior = behavior;
self
}
#[must_use]
pub fn with_query_strategy(mut self, strategy: QueryStrategy) -> Self {
self.query_strategy = strategy;
self
}
#[must_use]
pub fn with_cardinality_tracking(mut self) -> Self {
self.track_cardinality = true;
self.cardinality_sketches = vec![HyperLogLog::new()];
self
}
fn try_add_filter(&mut self) -> Result<()> {
let filter_index = self.filters.len();
if filter_index >= MAX_FILTERS {
return Err(BloomCraftError::max_filters_exceeded(
MAX_FILTERS,
filter_index,
));
}
let capacity = self.calculate_next_capacity(filter_index)?;
let fpr = self.calculate_next_fpr(filter_index);
let new_filter = StandardBloomFilter::with_hasher(capacity, fpr, self.hasher.clone());
self.filters.push(new_filter?);
self.filter_nonempty.push(false);
if self.track_cardinality {
self.cardinality_sketches.push(HyperLogLog::new());
}
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
self.growth_history.push_back(GrowthEvent {
timestamp,
filter_index,
capacity,
fpr,
total_items: self.total_items,
});
if self.growth_history.len() > MAX_GROWTH_HISTORY {
self.growth_history.pop_front();
}
self.items_in_current_filter = 0;
self.current_filter_threshold = ((capacity as f64) * self.fill_threshold).ceil() as usize;
self.current_filter_threshold = self.current_filter_threshold.max(1);
Ok(())
}
fn calculate_next_capacity(&self, filter_index: usize) -> Result<usize> {
const MAX_CAP: f64 = usize::MAX as f64;
let capacity = match self.growth {
GrowthStrategy::Constant => self.initial_capacity,
GrowthStrategy::Geometric(scale) => {
if filter_index == 0 {
self.initial_capacity
} else {
let scale_log = scale.ln();
let max_safe_exp =
(MAX_CAP.ln() - (self.initial_capacity as f64).ln()) / scale_log;
if filter_index as f64 >= max_safe_exp {
return Err(BloomCraftError::invalid_parameters(format!(
"Filter index {} would cause capacity overflow (max safe: {:.1})",
filter_index, max_safe_exp
)));
}
let computed = self.initial_capacity as f64 * scale.powi(filter_index as i32);
if computed > MAX_CAP || !computed.is_finite() {
return Err(BloomCraftError::invalid_parameters(format!(
"Computed capacity {:.2e} exceeds usize::MAX",
computed
)));
}
let new_cap = computed as usize;
if new_cap < self.initial_capacity {
return Err(BloomCraftError::invalid_parameters(
"Capacity calculation resulted in wraparound",
));
}
new_cap
}
}
GrowthStrategy::Adaptive { .. } => {
if filter_index == 0 {
self.initial_capacity
} else {
const SCALE: f64 = 2.0;
let max_safe_exp =
(MAX_CAP.ln() - (self.initial_capacity as f64).ln()) / SCALE.ln();
if filter_index as f64 >= max_safe_exp {
return Err(BloomCraftError::invalid_parameters(format!(
"Adaptive filter index {} would cause capacity overflow",
filter_index
)));
}
let computed = self.initial_capacity as f64 * SCALE.powi(filter_index as i32);
if computed > MAX_CAP || !computed.is_finite() {
return Err(BloomCraftError::invalid_parameters(
"Adaptive capacity overflow",
));
}
let new_cap = computed as usize;
if new_cap < self.initial_capacity {
return Err(BloomCraftError::invalid_parameters(
"Adaptive capacity wraparound",
));
}
new_cap
}
}
GrowthStrategy::Bounded {
scale,
max_filter_size,
} => {
if filter_index == 0 {
self.initial_capacity
} else {
let max_safe_exp =
(MAX_CAP.ln() - (self.initial_capacity as f64).ln()) / scale.ln();
if filter_index as f64 >= max_safe_exp {
return Err(BloomCraftError::invalid_parameters(format!(
"Bounded filter index {} would cause capacity overflow (max safe: {:.1})",
filter_index, max_safe_exp
)));
}
let computed = self.initial_capacity as f64 * scale.powi(filter_index as i32);
if computed > MAX_CAP || !computed.is_finite() {
return Err(BloomCraftError::invalid_parameters(
"Bounded capacity calculation overflow",
));
}
(computed as usize).min(max_filter_size)
}
}
};
Ok(capacity)
}
fn calculate_next_fpr(&mut self, filter_index: usize) -> f64 {
let ratio = match self.growth {
GrowthStrategy::Adaptive {
initial_ratio: _,
min_ratio,
max_ratio,
} => {
if filter_index > 0 && !self.filters.is_empty() {
let last_filter = &self.filters[filter_index - 1];
let actual_fill = last_filter.fill_rate();
if actual_fill > self.fill_threshold * 1.2 {
self.error_ratio = (self.error_ratio * 0.9).max(min_ratio);
} else if actual_fill < self.fill_threshold * 0.8 {
self.error_ratio = (self.error_ratio * 1.1).min(max_ratio);
}
}
self.error_ratio
}
_ => self.error_ratio,
};
const MAX_SAFE_EXP: i32 = 1000;
let safe_index = (filter_index as i32).min(MAX_SAFE_EXP);
let raw_fpr = self.target_fpr * ratio.powi(safe_index);
raw_fpr.clamp(MIN_FPR, 1.0)
}
#[inline]
fn should_grow(&self) -> bool {
if self.filters.is_empty() {
#[cfg(debug_assertions)]
eprintln!("[ScalableBloomFilter] WARNING: No filters exist, forcing growth");
return true;
}
if self.current_filter_threshold == 0 {
return self
.filters
.last()
.map(|f| f.fill_rate() >= self.fill_threshold)
.unwrap_or(true);
}
let should = self.items_in_current_filter >= self.current_filter_threshold;
#[cfg(debug_assertions)]
if should {
let current = self.filters.last().unwrap();
eprintln!(
"[ScalableBloomFilter] Growing: filter {} reached {} items (threshold {}), capacity {}, items {}",
self.filters.len(),
self.items_in_current_filter,
self.current_filter_threshold,
current.expected_items(),
current.len()
);
}
should
}
fn recalibrate_grow_state(&mut self) {
if let Some(current) = self.filters.last() {
let fill = current.fill_rate();
let estimated = (fill * current.expected_items() as f64).round() as usize;
self.items_in_current_filter = estimated;
self.current_filter_threshold =
((current.expected_items() as f64) * self.fill_threshold).ceil() as usize;
self.current_filter_threshold = self.current_filter_threshold.max(1);
}
self.filter_nonempty = self.filters.iter().map(|f| f.fill_rate() > 0.0).collect();
}
#[cfg(debug_assertions)]
fn assert_nonempty_invariant(&self) {
assert_eq!(
self.filters.len(),
self.filter_nonempty.len(),
"filter_nonempty length {} != filters length {}",
self.filter_nonempty.len(),
self.filters.len()
);
for (i, (filter, &flag)) in self
.filters
.iter()
.zip(self.filter_nonempty.iter())
.enumerate()
{
if flag {
debug_assert!(
filter.fill_rate() > 0.0,
"filter_nonempty[{i}] is true but filter {i} has fill_rate 0.0 — invariant violated"
);
}
}
}
}
impl<T, H> ScalableBloomFilter<T, H>
where
T: Hash,
H: BloomHasher + Clone + Default,
{
pub fn insert_checked(&mut self, item: &T) -> Result<()> {
if self.current_filter_threshold == 0 && !self.filters.is_empty() {
self.recalibrate_grow_state();
}
if self.should_grow() {
match self.try_add_filter() {
Ok(()) => {}
Err(e) => match self.capacity_behavior {
CapacityExhaustedBehavior::Silent => {
#[cfg(debug_assertions)]
eprintln!(
"[ScalableBloomFilter] WARNING: Cannot grow: {}. Continuing with degraded FPR.",
e
);
}
CapacityExhaustedBehavior::Error => {
return Err(e);
}
#[cfg(debug_assertions)]
CapacityExhaustedBehavior::Panic => {
panic!("Capacity exhausted: {}", e);
}
},
}
}
if let Some(current) = self.filters.last_mut() {
current.insert(item);
self.total_items += 1;
self.items_in_current_filter += 1;
let current_idx = self.filters.len() - 1;
self.filter_nonempty[current_idx] = true;
if self.track_cardinality {
if let Some(sketch) = self.cardinality_sketches.last_mut() {
sketch.add(item);
}
}
Ok(())
} else {
Err(BloomCraftError::internal_error("No filters available"))
}
}
pub fn insert(&mut self, item: &T) {
let _ = self.insert_checked(item);
}
pub fn insert_fast(&mut self, item: &T) {
if self.current_filter_threshold == 0 && !self.filters.is_empty() {
self.recalibrate_grow_state();
}
if self.should_grow() {
match self.try_add_filter() {
Ok(()) => {}
Err(_e) => {
match self.capacity_behavior {
CapacityExhaustedBehavior::Silent => {
#[cfg(debug_assertions)]
eprintln!(
"[ScalableBloomFilter] WARNING: Cannot grow: {}. Continuing with degraded FPR.",
_e
);
}
CapacityExhaustedBehavior::Error => {
}
#[cfg(debug_assertions)]
CapacityExhaustedBehavior::Panic => {
panic!("Capacity exhausted: {}", _e);
}
}
}
}
}
if let Some(current) = self.filters.last_mut() {
current.insert(item);
self.items_in_current_filter += 1;
let current_idx = self.filters.len() - 1;
self.filter_nonempty[current_idx] = true;
if self.track_cardinality {
if let Some(sketch) = self.cardinality_sketches.last_mut() {
sketch.add(item);
}
}
}
}
pub fn grow(&mut self) -> Result<()> {
if self.filters.len() >= MAX_FILTERS {
return match self.capacity_behavior {
CapacityExhaustedBehavior::Silent => Ok(()),
CapacityExhaustedBehavior::Error => Err(BloomCraftError::max_filters_exceeded(
MAX_FILTERS,
self.filters.len(),
)),
#[cfg(debug_assertions)]
CapacityExhaustedBehavior::Panic => {
panic!(
"ScalableBloomFilter::grow: MAX_FILTERS ({}) reached",
MAX_FILTERS
);
}
};
}
self.try_add_filter()
}
pub fn insert_batch(&mut self, items: &[T]) -> Result<()> {
if items.is_empty() {
return Ok(());
}
if self.current_filter_threshold == 0 && !self.filters.is_empty() {
self.recalibrate_grow_state();
}
self.total_items.checked_add(items.len()).ok_or_else(|| {
BloomCraftError::invalid_parameters(format!(
"Batch insert of {} items would overflow total_items counter \
(current: {}, max: {})",
items.len(),
self.total_items,
usize::MAX,
))
})?;
#[cfg(debug_assertions)]
let original_total = self.total_items;
let mut remaining = items;
while !remaining.is_empty() {
if self.should_grow() {
if self.is_at_max_capacity() {
match self.capacity_behavior {
CapacityExhaustedBehavior::Silent => {
#[cfg(debug_assertions)]
eprintln!(
"[ScalableBloomFilter] WARNING: MAX_FILTERS ({}) reached. \
Inserting into saturated filter; FPR is no longer bounded.",
MAX_FILTERS
);
}
CapacityExhaustedBehavior::Error => {
return Err(BloomCraftError::max_filters_exceeded(
MAX_FILTERS,
self.filters.len(),
));
}
#[cfg(debug_assertions)]
CapacityExhaustedBehavior::Panic => {
panic!(
"ScalableBloomFilter::insert_batch: MAX_FILTERS ({}) reached. \
Configure CapacityExhaustedBehavior::Error for non-panicking \
exhaustion handling.",
MAX_FILTERS
);
}
}
} else {
self.try_add_filter()?;
}
}
let space = self
.current_filter_threshold
.saturating_sub(self.items_in_current_filter);
let seg_len = remaining.len().min(space.max(1));
let (segment, rest) = remaining.split_at(seg_len);
remaining = rest;
let current_idx = self.filters.len() - 1;
{
let filter = &mut self.filters[current_idx];
for item in segment {
filter.insert(item);
}
}
self.filter_nonempty[current_idx] = true;
self.items_in_current_filter += seg_len;
self.total_items += seg_len;
if self.track_cardinality {
if let Some(sketch) = self.cardinality_sketches.get_mut(current_idx) {
for item in segment {
sketch.add(item);
}
}
}
}
#[cfg(debug_assertions)]
debug_assert_eq!(
self.total_items,
original_total + items.len(),
"insert_batch total_items drift: expected {}, got {}. \
A loop iteration incremented total_items by a value other than seg_len.",
original_total + items.len(),
self.total_items,
);
Ok(())
}
#[must_use]
pub fn contains(&self, item: &T) -> bool {
#[cfg(debug_assertions)]
self.assert_nonempty_invariant();
match self.query_strategy {
QueryStrategy::Forward => {
for (filter, &nonempty) in self.filters.iter().zip(self.filter_nonempty.iter()) {
if !nonempty {
continue;
}
if filter.contains(item) {
return true;
}
}
}
QueryStrategy::Reverse => {
for (filter, &nonempty) in
self.filters.iter().zip(self.filter_nonempty.iter()).rev()
{
if !nonempty {
continue;
}
if filter.contains(item) {
return true;
}
}
}
}
false
}
#[must_use]
pub fn contains_batch(&self, items: &[T]) -> Vec<bool> {
items.iter().map(|item| self.contains(item)).collect()
}
#[must_use]
pub fn contains_with_provenance(&self, item: &T) -> Option<usize> {
match self.query_strategy {
QueryStrategy::Forward => {
for (idx, (filter, &nonempty)) in self
.filters
.iter()
.zip(self.filter_nonempty.iter())
.enumerate()
{
if !nonempty {
continue;
}
if filter.contains(item) {
return Some(idx);
}
}
}
QueryStrategy::Reverse => {
for (idx, (filter, &nonempty)) in self
.filters
.iter()
.zip(self.filter_nonempty.iter())
.enumerate()
.rev()
{
if !nonempty {
continue;
}
if filter.contains(item) {
return Some(idx);
}
}
}
}
None
}
#[cfg(feature = "trace")]
#[must_use]
pub fn contains_traced(&self, item: &T) -> (bool, QueryTrace) {
use std::time::Instant;
let strategy_name = format!("{:?}", self.query_strategy);
let mut builder = QueryTraceBuilder::new(&strategy_name);
match self.query_strategy {
QueryStrategy::Forward => {
for (idx, (filter, &nonempty)) in self
.filters
.iter()
.zip(self.filter_nonempty.iter())
.enumerate()
{
if !nonempty {
continue;
}
let start = Instant::now();
let matched = filter.contains(item);
builder.record_filter(
idx,
matched,
filter.hash_count(),
filter.hash_count(),
filter.fill_rate(),
start,
);
if matched {
return (true, builder.finish());
}
}
}
QueryStrategy::Reverse => {
for (idx, (filter, &nonempty)) in self
.filters
.iter()
.zip(self.filter_nonempty.iter())
.enumerate()
.rev()
{
if !nonempty {
continue;
}
let start = Instant::now();
let matched = filter.contains(item);
builder.record_filter(
idx,
matched,
filter.hash_count(),
filter.hash_count(),
filter.fill_rate(),
start,
);
if matched {
return (true, builder.finish());
}
}
}
}
(false, builder.finish())
}
pub fn clear_checked(&mut self) -> Result<()> {
let capacity = self.calculate_next_capacity(0)?;
let fpr = self.target_fpr;
let replacement = StandardBloomFilter::with_hasher(capacity, fpr, self.hasher.clone())?;
let new_sketch = self.track_cardinality.then(HyperLogLog::new);
if let GrowthStrategy::Adaptive { initial_ratio, .. } = self.growth {
self.error_ratio = initial_ratio;
}
self.filters.clear();
self.filter_nonempty.clear();
self.cardinality_sketches.clear();
self.growth_history.clear();
self.total_items = 0;
self.items_in_current_filter = 0;
self.current_filter_threshold = ((capacity as f64) * self.fill_threshold).ceil() as usize;
self.current_filter_threshold = self.current_filter_threshold.max(1);
self.filters.push(replacement);
self.filter_nonempty.push(false);
if let Some(sketch) = new_sketch {
self.cardinality_sketches.push(sketch);
}
let ts = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
self.growth_history.push_back(GrowthEvent {
timestamp: ts,
filter_index: 0,
capacity,
fpr,
total_items: 0,
});
Ok(())
}
pub fn clear(&mut self) {
self.clear_checked()
.expect("ScalableBloomFilter::clear() failed to recreate initial filter")
}
}
impl<T, H> ScalableBloomFilter<T, H>
where
T: Hash,
H: BloomHasher + Clone + Default,
{
#[must_use]
pub fn predict_fpr(&self, at_item_count: usize) -> f64 {
if at_item_count <= self.total_items {
return self.estimate_fpr();
}
let mut cumulative = 0usize;
let mut estimated_filters = 0usize;
loop {
if cumulative >= at_item_count || estimated_filters >= MAX_FILTERS {
break;
}
let cap = self
.calculate_next_capacity(estimated_filters)
.unwrap_or(self.initial_capacity);
let usable = ((cap as f64) * self.fill_threshold) as usize;
cumulative = cumulative.saturating_add(usable.max(1));
estimated_filters += 1;
}
let n = estimated_filters.max(self.filters.len());
let mut product = 1.0f64;
for i in 0..n {
let fpr_i = (self.target_fpr * self.error_ratio.powi(i as i32)).max(MIN_FPR);
product *= 1.0 - fpr_i;
}
1.0 - product
}
#[must_use]
pub fn filter_fpr_breakdown(&self) -> Vec<(usize, f64, f64)> {
let total_fpr = self.estimate_fpr();
let n = self.filters.len();
self.filters
.iter()
.enumerate()
.map(|(idx, filter)| {
let individual_fpr = filter.estimate_fpr();
let contribution = if total_fpr > 0.0 {
individual_fpr / total_fpr
} else if n > 0 {
1.0 / n as f64
} else {
0.0
};
(idx, individual_fpr, contribution)
})
.collect()
}
#[must_use]
pub fn estimate_fpr_exact(&self) -> f64 {
if self.filters.is_empty() {
return 0.0;
}
1.0 - self
.filters
.iter()
.map(|f| 1.0 - f.estimate_fpr())
.product::<f64>()
}
#[must_use]
pub fn estimate_fpr(&self) -> f64 {
self.estimate_fpr_exact()
}
#[must_use]
pub fn max_fpr(&self) -> f64 {
self.filters.iter().map(|f| f.estimate_fpr()).sum()
}
#[must_use]
pub fn estimate_unique_count(&self) -> usize {
if !self.track_cardinality || self.cardinality_sketches.is_empty() {
return self.total_items; }
let mut merged = HyperLogLog::new();
for sketch in &self.cardinality_sketches {
merged.merge(sketch);
}
merged.estimate()
}
#[must_use]
pub fn cardinality_error_bound(&self) -> f64 {
1.04 / (HLL_REGISTER_COUNT as f64).sqrt()
}
#[must_use]
pub fn health_metrics(&self) -> ScalableHealthMetrics {
let avg_fill_rate = if !self.filters.is_empty() {
self.filters.iter().map(|f| f.fill_rate()).sum::<f64>() / self.filters.len() as f64
} else {
0.0
};
ScalableHealthMetrics {
filter_count: self.filters.len(),
total_capacity: self.total_capacity(),
total_items: self.total_items,
estimated_fpr: self.estimate_fpr(),
max_fpr: self.max_fpr(),
target_fpr: self.target_fpr,
current_error_ratio: self.error_ratio,
current_fill_rate: self.current_fill_rate(),
avg_fill_rate,
memory_bytes: self.memory_usage(),
remaining_growth: self.remaining_growth_capacity(),
growth_events: self.growth_history.len(),
query_strategy: self.query_strategy,
}
}
#[must_use]
pub fn filter_count(&self) -> usize {
self.filters.len()
}
#[must_use]
pub fn tier_count(&self) -> usize {
self.filter_count()
}
#[must_use]
pub fn target_fp_rate(&self) -> f64 {
self.target_fpr
}
#[must_use]
pub fn total_capacity(&self) -> usize {
self.filters.iter().map(|f| f.expected_items()).sum()
}
#[must_use]
pub fn current_capacity(&self) -> usize {
self.filters.last().map(|f| f.expected_items()).unwrap_or(0)
}
#[must_use]
pub fn len(&self) -> usize {
self.total_items
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.total_items == 0
}
#[must_use]
pub fn current_fill_rate(&self) -> f64 {
self.filters.last().map(|f| f.fill_rate()).unwrap_or(0.0)
}
#[must_use]
pub fn aggregate_fill_rate(&self) -> f64 {
if self.filters.is_empty() {
return 0.0;
}
let total_bits: usize = self.filters.iter().map(|f| f.size()).sum();
let set_bits: usize = self.filters.iter().map(|f| f.count_set_bits()).sum();
if total_bits == 0 {
0.0
} else {
set_bits as f64 / total_bits as f64
}
}
#[must_use]
pub fn memory_usage(&self) -> usize {
self.filters.iter().map(|f| f.memory_usage()).sum::<usize>()
+ std::mem::size_of::<Self>()
+ self
.cardinality_sketches
.iter()
.map(|h| h.memory_usage())
.sum::<usize>()
}
#[must_use]
pub fn is_at_max_capacity(&self) -> bool {
self.filters.len() >= MAX_FILTERS
}
#[must_use]
pub fn is_near_capacity(&self) -> bool {
self.filters.len() + CAPACITY_WARNING_THRESHOLD >= MAX_FILTERS
}
#[must_use]
pub fn remaining_growth_capacity(&self) -> usize {
MAX_FILTERS.saturating_sub(self.filters.len())
}
#[must_use]
pub fn filter_stats(&self) -> Vec<(usize, f64, f64)> {
self.filters
.iter()
.map(|f| (f.expected_items(), f.fill_rate(), f.estimate_fpr()))
.collect()
}
#[must_use]
pub fn growth_strategy(&self) -> GrowthStrategy {
self.growth
}
#[must_use]
pub fn error_ratio(&self) -> f64 {
self.error_ratio
}
#[must_use]
pub fn fill_threshold(&self) -> f64 {
self.fill_threshold
}
pub fn set_fill_threshold(&mut self, threshold: f64) -> Result<()> {
if threshold <= 0.0 || threshold >= 1.0 {
return Err(BloomCraftError::invalid_parameters(format!(
"fill_threshold must be in (0.0, 1.0), got {}",
threshold
)));
}
if threshold < 0.45 {
return Err(BloomCraftError::invalid_parameters(format!(
"fill_threshold must be >= 0.45 to preserve the FPR convergence bound, got {}",
threshold
)));
}
self.fill_threshold = threshold;
Ok(())
}
#[must_use]
pub fn target_fpr(&self) -> f64 {
self.target_fpr
}
#[must_use]
pub fn initial_capacity(&self) -> usize {
self.initial_capacity
}
#[must_use]
#[inline]
pub fn total_items(&self) -> usize {
self.total_items
}
}
impl<T, H> BloomFilter<T> for ScalableBloomFilter<T, H>
where
T: Hash + Send + Sync,
H: BloomHasher + Clone + Default + Send + Sync,
{
fn insert(&mut self, item: &T) {
ScalableBloomFilter::insert(self, item);
}
fn contains(&self, item: &T) -> bool {
ScalableBloomFilter::contains(self, item)
}
fn clear(&mut self) {
ScalableBloomFilter::clear(self);
}
fn len(&self) -> usize {
ScalableBloomFilter::len(self)
}
fn is_empty(&self) -> bool {
ScalableBloomFilter::is_empty(self)
}
fn false_positive_rate(&self) -> f64 {
self.estimate_fpr()
}
fn expected_items(&self) -> usize {
self.total_capacity()
}
fn bit_count(&self) -> usize {
self.filters.iter().map(|f| f.bit_count()).sum()
}
fn hash_count(&self) -> usize {
self.filters.first().map(|f| f.hash_count()).unwrap_or(0)
}
fn count_set_bits(&self) -> usize {
self.filters.iter().map(|f| f.count_set_bits()).sum()
}
fn estimate_count(&self) -> usize {
self.estimate_unique_count()
}
}
impl<T, H> super::super::core::filter::ScalableBloomFilter<T> for ScalableBloomFilter<T, H>
where
T: Hash + Send + Sync,
H: BloomHasher + Clone + Default + Send + Sync,
{
fn current_capacity(&self) -> usize {
ScalableBloomFilter::current_capacity(self)
}
fn target_fp_rate(&self) -> f64 {
ScalableBloomFilter::target_fp_rate(self)
}
fn grow(&mut self) {
ScalableBloomFilter::grow(self).expect("ScalableBloomFilter::grow failed");
}
fn tier_count(&self) -> usize {
ScalableBloomFilter::tier_count(self)
}
}
impl<T, H> fmt::Display for ScalableBloomFilter<T, H>
where
T: Hash,
H: BloomHasher + Clone + Default,
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"ScalableBloomFilter {{ filters: {}, capacity: {}, items: {}, fill: {:.1}%, est_fpr: {:.4}% }}",
self.filter_count(),
self.total_capacity(),
self.len(),
self.current_fill_rate() * 100.0,
self.estimate_fpr() * 100.0
)
}
}
impl<T, H> MutableBloomFilter<T> for ScalableBloomFilter<T, H>
where
T: Hash + Send + Sync,
H: BloomHasher + Clone + Default + Send + Sync,
{
}
impl<T, H> std::iter::Extend<T> for ScalableBloomFilter<T, H>
where
T: Hash,
H: BloomHasher + Clone + Default,
{
fn extend<I: IntoIterator<Item = T>>(&mut self, iter: I) {
for item in iter {
self.insert(&item);
}
}
}
impl<T> std::iter::FromIterator<T> for ScalableBloomFilter<T>
where
T: Hash,
{
fn from_iter<I: IntoIterator<Item = T>>(iter: I) -> Self {
let iter = iter.into_iter();
let (lower, _) = iter.size_hint();
let estimated_count = lower.max(100);
let mut filter = Self::new(estimated_count, 0.01)
.expect("ScalableBloomFilter::from_iter: failed to create filter");
for item in iter {
filter.insert(&item);
}
filter
}
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(feature = "concurrent")]
use crate::filters::atomic_scalable::AtomicScalableBloomFilter;
#[test]
fn test_new() {
let filter: ScalableBloomFilter<i32> = ScalableBloomFilter::new(1000, 0.01).unwrap();
assert_eq!(filter.filter_count(), 1);
assert!(filter.is_empty());
assert_eq!(filter.len(), 0);
assert_eq!(filter.total_capacity(), 1000);
}
#[test]
fn test_insert_and_contains() {
let mut filter: ScalableBloomFilter<&str> = ScalableBloomFilter::new(100, 0.01).unwrap();
filter.insert(&"hello");
assert!(filter.contains(&"hello"));
assert!(!filter.contains(&"world"));
assert_eq!(filter.len(), 1);
assert!(!filter.is_empty());
}
#[test]
fn test_no_false_negatives() {
let mut filter: ScalableBloomFilter<i32> = ScalableBloomFilter::new(100, 0.01).unwrap();
let items: Vec<i32> = (0..1000).collect();
for item in &items {
filter.insert(item);
}
for item in &items {
assert!(
filter.contains(item),
"False negative for {} (filter depth: {})",
item,
filter.filter_count()
);
}
}
#[test]
fn test_clear() {
let mut filter: ScalableBloomFilter<i32> = ScalableBloomFilter::new(10, 0.01).unwrap();
for i in 0..100 {
filter.insert(&i);
}
assert!(!filter.is_empty());
assert!(filter.filter_count() > 1);
filter.clear();
assert!(filter.is_empty());
assert_eq!(filter.filter_count(), 1);
assert!(!filter.contains(&42));
}
#[test]
fn test_automatic_growth() {
let mut filter: ScalableBloomFilter<i32> = ScalableBloomFilter::new(10, 0.01).unwrap();
assert_eq!(filter.filter_count(), 1);
for i in 0..100 {
filter.insert(&i);
}
assert!(
filter.filter_count() > 1,
"Filter should have grown, count: {}",
filter.filter_count()
);
for i in 0..100 {
assert!(filter.contains(&i));
}
}
#[test]
fn test_geometric_growth() {
let mut filter =
ScalableBloomFilter::with_strategy(10, 0.01, 0.5, GrowthStrategy::Geometric(2.0))
.unwrap();
for i in 0..200 {
filter.insert(&i);
}
let stats = filter.filter_stats();
if stats.len() >= 2 {
let ratio = stats[1].0 as f64 / stats[0].0 as f64;
assert!(
ratio > 1.5 && ratio < 2.5,
"Growth ratio should be ~2.0, got {}",
ratio
);
}
}
#[test]
fn test_constant_growth() {
let mut filter =
ScalableBloomFilter::with_strategy(10, 0.01, 0.5, GrowthStrategy::Constant).unwrap();
for i in 0..100 {
filter.insert(&i);
}
let stats = filter.filter_stats();
if stats.len() >= 2 {
assert_eq!(
stats[0].0, stats[1].0,
"All filters should have same capacity"
);
}
}
#[test]
fn test_bounded_growth() {
let mut filter = ScalableBloomFilter::with_strategy(
100,
0.01,
0.5,
GrowthStrategy::Bounded {
scale: 2.0,
max_filter_size: 500,
},
)
.unwrap();
for i in 0..2000 {
filter.insert(&i);
}
for (capacity, _, _) in filter.filter_stats() {
assert!(
capacity <= 500,
"Filter capacity {} exceeds max 500",
capacity
);
}
}
#[test]
fn test_negative_query_fast_path() {
let mut filter: ScalableBloomFilter<i32> = ScalableBloomFilter::new(100, 0.01).unwrap();
for i in 0..1000 {
filter.insert(&i);
}
assert!(!filter.contains(&99999));
assert!(filter.contains(&500));
}
#[test]
fn test_reverse_iteration() {
let mut filter = ScalableBloomFilter::new(10, 0.01)
.unwrap()
.with_query_strategy(QueryStrategy::Reverse);
for i in 0..100 {
filter.insert(&i);
}
assert!(filter.contains(&99));
assert!(filter.contains(&0));
}
#[test]
fn test_forward_iteration() {
let mut filter = ScalableBloomFilter::new(10, 0.01)
.unwrap()
.with_query_strategy(QueryStrategy::Forward);
for i in 0..100 {
filter.insert(&i);
}
assert!(filter.contains(&0));
assert!(filter.contains(&99));
}
#[test]
fn test_predict_fpr() {
let mut filter = ScalableBloomFilter::<i32>::new(100, 0.01).unwrap();
for i in 0..200 {
filter.insert(&i);
}
let fpr_1k = filter.predict_fpr(1000);
let fpr_10k = filter.predict_fpr(10000);
assert!(fpr_1k > 0.0, "FPR at 1K should be > 0");
assert!(fpr_10k > fpr_1k, "FPR should increase with scale");
assert!(fpr_10k < 0.1, "FPR should stay reasonable");
}
#[test]
fn test_fpr_breakdown() {
let mut filter: ScalableBloomFilter<i32> = ScalableBloomFilter::new(10, 0.01).unwrap();
for i in 0..100 {
filter.insert(&i);
}
let breakdown = filter.filter_fpr_breakdown();
assert!(!breakdown.is_empty());
for (_idx, individual_fpr, contribution) in breakdown {
assert!((0.0..=1.0).contains(&individual_fpr));
assert!((0.0..=1.0).contains(&contribution));
}
}
#[test]
fn test_capacity_exhausted_error() {
let mut filter = ScalableBloomFilter::with_strategy(
10,
0.01,
0.5,
GrowthStrategy::Bounded {
scale: 1.5,
max_filter_size: 50,
},
)
.unwrap()
.with_capacity_behavior(CapacityExhaustedBehavior::Error);
let mut exhausted = false;
for i in 0..100_000 {
match filter.insert_checked(&i) {
Err(BloomCraftError::MaxFiltersExceeded {
max_filters,
current_count,
}) => {
exhausted = true;
assert_eq!(max_filters, MAX_FILTERS);
assert_eq!(current_count, MAX_FILTERS);
break;
}
Err(e) => panic!("Unexpected error: {:?}", e),
Ok(_) => {}
}
}
assert!(
exhausted,
"Should have reached MAX_FILTERS, got {} filters",
filter.filter_count()
);
}
#[test]
fn test_contains_with_provenance() {
let mut filter: ScalableBloomFilter<i32> = ScalableBloomFilter::new(10, 0.01).unwrap();
for i in 0..100 {
filter.insert(&i);
}
let filter_idx = filter.contains_with_provenance(&50);
assert!(filter_idx.is_some());
let no_idx = filter.contains_with_provenance(&9999);
assert!(no_idx.is_none());
}
#[test]
fn test_adaptive_growth() {
let mut filter = ScalableBloomFilter::with_strategy(
100,
0.01,
0.5,
GrowthStrategy::Adaptive {
initial_ratio: 0.5,
min_ratio: 0.3,
max_ratio: 0.9,
},
)
.unwrap();
for i in 0..1000 {
filter.insert(&i);
}
let final_ratio = filter.error_ratio();
assert!((0.3..=0.9).contains(&final_ratio));
}
#[test]
fn test_cardinality_tracking() {
let mut filter: ScalableBloomFilter<i32> = ScalableBloomFilter::new(1000, 0.01)
.unwrap()
.with_cardinality_tracking();
for i in 0..1000 {
filter.insert(&i);
}
for i in 0..1000 {
filter.insert(&i);
}
assert_eq!(filter.len(), 2000);
let unique_count = filter.estimate_unique_count();
let error = (unique_count as f64 - 1000.0).abs() / 1000.0;
assert!(
error < 0.05,
"Cardinality error {:.2}% exceeds 5%",
error * 100.0
);
}
#[test]
fn test_cardinality_error_bound() {
let filter: ScalableBloomFilter<i32> = ScalableBloomFilter::new(1000, 0.01)
.unwrap()
.with_cardinality_tracking();
let error_bound = filter.cardinality_error_bound();
assert!(error_bound > 0.0 && error_bound < 0.02); }
#[test]
fn test_health_metrics() {
let mut filter: ScalableBloomFilter<i32> = ScalableBloomFilter::new(100, 0.01).unwrap();
for i in 0..500 {
filter.insert(&i);
}
let metrics = filter.health_metrics();
assert!(metrics.filter_count > 1);
assert_eq!(metrics.total_items, 500);
assert!(metrics.estimated_fpr > 0.0);
assert!(metrics.estimated_fpr < 0.1);
assert!((0.0..=1.0).contains(&metrics.current_fill_rate));
assert!(metrics.memory_bytes > 0);
}
#[test]
fn test_health_metrics_display() {
let mut filter: ScalableBloomFilter<i32> = ScalableBloomFilter::new(500, 0.01).unwrap();
for i in 0..2000 {
filter.insert(&i);
}
let metrics = filter.health_metrics();
let display = format!("{}", metrics);
assert!(display.contains("ScalableBloomFilter Health Metrics"));
assert!(display.contains("Filters:"));
assert!(display.contains("Estimated FPR:"));
}
#[test]
fn test_insert_batch() {
let mut filter: ScalableBloomFilter<i32> = ScalableBloomFilter::new(100, 0.01).unwrap();
let items: Vec<i32> = (0..1000).collect();
let _ = filter.insert_batch(&items);
assert_eq!(filter.len(), 1000);
for item in &items {
assert!(filter.contains(item));
}
}
#[test]
fn test_contains_batch() {
let mut filter: ScalableBloomFilter<i32> = ScalableBloomFilter::new(100, 0.01).unwrap();
let _ = filter.insert_batch(&[1, 2, 3]);
let results = filter.contains_batch(&[1, 2, 3, 4, 5]);
assert_eq!(results, vec![true, true, true, false, false]);
}
#[test]
fn test_display_trait() {
let mut filter: ScalableBloomFilter<i32> = ScalableBloomFilter::new(1000, 0.01).unwrap();
for i in 0..500 {
filter.insert(&i);
}
let display = format!("{}", filter);
assert!(display.contains("ScalableBloomFilter"));
assert!(display.contains("filters:"));
assert!(display.contains("capacity:"));
assert!(display.contains("items:"));
}
#[test]
fn test_extend_trait() {
let mut filter: ScalableBloomFilter<i32> = ScalableBloomFilter::new(100, 0.01).unwrap();
filter.extend(0..50);
assert_eq!(filter.len(), 50);
assert!(filter.contains(&25));
assert!(!filter.contains(&100));
}
#[test]
fn test_from_iterator() {
let filter: ScalableBloomFilter<i32> = (0..100).collect();
assert_eq!(filter.len(), 100);
assert!(filter.contains(&50));
assert!(!filter.contains(&200));
}
#[test]
fn test_bloom_filter_trait() {
let mut filter: ScalableBloomFilter<&str> = ScalableBloomFilter::new(100, 0.01).unwrap();
BloomFilter::insert(&mut filter, &"test");
assert!(BloomFilter::contains(&filter, &"test"));
assert!(!BloomFilter::is_empty(&filter));
BloomFilter::clear(&mut filter);
assert!(BloomFilter::is_empty(&filter));
}
#[test]
fn test_estimate_fpr_vs_max_fpr() {
let mut filter: ScalableBloomFilter<i32> = ScalableBloomFilter::new(100, 0.01).unwrap();
for i in 0..200 {
filter.insert(&i);
}
let max_fpr = filter.max_fpr();
let actual_fpr = filter.estimate_fpr();
assert!(
max_fpr >= actual_fpr - 1e-10,
"max_fpr ({}) should be >= actual_fpr ({})",
max_fpr,
actual_fpr
);
}
#[test]
fn test_fpr_increases_with_growth() {
let mut filter: ScalableBloomFilter<i32> = ScalableBloomFilter::new(10, 0.01).unwrap();
let initial_fpr = filter.estimate_fpr();
for i in 0..1000 {
filter.insert(&i);
}
let final_fpr = filter.estimate_fpr();
assert!(
final_fpr >= initial_fpr,
"FPR should not decrease with growth"
);
}
#[test]
fn test_capacity_monitoring() {
let mut filter: ScalableBloomFilter<i32> = ScalableBloomFilter::new(10, 0.01).unwrap();
assert!(!filter.is_at_max_capacity());
assert!(!filter.is_near_capacity());
assert_eq!(filter.remaining_growth_capacity(), MAX_FILTERS - 1);
for i in 0..1_000 {
filter.insert(&i);
if filter.is_at_max_capacity() {
assert_eq!(filter.remaining_growth_capacity(), 0);
break;
}
}
assert!(filter.filter_count() > 1);
}
#[test]
fn test_max_filters_limit_is_enforced() {
let mut filter: ScalableBloomFilter<i32> =
ScalableBloomFilter::with_strategy(1, 0.01, 0.5, GrowthStrategy::Constant).unwrap();
for i in 0..=MAX_FILTERS as i32 {
filter.insert(&i);
}
assert_eq!(
filter.filter_count(),
MAX_FILTERS,
"filter_count exceeded MAX_FILTERS"
);
}
#[test]
fn test_fpr_degradation_at_capacity() {
let mut filter: ScalableBloomFilter<i32> = ScalableBloomFilter::new(1, 0.01).unwrap();
let initial_fpr = filter.estimate_fpr();
for i in 0..10_000 {
filter.insert(&i);
if filter.is_at_max_capacity() {
break;
}
}
let start_over_capacity = 10_000;
for i in start_over_capacity..start_over_capacity + 5_000 {
filter.insert(&i);
}
let final_fpr = filter.estimate_fpr();
assert!(
final_fpr > initial_fpr,
"FPR should degrade at capacity: initial={}, final={}",
initial_fpr,
final_fpr
);
}
#[test]
fn test_large_scale_insertion() {
let mut filter: ScalableBloomFilter<i32> = ScalableBloomFilter::new(100, 0.01).unwrap();
for i in 0..10_000 {
filter.insert(&i);
}
assert_eq!(filter.len(), 10_000);
assert!(filter.filter_count() > 1);
for i in 0..10_000 {
assert!(filter.contains(&i), "False negative for {}", i);
}
let fpr = filter.estimate_fpr();
assert!(fpr < 0.05, "FPR {} is too high", fpr);
}
#[test]
fn test_accessors() {
let filter: ScalableBloomFilter<i32> =
ScalableBloomFilter::with_strategy(500, 0.02, 0.4, GrowthStrategy::Geometric(3.0))
.unwrap();
assert_eq!(filter.initial_capacity(), 500);
assert_eq!(filter.target_fpr(), 0.02);
assert_eq!(filter.error_ratio(), 0.4);
assert_eq!(filter.growth_strategy(), GrowthStrategy::Geometric(3.0));
assert_eq!(filter.fill_threshold(), DEFAULT_FILL_THRESHOLD);
}
#[test]
fn test_set_fill_threshold() {
let mut filter: ScalableBloomFilter<i32> = ScalableBloomFilter::new(100, 0.01).unwrap();
let _ = filter.set_fill_threshold(0.8);
assert_eq!(filter.fill_threshold(), 0.8);
}
#[test]
fn test_invalid_fill_threshold() {
let mut filter: ScalableBloomFilter<i32> = ScalableBloomFilter::new(100, 0.01).unwrap();
let result = filter.set_fill_threshold(1.5);
assert!(result.is_err());
}
#[test]
fn test_filter_stats() {
let mut filter: ScalableBloomFilter<i32> = ScalableBloomFilter::new(10, 0.01).unwrap();
for i in 0..100 {
filter.insert(&i);
}
let stats = filter.filter_stats();
assert!(!stats.is_empty());
for (capacity, fill_rate, fpr) in stats {
assert!(capacity > 0);
assert!((0.0..=1.0).contains(&fill_rate));
assert!((0.0..=1.0).contains(&fpr));
}
}
#[test]
fn test_memory_usage() {
let filter: ScalableBloomFilter<i32> = ScalableBloomFilter::new(1000, 0.01).unwrap();
let mem = filter.memory_usage();
assert!(mem > 0);
}
#[test]
fn test_clone() {
let mut filter1 = ScalableBloomFilter::new(100, 0.01).unwrap();
filter1.insert(&"test");
let filter2 = filter1.clone();
assert!(filter2.contains(&"test"));
assert_eq!(filter1.filter_count(), filter2.filter_count());
assert_eq!(filter1.len(), filter2.len());
}
#[test]
fn test_current_vs_aggregate_fill_rate() {
let mut filter: ScalableBloomFilter<i32> = ScalableBloomFilter::new(100, 0.01).unwrap();
for i in 0..300 {
filter.insert(&i);
}
let current = filter.current_fill_rate();
let aggregate = filter.aggregate_fill_rate();
assert!((0.0..=1.0).contains(¤t));
assert!((0.0..=1.0).contains(&aggregate));
if filter.filter_count() > 1 {
assert!(current > 0.0);
assert!(aggregate > 0.0);
}
}
#[test]
fn test_growth_strategy_default() {
let strategy = GrowthStrategy::default();
assert_eq!(strategy, GrowthStrategy::Geometric(2.0));
}
#[test]
fn test_fpr_precision_clamp() {
let mut filter: ScalableBloomFilter<i32> =
ScalableBloomFilter::with_strategy(10, 0.01, 0.1, GrowthStrategy::Geometric(2.0))
.unwrap();
for i in 0..10_000 {
filter.insert(&i);
}
let fpr = filter.estimate_fpr();
assert!(fpr >= MIN_FPR);
}
#[cfg(feature = "concurrent")]
#[test]
fn test_bit_statistics() {
let filter: AtomicScalableBloomFilter<i32> =
AtomicScalableBloomFilter::new(1_000, 0.01).unwrap();
let (total, set, utilization) = filter.bit_statistics();
assert!(total > 0, "Expected allocated bits, got 0");
assert_eq!(set, 0, "No bits should be set before any insert");
assert_eq!(utilization, 0.0);
for i in 0..200 {
filter.insert(&i);
}
let (total2, set2, utilization2) = filter.bit_statistics();
assert_eq!(
total2, total,
"Total bits changed unexpectedly — a growth event fired. \
Reduce insert count or increase initial_capacity."
);
assert!(set2 > 0, "Expected set bits after 200 inserts, got 0");
assert!(
utilization2 > 0.0 && utilization2 < 100.0,
"Utilization {:.2} out of (0, 100) range",
utilization2
);
for i in 200..600 {
filter.insert(&i);
}
let (total3, set3, _) = filter.bit_statistics();
assert!(
total3 >= total2,
"Total bits decreased after growth: {} < {}",
total3,
total2
);
assert!(set3 > set2, "Set bits did not increase after more inserts");
}
#[test]
fn test_insert_fast_does_not_increment_len() {
let mut filter: ScalableBloomFilter<i32> = ScalableBloomFilter::new(100, 0.01).unwrap();
filter.insert_fast(&42);
assert!(filter.contains(&42));
assert_eq!(filter.len(), 0);
}
#[test]
fn test_insert_fast_multiple_items() {
let mut filter: ScalableBloomFilter<i32> = ScalableBloomFilter::new(10, 0.01).unwrap();
for i in 0..100 {
filter.insert_fast(&i);
}
for i in 0..100 {
assert!(filter.contains(&i));
}
assert_eq!(filter.len(), 0);
}
#[test]
fn test_insert_fast_triggers_growth() {
let mut filter: ScalableBloomFilter<i32> = ScalableBloomFilter::new(10, 0.01).unwrap();
assert_eq!(filter.filter_count(), 1);
for i in 0..1000 {
filter.insert_fast(&i);
}
assert!(filter.filter_count() > 1);
assert_eq!(filter.len(), 0);
}
#[test]
fn test_grow_increases_filter_count() {
let mut filter: ScalableBloomFilter<i32> = ScalableBloomFilter::new(100, 0.01).unwrap();
assert_eq!(filter.filter_count(), 1);
filter.grow().unwrap();
assert_eq!(filter.filter_count(), 2);
filter.grow().unwrap();
assert_eq!(filter.filter_count(), 3);
}
#[test]
fn test_grow_then_insert_works() {
let mut filter: ScalableBloomFilter<i32> = ScalableBloomFilter::new(10, 0.01).unwrap();
filter.grow().unwrap();
filter.insert(&42);
assert!(filter.contains(&42));
assert_eq!(filter.len(), 1);
assert_eq!(filter.filter_count(), 2);
}
#[test]
fn test_grow_at_max_filters() {
let mut filter =
ScalableBloomFilter::<i32>::with_strategy(1, 0.01, 0.5, GrowthStrategy::Constant)
.unwrap()
.with_capacity_behavior(CapacityExhaustedBehavior::Error);
for _ in 0..MAX_FILTERS - 1 {
filter.grow().unwrap();
}
assert_eq!(filter.filter_count(), MAX_FILTERS);
let result = filter.grow();
assert!(result.is_err());
}
#[test]
fn test_current_capacity() {
let filter: ScalableBloomFilter<i32> = ScalableBloomFilter::new(1000, 0.01).unwrap();
assert_eq!(filter.current_capacity(), 1000);
}
}