Skip to main content

plugmem_core/index/
mod.rs

1//! The recall indexes: shared posting storage, the BM25
2//! lexical index, and plain id-list indexes (tag → facts,
3//! entity → facts) with sorted-list intersection.
4//!
5//! The temporal index needs no module of its own — it is an Ordered
6//! [`Arena`](plugmem_arena::Arena) of
7//! [`TemporalSlot`](crate::model::TemporalSlot)s queried with `range`,
8//! and lives directly in the engine.
9
10pub mod bm25;
11pub mod hnsw;
12pub mod postings;
13pub mod varint;
14pub mod vecpool;
15
16use alloc::vec::Vec;
17
18use crate::id::FactId;
19use postings::PostingStore;
20
21/// Key → sorted fact-id lists without term frequencies: the tag and
22/// entity indexes.
23pub type IdListIndex<'a> = PostingStore<'a, false>;
24
25/// Reusable scratch for [`intersect`] (two ping-pong buffers; after
26/// warm-up an intersection allocates nothing).
27#[derive(Debug, Default)]
28pub struct IntersectScratch {
29    a: Vec<u32>,
30    b: Vec<u32>,
31}
32
33impl IntersectScratch {
34    /// Empty scratch buffers.
35    pub fn new() -> Self {
36        Self::default()
37    }
38}
39
40/// Intersects the sorted lists of `keys` into `out` (ascending ids).
41///
42/// Starts from the shortest list (fewest candidates) and filters it
43/// against the others by merge — cost is O(shortest + Σ walked prefixes).
44/// An absent key means an empty list, so the intersection is empty; an
45/// empty `keys` slice yields an empty result (the caller treats "no tag
46/// filter" as "no allow-set", not as "allow everything").
47pub fn intersect(
48    index: &IdListIndex<'_>,
49    keys: &[u32],
50    scratch: &mut IntersectScratch,
51    out: &mut Vec<FactId>,
52) {
53    out.clear();
54    let Some((first, rest)) = keys
55        .iter()
56        .min_by_key(|&&k| index.count(k))
57        .map(|&smallest| {
58            (
59                smallest,
60                keys.iter().copied().filter(move |&k| k != smallest),
61            )
62        })
63    else {
64        return;
65    };
66    scratch.a.clear();
67    scratch.a.extend(index.entries(first).map(|(id, _)| id.0));
68    for key in rest {
69        if scratch.a.is_empty() {
70            break;
71        }
72        scratch.b.clear();
73        let mut other = index.entries(key).map(|(id, _)| id.0).peekable();
74        for &id in &scratch.a {
75            while other.peek().is_some_and(|&o| o < id) {
76                other.next();
77            }
78            if other.peek() == Some(&id) {
79                scratch.b.push(id);
80            }
81        }
82        core::mem::swap(&mut scratch.a, &mut scratch.b);
83    }
84    out.extend(scratch.a.iter().map(|&id| FactId(id)));
85}