use rayon::prelude::*;
use crate::rank::{rank_to_bucket, rank_transform};
pub struct MultiBucketBitmap {
dim: usize,
bits: u8,
n_buckets: usize,
qwords_per_bitmap: usize,
n_vectors: usize,
bitmaps: Vec<u64>,
}
impl MultiBucketBitmap {
pub fn new(dim: usize, bits: u8) -> Self {
assert!(matches!(bits, 1 | 2 | 4), "bits must be 1, 2, or 4");
assert!(dim > 0, "dim must be > 0");
assert_eq!(dim % 64, 0, "dim must be a multiple of 64");
let n_buckets = 1usize << bits;
let qpb = dim / 64;
assert_eq!(
dim % n_buckets,
0,
"dim must be a multiple of 2^bits for constant-composition",
);
Self {
dim,
bits,
n_buckets,
qwords_per_bitmap: qpb,
n_vectors: 0,
bitmaps: Vec::new(),
}
}
pub fn add(&mut self, vectors: &[f32]) {
let n = vectors.len() / self.dim;
assert_eq!(vectors.len(), n * self.dim);
crate::util::assert_all_finite(vectors);
let qpb = self.qwords_per_bitmap;
let nb = self.n_buckets;
let per_doc = nb * qpb;
let start = self.bitmaps.len();
self.bitmaps.resize(start + n * per_doc, 0u64);
let dim = self.dim;
let bits = self.bits;
self.bitmaps[start..]
.par_chunks_mut(per_doc)
.zip(vectors.par_chunks(dim))
.for_each(|(out, v)| {
let ranks = rank_transform(v);
for j in 0..dim {
let b = rank_to_bucket(ranks[j], dim, bits) as usize;
out[b * qpb + j / 64] |= 1u64 << (j % 64);
}
});
self.n_vectors += n;
}
pub fn query_bitmaps_from_ranks(&self, q: &[f32]) -> Vec<u64> {
assert_eq!(q.len(), self.dim);
crate::util::assert_all_finite(q);
let qpb = self.qwords_per_bitmap;
let nb = self.n_buckets;
let bits = self.bits;
let dim = self.dim;
let ranks = rank_transform(q);
let mut out = vec![0u64; nb * qpb];
for j in 0..dim {
let b = rank_to_bucket(ranks[j], dim, bits) as usize;
out[b * qpb + j / 64] |= 1u64 << (j % 64);
}
out
}
pub fn outer_product_weights(&self) -> Vec<f32> {
let nb = self.n_buckets;
let c = (nb as f32 - 1.0) / 2.0;
let mut w = vec![0.0f32; nb * nb];
for a in 0..nb {
for b in 0..nb {
w[a * nb + b] = (a as f32 - c) * (b as f32 - c);
}
}
w
}
pub fn bilinear_score(&self, q_bitmaps: &[u64], w: &[f32], doc_idx: usize) -> f32 {
let qpb = self.qwords_per_bitmap;
let nb = self.n_buckets;
assert!(
doc_idx < self.n_vectors,
"bilinear_score: doc_idx {doc_idx} out of range (n_vectors {})",
self.n_vectors,
);
debug_assert_eq!(q_bitmaps.len(), nb * qpb);
debug_assert_eq!(w.len(), nb * nb);
let doc_base = doc_idx * nb * qpb;
let mut acc = 0.0f32;
for a in 0..nb {
for b in 0..nb {
let weight = w[a * nb + b];
if weight == 0.0 {
continue;
}
let q_off = a * qpb;
let d_off = doc_base + b * qpb;
let mut overlap: u32 = 0;
for k in 0..qpb {
overlap += (q_bitmaps[q_off + k] & self.bitmaps[d_off + k]).count_ones();
}
acc += weight * (overlap as f32);
}
}
acc
}
pub fn top_m_bilinear(&self, q_bitmaps: &[u64], w: &[f32], m: usize) -> Vec<u32> {
let m_eff = m.min(self.n_vectors);
if m_eff == 0 {
return Vec::new();
}
let n = self.n_vectors;
let mut scores = vec![0.0f32; n];
scores.par_iter_mut().enumerate().for_each(|(di, s)| {
*s = self.bilinear_score(q_bitmaps, w, di);
});
let mut idx: Vec<u32> = (0..n as u32).collect();
idx.select_nth_unstable_by(m_eff - 1, |&a, &b| {
scores[b as usize]
.partial_cmp(&scores[a as usize])
.unwrap_or(std::cmp::Ordering::Equal)
});
let mut head = idx[..m_eff].to_vec();
head.sort_unstable_by(|&a, &b| {
scores[b as usize]
.partial_cmp(&scores[a as usize])
.unwrap_or(std::cmp::Ordering::Equal)
});
head
}
pub fn len(&self) -> usize {
self.n_vectors
}
pub fn is_empty(&self) -> bool {
self.n_vectors == 0
}
pub fn dim(&self) -> usize {
self.dim
}
pub fn bits(&self) -> u8 {
self.bits
}
pub fn n_buckets(&self) -> usize {
self.n_buckets
}
pub fn bytes_per_vec(&self) -> usize {
self.qwords_per_bitmap * self.n_buckets * 8
}
pub fn byte_size(&self) -> usize {
self.bitmaps.len() * std::mem::size_of::<u64>()
}
}