use crate::common::{Mergeable, Sketch, SketchError};
use rand::Rng;
use std::collections::HashMap;
use std::hash::{Hash, Hasher};
use twox_hash::XxHash64;
#[derive(Clone, Debug)]
struct SampledItem {
element_id: u64,
weight: f64,
}
#[derive(Clone, Debug)]
pub struct QSketch {
max_samples: usize,
samples: Vec<SampledItem>,
items_seen: HashMap<u64, f64>,
total_weight: f64,
threshold: f64,
rng: rand::rngs::SmallRng,
}
impl QSketch {
pub const DEFAULT_MAX_SAMPLES: usize = 256;
const MIN_SAMPLES: usize = 32;
pub fn new(max_samples: usize) -> Self {
use rand::SeedableRng;
assert!(
max_samples >= Self::MIN_SAMPLES,
"max_samples must be at least {}",
Self::MIN_SAMPLES
);
QSketch {
max_samples,
samples: Vec::with_capacity(max_samples),
items_seen: HashMap::new(),
total_weight: 0.0,
threshold: 0.0,
rng: rand::rngs::SmallRng::from_os_rng(),
}
}
pub fn with_seed(max_samples: usize, seed: u64) -> Self {
use rand::SeedableRng;
assert!(
max_samples >= Self::MIN_SAMPLES,
"max_samples must be at least {}",
Self::MIN_SAMPLES
);
QSketch {
max_samples,
samples: Vec::with_capacity(max_samples),
items_seen: HashMap::new(),
total_weight: 0.0,
threshold: 0.0,
rng: rand::rngs::SmallRng::seed_from_u64(seed),
}
}
#[inline]
pub fn max_samples(&self) -> usize {
self.max_samples
}
#[inline]
pub fn sample_count(&self) -> usize {
self.samples.len()
}
pub fn total_weight(&self) -> f64 {
self.total_weight
}
pub fn estimate_distinct_elements(&self) -> u64 {
if self.samples.is_empty() {
return 0;
}
if self.samples.len() < self.max_samples {
return self.items_seen.len() as u64;
}
let sample_size = self.samples.len() as f64;
let total_w = self.total_weight();
if total_w <= 0.0 {
return self.items_seen.len() as u64;
}
let estimated_distinct = (self.items_seen.len() as f64) * (total_w / sample_size).max(1.0);
estimated_distinct.ceil() as u64
}
pub fn estimate_weighted_cardinality(&self) -> (f64, f64) {
if self.samples.is_empty() {
return (0.0, 0.0);
}
let sample_weight: f64 = self.samples.iter().map(|s| s.weight).sum();
if sample_weight <= 0.0 {
return (0.0, 0.0);
}
let sample_size = self.samples.len() as f64;
let total_w = self.total_weight();
let estimate = if sample_size > 0.0 && sample_weight > 0.0 {
(total_w / sample_weight) * (self.items_seen.len() as f64)
} else {
self.items_seen.len() as f64
};
let std_error = self.compute_standard_error(sample_size, sample_weight);
let error_bound = 1.96 * std_error;
(estimate, error_bound)
}
#[inline]
fn compute_standard_error(&self, sample_size: f64, sample_weight: f64) -> f64 {
if sample_size < 2.0 || sample_weight <= 0.0 {
return 0.0;
}
let total_w = self.total_weight();
let mean_weight = sample_weight / sample_size;
let variance: f64 = self
.samples
.iter()
.map(|s| (s.weight - mean_weight).powi(2))
.sum::<f64>()
/ sample_size;
let se = (variance / sample_size).sqrt();
(total_w * se) / sample_weight
}
#[inline]
fn hash_item(&self, item: &[u8]) -> u64 {
let mut hasher = XxHash64::with_seed(0);
item.hash(&mut hasher);
hasher.finish()
}
pub fn update(&mut self, item: &[u8], weight: f64) {
assert!(
weight > 0.0 && weight.is_finite(),
"Weight must be positive and finite, got {}",
weight
);
let element_id = self.hash_item(item);
if let Some(existing) = self.items_seen.get_mut(&element_id) {
*existing += weight;
} else {
self.items_seen.insert(element_id, weight);
}
self.total_weight += weight;
self.maybe_add_sample(element_id, weight);
}
fn maybe_add_sample(&mut self, element_id: u64, weight: f64) {
if self.samples.len() < self.max_samples {
self.samples.push(SampledItem { element_id, weight });
self.update_threshold();
return;
}
if weight > self.threshold {
let prob = weight / (weight + self.threshold);
if self.rng.random::<f64>() < prob {
if let Some(min_idx) = self.find_min_weight_index() {
self.samples[min_idx] = SampledItem { element_id, weight };
self.update_threshold();
}
}
}
}
#[inline]
fn find_min_weight_index(&self) -> Option<usize> {
self.samples
.iter()
.enumerate()
.min_by(|a, b| {
a.1.weight
.partial_cmp(&b.1.weight)
.unwrap_or(std::cmp::Ordering::Equal)
})
.map(|(idx, _)| idx)
}
fn update_threshold(&mut self) {
if self.samples.is_empty() {
self.threshold = 0.0;
return;
}
self.threshold = self
.samples
.iter()
.map(|s| s.weight)
.fold(f64::MAX, f64::min);
}
pub fn reset(&mut self) {
self.samples.clear();
self.items_seen.clear();
self.total_weight = 0.0;
self.threshold = 0.0;
}
pub fn merge(&mut self, other: &Self) -> Result<(), SketchError> {
if self.max_samples != other.max_samples {
return Err(SketchError::IncompatibleSketches {
reason: format!(
"QSketch max_samples mismatch: {} vs {}",
self.max_samples, other.max_samples
),
});
}
for (&element_id, &weight) in &other.items_seen {
self.items_seen
.entry(element_id)
.and_modify(|w| *w += weight)
.or_insert(weight);
}
self.total_weight += other.total_weight;
self.samples.extend(other.samples.iter().cloned());
if self.samples.len() > self.max_samples {
self.samples.sort_by(|a, b| {
b.weight
.partial_cmp(&a.weight)
.unwrap_or(std::cmp::Ordering::Equal)
});
self.samples.truncate(self.max_samples);
}
self.update_threshold();
Ok(())
}
pub fn to_bytes(&self) -> Vec<u8> {
let mut bytes = Vec::new();
bytes.extend_from_slice(&(self.max_samples as u32).to_le_bytes());
bytes.extend_from_slice(&self.total_weight.to_le_bytes());
bytes.extend_from_slice(&(self.samples.len() as u32).to_le_bytes());
for sample in &self.samples {
bytes.extend_from_slice(&sample.element_id.to_le_bytes());
bytes.extend_from_slice(&sample.weight.to_le_bytes());
}
bytes.extend_from_slice(&(self.items_seen.len() as u32).to_le_bytes());
for (&element_id, &weight) in &self.items_seen {
bytes.extend_from_slice(&element_id.to_le_bytes());
bytes.extend_from_slice(&weight.to_le_bytes());
}
bytes
}
pub fn from_bytes(bytes: &[u8]) -> Result<Self, SketchError> {
if bytes.len() < 16 {
return Err(SketchError::DeserializationError(
"QSketch bytes too short".to_string(),
));
}
let mut offset = 0;
let max_samples = u32::from_le_bytes([
bytes[offset],
bytes[offset + 1],
bytes[offset + 2],
bytes[offset + 3],
]) as usize;
offset += 4;
let total_weight = f64::from_le_bytes([
bytes[offset],
bytes[offset + 1],
bytes[offset + 2],
bytes[offset + 3],
bytes[offset + 4],
bytes[offset + 5],
bytes[offset + 6],
bytes[offset + 7],
]);
offset += 8;
let num_samples = u32::from_le_bytes([
bytes[offset],
bytes[offset + 1],
bytes[offset + 2],
bytes[offset + 3],
]) as usize;
offset += 4;
let mut samples = Vec::with_capacity(num_samples);
for _ in 0..num_samples {
if offset + 16 > bytes.len() {
return Err(SketchError::DeserializationError(
"Invalid sample data".to_string(),
));
}
let element_id = u64::from_le_bytes([
bytes[offset],
bytes[offset + 1],
bytes[offset + 2],
bytes[offset + 3],
bytes[offset + 4],
bytes[offset + 5],
bytes[offset + 6],
bytes[offset + 7],
]);
offset += 8;
let weight = f64::from_le_bytes([
bytes[offset],
bytes[offset + 1],
bytes[offset + 2],
bytes[offset + 3],
bytes[offset + 4],
bytes[offset + 5],
bytes[offset + 6],
bytes[offset + 7],
]);
offset += 8;
samples.push(SampledItem { element_id, weight });
}
if offset + 4 > bytes.len() {
return Err(SketchError::DeserializationError(
"Invalid items_seen count".to_string(),
));
}
let num_items_seen = u32::from_le_bytes([
bytes[offset],
bytes[offset + 1],
bytes[offset + 2],
bytes[offset + 3],
]) as usize;
offset += 4;
let mut items_seen = HashMap::with_capacity(num_items_seen);
for _ in 0..num_items_seen {
if offset + 16 > bytes.len() {
return Err(SketchError::DeserializationError(
"Invalid items_seen data".to_string(),
));
}
let element_id = u64::from_le_bytes([
bytes[offset],
bytes[offset + 1],
bytes[offset + 2],
bytes[offset + 3],
bytes[offset + 4],
bytes[offset + 5],
bytes[offset + 6],
bytes[offset + 7],
]);
offset += 8;
let weight = f64::from_le_bytes([
bytes[offset],
bytes[offset + 1],
bytes[offset + 2],
bytes[offset + 3],
bytes[offset + 4],
bytes[offset + 5],
bytes[offset + 6],
bytes[offset + 7],
]);
offset += 8;
items_seen.insert(element_id, weight);
}
use rand::SeedableRng;
Ok(QSketch {
max_samples,
samples,
items_seen,
total_weight,
threshold: 0.0,
rng: rand::rngs::SmallRng::from_os_rng(),
})
}
}
impl Sketch for QSketch {
type Item = u64;
fn update(&mut self, _item: &Self::Item) {
unimplemented!("Use QSketch::update(item, weight) instead of the Sketch trait")
}
fn estimate(&self) -> f64 {
let (estimate, _) = self.estimate_weighted_cardinality();
estimate
}
fn is_empty(&self) -> bool {
self.samples.is_empty()
}
fn serialize(&self) -> Vec<u8> {
self.to_bytes()
}
fn deserialize(bytes: &[u8]) -> Result<Self, SketchError> {
Self::from_bytes(bytes)
}
}
impl Mergeable for QSketch {
fn merge(&mut self, other: &Self) -> Result<(), SketchError> {
self.merge(other)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_new_qsketch() {
let qsketch = QSketch::new(256);
assert!(qsketch.is_empty());
assert_eq!(qsketch.max_samples(), 256);
assert_eq!(qsketch.sample_count(), 0);
assert_eq!(qsketch.total_weight(), 0.0);
}
#[test]
fn test_update_single_element() {
let mut qsketch = QSketch::new(256);
qsketch.update(b"item_1", 10.0);
assert!(!qsketch.is_empty());
assert_eq!(qsketch.sample_count(), 1);
assert!((qsketch.total_weight() - 10.0).abs() < 0.001);
assert_eq!(qsketch.estimate_distinct_elements(), 1);
}
#[test]
fn test_update_multiple_elements() {
let mut qsketch = QSketch::new(256);
qsketch.update(b"item_1", 10.0);
qsketch.update(b"item_2", 20.0);
qsketch.update(b"item_3", 30.0);
assert_eq!(qsketch.sample_count(), 3);
assert!((qsketch.total_weight() - 60.0).abs() < 0.001);
assert_eq!(qsketch.estimate_distinct_elements(), 3);
}
#[test]
fn test_duplicate_elements() {
let mut qsketch = QSketch::new(256);
qsketch.update(b"item_1", 10.0);
qsketch.update(b"item_1", 5.0);
assert_eq!(qsketch.estimate_distinct_elements(), 1);
assert!((qsketch.total_weight() - 15.0).abs() < 0.001);
}
#[test]
fn test_uniform_weights() {
let mut qsketch = QSketch::new(256);
for i in 0..100 {
qsketch.update(format!("item_{}", i).as_bytes(), 1.0);
}
assert_eq!(qsketch.estimate_distinct_elements(), 100);
assert!((qsketch.total_weight() - 100.0).abs() < 0.001);
}
#[test]
fn test_skewed_weights() {
let mut qsketch = QSketch::new(256);
qsketch.update(b"item_1", 1000.0);
qsketch.update(b"item_2", 500.0);
qsketch.update(b"item_3", 250.0);
qsketch.update(b"item_4", 100.0);
qsketch.update(b"item_5", 50.0);
assert_eq!(qsketch.estimate_distinct_elements(), 5);
assert!((qsketch.total_weight() - 1900.0).abs() < 0.001);
}
#[test]
fn test_weighted_cardinality_estimate() {
let mut qsketch = QSketch::new(256);
qsketch.update(b"user_1", 100.0);
qsketch.update(b"user_2", 250.0);
qsketch.update(b"user_3", 50.0);
let (estimate, error_bound) = qsketch.estimate_weighted_cardinality();
assert!(estimate > 0.0);
assert!(error_bound >= 0.0);
}
#[test]
fn test_merge_sketches() {
let mut qsketch1 = QSketch::new(256);
let mut qsketch2 = QSketch::new(256);
qsketch1.update(b"item_1", 100.0);
qsketch1.update(b"item_2", 50.0);
qsketch2.update(b"item_3", 150.0);
qsketch2.update(b"item_4", 75.0);
qsketch1.merge(&qsketch2).unwrap();
assert!((qsketch1.total_weight() - 375.0).abs() < 0.001);
assert!(qsketch1.estimate_distinct_elements() >= 3);
}
#[test]
fn test_merge_with_duplicates() {
let mut qsketch1 = QSketch::new(256);
let mut qsketch2 = QSketch::new(256);
qsketch1.update(b"item_1", 100.0);
qsketch2.update(b"item_1", 50.0);
qsketch1.merge(&qsketch2).unwrap();
assert!((qsketch1.total_weight() - 150.0).abs() < 0.001);
assert_eq!(qsketch1.estimate_distinct_elements(), 1);
}
#[test]
fn test_merge_incompatible_config() {
let mut qsketch1 = QSketch::new(256);
let qsketch2 = QSketch::new(128);
qsketch1.update(b"item_1", 100.0);
let result = qsketch1.merge(&qsketch2);
assert!(result.is_err());
}
#[test]
fn test_total_weight_accuracy() {
let mut qsketch = QSketch::new(256);
let items = vec![
("item_1", 10.5),
("item_2", 20.3),
("item_3", 30.2),
("item_4", 15.0),
];
let mut expected_total = 0.0;
for (item, weight) in items {
qsketch.update(item.as_bytes(), weight);
expected_total += weight;
}
assert!((qsketch.total_weight() - expected_total).abs() < 0.001);
}
#[test]
fn test_distinct_element_counting() {
let mut qsketch = QSketch::new(256);
for i in 0..50 {
qsketch.update(format!("user_{}", i).as_bytes(), (i as f64) + 1.0);
}
let distinct = qsketch.estimate_distinct_elements();
assert!((45..=50).contains(&distinct));
}
#[test]
fn test_serialization_deserialization() {
let mut qsketch = QSketch::new(256);
qsketch.update(b"item_1", 10.0);
qsketch.update(b"item_2", 20.0);
qsketch.update(b"item_3", 30.0);
let bytes = qsketch.to_bytes();
let restored = QSketch::from_bytes(&bytes).unwrap();
assert_eq!(restored.max_samples(), qsketch.max_samples());
assert!((restored.total_weight() - qsketch.total_weight()).abs() < 0.001);
assert_eq!(restored.sample_count(), qsketch.sample_count());
}
#[test]
fn test_reset() {
let mut qsketch = QSketch::new(256);
qsketch.update(b"item_1", 10.0);
qsketch.update(b"item_2", 20.0);
assert!(!qsketch.is_empty());
qsketch.reset();
assert!(qsketch.is_empty());
assert_eq!(qsketch.sample_count(), 0);
assert_eq!(qsketch.total_weight(), 0.0);
assert_eq!(qsketch.estimate_distinct_elements(), 0);
}
#[test]
fn test_with_seed_reproducibility() {
let mut qsketch1 = QSketch::with_seed(256, 42);
let mut qsketch2 = QSketch::with_seed(256, 42);
for i in 1..=50 {
let weight = (i as f64) * 1.5;
qsketch1.update(format!("item_{}", i).as_bytes(), weight);
qsketch2.update(format!("item_{}", i).as_bytes(), weight);
}
assert_eq!(qsketch1.sample_count(), qsketch2.sample_count());
assert!((qsketch1.total_weight() - qsketch2.total_weight()).abs() < 0.0001);
}
}