Skip to main content

steeldb/
bitmap.rs

1//! Postings — an exact set of unsigned situation ids with roaring-compatible set algebra.
2//!
3//! The whole engine is bit-parallel logic over these: AND (∩), OR (∪), AND-NOT (−), popcount.
4//! `Postings` is the trait the query layer is generic over, so we can benchmark the historical
5//! `Set<number>` baseline (`SetPostings`, a `HashSet<u32>`) against real roaring containers
6//! (`RoarPostings`) without touching a single caller — exactly the swap the TS `Bitmap` was
7//! designed for ("swap in a native roaring library later without touching callers").
8//!
9//! `serialize_deltagap` uses the same varint delta-gap (LEB128 over sorted gaps) as the TS/Python
10//! parts store, so posting blobs stay byte-compatible and portable across engines.
11
12use roaring::RoaringBitmap;
13use std::collections::HashSet;
14
15/// Set-algebra surface the tokenql evaluator and index build against.
16pub trait Postings: Clone {
17    fn empty() -> Self;
18    /// Build from strictly-ascending, de-duplicated ids (the natural order of an inverted index).
19    fn from_sorted(ids: &[u32]) -> Self;
20    fn insert(&mut self, id: u32);
21    fn contains(&self, id: u32) -> bool;
22    fn len(&self) -> usize;
23    fn is_empty(&self) -> bool {
24        self.len() == 0
25    }
26    /// ∩
27    fn and(&self, other: &Self) -> Self;
28    /// ∪
29    fn or(&self, other: &Self) -> Self;
30    /// − (this and-not other)
31    fn and_not(&self, other: &Self) -> Self;
32    /// in-place ∪ (the `|=` of the build/OR loop)
33    fn or_inplace(&mut self, other: &Self);
34    /// ids in ascending order
35    fn to_sorted(&self) -> Vec<u32>;
36    /// byte size of this set's native serialized form (memory proxy)
37    fn native_bytes(&self) -> usize;
38
39    /// varint delta-gap of the sorted ids — byte-identical to the TS/Python parts store.
40    fn serialize_deltagap(&self) -> Vec<u8> {
41        let ids = self.to_sorted();
42        let mut out = Vec::new();
43        let mut prev: u32 = 0;
44        for id in ids {
45            let mut n = id.wrapping_sub(prev);
46            prev = id;
47            loop {
48                let b = (n & 0x7f) as u8;
49                n >>= 7;
50                if n != 0 {
51                    out.push(b | 0x80);
52                } else {
53                    out.push(b);
54                    break;
55                }
56            }
57        }
58        out
59    }
60
61    /// inverse of `serialize_deltagap`
62    fn deserialize_deltagap(bytes: &[u8]) -> Self
63    where
64        Self: Sized,
65    {
66        let mut ids = Vec::new();
67        let mut prev: u32 = 0;
68        let mut i = 0;
69        while i < bytes.len() {
70            let mut shift = 0u32;
71            let mut val: u32 = 0;
72            loop {
73                let b = bytes[i];
74                i += 1;
75                val |= ((b & 0x7f) as u32) << shift;
76                if b & 0x80 == 0 {
77                    break;
78                }
79                shift += 7;
80            }
81            prev = prev.wrapping_add(val);
82            ids.push(prev);
83        }
84        Self::from_sorted(&ids)
85    }
86}
87
88// ── Baseline: HashSet<u32> (the TS `Set<number>` engine, ported faithfully) ──────────────────
89
90#[derive(Clone, Default)]
91pub struct SetPostings(pub HashSet<u32>);
92
93impl Postings for SetPostings {
94    fn empty() -> Self {
95        SetPostings(HashSet::new())
96    }
97    fn from_sorted(ids: &[u32]) -> Self {
98        SetPostings(ids.iter().copied().collect())
99    }
100    fn insert(&mut self, id: u32) {
101        self.0.insert(id);
102    }
103    fn contains(&self, id: u32) -> bool {
104        self.0.contains(&id)
105    }
106    fn len(&self) -> usize {
107        self.0.len()
108    }
109    fn and(&self, other: &Self) -> Self {
110        // iterate the smaller, probe the larger (matches the TS `and`)
111        let (small, big) = if self.0.len() <= other.0.len() {
112            (&self.0, &other.0)
113        } else {
114            (&other.0, &self.0)
115        };
116        SetPostings(small.iter().copied().filter(|id| big.contains(id)).collect())
117    }
118    fn or(&self, other: &Self) -> Self {
119        let mut out = self.0.clone();
120        out.extend(other.0.iter().copied());
121        SetPostings(out)
122    }
123    fn and_not(&self, other: &Self) -> Self {
124        SetPostings(self.0.iter().copied().filter(|id| !other.0.contains(id)).collect())
125    }
126    fn or_inplace(&mut self, other: &Self) {
127        self.0.extend(other.0.iter().copied());
128    }
129    fn to_sorted(&self) -> Vec<u32> {
130        let mut v: Vec<u32> = self.0.iter().copied().collect();
131        v.sort_unstable();
132        v
133    }
134    fn native_bytes(&self) -> usize {
135        // hashbrown table: ~ capacity * (4 byte key + 1 control byte), load-factor ~7/8
136        let cap = (self.0.len() as f64 / 0.875).ceil() as usize;
137        cap * 5
138    }
139}
140
141// ── Roaring containers (the recommendation) ──────────────────────────────────────────────────
142
143#[derive(Clone, Default, Debug)]
144pub struct RoarPostings(pub RoaringBitmap);
145
146impl Postings for RoarPostings {
147    fn empty() -> Self {
148        RoarPostings(RoaringBitmap::new())
149    }
150    fn from_sorted(ids: &[u32]) -> Self {
151        // ids are ascending & unique → the fast bulk path that builds optimal containers
152        match RoaringBitmap::from_sorted_iter(ids.iter().copied()) {
153            Ok(b) => RoarPostings(b),
154            Err(_) => {
155                let mut b = RoaringBitmap::new();
156                b.extend(ids.iter().copied());
157                RoarPostings(b)
158            }
159        }
160    }
161    fn insert(&mut self, id: u32) {
162        self.0.insert(id);
163    }
164    fn contains(&self, id: u32) -> bool {
165        self.0.contains(id)
166    }
167    fn len(&self) -> usize {
168        self.0.len() as usize
169    }
170    fn and(&self, other: &Self) -> Self {
171        RoarPostings(&self.0 & &other.0)
172    }
173    fn or(&self, other: &Self) -> Self {
174        RoarPostings(&self.0 | &other.0)
175    }
176    fn and_not(&self, other: &Self) -> Self {
177        RoarPostings(&self.0 - &other.0)
178    }
179    fn or_inplace(&mut self, other: &Self) {
180        self.0 |= &other.0;
181    }
182    fn to_sorted(&self) -> Vec<u32> {
183        self.0.iter().collect()
184    }
185    fn native_bytes(&self) -> usize {
186        self.0.serialized_size()
187    }
188}
189
190#[cfg(test)]
191mod tests {
192    use super::*;
193
194    fn roundtrip<B: Postings>() {
195        let ids = [0u32, 1, 2, 5, 300, 70000, 70001, 1_000_000];
196        let b = B::from_sorted(&ids);
197        let bytes = b.serialize_deltagap();
198        let back = B::deserialize_deltagap(&bytes);
199        assert_eq!(back.to_sorted(), ids.to_vec());
200    }
201
202    #[test]
203    fn deltagap_roundtrip_both_backends() {
204        roundtrip::<SetPostings>();
205        roundtrip::<RoarPostings>();
206    }
207
208    #[test]
209    fn set_algebra_parity() {
210        let a_ids = [1u32, 2, 3, 4, 5, 100, 200];
211        let b_ids = [3u32, 4, 5, 6, 200, 300];
212        let (sa, sb) = (SetPostings::from_sorted(&a_ids), SetPostings::from_sorted(&b_ids));
213        let (ra, rb) = (RoarPostings::from_sorted(&a_ids), RoarPostings::from_sorted(&b_ids));
214        assert_eq!(sa.and(&sb).to_sorted(), ra.and(&rb).to_sorted());
215        assert_eq!(sa.or(&sb).to_sorted(), ra.or(&rb).to_sorted());
216        assert_eq!(sa.and_not(&sb).to_sorted(), ra.and_not(&rb).to_sorted());
217    }
218}