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
16use rudb_common::{Error, Result};
17
18use crate::fsst::{ESCAPE, SymbolTable};
19
20/// Marks a code whose step from a state has not been worked out yet. Never a state, because
21/// [`Sequence::new`] refuses pieces that would need this many.
22const UNKNOWN: u8 = u8::MAX;
23
24/// The automaton for some pieces that have to appear in order.
25#[derive(Debug, Clone)]
26pub struct Sequence {
27    /// Where a byte takes each state, 256 entries a state. The last state is the one every piece
28    /// has been found in, and every byte leaves it where it is.
29    next: Vec<u8>,
30    /// That last state.
31    done: u8,
32}
33
34impl Sequence {
35    /// The automaton for `pieces` in this order, or `None` when there is nothing to find or too much.
36    ///
37    /// Empty pieces are dropped, since finding nothing is always done. With nothing left every string
38    /// holds the pieces, which is not a question worth a walk, and pieces longer than 254 bytes
39    /// between them would need more states than a byte counts.
40    #[must_use]
41    pub fn new(pieces: &[&[u8]]) -> Option<Self> {
42        let pieces: Vec<&[u8]> = pieces.iter().copied().filter(|piece| !piece.is_empty()).collect();
43        let total: usize = pieces.iter().map(|piece| piece.len()).sum();
44        if pieces.is_empty() || total >= usize::from(UNKNOWN) {
45            return None;
46        }
47        let states = total + 1;
48        let mut next = vec![0_u8; states * 256];
49        // The usual table for finding one string in another, one piece at a time, with each piece's
50        // states numbered after the ones before it. The state one past a piece's last is the next
51        // piece's first, so completing a piece hands over to the next without a case of its own.
52        let mut base = 0;
53        for piece in &pieces {
54            let first = usize::from(piece[0]);
55            for byte in 0..256 {
56                next[base * 256 + byte] = base as u8;
57            }
58            next[base * 256 + first] = (base + 1) as u8;
59            let mut restart = base;
60            for (offset, &byte) in piece.iter().enumerate().skip(1) {
61                let state = base + offset;
62                let (before, row) = next.split_at_mut(state * 256);
63                row[..256].copy_from_slice(&before[restart * 256..restart * 256 + 256]);
64                row[usize::from(byte)] = (state + 1) as u8;
65                restart = usize::from(next[restart * 256 + usize::from(byte)]);
66            }
67            base += piece.len();
68        }
69        for byte in 0..256 {
70            next[total * 256 + byte] = total as u8;
71        }
72        Some(Self { next, done: total as u8 })
73    }
74
75    fn states(&self) -> usize {
76        usize::from(self.done) + 1
77    }
78
79    /// Whether `text` holds the pieces in order.
80    #[must_use]
81    pub fn holds(&self, text: &[u8]) -> bool {
82        let mut state = 0_u8;
83        for &byte in text {
84            state = self.next[usize::from(state) * 256 + usize::from(byte)];
85            if state == self.done {
86                return true;
87            }
88        }
89        false
90    }
91
92    /// A walker over strings compressed against `table`.
93    #[must_use]
94    pub fn over<'a>(&'a self, table: &'a SymbolTable) -> Coded<'a> {
95        Coded { sequence: self, table, steps: vec![UNKNOWN; self.states() * 256] }
96    }
97}
98
99/// [`Sequence`] over the codes of one symbol table, with where each code takes each state worked
100/// out the first time it is needed.
101#[derive(Debug)]
102pub struct Coded<'a> {
103    sequence: &'a Sequence,
104    table: &'a SymbolTable,
105    /// Where a code takes each state, 256 entries a state, or [`UNKNOWN`] for not yet worked out.
106    /// The entry for [`ESCAPE`] is never used, since what an escape does depends on the byte after it.
107    steps: Vec<u8>,
108}
109
110impl Coded<'_> {
111    /// Whether the string compressed to `codes` holds the pieces in order.
112    ///
113    /// # Errors
114    ///
115    /// If a code is not in the table or an escape is the last code.
116    pub fn holds(&mut self, codes: &[u8]) -> Result<bool> {
117        let done = self.sequence.done;
118        let mut state = 0_u8;
119        let mut at = 0;
120        while at < codes.len() {
121            let code = codes[at];
122            at += 1;
123            let slot = usize::from(state) * 256 + usize::from(code);
124            state = if code == ESCAPE {
125                let Some(&byte) = codes.get(at) else {
126                    return Err(Error::internal("a compressed string ends in an escape"));
127                };
128                at += 1;
129                self.sequence.next[usize::from(state) * 256 + usize::from(byte)]
130            } else if self.steps[slot] != UNKNOWN {
131                self.steps[slot]
132            } else {
133                self.learn(state, code)?
134            };
135            if state == done {
136                return Ok(true);
137            }
138        }
139        Ok(false)
140    }
141
142    #[cold]
143    fn learn(&mut self, from: u8, code: u8) -> Result<u8> {
144        let Some((bytes, len)) = self.table.symbol(code) else {
145            return Err(Error::internal(format!("code {code} is not in the table")));
146        };
147        let mut state = from;
148        for &byte in &bytes[..len] {
149            state = self.sequence.next[usize::from(state) * 256 + usize::from(byte)];
150        }
151        self.steps[usize::from(from) * 256 + usize::from(code)] = state;
152        Ok(state)
153    }
154}
155
156#[cfg(test)]
157mod tests {
158    use super::*;
159
160    /// The pieces found one after another with a plain search, which is what `LIKE` does.
161    fn searched(text: &[u8], pieces: &[&[u8]]) -> bool {
162        let mut rest = text;
163        for piece in pieces {
164            if piece.is_empty() {
165                continue;
166            }
167            match rest.windows(piece.len()).position(|window| window == *piece) {
168                Some(at) => rest = &rest[at + piece.len()..],
169                None => return false,
170            }
171        }
172        true
173    }
174
175    fn texts() -> Vec<Vec<u8>> {
176        let words = [
177            "special",
178            "requests",
179            "spec",
180            "specia",
181            "ial",
182            "requ",
183            "sts",
184            "the",
185            "furiously",
186            "aaa",
187            "aab",
188            "ab",
189            "é",
190            "ü",
191            "",
192            "s",
193        ];
194        let mut texts = Vec::new();
195        let mut seed = 7_u64;
196        for _ in 0..3000 {
197            let mut text = Vec::new();
198            seed = seed.wrapping_mul(6_364_136_223_846_793_005).wrapping_add(1);
199            for step in 0..(seed >> 60) {
200                let pick = (seed >> (step * 4 % 56)) as usize % words.len();
201                text.extend_from_slice(words[pick].as_bytes());
202                if (seed >> (step % 60)) & 1 == 1 {
203                    text.push(b' ');
204                }
205            }
206            texts.push(text);
207        }
208        texts
209    }
210
211    #[test]
212    fn the_walk_over_bytes_and_the_walk_over_codes_agree_with_a_search() {
213        let texts = texts();
214        let samples: Vec<&[u8]> = texts.iter().map(Vec::as_slice).collect();
215        let table = SymbolTable::train(&samples);
216        let patterns: [&[&[u8]]; 7] = [
217            &[b"special", b"requests"],
218            &[b"aab"],
219            &[b"ab", b"ab", b"ab"],
220            &[b"s", b"s"],
221            &["é".as_bytes(), b"ial"],
222            &[b"", b"spec", b""],
223            &[b"furiously the", b"sts"],
224        ];
225        let mut found = 0;
226        for pieces in patterns {
227            let sequence = Sequence::new(pieces).expect("something to find");
228            let mut coded = sequence.over(&table);
229            for text in &texts {
230                let wanted = searched(text, pieces);
231                found += usize::from(wanted);
232                assert_eq!(sequence.holds(text), wanted, "{pieces:?} in {text:?}");
233                let mut codes = Vec::new();
234                table.compress(text, &mut codes);
235                assert_eq!(coded.holds(&codes).expect("codes"), wanted, "{pieces:?} in {text:?}");
236            }
237        }
238        assert!(found > 1000, "only {found} matches");
239    }
240
241    #[test]
242    fn nothing_to_find_or_too_much_is_no_automaton() {
243        assert!(Sequence::new(&[]).is_none());
244        assert!(Sequence::new(&[b"", b""]).is_none());
245        assert!(Sequence::new(&[&[b'x'; 255]]).is_none());
246        assert!(Sequence::new(&[&[b'x'; 254]]).is_some());
247    }
248}