scirs2_io/columnar/
fsst.rs1use crate::error::{IoError, Result as IoResult};
18
19#[derive(Debug, Clone, Default)]
25pub struct FsstSymbol {
26 pub bytes: [u8; 2],
28 pub len: u8,
30 pub score: u64,
32}
33
34impl FsstSymbol {
35 fn one_byte(b: u8, freq: u64) -> Self {
36 Self {
37 bytes: [b, 0],
38 len: 1,
39 score: freq,
40 }
41 }
42
43 fn two_bytes(b0: u8, b1: u8, freq: u64) -> Self {
44 Self {
45 bytes: [b0, b1],
46 len: 2,
47 score: freq.saturating_mul(2),
48 }
49 }
50}
51
52#[derive(Debug, Clone)]
61pub struct FsstSymbolTable {
62 pub symbols: Vec<FsstSymbol>,
64 encode_2byte: Box<[[u8; 256]; 256]>,
66 encode_1byte: [u8; 256],
68}
69
70impl FsstSymbolTable {
71 pub fn train(samples: &[&[u8]], max_symbols: usize) -> Self {
75 let max_symbols = max_symbols.min(254);
76
77 let mut freq1 = vec![0u64; 256];
79 let mut freq2 = vec![vec![0u64; 256]; 256];
80
81 for sample in samples {
82 let len = sample.len();
83 for i in 0..len {
84 freq1[sample[i] as usize] += 1;
85 if i + 1 < len {
86 freq2[sample[i] as usize][sample[i + 1] as usize] += 1;
87 }
88 }
89 }
90
91 let mut candidates: Vec<FsstSymbol> = Vec::with_capacity(256 + 256 * 256);
93
94 for b in 0u8..=255u8 {
95 let f = freq1[b as usize];
96 if f > 0 {
97 candidates.push(FsstSymbol::one_byte(b, f));
98 }
99 }
100
101 for b0 in 0usize..256 {
102 for b1 in 0usize..256 {
103 let f = freq2[b0][b1];
104 if f > 0 {
105 candidates.push(FsstSymbol::two_bytes(b0 as u8, b1 as u8, f));
106 }
107 }
108 }
109
110 candidates.sort_by(|a, b| b.score.cmp(&a.score).then(b.len.cmp(&a.len)));
112 candidates.truncate(max_symbols);
113
114 let mut encode_2byte = Box::new([[255u8; 256]; 256]);
116 let mut encode_1byte = [255u8; 256];
117
118 for (code, sym) in candidates.iter().enumerate() {
119 let c = code as u8;
120 if sym.len == 1 {
121 encode_1byte[sym.bytes[0] as usize] = c;
122 } else {
123 encode_2byte[sym.bytes[0] as usize][sym.bytes[1] as usize] = c;
124 }
125 }
126
127 Self {
128 symbols: candidates,
129 encode_2byte,
130 encode_1byte,
131 }
132 }
133
134 pub fn compress(&self, input: &[u8]) -> Vec<u8> {
139 let mut out = Vec::with_capacity(input.len());
140 let mut i = 0;
141 let len = input.len();
142
143 while i < len {
144 if i + 1 < len {
146 let code = self.encode_2byte[input[i] as usize][input[i + 1] as usize];
147 if code != 255 {
148 out.push(code);
149 i += 2;
150 continue;
151 }
152 }
153 let code = self.encode_1byte[input[i] as usize];
155 if code != 255 {
156 out.push(code);
157 i += 1;
158 continue;
159 }
160 out.push(255);
162 out.push(input[i]);
163 i += 1;
164 }
165
166 out
167 }
168
169 pub fn decompress(&self, compressed: &[u8]) -> Vec<u8> {
171 let mut out = Vec::with_capacity(compressed.len() * 2);
172 let mut i = 0;
173 let len = compressed.len();
174
175 while i < len {
176 let code = compressed[i];
177 i += 1;
178
179 if code == 255 {
180 if i < len {
182 out.push(compressed[i]);
183 i += 1;
184 }
185 } else if (code as usize) < self.symbols.len() {
186 let sym = &self.symbols[code as usize];
187 out.push(sym.bytes[0]);
188 if sym.len == 2 {
189 out.push(sym.bytes[1]);
190 }
191 }
192 }
194
195 out
196 }
197
198 pub fn n_symbols(&self) -> usize {
200 self.symbols.len()
201 }
202
203 pub fn compression_ratio(&self, original: &[u8], compressed: &[u8]) -> f64 {
207 original.len() as f64 / compressed.len().max(1) as f64
208 }
209}
210
211pub struct FsstColumnEncoder {
217 pub table: FsstSymbolTable,
219}
220
221impl FsstColumnEncoder {
222 pub fn train(strings: &[&str], sample_fraction: f64) -> IoResult<Self> {
228 if strings.is_empty() {
229 return Ok(Self {
230 table: FsstSymbolTable::train(&[], 254),
231 });
232 }
233
234 let fraction = sample_fraction.clamp(1e-6, 1.0);
235 let step = (1.0 / fraction).floor() as usize;
236 let step = step.max(1);
237
238 let samples: Vec<&[u8]> = strings
239 .iter()
240 .enumerate()
241 .filter(|(i, _)| i % step == 0)
242 .map(|(_, s)| s.as_bytes())
243 .collect();
244
245 if samples.is_empty() {
246 return Err(IoError::FormatError(
247 "FSST training: no samples selected (sample_fraction too small?)".to_string(),
248 ));
249 }
250
251 let table = FsstSymbolTable::train(&samples, 254);
252 Ok(Self { table })
253 }
254
255 pub fn compress_column(&self, strings: &[&str]) -> Vec<Vec<u8>> {
257 strings
258 .iter()
259 .map(|s| self.table.compress(s.as_bytes()))
260 .collect()
261 }
262
263 pub fn decompress_column(&self, compressed: &[Vec<u8>]) -> IoResult<Vec<String>> {
267 compressed
268 .iter()
269 .enumerate()
270 .map(|(i, bytes)| {
271 let raw = self.table.decompress(bytes);
272 String::from_utf8(raw).map_err(|e| {
273 IoError::FormatError(format!(
274 "FSST decompress: string {} is not valid UTF-8: {e}",
275 i
276 ))
277 })
278 })
279 .collect()
280 }
281
282 pub fn column_compression_ratio(&self, strings: &[&str]) -> f64 {
284 if strings.is_empty() {
285 return 1.0;
286 }
287 let total_original: usize = strings.iter().map(|s| s.len()).sum();
288 let total_compressed: usize = strings
289 .iter()
290 .map(|s| self.table.compress(s.as_bytes()).len())
291 .sum();
292 total_original as f64 / total_compressed.max(1) as f64
293 }
294}
295
296#[cfg(test)]
301mod tests {
302 use super::*;
303
304 #[test]
305 fn test_fsst_compress_decompress_roundtrip() {
306 let samples: Vec<&str> = vec![
308 "hello world",
309 "hello rust",
310 "world of rust",
311 "hello hello world",
312 ];
313 let sample_bytes: Vec<&[u8]> = samples.iter().map(|s| s.as_bytes()).collect();
314 let table = FsstSymbolTable::train(&sample_bytes, 254);
315
316 for s in &samples {
318 let compressed = table.compress(s.as_bytes());
319 let decompressed = table.decompress(&compressed);
320 assert_eq!(decompressed, s.as_bytes(), "roundtrip failed for {:?}", s);
321 }
322 }
323
324 #[test]
325 fn test_fsst_table_size_bounded() {
326 let data: Vec<String> = (0..1000).map(|i| format!("item_{i}_data")).collect();
327 let samples: Vec<&[u8]> = data.iter().map(|s| s.as_bytes()).collect();
328 let table = FsstSymbolTable::train(&samples, 254);
329 assert!(
330 table.n_symbols() <= 254,
331 "symbol table exceeds 254: {}",
332 table.n_symbols()
333 );
334 }
335
336 #[test]
337 fn test_fsst_column_encoder_roundtrip() {
338 let strings: Vec<&str> = vec![
339 "sensor_lab_a",
340 "sensor_lab_b",
341 "sensor_lab_a",
342 "sensor_lab_c",
343 "sensor_lab_a",
344 "sensor_lab_b",
345 ];
346
347 let encoder = FsstColumnEncoder::train(&strings, 1.0).expect("training failed");
348
349 let compressed = encoder.compress_column(&strings);
350 let decompressed = encoder
351 .decompress_column(&compressed)
352 .expect("decompress failed");
353
354 let original: Vec<String> = strings.iter().map(|s| s.to_string()).collect();
355 assert_eq!(decompressed, original);
356 }
357
358 #[test]
359 fn test_fsst_compression_ratio_positive() {
360 let repeated = "aaaa bbbb cccc dddd aaaa bbbb aaaa";
362 let samples = &[repeated.as_bytes()];
363 let table = FsstSymbolTable::train(samples, 254);
364 let compressed = table.compress(repeated.as_bytes());
365 let ratio = table.compression_ratio(repeated.as_bytes(), &compressed);
366 assert!(ratio > 0.0, "ratio should be positive");
368 }
369
370 #[test]
371 fn test_fsst_empty_input() {
372 let table = FsstSymbolTable::train(&[], 254);
373 let compressed = table.compress(&[]);
374 assert!(compressed.is_empty());
375 let decompressed = table.decompress(&[]);
376 assert!(decompressed.is_empty());
377 }
378
379 #[test]
380 fn test_fsst_escape_byte_roundtrip() {
381 let train_data: Vec<&[u8]> = vec![b"aabbcc"];
383 let table = FsstSymbolTable::train(&train_data, 254);
384
385 let input: Vec<u8> = (0u8..=127).collect();
387 let compressed = table.compress(&input);
388 let decompressed = table.decompress(&compressed);
389 assert_eq!(decompressed, input);
390 }
391
392 #[test]
393 fn test_fsst_column_encoder_sample_fraction() {
394 let strings: Vec<String> = (0..100).map(|i| format!("item_{i}")).collect();
395 let refs: Vec<&str> = strings.iter().map(|s| s.as_str()).collect();
396
397 let encoder = FsstColumnEncoder::train(&refs, 0.1).expect("training failed");
399
400 let compressed = encoder.compress_column(&refs);
401 let decompressed = encoder
402 .decompress_column(&compressed)
403 .expect("decompress failed");
404
405 assert_eq!(decompressed.len(), strings.len());
406 for (a, b) in strings.iter().zip(decompressed.iter()) {
407 assert_eq!(a, b);
408 }
409 }
410}