Skip to main content

rudb_encoding/
sequence.rs

1//! Whether a string holds some pieces in order, answered on its FSST codes without decompressing it.
2//!
3//! `LIKE '%special%requests%'` asks whether `special` is somewhere in a string and `requests` is
4//! somewhere after it. Finding each piece as soon as it can be found is never worse than finding it
5//! later, so the question is a walk over the bytes with one automaton per piece, each one handing
6//! over to the next when its piece is complete. Laid end to end that is one automaton with a state
7//! for every byte of every piece and one more for having found them all, and a string holds the
8//! pieces when its walk ends in that last state.
9//!
10//! A compressed string is codes, and each code stands for up to eight bytes. Walking a code is
11//! walking its bytes, and since the bytes a code stands for do not change within a chunk, where a
12//! code takes each state can be worked out once per chunk and then looked up. A string is then one
13//! lookup per code rather than a decompression, a copy and a search over the bytes, and the lookups
14//! are filled in only for the states a string actually reached. See `spec/perf/44-like-on-codes.md`.
15//!
16//! Walking is still a step per code for every string, and a filter that keeps nearly every row
17//! walks nearly every string to the end. [`grams`] is the sketch that saves the walk: a bit for
18//! each run of three bytes a string holds, hashed into sixty four. A string that holds a piece
19//! holds every run of three in it, so a string whose sketch lacks one of the piece's bits cannot
20//! hold the piece, and only the strings whose sketch has all of them are walked. The writer keeps
21//! one sketch per row of a long text column, see `spec/graph/12-the-order-the-suite-asks-for.md`.
22
23use rudb_common::{Error, Result};
24
25use crate::fsst::{ESCAPE, SymbolTable};
26
27/// Marks a code whose step from a state has not been worked out yet. Never a state, because
28/// [`Sequence::new`] refuses pieces that would need this many.
29const UNKNOWN: u8 = u8::MAX;
30
31/// The automaton for some pieces that have to appear in order.
32#[derive(Debug, Clone)]
33pub struct Sequence {
34    /// Where a byte takes each state, 256 entries a state. The last state is the one every piece
35    /// has been found in, and every byte leaves it where it is.
36    next: Vec<u8>,
37    /// That last state.
38    done: u8,
39    /// The [`grams`] bits every string that holds the pieces has.
40    needs: u64,
41}
42
43/// The bit a run of three bytes sets in a sketch.
44#[inline]
45fn gram(a: u8, b: u8, c: u8) -> u64 {
46    let run = u32::from(a) << 16 | u32::from(b) << 8 | u32::from(c);
47    1 << (run.wrapping_mul(0x9E37_79B1) >> 26)
48}
49
50/// The sketch of `text`: a bit for every run of three bytes in it, hashed into sixty four.
51///
52/// Stored by the writer, one per row, and read against [`Sequence::needs`]. The hash is part of the
53/// file format, since a sketch written by one build is read by the next.
54#[must_use]
55pub fn grams(text: &[u8]) -> u64 {
56    text.windows(3).fold(0, |bits, run| bits | gram(run[0], run[1], run[2]))
57}
58
59impl Sequence {
60    /// The automaton for `pieces` in this order, or `None` when there is nothing to find or too much.
61    ///
62    /// Empty pieces are dropped, since finding nothing is always done. With nothing left every string
63    /// holds the pieces, which is not a question worth a walk, and pieces longer than 254 bytes
64    /// between them would need more states than a byte counts.
65    #[must_use]
66    pub fn new(pieces: &[&[u8]]) -> Option<Self> {
67        let pieces: Vec<&[u8]> = pieces.iter().copied().filter(|piece| !piece.is_empty()).collect();
68        let total: usize = pieces.iter().map(|piece| piece.len()).sum();
69        if pieces.is_empty() || total >= usize::from(UNKNOWN) {
70            return None;
71        }
72        let states = total + 1;
73        let mut next = vec![0_u8; states * 256];
74        // The usual table for finding one string in another, one piece at a time, with each piece's
75        // states numbered after the ones before it. The state one past a piece's last is the next
76        // piece's first, so completing a piece hands over to the next without a case of its own.
77        let mut base = 0;
78        for piece in &pieces {
79            let first = usize::from(piece[0]);
80            for byte in 0..256 {
81                next[base * 256 + byte] = base as u8;
82            }
83            next[base * 256 + first] = (base + 1) as u8;
84            let mut restart = base;
85            for (offset, &byte) in piece.iter().enumerate().skip(1) {
86                let state = base + offset;
87                let (before, row) = next.split_at_mut(state * 256);
88                row[..256].copy_from_slice(&before[restart * 256..restart * 256 + 256]);
89                row[usize::from(byte)] = (state + 1) as u8;
90                restart = usize::from(next[restart * 256 + usize::from(byte)]);
91            }
92            base += piece.len();
93        }
94        for byte in 0..256 {
95            next[total * 256 + byte] = total as u8;
96        }
97        let needs = pieces.iter().fold(0, |bits, piece| bits | grams(piece));
98        Some(Self { next, done: total as u8, needs })
99    }
100
101    /// The sketch bits a string has to have to hold the pieces. A string whose [`grams`] lack any
102    /// of them does not hold the pieces, and one that has them all may.
103    ///
104    /// Zero when no piece is three bytes long, which every string passes.
105    #[must_use]
106    pub fn needs(&self) -> u64 {
107        self.needs
108    }
109
110    fn states(&self) -> usize {
111        usize::from(self.done) + 1
112    }
113
114    /// Whether `text` holds the pieces in order.
115    #[must_use]
116    pub fn holds(&self, text: &[u8]) -> bool {
117        let mut state = 0_u8;
118        for &byte in text {
119            state = self.next[usize::from(state) * 256 + usize::from(byte)];
120            if state == self.done {
121                return true;
122            }
123        }
124        false
125    }
126
127    /// A walker over strings compressed against `table`.
128    #[must_use]
129    pub fn over<'a>(&'a self, table: &'a SymbolTable) -> Coded<'a> {
130        Coded { sequence: self, table, steps: vec![UNKNOWN; self.states() * 256] }
131    }
132}
133
134/// [`Sequence`] over the codes of one symbol table, with where each code takes each state worked
135/// out the first time it is needed.
136#[derive(Debug)]
137pub struct Coded<'a> {
138    sequence: &'a Sequence,
139    table: &'a SymbolTable,
140    /// Where a code takes each state, 256 entries a state, or [`UNKNOWN`] for not yet worked out.
141    /// The entry for [`ESCAPE`] is never used, since what an escape does depends on the byte after it.
142    steps: Vec<u8>,
143}
144
145impl Coded<'_> {
146    /// Whether the string compressed to `codes` holds the pieces in order.
147    ///
148    /// # Errors
149    ///
150    /// If a code is not in the table or an escape is the last code.
151    pub fn holds(&mut self, codes: &[u8]) -> Result<bool> {
152        let done = self.sequence.done;
153        let mut state = 0_u8;
154        let mut at = 0;
155        while at < codes.len() {
156            let code = codes[at];
157            at += 1;
158            let slot = usize::from(state) * 256 + usize::from(code);
159            state = if code == ESCAPE {
160                let Some(&byte) = codes.get(at) else {
161                    return Err(Error::internal("a compressed string ends in an escape"));
162                };
163                at += 1;
164                self.sequence.next[usize::from(state) * 256 + usize::from(byte)]
165            } else if self.steps[slot] != UNKNOWN {
166                self.steps[slot]
167            } else {
168                self.learn(state, code)?
169            };
170            if state == done {
171                return Ok(true);
172            }
173        }
174        Ok(false)
175    }
176
177    #[cold]
178    fn learn(&mut self, from: u8, code: u8) -> Result<u8> {
179        let Some((bytes, len)) = self.table.symbol(code) else {
180            return Err(Error::internal(format!("code {code} is not in the table")));
181        };
182        let mut state = from;
183        for &byte in &bytes[..len] {
184            state = self.sequence.next[usize::from(state) * 256 + usize::from(byte)];
185        }
186        self.steps[usize::from(from) * 256 + usize::from(code)] = state;
187        Ok(state)
188    }
189}
190
191#[cfg(test)]
192mod tests {
193    use super::*;
194
195    /// The pieces found one after another with a plain search, which is what `LIKE` does.
196    fn searched(text: &[u8], pieces: &[&[u8]]) -> bool {
197        let mut rest = text;
198        for piece in pieces {
199            if piece.is_empty() {
200                continue;
201            }
202            match rest.windows(piece.len()).position(|window| window == *piece) {
203                Some(at) => rest = &rest[at + piece.len()..],
204                None => return false,
205            }
206        }
207        true
208    }
209
210    /// A string that holds the pieces always has the bits they need, so the sketch never turns
211    /// away a string the walk would have kept.
212    #[test]
213    fn a_string_that_holds_the_pieces_has_every_bit_they_need() {
214        let cases: [&[&[u8]]; 4] =
215            [&[b"special", b"requests"], &[b"furiously"], &[b"ab"], &[b"aab", b"sts", b"\xc3\xa9"]];
216        for pieces in cases {
217            let sequence = Sequence::new(pieces).expect("an automaton");
218            let mut kept = 0;
219            for text in texts() {
220                let has = grams(&text) & sequence.needs() == sequence.needs();
221                if searched(&text, pieces) {
222                    assert!(has, "{:?} holds {pieces:?} and its sketch says not", text);
223                    kept += 1;
224                }
225            }
226            assert!(kept > 0, "{pieces:?} is held somewhere, so the test tests something");
227        }
228        assert_eq!(Sequence::new(&[b"ab"]).expect("an automaton").needs(), 0);
229        assert_eq!(grams(b"ab"), 0, "no run of three");
230        assert_ne!(grams(b"special") & grams(b"requests"), grams(b"special"));
231    }
232
233    fn texts() -> Vec<Vec<u8>> {
234        let words = [
235            "special",
236            "requests",
237            "spec",
238            "specia",
239            "ial",
240            "requ",
241            "sts",
242            "the",
243            "furiously",
244            "aaa",
245            "aab",
246            "ab",
247            "é",
248            "ü",
249            "",
250            "s",
251        ];
252        let mut texts = Vec::new();
253        let mut seed = 7_u64;
254        for _ in 0..3000 {
255            let mut text = Vec::new();
256            seed = seed.wrapping_mul(6_364_136_223_846_793_005).wrapping_add(1);
257            for step in 0..(seed >> 60) {
258                let pick = (seed >> (step * 4 % 56)) as usize % words.len();
259                text.extend_from_slice(words[pick].as_bytes());
260                if (seed >> (step % 60)) & 1 == 1 {
261                    text.push(b' ');
262                }
263            }
264            texts.push(text);
265        }
266        texts
267    }
268
269    #[test]
270    fn the_walk_over_bytes_and_the_walk_over_codes_agree_with_a_search() {
271        let texts = texts();
272        let samples: Vec<&[u8]> = texts.iter().map(Vec::as_slice).collect();
273        let table = SymbolTable::train(&samples);
274        let patterns: [&[&[u8]]; 7] = [
275            &[b"special", b"requests"],
276            &[b"aab"],
277            &[b"ab", b"ab", b"ab"],
278            &[b"s", b"s"],
279            &["é".as_bytes(), b"ial"],
280            &[b"", b"spec", b""],
281            &[b"furiously the", b"sts"],
282        ];
283        let mut found = 0;
284        for pieces in patterns {
285            let sequence = Sequence::new(pieces).expect("something to find");
286            let mut coded = sequence.over(&table);
287            for text in &texts {
288                let wanted = searched(text, pieces);
289                found += usize::from(wanted);
290                assert_eq!(sequence.holds(text), wanted, "{pieces:?} in {text:?}");
291                let mut codes = Vec::new();
292                table.compress(text, &mut codes);
293                assert_eq!(coded.holds(&codes).expect("codes"), wanted, "{pieces:?} in {text:?}");
294            }
295        }
296        assert!(found > 1000, "only {found} matches");
297    }
298
299    #[test]
300    fn nothing_to_find_or_too_much_is_no_automaton() {
301        assert!(Sequence::new(&[]).is_none());
302        assert!(Sequence::new(&[b"", b""]).is_none());
303        assert!(Sequence::new(&[&[b'x'; 255]]).is_none());
304        assert!(Sequence::new(&[&[b'x'; 254]]).is_some());
305    }
306}