use std::collections::HashSet;
use arrow_array::{Array, FixedSizeBinaryArray};
pub const TRACE_IDX: &str = "trace.idx";
pub const ATTR_IDX: &str = "attr.idx";
const MAGIC: [u8; 4] = *b"MBLM";
const VERSION: u8 = 1;
const HEADER: usize = 16;
pub const HAS_DOUBLE: u8 = 1;
const BITS_PER_KEY: usize = 10;
const K: u32 = 7;
pub fn build(ids: &FixedSizeBinaryArray) -> Option<Vec<u8>> {
if ids.value_length() != 16 {
return None;
}
let keys = || Runs {
ids,
i: 0,
prev: None,
};
let n = keys().count();
encode(n, 0, keys().map(halves))
}
#[derive(Default)]
pub struct Keys {
seen: HashSet<(u64, u64)>,
flags: u8,
full: bool,
}
const MAX_KEYS: usize = 1 << 20;
impl Keys {
pub fn insert(&mut self, h: (u64, u64)) {
if self.seen.len() < MAX_KEYS {
self.seen.insert(h);
} else {
self.full = true;
}
}
pub fn flag(&mut self, bit: u8) {
self.flags |= bit;
}
pub fn build(&self) -> Option<Vec<u8>> {
if self.full {
return None;
}
encode(self.seen.len(), self.flags, self.seen.iter().copied())
}
}
fn encode(n: usize, flags: u8, keys: impl Iterator<Item = (u64, u64)>) -> Option<Vec<u8>> {
if n == 0 {
return None;
}
let words = ((n * BITS_PER_KEY).div_ceil(64)).next_power_of_two();
let mut bits = vec![0u64; words];
let mask = (words as u64 * 64) - 1;
for (h1, h2) in keys {
for i in 0..K {
let bit = h1.wrapping_add((i as u64).wrapping_mul(h2)) & mask;
bits[bit as usize / 64] |= 1 << (bit % 64);
}
}
let mut out = Vec::with_capacity(HEADER + words * 8);
out.extend_from_slice(&MAGIC);
out.push(VERSION);
out.push(K as u8);
out.push(flags);
out.push(0);
out.extend_from_slice(&(words as u32).to_le_bytes());
let body: Vec<u8> = bits.iter().flat_map(|w| w.to_le_bytes()).collect();
out.extend_from_slice(&crc32fast::hash(&body).to_le_bytes());
out.extend_from_slice(&body);
Some(out)
}
pub struct Filter<'a> {
body: &'a [u8],
k: u32,
mask: u64,
pub flags: u8,
}
impl<'a> Filter<'a> {
pub fn open(file: &'a [u8]) -> Option<Filter<'a>> {
if file.len() < HEADER || file[..4] != MAGIC || file[4] != VERSION {
return None;
}
let k = file[5] as u32;
let flags = file[6];
let words = u32::from_le_bytes(file[8..12].try_into().expect("4 bytes")) as usize;
let crc = u32::from_le_bytes(file[12..16].try_into().expect("4 bytes"));
let body = &file[HEADER..];
if k == 0 || !words.is_power_of_two() || body.len() != words * 8 {
return None;
}
if crc32fast::hash(body) != crc {
return None;
}
Some(Filter {
body,
k,
mask: (words as u64 * 64) - 1,
flags,
})
}
pub fn may_contain(&self, (h1, h2): (u64, u64)) -> bool {
for i in 0..self.k {
let bit = h1.wrapping_add((i as u64).wrapping_mul(h2)) & self.mask;
let word = u64::from_le_bytes(
self.body[(bit as usize / 64) * 8..][..8]
.try_into()
.expect("slice of 8"),
);
if word & (1 << (bit % 64)) == 0 {
return false;
}
}
true
}
}
pub fn may_contain(file: &[u8], id: &[u8; 16]) -> bool {
Filter::open(file).is_none_or(|f| f.may_contain(halves(id)))
}
pub fn attr_hash(key: &str, value: &[u8]) -> (u64, u64) {
let k = crate::identity::hash64(key.as_bytes());
let v = crate::identity::hash64(value);
(
mix(k ^ v.rotate_left(17)),
mix(k.wrapping_mul(0x9e37_79b9_7f4a_7c15) ^ v) | 1,
)
}
struct Runs<'a> {
ids: &'a FixedSizeBinaryArray,
i: usize,
prev: Option<&'a [u8]>,
}
impl<'a> Iterator for Runs<'a> {
type Item = &'a [u8];
fn next(&mut self) -> Option<&'a [u8]> {
while self.i < self.ids.len() {
let i = self.i;
self.i += 1;
if self.ids.is_null(i) {
continue;
}
let v = self.ids.value(i);
if self.prev != Some(v) {
self.prev = Some(v);
return Some(v);
}
}
None
}
}
fn halves(id: &[u8]) -> (u64, u64) {
let h1 = mix(u64::from_le_bytes(id[..8].try_into().expect("16-byte id")));
let h2 = mix(u64::from_le_bytes(
id[8..16].try_into().expect("16-byte id"),
)) | 1;
(h1, h2)
}
fn mix(mut x: u64) -> u64 {
x ^= x >> 30;
x = x.wrapping_mul(0xbf58_476d_1ce4_e5b9);
x ^= x >> 27;
x = x.wrapping_mul(0x94d0_49bb_1331_11eb);
x ^ (x >> 31)
}
#[cfg(test)]
mod tests {
use super::*;
use arrow_array::FixedSizeBinaryArray;
fn id(n: u64) -> [u8; 16] {
let mut b = [0u8; 16];
b[..8].copy_from_slice(&crate::identity::hash64(&n.to_le_bytes()).to_le_bytes());
b[8..].copy_from_slice(&crate::identity::hash64(&(!n).to_le_bytes()).to_le_bytes());
b
}
fn filter(n: u64) -> Vec<u8> {
let ids: Vec<[u8; 16]> = (0..n).map(id).collect();
let arr = FixedSizeBinaryArray::try_from_iter(ids.iter().map(|v| v.as_slice())).unwrap();
build(&arr).unwrap()
}
#[test]
fn no_false_negatives_and_few_false_positives() {
let n = 10_000u64;
let f = filter(n);
assert_eq!(f.len(), HEADER + 2048 * 8);
for i in 0..n {
assert!(may_contain(&f, &id(i)), "false negative at {i}");
}
let probes = 100_000u64;
let fp = (n..n + probes).filter(|&i| may_contain(&f, &id(i))).count();
let rate = fp as f64 / probes as f64;
assert!(rate < 0.02, "false positive rate {rate}");
}
#[test]
fn a_damaged_filter_says_maybe() {
let f = filter(1_000);
let probe = id(999_999);
assert!(!may_contain(&f, &probe), "test needs a known-absent id");
assert!(may_contain(&[], &probe), "empty");
assert!(may_contain(&f[..HEADER - 1], &probe), "truncated header");
assert!(may_contain(&f[..f.len() - 8], &probe), "truncated body");
let mut bad = f.clone();
bad[0] = b'X';
assert!(may_contain(&bad, &probe), "wrong magic");
let mut bad = f.clone();
bad[4] = VERSION + 1;
assert!(may_contain(&bad, &probe), "future version");
let mut bad = f.clone();
bad[8..12].copy_from_slice(&0u32.to_le_bytes());
assert!(may_contain(&bad, &probe), "no bits at all");
let mut bad = f.clone();
bad[8..12].copy_from_slice(&3u32.to_le_bytes());
assert!(may_contain(&bad, &probe), "word count not a power of two");
let mut bad = f.clone();
bad[5] = 0;
assert!(may_contain(&bad, &probe), "no hash functions");
let mut bad = f;
bad[HEADER + 3] ^= 0x40;
assert!(may_contain(&bad, &probe), "corrupt body");
}
#[test]
fn a_column_that_is_not_a_16_byte_id_indexes_nothing() {
let narrow = [[7u8; 8].as_slice(), [9u8; 8].as_slice()];
let arr = FixedSizeBinaryArray::try_from_iter(narrow.into_iter()).unwrap();
assert_eq!(arr.value_length(), 8);
assert!(build(&arr).is_none());
assert!(build(&FixedSizeBinaryArray::new_null(32, 4)).is_none());
}
#[test]
fn too_many_distinct_keys_write_no_filter_rather_than_a_huge_one() {
let mut keys = Keys::default();
for i in 0..MAX_KEYS as u64 {
keys.insert((i, i | 1));
}
assert!(keys.build().is_some(), "MAX_KEYS keys still fit");
keys.insert((u64::MAX, 1));
assert!(keys.build().is_none(), "one key past the cap");
}
#[test]
fn structured_ids_still_spread() {
let structured = |n: u64| {
let mut b = [0u8; 16];
b[..8].copy_from_slice(&(n as u32 as u64).to_be_bytes());
b[8..].copy_from_slice(&(0x5555_5555_5500_0000 | n).to_be_bytes());
b
};
let ids: Vec<[u8; 16]> = (0..10_000u64).map(structured).collect();
let arr = FixedSizeBinaryArray::try_from_iter(ids.iter().map(|v| v.as_slice())).unwrap();
let f = build(&arr).unwrap();
for i in 0..10_000u64 {
assert!(may_contain(&f, &structured(i)), "false negative at {i}");
}
let probes = 100_000u64;
let fp = (10_000..10_000 + probes)
.filter(|&i| may_contain(&f, &structured(i)))
.count();
let rate = fp as f64 / probes as f64;
assert!(rate < 0.02, "false positive rate {rate}");
}
#[test]
fn adjacent_duplicates_do_not_inflate_the_filter() {
let ids: Vec<[u8; 16]> = (0..1_000u64).flat_map(|n| [id(n); 8]).collect();
let arr = FixedSizeBinaryArray::try_from_iter(ids.iter().map(|v| v.as_slice())).unwrap();
let fanned = build(&arr).unwrap();
assert_eq!(fanned.len(), filter(1_000).len());
for i in 0..1_000u64 {
assert!(may_contain(&fanned, &id(i)), "false negative at {i}");
}
}
#[test]
fn nothing_to_index_writes_nothing() {
assert!(build(&FixedSizeBinaryArray::new_null(16, 0)).is_none());
assert!(build(&FixedSizeBinaryArray::new_null(16, 100)).is_none());
}
}