Skip to main content

nodedb_vector/collection/
payload_index.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! In-memory payload bitmap indexes for vector-primary collections.
4//!
5//! Each indexed field stores a `HashMap<PayloadKey, RoaringBitmap>` mapping
6//! distinct field values to the set of HNSW node ids that carry that value.
7//! Pre-filter queries intersect/union bitmaps to produce a node-id allow-set
8//! that is passed directly into HNSW search, eliminating costly post-filter
9//! passes for high-selectivity predicates.
10//!
11//! **Ordering invariant**: callers must call `insert_row` *after* the HNSW
12//! node id is assigned so that the id is stable. Similarly, `delete_row`
13//! should be called *before* or *after* HNSW deletion is attempted — the
14//! bitmap and the HNSW graph are always consistent after a successful pair.
15
16use std::collections::HashMap;
17
18use roaring::RoaringBitmap;
19
20use nodedb_types::Value;
21
22// ── Key type ─────────────────────────────────────────────────────────────────
23
24/// A deterministic, hashable representation of a `Value` used as a bitmap key.
25///
26/// `Float` stores the total-ordered bit pattern of the `f64` (NaN is
27/// canonicalised to a single sentinel bit-pattern so two NaNs map to the
28/// same key).
29#[derive(
30    Debug,
31    Clone,
32    PartialEq,
33    Eq,
34    PartialOrd,
35    Ord,
36    Hash,
37    serde::Serialize,
38    serde::Deserialize,
39    zerompk::ToMessagePack,
40    zerompk::FromMessagePack,
41)]
42#[non_exhaustive]
43pub enum PayloadKey {
44    Null,
45    Bool(bool),
46    Integer(i64),
47    /// Total-ordered f64 bits. NaN is canonicalised to `u64::MAX`.
48    Float(u64),
49    String(String),
50    Bytes(Vec<u8>),
51    /// DateTime stored as a string for deterministic hashing.
52    DateTime(String),
53    Uuid(String),
54}
55
56impl PayloadKey {
57    /// Convert a `Value` to a `PayloadKey`.
58    ///
59    /// Returns `None` for values that cannot serve as index keys (e.g. nested
60    /// objects, arrays, geometry). The caller should skip the field rather than
61    /// panic.
62    pub fn from_value(v: &Value) -> Option<PayloadKey> {
63        match v {
64            Value::Null => Some(PayloadKey::Null),
65            Value::Bool(b) => Some(PayloadKey::Bool(*b)),
66            Value::Integer(i) => Some(PayloadKey::Integer(*i)),
67            Value::Float(f) => {
68                // Canonical NaN: map every NaN to the same sentinel.
69                let bits = if f.is_nan() { u64::MAX } else { f.to_bits() };
70                Some(PayloadKey::Float(bits))
71            }
72            Value::String(s) => Some(PayloadKey::String(s.clone())),
73            Value::Bytes(b) => Some(PayloadKey::Bytes(b.clone())),
74            Value::Uuid(s) | Value::Ulid(s) | Value::Regex(s) => Some(PayloadKey::Uuid(s.clone())),
75            Value::DateTime(dt) | Value::NaiveDateTime(dt) => {
76                Some(PayloadKey::DateTime(format!("{dt:?}")))
77            }
78            Value::Decimal(d) => Some(PayloadKey::String(d.to_string())),
79            // Complex values that cannot be equality-indexed.
80            Value::Array(_)
81            | Value::Object(_)
82            | Value::Set(_)
83            | Value::Geometry(_)
84            | Value::Duration(_)
85            | Value::Range { .. }
86            | Value::Record { .. }
87            | Value::ArrayCell(_) => None,
88            // Value is #[non_exhaustive]; future variants with no known
89            // payload key representation are not indexable.
90            _ => None,
91        }
92    }
93}
94
95// ── Index kind ────────────────────────────────────────────────────────────────
96
97/// Re-export the kind enum from `nodedb-types` so the on-disk snapshot and
98/// the DDL config wire format share a single definition.
99pub use nodedb_types::PayloadIndexKind;
100
101// ── Single-field index ────────────────────────────────────────────────────────
102
103/// Per-field bitmap storage. `Equality` indexes use a hash map (O(1)
104/// point lookup); `Range` indexes use a B-tree (O(log n) point + range
105/// scan over keys).
106#[derive(Debug, Clone)]
107#[non_exhaustive]
108pub enum PayloadIndexBitmaps {
109    Equality(HashMap<PayloadKey, RoaringBitmap>),
110    Range(std::collections::BTreeMap<PayloadKey, RoaringBitmap>),
111}
112
113impl PayloadIndexBitmaps {
114    fn for_kind(kind: PayloadIndexKind) -> Self {
115        match kind {
116            PayloadIndexKind::Range => Self::Range(std::collections::BTreeMap::new()),
117            // Boolean is rare-cardinality; equality storage is fine.
118            // Unknown future variants also default to equality storage.
119            _ => Self::Equality(HashMap::new()),
120        }
121    }
122
123    fn get(&self, key: &PayloadKey) -> Option<&RoaringBitmap> {
124        match self {
125            Self::Equality(m) => m.get(key),
126            Self::Range(m) => m.get(key),
127        }
128    }
129
130    fn entry_or_default(&mut self, key: PayloadKey) -> &mut RoaringBitmap {
131        match self {
132            Self::Equality(m) => m.entry(key).or_default(),
133            Self::Range(m) => m.entry(key).or_default(),
134        }
135    }
136
137    fn get_mut(&mut self, key: &PayloadKey) -> Option<&mut RoaringBitmap> {
138        match self {
139            Self::Equality(m) => m.get_mut(key),
140            Self::Range(m) => m.get_mut(key),
141        }
142    }
143
144    fn remove(&mut self, key: &PayloadKey) -> Option<RoaringBitmap> {
145        match self {
146            Self::Equality(m) => m.remove(key),
147            Self::Range(m) => m.remove(key),
148        }
149    }
150
151    fn iter(&self) -> Box<dyn Iterator<Item = (&PayloadKey, &RoaringBitmap)> + '_> {
152        match self {
153            Self::Equality(m) => Box::new(m.iter()),
154            Self::Range(m) => Box::new(m.iter()),
155        }
156    }
157}
158
159/// Bitmap index for a single payload field.
160#[derive(Debug, Clone)]
161pub struct PayloadIndex {
162    /// The field name this index covers.
163    pub field: String,
164    /// Index kind.
165    pub kind: PayloadIndexKind,
166    /// `value → set of node ids`. Storage layout depends on `kind`.
167    pub bitmaps: PayloadIndexBitmaps,
168}
169
170impl Default for PayloadIndex {
171    fn default() -> Self {
172        Self::new(String::new(), PayloadIndexKind::Equality)
173    }
174}
175
176impl PayloadIndex {
177    /// Create a new empty index for `field`.
178    pub fn new(field: impl Into<String>, kind: PayloadIndexKind) -> Self {
179        Self {
180            field: field.into(),
181            kind,
182            bitmaps: PayloadIndexBitmaps::for_kind(kind),
183        }
184    }
185
186    /// Record that `node_id` has `value` for this field.
187    pub fn insert(&mut self, node_id: u32, value: &Value) {
188        let Some(key) = PayloadKey::from_value(value) else {
189            return;
190        };
191        self.bitmaps.entry_or_default(key).insert(node_id);
192    }
193
194    /// Remove `node_id` from the bitmap for `value`.
195    pub fn delete(&mut self, node_id: u32, value: &Value) {
196        let Some(key) = PayloadKey::from_value(value) else {
197            return;
198        };
199        if let Some(bm) = self.bitmaps.get_mut(&key) {
200            bm.remove(node_id);
201            if bm.is_empty() {
202                self.bitmaps.remove(&key);
203            }
204        }
205    }
206
207    /// Return the bitmap of node ids with `field = value`, or `None` if the
208    /// value has never been inserted (empty set).
209    pub fn equality(&self, value: &Value) -> Option<&RoaringBitmap> {
210        let key = PayloadKey::from_value(value)?;
211        self.bitmaps.get(&key)
212    }
213
214    /// Return the union bitmap of all entries whose key falls in
215    /// `[low, high]` (with `low_inclusive` / `high_inclusive` controlling
216    /// strictness). `None` bounds mean "open on that side". Returns
217    /// `None` when this index is not a `Range` index — the caller falls
218    /// back to a full post-filter scan.
219    pub fn range(
220        &self,
221        low: Option<&Value>,
222        low_inclusive: bool,
223        high: Option<&Value>,
224        high_inclusive: bool,
225    ) -> Option<RoaringBitmap> {
226        let PayloadIndexBitmaps::Range(map) = &self.bitmaps else {
227            return None;
228        };
229        use std::ops::Bound;
230        let low_key = low.and_then(PayloadKey::from_value);
231        let high_key = high.and_then(PayloadKey::from_value);
232        let low_bound = match (&low_key, low_inclusive) {
233            (Some(k), true) => Bound::Included(k.clone()),
234            (Some(k), false) => Bound::Excluded(k.clone()),
235            (None, _) => Bound::Unbounded,
236        };
237        let high_bound = match (&high_key, high_inclusive) {
238            (Some(k), true) => Bound::Included(k.clone()),
239            (Some(k), false) => Bound::Excluded(k.clone()),
240            (None, _) => Bound::Unbounded,
241        };
242        let mut acc = RoaringBitmap::new();
243        for (_, bm) in map.range((low_bound, high_bound)) {
244            acc |= bm;
245        }
246        Some(acc)
247    }
248}
249
250// ── Multi-field index set ─────────────────────────────────────────────────────
251
252/// Set of per-field payload bitmap indexes for a collection.
253#[derive(Debug, Default)]
254pub struct PayloadIndexSet {
255    indexes: HashMap<String, PayloadIndex>,
256}
257
258impl PayloadIndexSet {
259    /// Register a new field index. Idempotent — safe to call multiple times for
260    /// the same field (subsequent calls are no-ops).
261    pub fn add_index(&mut self, field: impl Into<String>, kind: PayloadIndexKind) {
262        let field = field.into();
263        self.indexes
264            .entry(field.clone())
265            .or_insert_with(|| PayloadIndex::new(field, kind));
266    }
267
268    /// Returns `true` if any index is registered.
269    pub fn is_empty(&self) -> bool {
270        self.indexes.is_empty()
271    }
272
273    /// Insert all indexed fields from `fields` for `node_id`.
274    pub fn insert_row(&mut self, node_id: u32, fields: &HashMap<String, Value>) {
275        for (field, idx) in &mut self.indexes {
276            if let Some(value) = fields.get(field) {
277                idx.insert(node_id, value);
278            }
279        }
280    }
281
282    /// Remove all indexed fields for `node_id`.
283    pub fn delete_row(&mut self, node_id: u32, fields: &HashMap<String, Value>) {
284        for (field, idx) in &mut self.indexes {
285            if let Some(value) = fields.get(field) {
286                idx.delete(node_id, value);
287            }
288        }
289    }
290
291    /// Evaluate a filter predicate against the bitmap indexes.
292    ///
293    /// Returns `Some(bitmap)` when the predicate is **fully covered** by
294    /// indexed fields (every leaf field has an index). Returns `None` when any
295    /// leaf references an un-indexed field — the caller must then fall back to a
296    /// full post-filter scan. This avoids silently dropping un-indexed
297    /// predicates.
298    pub fn pre_filter(&self, predicate: &FilterPredicate) -> Option<RoaringBitmap> {
299        match predicate {
300            FilterPredicate::Eq { field, value } => {
301                let idx = self.indexes.get(field)?;
302                Some(
303                    idx.equality(value)
304                        .cloned()
305                        .unwrap_or_else(RoaringBitmap::new),
306                )
307            }
308            FilterPredicate::In { field, values } => {
309                let idx = self.indexes.get(field)?;
310                let mut acc = RoaringBitmap::new();
311                for v in values {
312                    if let Some(bm) = idx.equality(v) {
313                        acc |= bm;
314                    }
315                }
316                Some(acc)
317            }
318            FilterPredicate::Range {
319                field,
320                low,
321                low_inclusive,
322                high,
323                high_inclusive,
324            } => {
325                let idx = self.indexes.get(field)?;
326                idx.range(low.as_ref(), *low_inclusive, high.as_ref(), *high_inclusive)
327            }
328            FilterPredicate::And(preds) => {
329                let mut result: Option<RoaringBitmap> = None;
330                for pred in preds {
331                    let bm = self.pre_filter(pred)?;
332                    result = Some(match result {
333                        None => bm,
334                        Some(acc) => acc & bm,
335                    });
336                }
337                Some(result.unwrap_or_default())
338            }
339            FilterPredicate::Or(preds) => {
340                let mut result = RoaringBitmap::new();
341                for pred in preds {
342                    let bm = self.pre_filter(pred)?;
343                    result |= bm;
344                }
345                Some(result)
346            }
347            FilterPredicate::Not(pred) => {
348                // NOT requires knowing the full set of node ids to complement
349                // against; we do not have that here, so return None (full scan).
350                // The predicate is still valid — callers handle it via
351                // post-filter on the HNSW results.
352                let _ = pred;
353                None
354            }
355        }
356    }
357
358    /// Return an iterator over all registered field names.
359    pub fn field_names(&self) -> impl Iterator<Item = &str> {
360        self.indexes.keys().map(String::as_str)
361    }
362}
363
364// ── Filter predicate ──────────────────────────────────────────────────────────
365
366/// A structured filter predicate that can be evaluated against the bitmap
367/// indexes. Mirrors the SQL WHERE predicates the planner builds.
368#[derive(Debug, Clone)]
369#[non_exhaustive]
370pub enum FilterPredicate {
371    /// `field = value` equality.
372    Eq { field: String, value: Value },
373    /// `field IN (v1, v2, ...)` — union of per-value bitmaps.
374    /// Lowered to `Or(Eq, Eq, ...)` internally.
375    In { field: String, values: Vec<Value> },
376    /// `field` within bounded range against a sorted (`Range`) payload
377    /// index. Either bound being `None` means open on that side.
378    Range {
379        field: String,
380        low: Option<Value>,
381        low_inclusive: bool,
382        high: Option<Value>,
383        high_inclusive: bool,
384    },
385    /// Conjunction.
386    And(Vec<FilterPredicate>),
387    /// Disjunction.
388    Or(Vec<FilterPredicate>),
389    /// Negation — always falls back to full scan (no complement support).
390    Not(Box<FilterPredicate>),
391}
392
393// ── Serialization helpers ─────────────────────────────────────────────────────
394
395/// Serializable snapshot of a single `PayloadIndex` for checkpointing.
396#[derive(
397    serde::Serialize, serde::Deserialize, zerompk::ToMessagePack, zerompk::FromMessagePack,
398)]
399pub struct PayloadIndexSnapshot {
400    pub field: String,
401    pub kind: PayloadIndexKind,
402    /// `(key_bytes, bitmap_bytes)` pairs — key serialised as msgpack,
403    /// bitmap serialised via its standard serialization.
404    pub entries: Vec<(Vec<u8>, Vec<u8>)>,
405}
406
407/// Serializable snapshot of the full `PayloadIndexSet`.
408#[derive(
409    serde::Serialize, serde::Deserialize, zerompk::ToMessagePack, zerompk::FromMessagePack,
410)]
411pub struct PayloadIndexSetSnapshot {
412    pub indexes: Vec<PayloadIndexSnapshot>,
413}
414
415impl PayloadIndexSet {
416    /// Serialize to a snapshot for checkpointing.
417    pub fn to_snapshot(&self) -> PayloadIndexSetSnapshot {
418        let indexes = self
419            .indexes
420            .values()
421            .map(|idx| {
422                let entries = idx
423                    .bitmaps
424                    .iter()
425                    .filter_map(|(key, bm)| {
426                        let key_bytes = zerompk::to_msgpack_vec(key).ok()?;
427                        let mut bm_bytes = Vec::new();
428                        bm.serialize_into(&mut bm_bytes).ok()?;
429                        Some((key_bytes, bm_bytes))
430                    })
431                    .collect();
432                PayloadIndexSnapshot {
433                    field: idx.field.clone(),
434                    kind: idx.kind,
435                    entries,
436                }
437            })
438            .collect();
439        PayloadIndexSetSnapshot { indexes }
440    }
441
442    /// Restore from a checkpoint snapshot.
443    pub fn from_snapshot(snap: PayloadIndexSetSnapshot) -> Self {
444        let mut indexes = HashMap::new();
445        for idx_snap in snap.indexes {
446            let mut bitmaps = PayloadIndexBitmaps::for_kind(idx_snap.kind);
447            for (key_bytes, bm_bytes) in idx_snap.entries {
448                let Ok(key) = zerompk::from_msgpack::<PayloadKey>(&key_bytes) else {
449                    continue;
450                };
451                let Ok(bm) = RoaringBitmap::deserialize_from(bm_bytes.as_slice()) else {
452                    continue;
453                };
454                *bitmaps.entry_or_default(key) = bm;
455            }
456            let idx = PayloadIndex {
457                field: idx_snap.field.clone(),
458                kind: idx_snap.kind,
459                bitmaps,
460            };
461            indexes.insert(idx_snap.field, idx);
462        }
463        Self { indexes }
464    }
465}
466
467// ── Tests ─────────────────────────────────────────────────────────────────────
468
469#[cfg(test)]
470mod tests {
471    use super::*;
472
473    fn string_val(s: &str) -> Value {
474        Value::String(s.to_string())
475    }
476    fn int_val(i: i64) -> Value {
477        Value::Integer(i)
478    }
479
480    #[test]
481    fn insert_and_equality_lookup() {
482        let mut idx = PayloadIndex::new("category", PayloadIndexKind::Equality);
483        idx.insert(0, &string_val("A"));
484        idx.insert(1, &string_val("B"));
485        idx.insert(2, &string_val("A"));
486
487        let a_bm = idx.equality(&string_val("A")).unwrap();
488        assert!(a_bm.contains(0));
489        assert!(a_bm.contains(2));
490        assert!(!a_bm.contains(1));
491
492        let b_bm = idx.equality(&string_val("B")).unwrap();
493        assert!(b_bm.contains(1));
494    }
495
496    #[test]
497    fn delete_removes_from_bitmap() {
498        let mut idx = PayloadIndex::new("category", PayloadIndexKind::Equality);
499        idx.insert(0, &string_val("A"));
500        idx.insert(1, &string_val("A"));
501        idx.delete(0, &string_val("A"));
502
503        let bm = idx.equality(&string_val("A")).unwrap();
504        assert!(!bm.contains(0));
505        assert!(bm.contains(1));
506    }
507
508    #[test]
509    fn delete_empty_bitmap_removed() {
510        let mut idx = PayloadIndex::new("x", PayloadIndexKind::Equality);
511        idx.insert(5, &string_val("v"));
512        idx.delete(5, &string_val("v"));
513        // Entry for "v" should be gone.
514        assert!(idx.equality(&string_val("v")).is_none());
515    }
516
517    #[test]
518    fn pre_filter_equality() {
519        let mut set = PayloadIndexSet::default();
520        set.add_index("category", PayloadIndexKind::Equality);
521
522        let mut fields = HashMap::new();
523        for i in 0u32..10 {
524            fields.clear();
525            fields.insert(
526                "category".to_string(),
527                string_val(if i < 5 { "A" } else { "B" }),
528            );
529            set.insert_row(i, &fields);
530        }
531
532        let pred = FilterPredicate::Eq {
533            field: "category".to_string(),
534            value: string_val("A"),
535        };
536        let bm = set.pre_filter(&pred).unwrap();
537        assert_eq!(bm.len(), 5);
538        for i in 0..5u32 {
539            assert!(bm.contains(i));
540        }
541    }
542
543    #[test]
544    fn pre_filter_and() {
545        let mut set = PayloadIndexSet::default();
546        set.add_index("category", PayloadIndexKind::Equality);
547        set.add_index("active", PayloadIndexKind::Boolean);
548
549        let mut fields = HashMap::new();
550        // node 0: A + true
551        fields.insert("category".to_string(), string_val("A"));
552        fields.insert("active".to_string(), Value::Bool(true));
553        set.insert_row(0, &fields);
554        // node 1: A + false
555        fields.insert("active".to_string(), Value::Bool(false));
556        set.insert_row(1, &fields);
557        // node 2: B + true
558        fields.insert("category".to_string(), string_val("B"));
559        fields.insert("active".to_string(), Value::Bool(true));
560        set.insert_row(2, &fields);
561
562        let pred = FilterPredicate::And(vec![
563            FilterPredicate::Eq {
564                field: "category".to_string(),
565                value: string_val("A"),
566            },
567            FilterPredicate::Eq {
568                field: "active".to_string(),
569                value: Value::Bool(true),
570            },
571        ]);
572        let bm = set.pre_filter(&pred).unwrap();
573        assert_eq!(bm.len(), 1);
574        assert!(bm.contains(0));
575    }
576
577    #[test]
578    fn pre_filter_or() {
579        let mut set = PayloadIndexSet::default();
580        set.add_index("category", PayloadIndexKind::Equality);
581
582        let mut fields = HashMap::new();
583        for i in 0u32..9 {
584            fields.clear();
585            let cat = match i % 3 {
586                0 => "A",
587                1 => "B",
588                _ => "C",
589            };
590            fields.insert("category".to_string(), string_val(cat));
591            set.insert_row(i, &fields);
592        }
593
594        let pred = FilterPredicate::Or(vec![
595            FilterPredicate::Eq {
596                field: "category".to_string(),
597                value: string_val("A"),
598            },
599            FilterPredicate::Eq {
600                field: "category".to_string(),
601                value: string_val("B"),
602            },
603        ]);
604        let bm = set.pre_filter(&pred).unwrap();
605        assert_eq!(bm.len(), 6);
606    }
607
608    #[test]
609    fn pre_filter_missing_index_returns_none() {
610        let set = PayloadIndexSet::default(); // no indexes
611
612        let pred = FilterPredicate::Eq {
613            field: "category".to_string(),
614            value: string_val("A"),
615        };
616        assert!(set.pre_filter(&pred).is_none());
617    }
618
619    #[test]
620    fn pre_filter_not_returns_none() {
621        let mut set = PayloadIndexSet::default();
622        set.add_index("category", PayloadIndexKind::Equality);
623
624        let pred = FilterPredicate::Not(Box::new(FilterPredicate::Eq {
625            field: "category".to_string(),
626            value: string_val("A"),
627        }));
628        // NOT always requires full scan.
629        assert!(set.pre_filter(&pred).is_none());
630    }
631
632    #[test]
633    fn pre_filter_and_partial_index_returns_none() {
634        // AND where one field is indexed but the other is not — must return None.
635        let mut set = PayloadIndexSet::default();
636        set.add_index("category", PayloadIndexKind::Equality);
637
638        let pred = FilterPredicate::And(vec![
639            FilterPredicate::Eq {
640                field: "category".to_string(),
641                value: string_val("A"),
642            },
643            FilterPredicate::Eq {
644                field: "unindexed_field".to_string(),
645                value: int_val(42),
646            },
647        ]);
648        assert!(set.pre_filter(&pred).is_none());
649    }
650
651    #[test]
652    fn nan_equality() {
653        let mut idx = PayloadIndex::new("score", PayloadIndexKind::Equality);
654        idx.insert(0, &Value::Float(f64::NAN));
655        idx.insert(1, &Value::Float(f64::NAN));
656        idx.insert(2, &Value::Float(1.0));
657
658        // Both NaN-bearing nodes should be in the same bucket.
659        let bm = idx.equality(&Value::Float(f64::NAN)).unwrap();
660        assert_eq!(bm.len(), 2);
661        assert!(bm.contains(0));
662        assert!(bm.contains(1));
663    }
664
665    #[test]
666    fn null_value_indexed() {
667        let mut idx = PayloadIndex::new("f", PayloadIndexKind::Equality);
668        idx.insert(0, &Value::Null);
669        idx.insert(1, &Value::Null);
670        let bm = idx.equality(&Value::Null).unwrap();
671        assert_eq!(bm.len(), 2);
672    }
673
674    #[test]
675    fn snapshot_roundtrip() {
676        let mut set = PayloadIndexSet::default();
677        set.add_index("category", PayloadIndexKind::Equality);
678
679        let mut fields = HashMap::new();
680        for i in 0u32..6 {
681            fields.clear();
682            fields.insert(
683                "category".to_string(),
684                string_val(if i < 3 { "A" } else { "B" }),
685            );
686            set.insert_row(i, &fields);
687        }
688
689        let snap = set.to_snapshot();
690        let snap_bytes = zerompk::to_msgpack_vec(&snap).unwrap();
691        let restored_snap: PayloadIndexSetSnapshot = zerompk::from_msgpack(&snap_bytes).unwrap();
692        let restored = PayloadIndexSet::from_snapshot(restored_snap);
693
694        let pred = FilterPredicate::Eq {
695            field: "category".to_string(),
696            value: string_val("A"),
697        };
698        let bm = restored.pre_filter(&pred).unwrap();
699        assert_eq!(bm.len(), 3);
700    }
701}