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 core::cmp::Reverse;
31use cover::*;
32use std::{
33    boxed::Box,
34    collections::{BinaryHeap, HashMap},
35    fs::{self, File},
36    io::{self, Read},
37    path::{Path, PathBuf},
38    vec::Vec,
39};
40
41/// A set of values that are used during dictionary construction.
42///
43/// Changing these values can improve the resulting dictionary size for certain datasets.
44// TODO: move `k` here.
45pub(super) struct DictParams {
46    /// Segment size.
47    ///
48    /// As found under "4. Experiments - Varying Segment Size" in the original paper, a
49    /// segment size of 2 kiB was effective.
50    ///
51    /// "We explored a range of \[`segment_size`\] values and found the performance of LMC is insensitive
52    /// to \[`segment_size`\]. We fix \[`segment_size`\] to 2kiB
53    ///
54    /// Reasonable range: [16, 2048+]
55    pub segment_size: u32,
56}
57
58/// Creates a "raw content" dictionary, training off of every file in this directory and all
59/// sub-directories.
60///
61/// The resulting dictionary will be approxamitely `dict_size` or less, and written to `output`.
62///
63/// # Errors
64/// This function returns `Ok(())` if the dictionary was created successfully, and an
65/// `Err(io::Error)` if an error was encountered reading the input directory.
66///
67/// # Examples
68/// ```no_run
69/// use std::fs::File;
70/// // Create a roughly 1mb dictionary, training off of file in `sample_files`
71/// let input_folder = "sample_files/";
72/// let mut output = File::create("output.dict").unwrap();
73/// zstd_complete::dictionary::create_raw_dict_from_dir(input_folder, &mut output, 1_000_000);
74/// ```
75pub fn create_raw_dict_from_dir<P: AsRef<Path>, W: io::Write>(
76    path: P,
77    output: &mut W,
78    dict_size: usize,
79) -> Result<(), io::Error> {
80    // Collect a list of a path to every file in the directory into `file_paths`
81    let mut file_paths: Vec<PathBuf> = Vec::new();
82    let dir: fs::ReadDir = fs::read_dir(path)?;
83    fn recurse_read(dir: fs::ReadDir, file_paths: &mut Vec<PathBuf>) -> Result<(), io::Error> {
84        for entry in dir {
85            let entry = entry?;
86            if entry.file_type()?.is_dir() {
87                recurse_read(fs::read_dir(entry.path())?, file_paths)?;
88            } else {
89                file_paths.push(entry.path());
90            }
91        }
92        Ok(())
93    }
94    recurse_read(dir, &mut file_paths)?;
95
96    // Open each file and chain the readers together
97    let mut total_file_len: u64 = 0;
98    let mut file_handles: Vec<fs::File> = Vec::new();
99    for path in file_paths {
100        let handle = File::open(path)?;
101        total_file_len += handle.metadata()?.len();
102        file_handles.push(handle);
103    }
104    let empty_reader: Box<dyn Read> = Box::new(io::empty());
105    let chained_files = file_handles
106        .iter()
107        .fold(empty_reader, |acc, reader| Box::new(acc.chain(reader)));
108
109    // Create a dict using the new reader
110    create_raw_dict_from_source(chained_files, total_file_len as usize, output, dict_size);
111    Ok(())
112}
113
114/// Read from `source` to create a "raw content" dictionary of `dict_size`.
115/// The completed dictionary is written to `output`.
116///
117/// - `source` will be used as training data for the entire dictionary.
118/// - `source_size` influences how the data is divided and sampled and is measured
119///   in bytes. While this does not need to be exact, estimates should attempt to be
120///   larger than the actual collection size.
121/// - `output` is where the completed dictionary will be written.
122/// - `dict_size` determines how large the complete dictionary should be. The completed
123///   dictionary will be this size or smaller.
124///
125/// This function buffers the training source internally so sampling and epoch scoring
126/// can both inspect the same training data.
127pub fn create_raw_dict_from_source<R: io::Read, W: io::Write>(
128    mut source: R,
129    source_size: usize,
130    output: &mut W,
131    dict_size: usize,
132) {
133    if source_size < 16 {
134        let mut source = source;
135        let mut buf = Vec::new();
136        source
137            .read_to_end(&mut buf)
138            .expect("Could not read from source");
139        output.write_all(&buf).expect("Could not write to output");
140        return;
141    }
142    vprintln!("create_dict: creating {dict_size} byte dict from {source_size} byte source");
143    if dict_size == 0 {
144        return;
145    }
146
147    let mut source_data = Vec::with_capacity(source_size);
148    source
149        .read_to_end(&mut source_data)
150        .expect("can read input");
151    if source_data.is_empty() {
152        return;
153    }
154
155    if source_data.len() < K {
156        output
157            .write_all(&source_data[..usize::min(dict_size, source_data.len())])
158            .expect("can write to output");
159        return;
160    }
161
162    let params = DictParams { segment_size: 2048 };
163    let segment_size = params.segment_size as usize;
164    let num_segments = usize::max(1, source_data.len().div_ceil(segment_size));
165    // According to 4. Experiments - Varying Reservoir Sampler Thresholds,
166    // setting reservoir size to collection size / min{collection size / (2 * number of segments),
167    // 256} was effective
168    let sample_divisor = usize::min(usize::max(1, source_data.len() / (2 * num_segments)), 256);
169    let sample_size = usize::max(K, source_data.len() / sample_divisor);
170    vprintln!("create_dict: creating {sample_size} byte sample of collection");
171    let collection_sample = create_sample(&mut source_data.as_slice(), sample_size);
172
173    // A collection of segments to be used in the final dictionary.
174    //
175    // Contains the best segment from every epoch.
176    // Reverse is used because we want a min heap, where
177    // the lowest scoring items come first
178    let mut pool: BinaryHeap<Reverse<Segment>> = BinaryHeap::new();
179    let (_, epoch_kmers) = compute_epoch_info(&params, dict_size, source_data.len() / K);
180    let epoch_size = usize::min(source_data.len(), usize::max(K, epoch_kmers * K));
181    let num_epochs = source_data.len().div_ceil(epoch_size);
182    vprintln!("create_dict: computed epoch info, using {num_epochs} epochs of {epoch_size} bytes");
183    let mut ctx = Context {
184        frequencies: HashMap::with_capacity(epoch_size / K),
185    };
186    // Score each segment in the epoch and select the highest scoring segment
187    // for the pool
188    for (epoch_index, current_epoch) in source_data.chunks(epoch_size).enumerate() {
189        let Some(best_segment) =
190            pick_best_segment(&params, &mut ctx, current_epoch, &collection_sample)
191        else {
192            continue;
193        };
194        vprintln!(
195            "\tcreate_dict: epoch {}/{} has best segment score {}",
196            epoch_index + 1,
197            num_epochs,
198            best_segment.score
199        );
200        pool.push(Reverse(best_segment));
201        // Wipe frequency list for next epoch
202        ctx.frequencies.clear();
203    }
204    vprintln!(
205        "create_dict: {num_epochs} epochs written, writing {} segments",
206        pool.len()
207    );
208    // Write the dictionary with the highest scoring segment last because
209    // closer items can be represented with a smaller offset
210    let mut remaining = dict_size;
211    while let Some(segment) = pool.pop() {
212        if remaining == 0 {
213            break;
214        }
215        let bytes_to_write = usize::min(remaining, segment.0.raw.len());
216        output
217            .write_all(&segment.0.raw[..bytes_to_write])
218            .expect("can write to output");
219        remaining -= bytes_to_write;
220    }
221}
222
223#[cfg(test)]
224mod tests {
225    use super::create_raw_dict_from_source;
226    use std::vec::Vec;
227
228    #[test]
229    fn raw_dict_builder_scores_epochs_after_sampling() {
230        let mut source = Vec::new();
231        for _ in 0..512 {
232            source.extend_from_slice(b"aaaaaaaaaaaaaaaabbbbbbbbbbbbbbbb");
233            source.extend_from_slice(b"ccccccccccccccccdddddddddddddddd");
234        }
235        let mut dictionary = Vec::new();
236
237        create_raw_dict_from_source(source.as_slice(), source.len(), &mut dictionary, 1024);
238
239        assert!(!dictionary.is_empty());
240        assert!(dictionary.len() <= 1024);
241    }
242
243    #[test]
244    fn raw_dict_builder_handles_tiny_sources() {
245        let source = b"tiny";
246        let mut dictionary = Vec::new();
247
248        create_raw_dict_from_source(source.as_slice(), source.len(), &mut dictionary, 1024);
249
250        assert_eq!(dictionary, source);
251    }
252}
253
254#[test]
255fn create_raw_dict_from_source_no_panics_on_small_input() {
256    use std::io::Cursor;
257
258    for size in 0..1024 {
259        let input = alloc::vec![b'A'; size];
260        let mut output = Vec::new();
261
262        create_raw_dict_from_source(Cursor::new(input.clone()), input.len(), &mut output, 64);
263    }
264}