mod cover;
mod fastcover;
mod finalize;
mod legacy;
mod samples;
mod selection;
mod suffix_array;
pub use legacy::DEFAULT_SELECTIVITY;
pub use samples::TrainingError;
use samples::refuse;
use std::{
format,
fs::{self, File},
io::{self, Read},
path::{Path, PathBuf},
vec::Vec,
};
const MAX_TRAINING_PREALLOC_BYTES: usize = 8 * 1024 * 1024;
pub const MIN_TRAINED_DICT_SIZE: usize = finalize::DICT_SIZE_MIN;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct CoverOptions {
pub k: u32,
pub d: u32,
pub steps: u32,
pub split_point: f64,
pub shrink: Option<u32>,
}
impl Default for CoverOptions {
fn default() -> Self {
Self {
k: 0,
d: 8,
steps: 4,
split_point: 1.0,
shrink: None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct FastCoverOptions {
pub cover: CoverOptions,
pub f: u32,
pub accel: u32,
}
impl Default for FastCoverOptions {
fn default() -> Self {
Self {
cover: CoverOptions {
split_point: 0.75,
..CoverOptions::default()
},
f: 20,
accel: 1,
}
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct FinalizeOptions {
pub dict_id: Option<u32>,
pub level: i32,
}
pub fn create_raw_dict_from_dir<P: AsRef<Path>, W: io::Write>(
path: P,
output: &mut W,
dict_size: usize,
) -> Result<(), io::Error> {
let mut file_paths: Vec<PathBuf> = Vec::new();
let dir: fs::ReadDir = fs::read_dir(path)?;
fn recurse_read(dir: fs::ReadDir, file_paths: &mut Vec<PathBuf>) -> Result<(), io::Error> {
for entry in dir {
let entry = entry?;
if entry.file_type()?.is_dir() {
recurse_read(fs::read_dir(entry.path())?, file_paths)?;
} else {
file_paths.push(entry.path());
}
}
Ok(())
}
recurse_read(dir, &mut file_paths)?;
let mut corpus = Vec::new();
let mut sizes = Vec::with_capacity(file_paths.len());
for path in file_paths {
let before = corpus.len();
File::open(path)?.read_to_end(&mut corpus)?;
sizes.push(corpus.len() - before);
}
output.write_all(&fastcover_raw_content(&corpus, Some(&sizes), dict_size)?)
}
pub fn create_raw_dict_from_source<R: io::Read, W: io::Write>(
mut source: R,
source_size: usize,
output: &mut W,
dict_size: usize,
) -> io::Result<()> {
if dict_size == 0 {
return Ok(());
}
let prealloc = source_size.min(MAX_TRAINING_PREALLOC_BYTES);
let mut all = Vec::with_capacity(prealloc);
source.read_to_end(&mut all)?;
create_raw_dict_from_slice(&all, output, dict_size)
}
pub fn create_raw_dict_from_slice<W: io::Write>(
all: &[u8],
output: &mut W,
dict_size: usize,
) -> io::Result<()> {
output.write_all(&fastcover_raw_content(all, None, dict_size)?)
}
const RAW_SAMPLE_MAX: usize = 128 << 10;
const RAW_SAMPLES_MIN: usize = 16;
fn fastcover_raw_content(
corpus: &[u8],
sizes: Option<&[usize]>,
dict_size: usize,
) -> io::Result<Vec<u8>> {
if corpus.len() <= dict_size {
return Ok(corpus.to_vec());
}
let refused =
|result: &io::Result<Vec<u8>>| result.as_ref().err().and_then(TrainingError::of).is_some();
if let Some(sizes) = sizes {
let trained = search_raw_content(corpus, sizes, dict_size);
if !refused(&trained) {
return trained;
}
}
let cut = corpus.len().div_ceil(RAW_SAMPLES_MIN).min(RAW_SAMPLE_MAX);
let even: Vec<usize> = corpus.chunks(cut).map(<[u8]>::len).collect();
let trained = search_raw_content(corpus, &even, dict_size);
if refused(&trained) {
return Ok(corpus[corpus.len() - dict_size..].to_vec());
}
trained
}
fn search_raw_content(corpus: &[u8], sizes: &[usize], dict_size: usize) -> io::Result<Vec<u8>> {
let options = FastCoverOptions::default();
let space = SearchSpace::optimizing(&options.cover, 0.75)?;
let (content, _) = run_fastcover(
corpus,
sizes,
dict_size,
&options,
FinalizeOptions::default(),
&space,
selection::Keep::Content,
)?;
Ok(content)
}
pub fn finalize_raw_dict(
raw_content: &[u8],
samples: &[u8],
sample_sizes: &[usize],
dict_size: usize,
options: FinalizeOptions,
) -> io::Result<Vec<u8>> {
check_finalize_dict_size(dict_size)?;
let set = samples::SampleSet::new(samples, sample_sizes)?;
finalize::finalize(raw_content, &set, set.count(), dict_size, options)
}
pub fn check_finalize_dict_size(dict_size: usize) -> io::Result<()> {
finalize::check_size(dict_size)
}
struct SearchSpace {
d: core::ops::RangeInclusive<u32>,
k: core::ops::RangeInclusive<u32>,
k_step: usize,
steps: u32,
split_point: f64,
}
impl SearchSpace {
fn optimizing(options: &CoverOptions, default_split: f64) -> io::Result<Self> {
if !options.split_point.is_finite() {
return Err(refuse(
TrainingError::Parameter,
"the split point must lie in (0, 1]",
));
}
let split_point = if options.split_point <= 0.0 {
default_split
} else {
options.split_point
};
if split_point > 1.0 {
return Err(refuse(
TrainingError::Parameter,
"the split point must lie in (0, 1]",
));
}
let d = if options.d == 0 {
6..=8
} else {
options.d..=options.d
};
let k = if options.k == 0 {
50..=2000
} else {
options.k..=options.k
};
if *k.start() < *d.end() {
return Err(refuse(TrainingError::Parameter, "k must be at least d"));
}
let steps = if options.steps == 0 {
40
} else {
options.steps
};
let k_step = ((k.end() - k.start()) / steps).max(1) as usize;
Ok(Self {
d,
k,
k_step,
steps,
split_point,
})
}
fn fixed(options: &CoverOptions) -> io::Result<Self> {
if options.k == 0 || options.d == 0 {
return Err(refuse(
TrainingError::Parameter,
"k and d are required; zero asks for a search, which the optimize_* trainers run",
));
}
Ok(Self {
d: options.d..=options.d,
k: options.k..=options.k,
k_step: 1,
steps: options.steps,
split_point: 1.0,
})
}
fn pairs(&self) -> impl Iterator<Item = (usize, usize)> + '_ {
self.d.clone().step_by(2).flat_map(move |d| {
self.k
.clone()
.step_by(self.k_step)
.map(move |k| (d as usize, k as usize))
})
}
fn check_fits(&self, dict_size: usize) -> io::Result<()> {
if self.pairs().any(|(d, k)| segment_fits(k, d, dict_size)) {
return Ok(());
}
Err(refuse(
TrainingError::Parameter,
"no parameter combination is valid for this dictionary size",
))
}
fn selected(options: &CoverOptions, default_split: f64) -> io::Result<Self> {
if options.k != 0 && options.d != 0 {
Self::fixed(options)
} else {
Self::optimizing(options, default_split)
}
}
}
pub fn check_cover_options(options: &CoverOptions, dict_size: usize) -> io::Result<()> {
let space = SearchSpace::selected(options, 1.0)?;
space.check_fits(dict_size)?;
check_dict_size(dict_size)
}
pub fn check_fastcover_options(options: &FastCoverOptions, dict_size: usize) -> io::Result<()> {
let space = SearchSpace::selected(&options.cover, 0.75)?;
fastcover_knobs(options, &space)?;
space.check_fits(dict_size)?;
check_dict_size(dict_size)
}
pub fn check_cover_sample_count(options: &CoverOptions, sample_count: usize) -> io::Result<()> {
let space = SearchSpace::selected(options, 1.0)?;
samples::split_count(sample_count, space.split_point).map(drop)
}
pub fn check_fastcover_sample_count(
options: &FastCoverOptions,
sample_count: usize,
) -> io::Result<()> {
let space = SearchSpace::selected(&options.cover, 0.75)?;
samples::split_count(sample_count, space.split_point).map(drop)
}
fn fastcover_knobs(options: &FastCoverOptions, space: &SearchSpace) -> io::Result<(u32, u32)> {
let f = if options.f == 0 { 20 } else { options.f };
let accel = if options.accel == 0 { 1 } else { options.accel };
if f > fastcover::MAX_F {
return Err(refuse(
TrainingError::Parameter,
&format!("f must be in 1..={}, got {f}", fastcover::MAX_F),
));
}
if accel > fastcover::MAX_ACCEL {
return Err(refuse(
TrainingError::Parameter,
&format!("accel must be in 1..={}, got {accel}", fastcover::MAX_ACCEL),
));
}
if *space.d.start() < 4 {
return Err(refuse(
TrainingError::Parameter,
"FastCOVER needs d of at least 4",
));
}
Ok((f, accel))
}
fn segment_fits(k: usize, d: usize, dict_size: usize) -> bool {
d > 0 && d <= k && k <= dict_size
}
fn check_samples_and_dict_size<'s>(
samples: &'s [u8],
sample_sizes: &[usize],
dict_size: usize,
) -> io::Result<samples::SampleSet<'s>> {
if sample_sizes.is_empty() {
return Err(refuse(TrainingError::Samples, "there are no samples"));
}
check_dict_size(dict_size)?;
samples::SampleSet::new(samples, sample_sizes)
}
fn check_dict_size(dict_size: usize) -> io::Result<()> {
if dict_size < MIN_TRAINED_DICT_SIZE {
return Err(refuse(
TrainingError::DictionaryTooSmall,
&format!("a dictionary must be at least {MIN_TRAINED_DICT_SIZE} bytes"),
));
}
Ok(())
}
pub fn train_cover_dict(
samples: &[u8],
sample_sizes: &[usize],
dict_size: usize,
options: &CoverOptions,
finalize: FinalizeOptions,
) -> io::Result<Vec<u8>> {
let space = SearchSpace::fixed(options)?;
let (dict, _) = run_cover(samples, sample_sizes, dict_size, options, finalize, &space)?;
Ok(dict)
}
pub fn optimize_cover_dict(
samples: &[u8],
sample_sizes: &[usize],
dict_size: usize,
options: &CoverOptions,
finalize: FinalizeOptions,
) -> io::Result<(Vec<u8>, CoverOptions)> {
let space = SearchSpace::optimizing(options, 1.0)?;
run_cover(samples, sample_sizes, dict_size, options, finalize, &space)
}
fn run_cover(
samples: &[u8],
sample_sizes: &[usize],
dict_size: usize,
options: &CoverOptions,
finalize: FinalizeOptions,
space: &SearchSpace,
) -> io::Result<(Vec<u8>, CoverOptions)> {
space.check_fits(dict_size)?;
let set = check_samples_and_dict_size(samples, sample_sizes, dict_size)?;
let split = set.split(space.split_point)?;
let plain = space.k.start() == space.k.end() && space.d.start() == space.d.end();
let scored = !plain || options.shrink.is_some() || space.split_point < 1.0;
let mut evaluator =
selection::Evaluator::new(&set, split.train, split.test.clone(), dict_size, finalize);
let mut best = selection::Best::new(selection::Keep::Dictionary {
shrink: options.shrink,
});
let mut state = Vec::new();
let mut content_scratch = Vec::new();
let mut context: Option<(usize, Option<cover::CoverContext<'_>>)> = None;
for (d, k) in space.pairs() {
if !segment_fits(k, d, dict_size) {
continue;
}
let chosen = CoverOptions {
k: k as u32,
d: d as u32,
steps: space.steps,
split_point: space.split_point,
..*options
};
if context
.as_ref()
.is_none_or(|(built_for, _)| *built_for != d)
{
drop(context.take());
state = Vec::new();
let built = match cover::CoverContext::new(&set, split.train, d) {
Ok(ctx) => Some(ctx),
Err(err) => {
best.offer(Err(err), &[], chosen);
None
}
};
context = Some((d, built));
}
let Some(ctx) = context.as_ref().and_then(|(_, built)| built.as_ref()) else {
continue;
};
let content = ctx.build(&mut state, &mut content_scratch, dict_size, k);
if !scored {
return Ok((evaluator.finalize(content)?, chosen));
}
best.offer(evaluator.score(content), content, chosen);
}
best.finish(&mut evaluator)
}
pub fn train_fastcover_dict(
samples: &[u8],
sample_sizes: &[usize],
dict_size: usize,
options: &FastCoverOptions,
finalize: FinalizeOptions,
) -> io::Result<Vec<u8>> {
let space = SearchSpace::fixed(&options.cover)?;
let keep = selection::Keep::Dictionary {
shrink: options.cover.shrink,
};
let (dict, _) = run_fastcover(
samples,
sample_sizes,
dict_size,
options,
finalize,
&space,
keep,
)?;
Ok(dict)
}
pub fn optimize_fastcover_dict(
samples: &[u8],
sample_sizes: &[usize],
dict_size: usize,
options: &FastCoverOptions,
finalize: FinalizeOptions,
) -> io::Result<(Vec<u8>, FastCoverOptions)> {
let space = SearchSpace::optimizing(&options.cover, 0.75)?;
let keep = selection::Keep::Dictionary {
shrink: options.cover.shrink,
};
run_fastcover(
samples,
sample_sizes,
dict_size,
options,
finalize,
&space,
keep,
)
}
fn run_fastcover(
samples: &[u8],
sample_sizes: &[usize],
dict_size: usize,
options: &FastCoverOptions,
finalize: FinalizeOptions,
space: &SearchSpace,
keep: selection::Keep,
) -> io::Result<(Vec<u8>, FastCoverOptions)> {
let (f, accel) = fastcover_knobs(options, space)?;
space.check_fits(dict_size)?;
let set = check_samples_and_dict_size(samples, sample_sizes, dict_size)?;
let split = set.split(space.split_point)?;
let plain = space.k.start() == space.k.end() && space.d.start() == space.d.end();
let scored = !plain || options.cover.shrink.is_some() || space.split_point < 1.0;
let mut window = fastcover::WindowCounts::default();
let mut freqs = Vec::new();
let mut content_scratch = Vec::new();
let mut best = selection::Best::new(keep);
let mut context: Option<(usize, Option<fastcover::FastCoverContext<'_>>)> = None;
let mut evaluator = selection::Evaluator::new(
&set,
fastcover::finalize_samples(split.train, accel),
split.test.clone(),
dict_size,
finalize,
);
for (d, k) in space.pairs() {
if !segment_fits(k, d, dict_size) {
continue;
}
let chosen = FastCoverOptions {
cover: CoverOptions {
k: k as u32,
d: d as u32,
steps: space.steps,
split_point: space.split_point,
..options.cover
},
f,
accel,
};
if context
.as_ref()
.is_none_or(|(built_for, _)| *built_for != d)
{
drop(context.take());
let built = match fastcover::FastCoverContext::new(&set, split.train, d, f, accel) {
Ok(ctx) => Some(ctx),
Err(err) => {
best.offer(Err(err), &[], chosen);
None
}
};
context = Some((d, built));
}
let Some(ctx) = context.as_ref().and_then(|(_, built)| built.as_ref()) else {
continue;
};
let content = ctx.build(&mut freqs, &mut window, &mut content_scratch, dict_size, k)?;
if !scored {
return match keep {
selection::Keep::Content => Ok((content.to_vec(), chosen)),
selection::Keep::Dictionary { .. } => Ok((evaluator.finalize(content)?, chosen)),
};
}
best.offer(evaluator.score(content), content, chosen);
}
best.finish(&mut evaluator)
}
pub fn create_legacy_dict_from_slice<W: io::Write>(
samples: &[u8],
sample_sizes: &[usize],
output: &mut W,
dict_size: usize,
selectivity: u32,
finalize: FinalizeOptions,
) -> io::Result<()> {
let described = sample_sizes
.iter()
.try_fold(0usize, |total, &size| total.checked_add(size));
if described != Some(samples.len()) {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"the sample sizes do not add up to the corpus",
));
}
let content =
legacy::train_legacy_raw(samples, sample_sizes, dict_size, selectivity).map_err(|err| {
let reason = match err {
legacy::TooSmall::Dictionary => format!(
"a legacy dictionary must be at least {} bytes",
legacy::DICT_SIZE_MIN
),
legacy::TooSmall::Corpus => format!(
"the samples total {} bytes; the legacy trainer needs at least {}",
samples.len(),
legacy::MIN_SAMPLES_SIZE
),
legacy::TooSmall::Content => {
"the samples repeat too little to yield dictionary content".into()
}
};
io::Error::new(io::ErrorKind::InvalidInput, reason)
})?;
let finalized = finalize_raw_dict(
content.as_slice(),
samples,
sample_sizes,
dict_size,
finalize,
)?;
output.write_all(finalized.as_slice())
}
#[cfg(feature = "bench-internals")]
pub(crate) fn legacy_dict_content(
samples: &[u8],
sample_sizes: &[usize],
dict_size: usize,
selectivity: u32,
) -> Option<Vec<u8>> {
legacy::train_legacy_raw(samples, sample_sizes, dict_size, selectivity).ok()
}
#[cfg(feature = "bench-internals")]
pub(crate) fn dict_roundtrip_fixture() -> (
alloc::vec::Vec<u8>,
alloc::vec::Vec<u8>,
alloc::vec::Vec<u8>,
) {
use crate::decoding::Dictionary;
use crate::encoding::{CompressionLevel, FrameCompressor};
let mut sample = alloc::vec::Vec::new();
let mut sizes = alloc::vec::Vec::new();
for i in 0..512u32 {
let line = alloc::format!(
"tenant=demo table=orders key={i} region=eu payload=aaaaabbbbbcccccdddddeeeee\n"
);
sizes.push(line.len());
sample.extend_from_slice(line.as_bytes());
}
let options = FastCoverOptions {
cover: CoverOptions {
k: 256,
..FastCoverOptions::default().cover
},
..FastCoverOptions::default()
};
let finalized = train_fastcover_dict(
sample.as_slice(),
&sizes,
4096,
&options,
FinalizeOptions::default(),
)
.expect("training should succeed");
let parsed =
Dictionary::decode_dict(finalized.as_slice()).expect("finalized dictionary should parse");
assert!(!parsed.dict_content.is_empty());
let mut payload = alloc::vec::Vec::new();
for idx in 0..96u32 {
payload.extend_from_slice(
alloc::format!("tenant=demo op=put key={idx} value=aaaaabbbbbcccccdddddeeeee\n")
.as_bytes(),
);
}
let mut compressed = alloc::vec::Vec::new();
let mut compressor = FrameCompressor::new(CompressionLevel::Fastest);
compressor
.set_dictionary(parsed)
.expect("dictionary should attach");
compressor.set_source(payload.as_slice());
compressor.set_drain(&mut compressed);
compressor.compress();
(finalized, compressed, payload)
}
#[cfg(test)]
mod tests;