Skip to main content

lance_encoding/encodings/physical/
fsst.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright The Lance Authors
3
4//! FSST encoding
5//!
6//! FSST is a lightweight encoding for variable width data.  This module includes
7//! adapters for both miniblock and per-value encoding.
8//!
9//! FSST encoding creates a small symbol table that is needed for decoding.  Currently
10//! we create one symbol table per disk page and store it in the description.
11//!
12//! TODO: This seems to be potentially limiting.  Perhaps we should create one symbol
13//! table per mini-block chunk?  In the per-value compression it may even make sense to
14//! create multiple symbol tables for a single value!
15//!
16//! FSST encoding is transparent.
17
18use lance_core::{Error, Result};
19
20use crate::{
21    buffer::LanceBuffer,
22    compression::{MiniBlockDecompressor, VariablePerValueDecompressor},
23    data::{BlockInfo, DataBlock, VariableWidthBlock},
24    encodings::logical::primitive::{
25        fullzip::{PerValueCompressor, PerValueDataBlock},
26        miniblock::{MiniBlockCompressed, MiniBlockCompressionContext, MiniBlockCompressor},
27    },
28    format::{
29        ProtobufUtils21,
30        pb21::{self, CompressiveEncoding},
31    },
32};
33
34use super::binary::BinaryMiniBlockEncoder;
35
36struct FsstCompressed {
37    data: VariableWidthBlock,
38    symbol_table: Vec<u8>,
39}
40
41impl FsstCompressed {
42    fn fsst_compress(data: DataBlock) -> Result<Self> {
43        match data {
44            DataBlock::VariableWidth(variable_width) => {
45                match variable_width.bits_per_offset {
46                    32 => {
47                        let offsets = variable_width.offsets.borrow_to_typed_slice::<i32>();
48                        let offsets_slice = offsets.as_ref();
49                        let bytes_data = variable_width.data.into_buffer();
50
51                        // prepare compression output buffer
52                        let mut dest_offsets = vec![0_i32; offsets_slice.len() * 2];
53                        let mut dest_values = vec![0_u8; bytes_data.len() * 2];
54                        let mut symbol_table = vec![0_u8; fsst::fsst::FSST_SYMBOL_TABLE_SIZE];
55
56                        // fsst compression
57                        fsst::fsst::compress(
58                            &mut symbol_table,
59                            bytes_data.as_slice(),
60                            offsets_slice,
61                            &mut dest_values,
62                            &mut dest_offsets,
63                        )?;
64
65                        // construct `DataBlock` for BinaryMiniBlockEncoder, we may want some `DataBlock` construct methods later
66                        let compressed = VariableWidthBlock {
67                            data: LanceBuffer::reinterpret_vec(dest_values),
68                            bits_per_offset: 32,
69                            offsets: LanceBuffer::reinterpret_vec(dest_offsets),
70                            num_values: variable_width.num_values,
71                            block_info: BlockInfo::new(),
72                        };
73
74                        Ok(Self {
75                            data: compressed,
76                            symbol_table,
77                        })
78                    }
79                    64 => {
80                        let offsets = variable_width.offsets.borrow_to_typed_slice::<i64>();
81                        let offsets_slice = offsets.as_ref();
82                        let bytes_data = variable_width.data.into_buffer();
83
84                        // prepare compression output buffer
85                        let mut dest_offsets = vec![0_i64; offsets_slice.len() * 2];
86                        let mut dest_values = vec![0_u8; bytes_data.len() * 2];
87                        let mut symbol_table = vec![0_u8; fsst::fsst::FSST_SYMBOL_TABLE_SIZE];
88
89                        // fsst compression
90                        fsst::fsst::compress(
91                            &mut symbol_table,
92                            bytes_data.as_slice(),
93                            offsets_slice,
94                            &mut dest_values,
95                            &mut dest_offsets,
96                        )?;
97
98                        // construct `DataBlock` for BinaryMiniBlockEncoder, we may want some `DataBlock` construct methods later
99                        let compressed = VariableWidthBlock {
100                            data: LanceBuffer::reinterpret_vec(dest_values),
101                            bits_per_offset: 64,
102                            offsets: LanceBuffer::reinterpret_vec(dest_offsets),
103                            num_values: variable_width.num_values,
104                            block_info: BlockInfo::new(),
105                        };
106
107                        Ok(Self {
108                            data: compressed,
109                            symbol_table,
110                        })
111                    }
112                    _ => panic!(
113                        "Unsupported offsets type {}",
114                        variable_width.bits_per_offset
115                    ),
116                }
117            }
118            _ => Err(Error::invalid_input_source(
119                format!(
120                    "Cannot compress a data block of type {} with FsstEncoder",
121                    data.name()
122                )
123                .into(),
124            )),
125        }
126    }
127}
128
129#[derive(Debug, Default)]
130pub struct FsstMiniBlockEncoder {
131    minichunk_size: Option<i64>,
132}
133
134impl FsstMiniBlockEncoder {
135    pub fn new(minichunk_size: Option<i64>) -> Self {
136        Self { minichunk_size }
137    }
138}
139
140impl MiniBlockCompressor for FsstMiniBlockEncoder {
141    fn compress(
142        &self,
143        context: MiniBlockCompressionContext,
144        data: DataBlock,
145    ) -> Result<(MiniBlockCompressed, CompressiveEncoding)> {
146        let compressed = FsstCompressed::fsst_compress(data)?;
147
148        let data_block = DataBlock::VariableWidth(compressed.data);
149
150        // compress the fsst compressed data using `BinaryMiniBlockEncoder`
151        let binary_compressor = Box::new(BinaryMiniBlockEncoder::new(self.minichunk_size))
152            as Box<dyn MiniBlockCompressor>;
153
154        let (binary_miniblock_compressed, binary_array_encoding) =
155            binary_compressor.compress(context, data_block)?;
156
157        Ok((
158            binary_miniblock_compressed,
159            ProtobufUtils21::fsst(binary_array_encoding, compressed.symbol_table),
160        ))
161    }
162}
163
164#[derive(Debug)]
165pub struct FsstPerValueEncoder {
166    inner: Box<dyn PerValueCompressor>,
167}
168
169impl FsstPerValueEncoder {
170    pub fn new(inner: Box<dyn PerValueCompressor>) -> Self {
171        Self { inner }
172    }
173}
174
175impl PerValueCompressor for FsstPerValueEncoder {
176    fn compress(&self, data: DataBlock) -> Result<(PerValueDataBlock, CompressiveEncoding)> {
177        let compressed = FsstCompressed::fsst_compress(data)?;
178
179        let data_block = DataBlock::VariableWidth(compressed.data);
180
181        let (binary_compressed, binary_array_encoding) = self.inner.compress(data_block)?;
182
183        Ok((
184            binary_compressed,
185            ProtobufUtils21::fsst(binary_array_encoding, compressed.symbol_table),
186        ))
187    }
188}
189
190#[derive(Debug)]
191pub struct FsstPerValueDecompressor {
192    symbol_table: LanceBuffer,
193    inner_decompressor: Box<dyn VariablePerValueDecompressor>,
194}
195
196impl FsstPerValueDecompressor {
197    pub fn new(
198        symbol_table: LanceBuffer,
199        inner_decompressor: Box<dyn VariablePerValueDecompressor>,
200    ) -> Self {
201        Self {
202            symbol_table,
203            inner_decompressor,
204        }
205    }
206}
207
208impl VariablePerValueDecompressor for FsstPerValueDecompressor {
209    fn decompress(&self, data: VariableWidthBlock) -> Result<DataBlock> {
210        // Step 1. Run inner decompressor
211        let compressed_variable_data = self
212            .inner_decompressor
213            .decompress(data)?
214            .as_variable_width()
215            .unwrap();
216
217        // Step 2. FSST decompress
218        let bytes = compressed_variable_data.data.borrow_to_typed_slice::<u8>();
219        let bytes = bytes.as_ref();
220
221        match compressed_variable_data.bits_per_offset {
222            32 => {
223                let offsets = compressed_variable_data
224                    .offsets
225                    .borrow_to_typed_slice::<i32>();
226                let offsets = offsets.as_ref();
227                let num_values = compressed_variable_data.num_values;
228
229                // The data will expand at most 8 times
230                // The offsets will be the same size because we have the same # of strings
231                let mut decompress_bytes_buf = vec![0u8; bytes.len() * 8];
232                let mut decompress_offset_buf = vec![0i32; offsets.len()];
233                fsst::fsst::decompress(
234                    &self.symbol_table,
235                    bytes,
236                    offsets,
237                    &mut decompress_bytes_buf,
238                    &mut decompress_offset_buf,
239                )?;
240
241                // Ensure the offsets array is trimmed to exactly num_values + 1 elements
242                decompress_offset_buf.truncate((num_values + 1) as usize);
243
244                Ok(DataBlock::VariableWidth(VariableWidthBlock {
245                    data: LanceBuffer::from(decompress_bytes_buf),
246                    offsets: LanceBuffer::reinterpret_vec(decompress_offset_buf),
247                    bits_per_offset: 32,
248                    num_values,
249                    block_info: BlockInfo::new(),
250                }))
251            }
252            64 => {
253                let offsets = compressed_variable_data
254                    .offsets
255                    .borrow_to_typed_slice::<i64>();
256                let offsets = offsets.as_ref();
257                let num_values = compressed_variable_data.num_values;
258
259                // The data will expand at most 8 times
260                // The offsets will be the same size because we have the same # of strings
261                let mut decompress_bytes_buf = vec![0u8; bytes.len() * 8];
262                let mut decompress_offset_buf = vec![0i64; offsets.len()];
263                fsst::fsst::decompress(
264                    &self.symbol_table,
265                    bytes,
266                    offsets,
267                    &mut decompress_bytes_buf,
268                    &mut decompress_offset_buf,
269                )?;
270
271                // Ensure the offsets array is trimmed to exactly num_values + 1 elements
272                decompress_offset_buf.truncate((num_values + 1) as usize);
273
274                Ok(DataBlock::VariableWidth(VariableWidthBlock {
275                    data: LanceBuffer::from(decompress_bytes_buf),
276                    offsets: LanceBuffer::reinterpret_vec(decompress_offset_buf),
277                    bits_per_offset: 64,
278                    num_values,
279                    block_info: BlockInfo::new(),
280                }))
281            }
282            _ => panic!(
283                "Unsupported offset type {}",
284                compressed_variable_data.bits_per_offset,
285            ),
286        }
287    }
288}
289
290#[derive(Debug)]
291pub struct FsstMiniBlockDecompressor {
292    symbol_table: LanceBuffer,
293    inner_decompressor: Box<dyn MiniBlockDecompressor>,
294}
295
296impl FsstMiniBlockDecompressor {
297    pub fn new(
298        description: &pb21::Fsst,
299        inner_decompressor: Box<dyn MiniBlockDecompressor>,
300    ) -> Self {
301        Self {
302            symbol_table: LanceBuffer::from_bytes(description.symbol_table.clone(), 1),
303            inner_decompressor,
304        }
305    }
306}
307
308impl MiniBlockDecompressor for FsstMiniBlockDecompressor {
309    fn decompress(&self, data: Vec<LanceBuffer>, num_values: u64) -> Result<DataBlock> {
310        // Step 1. decompress data use `BinaryMiniBlockDecompressor`
311        // Extract the bits_per_offset from the binary encoding
312        let compressed_data_block = self.inner_decompressor.decompress(data, num_values)?;
313        let DataBlock::VariableWidth(compressed_data_block) = compressed_data_block else {
314            panic!("BinaryMiniBlockDecompressor should output VariableWidth DataBlock")
315        };
316
317        // Step 2. FSST decompress
318        let bytes = &compressed_data_block.data;
319        let (decompress_bytes_buf, decompress_offset_buf) =
320            if compressed_data_block.bits_per_offset == 64 {
321                let offsets = compressed_data_block.offsets.borrow_to_typed_slice::<i64>();
322                let offsets = offsets.as_ref();
323
324                // The data will expand at most 8 times
325                // The offsets will be the same size because we have the same # of strings
326                let mut decompress_bytes_buf = vec![0u8; bytes.len() * 8];
327                let mut decompress_offset_buf = vec![0i64; offsets.len()];
328                fsst::fsst::decompress(
329                    &self.symbol_table,
330                    bytes.as_ref(),
331                    offsets,
332                    &mut decompress_bytes_buf,
333                    &mut decompress_offset_buf,
334                )?;
335
336                // Ensure the offsets array is trimmed to exactly num_values + 1 elements
337                decompress_offset_buf.truncate((num_values + 1) as usize);
338
339                (
340                    decompress_bytes_buf,
341                    LanceBuffer::reinterpret_vec(decompress_offset_buf),
342                )
343            } else {
344                let offsets = compressed_data_block.offsets.borrow_to_typed_slice::<i32>();
345                let offsets = offsets.as_ref();
346
347                // The data will expand at most 8 times
348                // The offsets will be the same size because we have the same # of strings
349                let mut decompress_bytes_buf = vec![0u8; bytes.len() * 8];
350                let mut decompress_offset_buf = vec![0i32; offsets.len()];
351                fsst::fsst::decompress(
352                    &self.symbol_table,
353                    bytes.as_ref(),
354                    offsets,
355                    &mut decompress_bytes_buf,
356                    &mut decompress_offset_buf,
357                )?;
358
359                // Ensure the offsets array is trimmed to exactly num_values + 1 elements
360                decompress_offset_buf.truncate((num_values + 1) as usize);
361
362                (
363                    decompress_bytes_buf,
364                    LanceBuffer::reinterpret_vec(decompress_offset_buf),
365                )
366            };
367
368        Ok(DataBlock::VariableWidth(VariableWidthBlock {
369            data: LanceBuffer::from(decompress_bytes_buf),
370            offsets: decompress_offset_buf,
371            bits_per_offset: compressed_data_block.bits_per_offset,
372            num_values,
373            block_info: BlockInfo::new(),
374        }))
375    }
376}
377
378#[cfg(test)]
379mod tests {
380    use std::collections::HashMap;
381
382    use lance_datagen::{ByteCount, RowCount};
383
384    use crate::testing::{TestCases, check_round_trip_encoding_of_data};
385
386    #[test_log::test(tokio::test)]
387    async fn test_fsst() {
388        let test_cases = TestCases::default()
389            .with_expected_encoding("fsst")
390            .with_structural_encodings();
391
392        // Generate data suitable for FSST (large strings, total size > 32KB)
393        let arr = lance_datagen::gen_batch()
394            .anon_col(lance_datagen::array::rand_utf8(ByteCount::from(100), false))
395            .into_batch_rows(RowCount::from(5000))
396            .unwrap()
397            .column(0)
398            .clone();
399
400        // Test both explicit metadata and automatic selection
401        // 1. Test with explicit FSST metadata
402        let metadata_explicit =
403            HashMap::from([("lance-encoding:compression".to_string(), "fsst".to_string())]);
404        check_round_trip_encoding_of_data(vec![arr.clone()], &test_cases, metadata_explicit).await;
405
406        // 2. Test automatic FSST selection based on data characteristics
407        // FSST should be chosen automatically: max_len >= 5 and total_size >= 32KB
408        check_round_trip_encoding_of_data(vec![arr], &test_cases, HashMap::new()).await;
409    }
410}