Skip to main content

zstd_complete/dictionary/
mod.rs

1//! Code for creating a separate content dictionary.
2//!
3//! Effective dictionaries are up to 1% the size of the complete training body,
4//! and are trained on many examples of the original data.
5//!
6//! Implemented following the paper "Effective construction of
7//! Relative Lempel-Ziv Dictionaries", by Kewen Liao, Matthias Petri,
8//! Alistair Moffat, and Anthony Wirth
9
10// The algorithm is summarized here
11// 1. The text is split into "epochs", or chunks from the original source
12// 2. From within each epoch, we select the "segment", or 1 KiB contiguous section
13//    that's predicted to be the best option to include in the dictionary. Concatenated,
14//    these segments form the dictionary.
15//
16// This segment scoring algorithm operates as follows:
17// For a given epoch:
18//  - Run a reservoir sampler over the entire epoch, creating a
19//    reservoir of n/t, where `t` is the desired number of occurances
20//    we want the most common k-mers to have
21//  - Have the ability to estimate
22//    the frequency of a given k-mer: `f(w: k-mer)` calculates
23//    the frequency of w in the reservoir using a rolling karp-rabin hash
24//  - The score of a segment is the sum of `f(w)` called on every kmer within the segment
25mod cover;
26mod frequency;
27mod reservoir;
28
29use crate::dictionary::reservoir::create_sample;
30use crate::{
31    bit_io::BitWriter,
32    decoding::dictionary::MAGIC_NUM,
33    fse::fse_encoder::{default_ll_table, default_ml_table, default_of_table},
34    huff0::huff0_encoder::HuffmanTable,
35};
36use core::cmp::Reverse;
37use core::fmt;
38use cover::*;
39use std::{
40    boxed::Box,
41    collections::{BinaryHeap, HashMap},
42    fs::{self, File},
43    io::{self, Read},
44    path::{Path, PathBuf},
45    vec::Vec,
46};
47
48/// Options for producing a standard formatted Zstandard dictionary.
49#[derive(Clone, Copy, Debug, Eq, PartialEq)]
50pub struct DictionaryTrainerOptions {
51    dictionary_size: usize,
52    dictionary_id: u32,
53    max_training_bytes: usize,
54}
55
56impl DictionaryTrainerOptions {
57    /// Creates options for a dictionary no larger than `dictionary_size`.
58    pub const fn new(dictionary_size: usize, dictionary_id: u32) -> Self {
59        Self {
60            dictionary_size,
61            dictionary_id,
62            max_training_bytes: 64 * 1024 * 1024,
63        }
64    }
65
66    /// Bounds the amount of sample data retained while training.
67    pub const fn with_max_training_bytes(mut self, bytes: usize) -> Self {
68        self.max_training_bytes = bytes;
69        self
70    }
71
72    pub const fn dictionary_size(&self) -> usize {
73        self.dictionary_size
74    }
75
76    pub const fn dictionary_id(&self) -> u32 {
77        self.dictionary_id
78    }
79
80    pub const fn max_training_bytes(&self) -> usize {
81        self.max_training_bytes
82    }
83}
84
85/// Failures reported before or during formatted dictionary training.
86#[derive(Clone, Copy, Debug, Eq, PartialEq)]
87pub enum DictionaryTrainingError {
88    EmptySamples,
89    ZeroDictionaryId,
90    DictionaryTooSmall,
91    TrainingLimitTooSmall,
92}
93
94impl fmt::Display for DictionaryTrainingError {
95    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
96        formatter.write_str(match self {
97            Self::EmptySamples => "dictionary training requires at least one non-empty sample",
98            Self::ZeroDictionaryId => "a formatted dictionary ID must be non-zero",
99            Self::DictionaryTooSmall => "dictionary size is too small for formatted metadata",
100            Self::TrainingLimitTooSmall => "training byte limit must be greater than zero",
101        })
102    }
103}
104
105impl std::error::Error for DictionaryTrainingError {}
106
107/// Trains a standard formatted dictionary from independent representative samples.
108///
109/// The content dictionary is selected with the native COVER-style trainer. The
110/// literal table is learned from the sample distribution; portable predefined
111/// sequence distributions provide complete initial FSE tables. The result has a
112/// dictionary ID and can be passed directly to [`crate::encoding::EncoderDictionary`]
113/// or [`crate::decoding::Dictionary::decode_dict`].
114pub fn train_dictionary<S: AsRef<[u8]>>(
115    samples: &[S],
116    options: DictionaryTrainerOptions,
117) -> Result<Vec<u8>, DictionaryTrainingError> {
118    if options.dictionary_id == 0 {
119        return Err(DictionaryTrainingError::ZeroDictionaryId);
120    }
121    if options.max_training_bytes == 0 {
122        return Err(DictionaryTrainingError::TrainingLimitTooSmall);
123    }
124
125    let mut training = Vec::new();
126    let mut literal_counts = [1_usize; 256];
127    for sample in samples {
128        let sample = sample.as_ref();
129        let remaining = options.max_training_bytes.saturating_sub(training.len());
130        let retained = &sample[..sample.len().min(remaining)];
131        for &byte in retained {
132            literal_counts[usize::from(byte)] = literal_counts[usize::from(byte)].saturating_add(1);
133        }
134        training.extend_from_slice(retained);
135        if training.len() == options.max_training_bytes {
136            break;
137        }
138    }
139    if training.is_empty() {
140        return Err(DictionaryTrainingError::EmptySamples);
141    }
142
143    let huffman = HuffmanTable::build_from_counts(&literal_counts);
144    let mut metadata = Vec::new();
145    metadata.extend_from_slice(&MAGIC_NUM);
146    metadata.extend_from_slice(&options.dictionary_id.to_le_bytes());
147    metadata.extend_from_slice(huffman.table_description());
148    append_fse_description(&mut metadata, default_of_table());
149    append_fse_description(&mut metadata, default_ml_table());
150    append_fse_description(&mut metadata, default_ll_table());
151
152    let minimum_content = 8;
153    let Some(content_budget) = options
154        .dictionary_size
155        .checked_sub(metadata.len().saturating_add(12))
156        .filter(|budget| *budget >= minimum_content)
157    else {
158        return Err(DictionaryTrainingError::DictionaryTooSmall);
159    };
160
161    let mut content = Vec::new();
162    create_raw_dict_from_source(
163        training.as_slice(),
164        training.len(),
165        &mut content,
166        content_budget,
167    );
168    if content.len() < minimum_content {
169        let missing = minimum_content - content.len();
170        content.extend(training.iter().copied().cycle().take(missing));
171    }
172    content.truncate(content_budget);
173
174    metadata.extend_from_slice(&1_u32.to_le_bytes());
175    metadata.extend_from_slice(&(content.len().min(4) as u32).to_le_bytes());
176    metadata.extend_from_slice(&(content.len().min(8) as u32).to_le_bytes());
177    metadata.extend_from_slice(&content);
178    Ok(metadata)
179}
180
181fn append_fse_description(output: &mut Vec<u8>, table: crate::fse::fse_encoder::FSETable) {
182    let mut writer = BitWriter::new();
183    table.write_table(&mut writer).expect("owned dictionary table allocation failed");
184    output.extend_from_slice(&writer.dump().expect("owned dictionary output allocation failed"));
185}
186
187/// A set of values that are used during dictionary construction.
188///
189/// Changing these values can improve the resulting dictionary size for certain datasets.
190// TODO: move `k` here.
191pub(super) struct DictParams {
192    /// Segment size.
193    ///
194    /// As found under "4. Experiments - Varying Segment Size" in the original paper, a
195    /// segment size of 2 kiB was effective.
196    ///
197    /// "We explored a range of \[`segment_size`\] values and found the performance of LMC is insensitive
198    /// to \[`segment_size`\]. We fix \[`segment_size`\] to 2kiB
199    ///
200    /// Reasonable range: [16, 2048+]
201    pub segment_size: u32,
202}
203
204/// Creates a "raw content" dictionary, training off of every file in this directory and all
205/// sub-directories.
206///
207/// The resulting dictionary will be approxamitely `dict_size` or less, and written to `output`.
208///
209/// # Errors
210/// This function returns `Ok(())` if the dictionary was created successfully, and an
211/// `Err(io::Error)` if an error was encountered reading the input directory.
212///
213/// # Examples
214/// ```no_run
215/// use std::fs::File;
216/// // Create a roughly 1mb dictionary, training off of file in `sample_files`
217/// let input_folder = "sample_files/";
218/// let mut output = File::create("output.dict").unwrap();
219/// zstd_complete::dictionary::create_raw_dict_from_dir(input_folder, &mut output, 1_000_000);
220/// ```
221pub fn create_raw_dict_from_dir<P: AsRef<Path>, W: io::Write>(
222    path: P,
223    output: &mut W,
224    dict_size: usize,
225) -> Result<(), io::Error> {
226    // Collect a list of a path to every file in the directory into `file_paths`
227    let mut file_paths: Vec<PathBuf> = Vec::new();
228    let dir: fs::ReadDir = fs::read_dir(path)?;
229    fn recurse_read(dir: fs::ReadDir, file_paths: &mut Vec<PathBuf>) -> Result<(), io::Error> {
230        for entry in dir {
231            let entry = entry?;
232            if entry.file_type()?.is_dir() {
233                recurse_read(fs::read_dir(entry.path())?, file_paths)?;
234            } else {
235                file_paths.push(entry.path());
236            }
237        }
238        Ok(())
239    }
240    recurse_read(dir, &mut file_paths)?;
241
242    // Open each file and chain the readers together
243    let mut total_file_len: u64 = 0;
244    let mut file_handles: Vec<fs::File> = Vec::new();
245    for path in file_paths {
246        let handle = File::open(path)?;
247        total_file_len += handle.metadata()?.len();
248        file_handles.push(handle);
249    }
250    let empty_reader: Box<dyn Read> = Box::new(io::empty());
251    let chained_files = file_handles
252        .iter()
253        .fold(empty_reader, |acc, reader| Box::new(acc.chain(reader)));
254
255    // Create a dict using the new reader
256    create_raw_dict_from_source(chained_files, total_file_len as usize, output, dict_size);
257    Ok(())
258}
259
260/// Read from `source` to create a "raw content" dictionary of `dict_size`.
261/// The completed dictionary is written to `output`.
262///
263/// - `source` will be used as training data for the entire dictionary.
264/// - `source_size` influences how the data is divided and sampled and is measured
265///   in bytes. While this does not need to be exact, estimates should attempt to be
266///   larger than the actual collection size.
267/// - `output` is where the completed dictionary will be written.
268/// - `dict_size` determines how large the complete dictionary should be. The completed
269///   dictionary will be this size or smaller.
270///
271/// This function buffers the training source internally so sampling and epoch scoring
272/// can both inspect the same training data.
273pub fn create_raw_dict_from_source<R: io::Read, W: io::Write>(
274    mut source: R,
275    source_size: usize,
276    output: &mut W,
277    dict_size: usize,
278) {
279    if source_size < 16 {
280        let mut source = source;
281        let mut buf = Vec::new();
282        source
283            .read_to_end(&mut buf)
284            .expect("Could not read from source");
285        output.write_all(&buf).expect("Could not write to output");
286        return;
287    }
288    vprintln!("create_dict: creating {dict_size} byte dict from {source_size} byte source");
289    if dict_size == 0 {
290        return;
291    }
292
293    let mut source_data = Vec::with_capacity(source_size);
294    source
295        .read_to_end(&mut source_data)
296        .expect("can read input");
297    if source_data.is_empty() {
298        return;
299    }
300
301    if source_data.len() < K {
302        output
303            .write_all(&source_data[..usize::min(dict_size, source_data.len())])
304            .expect("can write to output");
305        return;
306    }
307
308    let params = DictParams { segment_size: 2048 };
309    let segment_size = params.segment_size as usize;
310    let num_segments = usize::max(1, source_data.len().div_ceil(segment_size));
311    // According to 4. Experiments - Varying Reservoir Sampler Thresholds,
312    // setting reservoir size to collection size / min{collection size / (2 * number of segments),
313    // 256} was effective
314    let sample_divisor = usize::min(usize::max(1, source_data.len() / (2 * num_segments)), 256);
315    let sample_size = usize::max(K, source_data.len() / sample_divisor);
316    vprintln!("create_dict: creating {sample_size} byte sample of collection");
317    let collection_sample = create_sample(&mut source_data.as_slice(), sample_size);
318
319    // A collection of segments to be used in the final dictionary.
320    //
321    // Contains the best segment from every epoch.
322    // Reverse is used because we want a min heap, where
323    // the lowest scoring items come first
324    let mut pool: BinaryHeap<Reverse<Segment>> = BinaryHeap::new();
325    let (_, epoch_kmers) = compute_epoch_info(&params, dict_size, source_data.len() / K);
326    let epoch_size = usize::min(source_data.len(), usize::max(K, epoch_kmers * K));
327    let num_epochs = source_data.len().div_ceil(epoch_size);
328    vprintln!("create_dict: computed epoch info, using {num_epochs} epochs of {epoch_size} bytes");
329    let mut ctx = Context {
330        frequencies: HashMap::with_capacity(epoch_size / K),
331    };
332    // Score each segment in the epoch and select the highest scoring segment
333    // for the pool
334    for (epoch_index, current_epoch) in source_data.chunks(epoch_size).enumerate() {
335        let Some(best_segment) =
336            pick_best_segment(&params, &mut ctx, current_epoch, &collection_sample)
337        else {
338            continue;
339        };
340        vprintln!(
341            "\tcreate_dict: epoch {}/{} has best segment score {}",
342            epoch_index + 1,
343            num_epochs,
344            best_segment.score
345        );
346        pool.push(Reverse(best_segment));
347        // Wipe frequency list for next epoch
348        ctx.frequencies.clear();
349    }
350    vprintln!(
351        "create_dict: {num_epochs} epochs written, writing {} segments",
352        pool.len()
353    );
354    // Write the dictionary with the highest scoring segment last because
355    // closer items can be represented with a smaller offset
356    let mut remaining = dict_size;
357    while let Some(segment) = pool.pop() {
358        if remaining == 0 {
359            break;
360        }
361        let bytes_to_write = usize::min(remaining, segment.0.raw.len());
362        output
363            .write_all(&segment.0.raw[..bytes_to_write])
364            .expect("can write to output");
365        remaining -= bytes_to_write;
366    }
367}
368
369#[cfg(test)]
370mod tests {
371    use super::{
372        create_raw_dict_from_source, train_dictionary, DictionaryTrainerOptions,
373        DictionaryTrainingError,
374    };
375    use std::{io::Read, vec::Vec};
376
377    #[test]
378    fn raw_dict_builder_scores_epochs_after_sampling() {
379        let mut source = Vec::new();
380        for _ in 0..512 {
381            source.extend_from_slice(b"aaaaaaaaaaaaaaaabbbbbbbbbbbbbbbb");
382            source.extend_from_slice(b"ccccccccccccccccdddddddddddddddd");
383        }
384        let mut dictionary = Vec::new();
385
386        create_raw_dict_from_source(source.as_slice(), source.len(), &mut dictionary, 1024);
387
388        assert!(!dictionary.is_empty());
389        assert!(dictionary.len() <= 1024);
390    }
391
392    #[test]
393    fn raw_dict_builder_handles_tiny_sources() {
394        let source = b"tiny";
395        let mut dictionary = Vec::new();
396
397        create_raw_dict_from_source(source.as_slice(), source.len(), &mut dictionary, 1024);
398
399        assert_eq!(dictionary, source);
400    }
401
402    #[test]
403    fn formatted_dictionary_is_usable_by_encoder_and_decoder() {
404        let samples = [
405            b"customer=alice action=login status=success".repeat(100),
406            b"customer=bob action=logout status=success".repeat(100),
407        ];
408        let dictionary =
409            train_dictionary(&samples, DictionaryTrainerOptions::new(2048, 0xC0DE)).unwrap();
410        let decoded = crate::decoding::Dictionary::decode_dict(&dictionary).unwrap();
411        assert_eq!(decoded.id, 0xC0DE);
412        assert!(dictionary.len() <= 2048);
413
414        let input = b"customer=alice action=logout status=success".repeat(200);
415        let encoder_dictionary = crate::encoding::EncoderDictionary::copy(&dictionary).unwrap();
416        let compressed = crate::encoding::encode_all(
417            input.as_slice(),
418            crate::encoding::EncoderOptions::default().with_dictionary(encoder_dictionary),
419        )
420        .unwrap();
421        let mut c_decoder =
422            zstd::stream::read::Decoder::with_dictionary(compressed.as_slice(), &dictionary)
423                .unwrap();
424        let mut output = Vec::new();
425        c_decoder.read_to_end(&mut output).unwrap();
426        assert_eq!(output, input);
427    }
428
429    #[test]
430    fn formatted_dictionary_options_are_validated() {
431        assert_eq!(
432            train_dictionary(&[b"sample"], DictionaryTrainerOptions::new(1024, 0)),
433            Err(DictionaryTrainingError::ZeroDictionaryId)
434        );
435        assert_eq!(
436            train_dictionary(&[b"sample"], DictionaryTrainerOptions::new(8, 1)),
437            Err(DictionaryTrainingError::DictionaryTooSmall)
438        );
439    }
440}
441
442#[test]
443fn create_raw_dict_from_source_no_panics_on_small_input() {
444    use std::io::Cursor;
445
446    for size in 0..1024 {
447        let input = alloc::vec![b'A'; size];
448        let mut output = Vec::new();
449
450        create_raw_dict_from_source(Cursor::new(input.clone()), input.len(), &mut output, 64);
451    }
452}