Skip to main content

sparse_vector/wand/
postings.rs

1//! In-RAM posting list with mutation and ceiling maintenance.
2
3use super::cursor::SliceCursor;
4use super::{Posting, RecordId, Weight};
5
6/// A posting list held in RAM: elements sorted by record id, unique ids,
7/// every element carrying its `tail_max` ceiling (see the module docs).
8#[derive(Clone, Debug, Default, PartialEq)]
9pub struct Postings {
10    items: Vec<Posting>,
11}
12
13impl Postings {
14    /// An empty list.
15    pub fn new() -> Self {
16        Self::default()
17    }
18
19    /// Build from `(id, weight)` pairs in any order. Later pairs win over
20    /// earlier ones with the same id.
21    pub fn from_pairs<I>(pairs: I) -> Self
22    where
23        I: IntoIterator<Item = (RecordId, Weight)>,
24    {
25        let mut builder = PostingsBuilder::new();
26        for (id, w) in pairs {
27            builder.add(id, w);
28        }
29        builder.build()
30    }
31
32    /// Build from pairs already sorted by strictly increasing id. Panics in
33    /// debug builds if the order is violated.
34    pub fn from_sorted_pairs(pairs: &[(RecordId, Weight)]) -> Self {
35        debug_assert!(pairs.windows(2).all(|w| w[0].0 < w[1].0));
36        let mut items: Vec<Posting> = pairs
37            .iter()
38            .map(|&(id, w)| Posting::solo(id, w))
39            .collect();
40        rebuild_tail_max(&mut items);
41        Self { items }
42    }
43
44    /// The elements, sorted by id.
45    pub fn as_slice(&self) -> &[Posting] {
46        &self.items
47    }
48
49    pub fn len(&self) -> usize {
50        self.items.len()
51    }
52
53    pub fn is_empty(&self) -> bool {
54        self.items.is_empty()
55    }
56
57    /// A cursor at the start of the list.
58    pub fn cursor(&self) -> SliceCursor<'_> {
59        SliceCursor::new(&self.items)
60    }
61
62    /// Weight of `id`, if present.
63    pub fn get(&self, id: RecordId) -> Option<Weight> {
64        self.locate(id).ok().map(|i| self.items[i].weight)
65    }
66
67    /// Insert `id` with `weight`, or replace its weight if it is already
68    /// present. Returns the previous weight when replacing.
69    ///
70    /// Appending an id above every existing one costs O(1) unless the new
71    /// weight raises existing ceilings, and then only the raised prefix is
72    /// rewritten: inserting a corpus in id order is amortised O(1) per
73    /// element for weights without a rising trend.
74    pub fn upsert(&mut self, id: RecordId, weight: Weight) -> Option<Weight> {
75        match self.locate(id) {
76            Ok(i) => {
77                let old = self.items[i].weight;
78                if old == weight {
79                    return Some(old);
80                }
81                self.items[i].weight = weight;
82                self.items[i].tail_max = weight.max(suffix_ceiling(&self.items, i + 1));
83                repair_tail_max(&mut self.items, i);
84                Some(old)
85            }
86            Err(i) => {
87                let tail_max = weight.max(suffix_ceiling(&self.items, i));
88                self.items.insert(
89                    i,
90                    Posting {
91                        id,
92                        weight,
93                        tail_max,
94                    },
95                );
96                repair_tail_max(&mut self.items, i);
97                None
98            }
99        }
100    }
101
102    /// Remove `id`. Returns its weight if it was present.
103    pub fn delete(&mut self, id: RecordId) -> Option<Weight> {
104        let i = self.locate(id).ok()?;
105        let removed = self.items.remove(i);
106        // Elements now at `i..` are unchanged; the ones before lost a
107        // member of their suffix.
108        repair_tail_max(&mut self.items, i);
109        Some(removed.weight)
110    }
111
112    /// Recompute every ceiling from scratch (after bulk edits through
113    /// [`items_mut`](Self::items_mut)).
114    pub fn recompute_tail_max(&mut self) {
115        rebuild_tail_max(&mut self.items);
116    }
117
118    /// Mutable access to the raw elements for bulk edits. The caller must
119    /// keep ids sorted and unique, and call
120    /// [`recompute_tail_max`](Self::recompute_tail_max) afterwards.
121    pub fn items_mut(&mut self) -> &mut Vec<Posting> {
122        &mut self.items
123    }
124
125    /// Check the structural invariants: ids strictly increasing, and every
126    /// `tail_max` equal to the maximum weight of its inclusive suffix.
127    /// Returns a description of the first violation.
128    pub fn check_invariants(&self) -> Result<(), String> {
129        check_ceilings(self.items.iter().copied())
130    }
131
132    fn locate(&self, id: RecordId) -> Result<usize, usize> {
133        self.items.binary_search_by(|p| p.id.cmp(&id))
134    }
135}
136
137/// Ceiling of the suffix starting at `from`: `tail_max` of that element, or
138/// `-inf` past the end.
139#[inline]
140fn suffix_ceiling(items: &[Posting], from: usize) -> Weight {
141    items.get(from).map_or(Weight::NEG_INFINITY, |p| p.tail_max)
142}
143
144/// Recompute every ceiling from the weights alone.
145fn rebuild_tail_max(items: &mut [Posting]) {
146    let mut running = Weight::NEG_INFINITY;
147    for p in items.iter_mut().rev() {
148        running = running.max(p.weight);
149        p.tail_max = running;
150    }
151}
152
153/// Bring the ceilings of `0..end` back in line after a change confined to
154/// the suffix `end..`, whose ceilings are already correct, given that the
155/// ceilings of `0..end` were correct before the change.
156///
157/// Walking leftwards, the recomputed ceiling at `j` depends only on the
158/// weights of `j..end` and on the ceiling at `end`. Once the recomputed
159/// value equals the stored one at some `j`, every ceiling left of `j`
160/// derives from the same unchanged weights and that same value, so it is
161/// already right: the walk stops there. A change that raises no ceiling
162/// costs a single comparison.
163fn repair_tail_max(items: &mut [Posting], end: usize) {
164    let end = end.min(items.len());
165    let mut running = suffix_ceiling(items, end);
166    for p in items[..end].iter_mut().rev() {
167        running = running.max(p.weight);
168        if p.tail_max == running {
169            break;
170        }
171        p.tail_max = running;
172    }
173}
174
175/// Validate id order and ceilings for any sequence of postings, in list
176/// order. Shared by the RAM list and the storage adapters' tests.
177pub fn check_ceilings(items: impl IntoIterator<Item = Posting>) -> Result<(), String> {
178    let items: Vec<Posting> = items.into_iter().collect();
179    for (i, w) in items.windows(2).enumerate() {
180        if w[0].id >= w[1].id {
181            return Err(format!(
182                "ids not strictly increasing at {i}: {} then {}",
183                w[0].id, w[1].id
184            ));
185        }
186    }
187    let mut running = Weight::NEG_INFINITY;
188    for (i, p) in items.iter().enumerate().rev() {
189        running = running.max(p.weight);
190        if p.tail_max < running {
191            return Err(format!(
192                "tail_max {} at index {i} (id {}) below suffix max {running}",
193                p.tail_max, p.id
194            ));
195        }
196    }
197    Ok(())
198}
199
200/// Accumulates `(id, weight)` pairs and produces a [`Postings`].
201#[derive(Clone, Debug, Default)]
202pub struct PostingsBuilder {
203    pairs: Vec<(RecordId, Weight)>,
204}
205
206impl PostingsBuilder {
207    pub fn new() -> Self {
208        Self::default()
209    }
210
211    /// Queue a pair. Duplicated ids are resolved at build time, last wins.
212    pub fn add(&mut self, id: RecordId, weight: Weight) -> &mut Self {
213        self.pairs.push((id, weight));
214        self
215    }
216
217    pub fn len(&self) -> usize {
218        self.pairs.len()
219    }
220
221    pub fn is_empty(&self) -> bool {
222        self.pairs.is_empty()
223    }
224
225    /// Sort, deduplicate (last occurrence wins) and compute ceilings.
226    pub fn build(mut self) -> Postings {
227        // Stable sort keeps insertion order among equal ids, so the last
228        // inserted duplicate is the last of its run.
229        self.pairs.sort_by_key(|&(id, _)| id);
230        let mut unique: Vec<(RecordId, Weight)> = Vec::with_capacity(self.pairs.len());
231        for (id, w) in self.pairs {
232            match unique.last_mut() {
233                Some(last) if last.0 == id => last.1 = w,
234                _ => unique.push((id, w)),
235            }
236        }
237        Postings::from_sorted_pairs(&unique)
238    }
239}