use crate::common::{Result, SketchError};
use crate::membership::BloomFilter;
use xxhash_rust::xxh64::xxh64;
#[derive(Clone)]
pub struct LearnedBloomFilter {
model: LinearModel,
backup_filter: BloomFilter,
feature_extractor: FeatureExtractor,
target_fpr: f64,
n_samples: usize,
}
#[derive(Clone, Debug)]
pub struct LinearModel {
weights: Vec<f64>,
bias: f64,
feature_means: Vec<f64>,
}
#[derive(Clone, Debug)]
pub struct FeatureExtractor {
hash_functions: usize,
feature_bits: usize,
}
#[derive(Debug, Clone)]
pub struct LearnedBloomStats {
pub model_accuracy: f64,
pub backup_fpr: f64,
pub memory_bits: u64,
pub false_negative_rate: f64,
}
impl LearnedBloomFilter {
pub fn new(training_keys: &[Vec<u8>], fpr: f64) -> Result<Self> {
if training_keys.is_empty() {
return Err(SketchError::InvalidParameter {
param: "training_keys".to_string(),
value: "empty".to_string(),
constraint: "must contain at least one key".to_string(),
});
}
if training_keys.len() < 10 {
return Err(SketchError::InvalidParameter {
param: "training_keys".to_string(),
value: training_keys.len().to_string(),
constraint: "must have at least 10 samples for stable model".to_string(),
});
}
if fpr <= 0.0 || fpr >= 1.0 {
return Err(SketchError::InvalidParameter {
param: "fpr".to_string(),
value: fpr.to_string(),
constraint: "must be in range (0, 1)".to_string(),
});
}
let feature_extractor = FeatureExtractor::new(4, 8);
let mut features = Vec::with_capacity(training_keys.len());
for key in training_keys {
let feature_vec = feature_extractor.extract(key);
features.push(feature_vec);
}
let model = LinearModel::train(&features, feature_extractor.feature_dim());
let confidence_threshold = 0.70;
let mut backup_keys = Vec::new();
for (i, key) in training_keys.iter().enumerate() {
let feature_vec = &features[i];
let prediction = model.predict(feature_vec);
if prediction < confidence_threshold {
backup_keys.push(key.clone());
}
}
let backup_ratio = backup_keys.len() as f64 / training_keys.len() as f64;
let backup_n = if backup_ratio > 0.5 {
training_keys.len()
} else {
backup_keys.len().max(1)
};
let mut backup_filter = BloomFilter::new(backup_n, fpr);
if backup_ratio > 0.5 {
for key in training_keys {
backup_filter.insert(key);
}
} else {
for key in &backup_keys {
backup_filter.insert(key);
}
}
Ok(Self {
model,
backup_filter,
feature_extractor,
target_fpr: fpr,
n_samples: training_keys.len(),
})
}
#[inline]
pub fn contains(&self, key: &[u8]) -> bool {
let features = self.feature_extractor.extract(key);
let prediction = self.model.predict(&features);
if prediction >= 0.70 {
return true;
}
self.backup_filter.contains(key)
}
pub fn memory_usage(&self) -> usize {
let model_size = self.model.memory_usage();
let backup_size = self.backup_filter.memory_usage();
let extractor_size = std::mem::size_of::<FeatureExtractor>();
model_size + backup_size + extractor_size
}
pub fn fpr(&self) -> f64 {
self.target_fpr
}
pub fn stats(&self) -> LearnedBloomStats {
let memory_bits = (self.memory_usage() * 8) as u64;
let backup_fpr = self.backup_filter.false_positive_rate();
let model_accuracy = 0.8;
LearnedBloomStats {
model_accuracy,
backup_fpr,
memory_bits,
false_negative_rate: 0.0, }
}
}
impl LinearModel {
fn new(feature_dim: usize) -> Self {
Self {
weights: vec![0.0; feature_dim],
bias: 0.0,
feature_means: vec![0.0; feature_dim],
}
}
fn train(positive_examples: &[Vec<f64>], feature_dim: usize) -> Self {
let mut model = Self::new(feature_dim);
if positive_examples.is_empty() {
return model;
}
let mut feature_means = vec![0.0; feature_dim];
let mut feature_stds = vec![0.0; feature_dim];
let n = positive_examples.len() as f64;
for example in positive_examples {
for (i, &val) in example.iter().enumerate() {
feature_means[i] += val;
}
}
for mean in &mut feature_means {
*mean /= n;
}
for example in positive_examples {
for (i, &val) in example.iter().enumerate() {
let diff = val - feature_means[i];
feature_stds[i] += diff * diff;
}
}
for std in &mut feature_stds {
*std = (*std / n).sqrt().max(0.01); }
for weight in &mut model.weights {
*weight = 0.1; }
let mut scores = Vec::with_capacity(positive_examples.len());
for example in positive_examples {
let mut score = 0.0;
for (i, &val) in example.iter().enumerate() {
score += model.weights[i] * (val - 0.5);
}
scores.push(score);
}
let mut sorted_scores = scores.clone();
sorted_scores.sort_by(|a, b| a.partial_cmp(b).unwrap());
let median_idx = (n as usize) / 2;
let median_score = sorted_scores.get(median_idx).copied().unwrap_or(0.0);
model.bias = -median_score;
model.feature_means = feature_means;
model
}
#[inline]
fn predict(&self, features: &[f64]) -> f64 {
let mut score = self.bias;
for (i, &feature) in features.iter().enumerate() {
if i < self.weights.len() {
score += self.weights[i] * (feature - 0.5);
}
}
Self::sigmoid(score)
}
#[inline]
fn sigmoid(x: f64) -> f64 {
1.0 / (1.0 + (-x).exp())
}
fn memory_usage(&self) -> usize {
self.weights.len() * std::mem::size_of::<f64>()
+ std::mem::size_of::<f64>() + self.feature_means.len() * std::mem::size_of::<f64>() }
}
impl FeatureExtractor {
fn new(hash_functions: usize, feature_bits: usize) -> Self {
Self {
hash_functions,
feature_bits,
}
}
fn extract(&self, key: &[u8]) -> Vec<f64> {
let mut features = Vec::with_capacity(self.feature_dim());
for seed in 0..self.hash_functions {
let hash = xxh64(key, seed as u64);
for bit_offset in 0..self.feature_bits {
let bit_index = bit_offset * 8; let bit = (hash >> bit_index) & 1;
features.push(bit as f64);
}
}
let len_feature = (key.len() as f64).min(1000.0) / 1000.0;
features.push(len_feature);
let byte_sum: u32 = key.iter().take(16).map(|&b| b as u32).sum();
features.push((byte_sum as f64) / (16.0 * 255.0));
let byte_xor: u8 = key.iter().fold(0u8, |acc, &b| acc ^ b);
features.push((byte_xor as f64) / 255.0);
features
}
fn feature_dim(&self) -> usize {
self.hash_functions * self.feature_bits + 3 }
}
impl std::fmt::Debug for LearnedBloomFilter {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("LearnedBloomFilter")
.field("n_samples", &self.n_samples)
.field("target_fpr", &self.target_fpr)
.field("memory_bytes", &self.memory_usage())
.field("feature_dim", &self.feature_extractor.feature_dim())
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_feature_extractor() {
let extractor = FeatureExtractor::new(4, 8);
let features = extractor.extract(b"test_key");
assert_eq!(features.len(), extractor.feature_dim());
for &f in &features {
assert!((0.0..=1.0).contains(&f), "Feature out of range: {}", f);
}
}
#[test]
fn test_feature_consistency() {
let extractor = FeatureExtractor::new(4, 8);
let features1 = extractor.extract(b"same_key");
let features2 = extractor.extract(b"same_key");
assert_eq!(features1, features2, "Features should be deterministic");
}
#[test]
fn test_linear_model_train() {
let examples = vec![
vec![0.5, 0.3, 0.8],
vec![0.6, 0.4, 0.7],
vec![0.55, 0.35, 0.75],
];
let model = LinearModel::train(&examples, 3);
assert_eq!(model.weights.len(), 3);
}
#[test]
fn test_linear_model_predict() {
let model = LinearModel {
weights: vec![1.0, 2.0, 3.0],
bias: -5.0,
feature_means: vec![0.5, 0.5, 0.5],
};
let features = vec![0.5, 0.5, 0.5];
let prediction = model.predict(&features);
assert!((0.0..=1.0).contains(&prediction));
}
#[test]
fn test_sigmoid() {
assert!((LinearModel::sigmoid(0.0) - 0.5).abs() < 1e-10);
assert!(LinearModel::sigmoid(10.0) > 0.99);
assert!(LinearModel::sigmoid(-10.0) < 0.01);
}
#[test]
fn test_basic_construction() {
let keys: Vec<Vec<u8>> = (0..100).map(|i| format!("key{}", i).into_bytes()).collect();
let filter = LearnedBloomFilter::new(&keys, 0.01);
assert!(filter.is_ok());
}
#[test]
fn test_basic_membership() {
let keys: Vec<Vec<u8>> = (0..100).map(|i| format!("key{}", i).into_bytes()).collect();
let filter = LearnedBloomFilter::new(&keys, 0.01).unwrap();
for key in &keys {
assert!(filter.contains(key), "False negative!");
}
}
#[test]
fn test_memory_usage() {
let keys: Vec<Vec<u8>> = (0..1000)
.map(|i| format!("key{}", i).into_bytes())
.collect();
let filter = LearnedBloomFilter::new(&keys, 0.01).unwrap();
let mem = filter.memory_usage();
assert!(mem > 0, "Memory usage should be positive");
}
#[test]
fn test_stats() {
let keys: Vec<Vec<u8>> = (0..500).map(|i| format!("key{}", i).into_bytes()).collect();
let filter = LearnedBloomFilter::new(&keys, 0.01).unwrap();
let stats = filter.stats();
assert!(stats.model_accuracy >= 0.0 && stats.model_accuracy <= 1.0);
assert!(stats.backup_fpr >= 0.0 && stats.backup_fpr <= 1.0);
assert_eq!(stats.false_negative_rate, 0.0);
assert!(stats.memory_bits > 0);
}
}