Skip to main content

lance_encoding/array_encoding/
strategy.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright The Lance Authors
3
4use std::{collections::HashMap, env, hash::RandomState, sync::Arc};
5
6#[cfg(test)]
7use arrow_array::cast::AsArray;
8use arrow_array::{ArrayRef, UInt8Array};
9use arrow_schema::DataType;
10use hyperloglogplus::{HyperLogLog, HyperLogLogPlus};
11
12use crate::{
13    array_encoding::{
14        logical::{
15            blob::BlobFieldEncoder, list::ListFieldEncoder, primitive::PrimitiveFieldEncoder,
16        },
17        physical::{
18            basic::BasicEncoder,
19            binary::BinaryEncoder,
20            dictionary::{AlreadyDictionaryEncoder, DictionaryEncoder},
21            fixed_size_list::FslEncoder,
22            fsst::FsstArrayEncoder,
23            packed_struct::PackedStructEncoder,
24        },
25    },
26    constants::{
27        COMPRESSION_LEVEL_META_KEY, COMPRESSION_META_KEY, PACKED_STRUCT_LEGACY_META_KEY,
28        PACKED_STRUCT_META_KEY,
29    },
30    encoder::{
31        ArrayEncoder, ArrayEncodingStrategy, ColumnIndexSequence, FieldEncoder,
32        FieldEncodingContext, FieldEncodingStrategy,
33    },
34    encodings::{
35        logical::r#struct::StructFieldEncoder,
36        physical::{
37            block::{CompressionConfig, CompressionScheme},
38            value::ValueEncoder,
39        },
40    },
41};
42
43use lance_arrow::BLOB_META_KEY;
44use lance_core::datatypes::{BLOB_DESC_FIELD, Field};
45use lance_core::{Error, Result};
46
47/// Field-to-column composition for the `pb::ArrayEncoding` grammar.
48#[derive(Debug)]
49pub struct ArrayFieldEncodingStrategy {
50    array_encoding_strategy: Arc<dyn ArrayEncodingStrategy>,
51}
52
53impl ArrayFieldEncodingStrategy {
54    /// Create the field strategy for the `pb::ArrayEncoding` grammar.
55    ///
56    /// ```
57    /// use lance_encoding::encoder::ArrayFieldEncodingStrategy;
58    ///
59    /// let strategy = ArrayFieldEncodingStrategy::new();
60    /// ```
61    pub fn new() -> Self {
62        Self {
63            array_encoding_strategy: Arc::new(ArrayStrategy),
64        }
65    }
66
67    fn is_primitive_type(data_type: &DataType) -> bool {
68        matches!(
69            data_type,
70            DataType::Boolean
71                | DataType::Date32
72                | DataType::Date64
73                | DataType::Decimal128(_, _)
74                | DataType::Decimal256(_, _)
75                | DataType::Duration(_)
76                | DataType::Float16
77                | DataType::Float32
78                | DataType::Float64
79                | DataType::Int16
80                | DataType::Int32
81                | DataType::Int64
82                | DataType::Int8
83                | DataType::Interval(_)
84                | DataType::Null
85                | DataType::Time32(_)
86                | DataType::Time64(_)
87                | DataType::Timestamp(_, _)
88                | DataType::UInt16
89                | DataType::UInt32
90                | DataType::UInt64
91                | DataType::UInt8
92                | DataType::FixedSizeBinary(_)
93                | DataType::FixedSizeList(_, _)
94                | DataType::Binary
95                | DataType::LargeBinary
96                | DataType::Utf8
97                | DataType::LargeUtf8,
98        )
99    }
100}
101
102impl Default for ArrayFieldEncodingStrategy {
103    fn default() -> Self {
104        Self::new()
105    }
106}
107
108impl FieldEncodingStrategy for ArrayFieldEncodingStrategy {
109    fn create_field_encoder(
110        &self,
111        field: &Field,
112        column_index: &mut ColumnIndexSequence,
113        context: &FieldEncodingContext<'_>,
114    ) -> Result<Box<dyn FieldEncoder>> {
115        let options = context.options;
116        let data_type = field.data_type();
117        if Self::is_primitive_type(&data_type) {
118            let column_index = column_index.next_column_index(field.id as u32);
119            if field.metadata.contains_key(BLOB_META_KEY) {
120                let mut packed_meta = HashMap::new();
121                packed_meta.insert(PACKED_STRUCT_META_KEY.to_string(), "true".to_string());
122                let desc_field =
123                    Field::try_from(BLOB_DESC_FIELD.clone().with_metadata(packed_meta)).unwrap();
124                let desc_encoder = Box::new(PrimitiveFieldEncoder::try_new(
125                    options,
126                    self.array_encoding_strategy.clone(),
127                    column_index,
128                    desc_field,
129                )?);
130                Ok(Box::new(BlobFieldEncoder::new(desc_encoder)))
131            } else {
132                Ok(Box::new(PrimitiveFieldEncoder::try_new(
133                    options,
134                    self.array_encoding_strategy.clone(),
135                    column_index,
136                    field.clone(),
137                )?))
138            }
139        } else {
140            match data_type {
141                DataType::List(_child) | DataType::LargeList(_child) => {
142                    let list_idx = column_index.next_column_index(field.id as u32);
143                    let inner_encoding = context.strategy.create_field_encoder(
144                        &field.children[0],
145                        column_index,
146                        context,
147                    )?;
148                    let offsets_encoder =
149                        Arc::new(BasicEncoder::new(Box::new(ValueEncoder::default())));
150                    Ok(Box::new(ListFieldEncoder::new(
151                        inner_encoding,
152                        offsets_encoder,
153                        options.cache_bytes_per_column,
154                        options.keep_original_array,
155                        list_idx,
156                    )))
157                }
158                DataType::Struct(_) => {
159                    let field_metadata = &field.metadata;
160                    if field_metadata
161                        .get(PACKED_STRUCT_LEGACY_META_KEY)
162                        .map(|v| v == "true")
163                        .unwrap_or(field_metadata.contains_key(PACKED_STRUCT_META_KEY))
164                    {
165                        Ok(Box::new(PrimitiveFieldEncoder::try_new(
166                            options,
167                            self.array_encoding_strategy.clone(),
168                            column_index.next_column_index(field.id as u32),
169                            field.clone(),
170                        )?))
171                    } else {
172                        let header_idx = column_index.next_column_index(field.id as u32);
173                        let children_encoders = field
174                            .children
175                            .iter()
176                            .map(|field| {
177                                context
178                                    .strategy
179                                    .create_field_encoder(field, column_index, context)
180                            })
181                            .collect::<Result<Vec<_>>>()?;
182                        Ok(Box::new(StructFieldEncoder::new(
183                            children_encoders,
184                            header_idx,
185                        )))
186                    }
187                }
188                DataType::Dictionary(_, value_type) => {
189                    // A dictionary of primitive is, itself, primitive
190                    if Self::is_primitive_type(&value_type) {
191                        Ok(Box::new(PrimitiveFieldEncoder::try_new(
192                            options,
193                            self.array_encoding_strategy.clone(),
194                            column_index.next_column_index(field.id as u32),
195                            field.clone(),
196                        )?))
197                    } else {
198                        // A dictionary of logical is, itself, logical and we don't support that today
199                        // It could be possible (e.g. store indices in one column and values in remaining columns)
200                        // but would be a significant amount of work
201                        //
202                        // An easier fallback implementation would be to decode-on-write and encode-on-read
203                        Err(Error::not_supported_source(format!("cannot encode a dictionary column whose value type is a logical type ({})", value_type).into()))
204                    }
205                }
206                _ => todo!("Implement encoding for field {}", field),
207            }
208        }
209    }
210}
211
212/// Page-encoding selection for the `pb::ArrayEncoding` grammar.
213#[derive(Debug)]
214struct ArrayStrategy;
215
216impl ArrayStrategy {
217    fn get_field_compression(field_meta: &HashMap<String, String>) -> Option<CompressionConfig> {
218        let compression = field_meta.get(COMPRESSION_META_KEY)?;
219        let compression_scheme = compression.parse::<CompressionScheme>();
220        match compression_scheme {
221            Ok(compression_scheme) => Some(CompressionConfig::new(
222                compression_scheme,
223                field_meta
224                    .get(COMPRESSION_LEVEL_META_KEY)
225                    .and_then(|level| level.parse().ok()),
226            )),
227            Err(_) => None,
228        }
229    }
230
231    fn default_binary_encoder(
232        arrays: &[ArrayRef],
233        field_meta: Option<&HashMap<String, String>>,
234        data_size: u64,
235    ) -> Result<Box<dyn ArrayEncoder>> {
236        let bin_indices_encoder =
237            Self::choose_array_encoder(arrays, &DataType::UInt64, data_size, false, None)?;
238
239        if let Some(compression) = field_meta.and_then(Self::get_field_compression) {
240            if compression.scheme() == CompressionScheme::Fsst {
241                // User requested FSST
242                let raw_encoder = Box::new(BinaryEncoder::try_new(bin_indices_encoder, None)?);
243                Ok(Box::new(FsstArrayEncoder::new(raw_encoder)))
244            } else {
245                // Generic compression
246                Ok(Box::new(BinaryEncoder::try_new(
247                    bin_indices_encoder,
248                    Some(compression),
249                )?))
250            }
251        } else {
252            Ok(Box::new(BinaryEncoder::try_new(bin_indices_encoder, None)?))
253        }
254    }
255
256    fn choose_array_encoder(
257        arrays: &[ArrayRef],
258        data_type: &DataType,
259        data_size: u64,
260        use_dict_encoding: bool,
261        field_meta: Option<&HashMap<String, String>>,
262    ) -> Result<Box<dyn ArrayEncoder>> {
263        match data_type {
264            DataType::FixedSizeList(inner, dimension) => {
265                Ok(Box::new(BasicEncoder::new(Box::new(FslEncoder::new(
266                    Self::choose_array_encoder(
267                        arrays,
268                        inner.data_type(),
269                        data_size,
270                        use_dict_encoding,
271                        None,
272                    )?,
273                    *dimension as u32,
274                )))))
275            }
276            DataType::Dictionary(key_type, value_type) => {
277                let key_encoder =
278                    Self::choose_array_encoder(arrays, key_type, data_size, false, None)?;
279                let value_encoder =
280                    Self::choose_array_encoder(arrays, value_type, data_size, false, None)?;
281
282                Ok(Box::new(AlreadyDictionaryEncoder::new(
283                    key_encoder,
284                    value_encoder,
285                )))
286            }
287            DataType::Utf8 | DataType::LargeUtf8 | DataType::Binary | DataType::LargeBinary => {
288                if use_dict_encoding {
289                    let dict_indices_encoder = Self::choose_array_encoder(
290                        // We need to pass arrays to this method to figure out what kind of compression to
291                        // use but we haven't actually calculated the indices yet.  For now, we just assume
292                        // worst case and use the full range.  In the future maybe we can pass in statistics
293                        // instead of the actual data
294                        &[Arc::new(UInt8Array::from_iter_values(0_u8..255_u8))],
295                        &DataType::UInt8,
296                        data_size,
297                        false,
298                        None,
299                    )?;
300                    let dict_items_encoder = Self::choose_array_encoder(
301                        arrays,
302                        &DataType::Utf8,
303                        data_size,
304                        false,
305                        None,
306                    )?;
307
308                    Ok(Box::new(DictionaryEncoder::new(
309                        dict_indices_encoder,
310                        dict_items_encoder,
311                    )))
312                } else {
313                    Self::default_binary_encoder(arrays, field_meta, data_size)
314                }
315            }
316            DataType::Struct(fields) => {
317                let num_fields = fields.len();
318                let mut inner_encoders = Vec::new();
319
320                for i in 0..num_fields {
321                    let inner_datatype = fields[i].data_type();
322                    let inner_encoder = Self::choose_array_encoder(
323                        arrays,
324                        inner_datatype,
325                        data_size,
326                        use_dict_encoding,
327                        None,
328                    )?;
329                    inner_encoders.push(inner_encoder);
330                }
331
332                Ok(Box::new(PackedStructEncoder::new(inner_encoders)))
333            }
334            DataType::UInt8 | DataType::UInt16 | DataType::UInt32 | DataType::UInt64 => Ok(
335                Box::new(BasicEncoder::new(Box::new(ValueEncoder::default()))),
336            ),
337
338            // TODO: for signed integers, I intend to make it a cascaded encoding, a sparse array for the negative values and very wide(bit-width) values,
339            // then a bitpacked array for the narrow(bit-width) values, I need `BitpackedForNeg` to be merged first, I am
340            // thinking about putting this sparse array in the metadata so bitpacking remain using one page buffer only.
341            DataType::Int8 | DataType::Int16 | DataType::Int32 | DataType::Int64 => Ok(Box::new(
342                BasicEncoder::new(Box::new(ValueEncoder::default())),
343            )),
344            _ => Ok(Box::new(BasicEncoder::new(Box::new(
345                ValueEncoder::default(),
346            )))),
347        }
348    }
349}
350
351fn get_dict_encoding_threshold() -> u64 {
352    env::var("LANCE_DICT_ENCODING_THRESHOLD")
353        .ok()
354        .and_then(|val| val.parse().ok())
355        .unwrap_or(100)
356}
357
358// check whether we want to use dictionary encoding or not
359// by applying a threshold on cardinality
360// returns true if cardinality < threshold but false if the total number of rows is less than the threshold
361// The choice to use 100 is just a heuristic for now
362// hyperloglog is used for cardinality estimation
363// error rate = 1.04 / sqrt(2^p), where p is the precision
364// and error rate is 1.04 / sqrt(2^12) = 1.56%
365fn check_dict_encoding(arrays: &[ArrayRef], threshold: u64) -> bool {
366    let num_total_rows = arrays.iter().map(|arr| arr.len()).sum::<usize>();
367    if num_total_rows < threshold as usize {
368        return false;
369    }
370    const PRECISION: u8 = 12;
371
372    let mut hll: HyperLogLogPlus<String, RandomState> =
373        HyperLogLogPlus::new(PRECISION, RandomState::new()).unwrap();
374
375    for arr in arrays {
376        let string_array = arrow_array::cast::as_string_array(arr);
377        for value in string_array.iter().flatten() {
378            hll.insert(value);
379            let estimated_cardinality = hll.count() as u64;
380            if estimated_cardinality >= threshold {
381                return false;
382            }
383        }
384    }
385
386    true
387}
388
389#[cfg(test)]
390fn check_fixed_size_encoding(arrays: &[ArrayRef]) -> Option<u64> {
391    if arrays.is_empty() {
392        return None;
393    }
394
395    // make sure no array has an empty string
396    if !arrays.iter().all(|arr| {
397        if let Some(arr) = arr.as_string_opt::<i32>() {
398            arr.iter().flatten().all(|s| !s.is_empty())
399        } else if let Some(arr) = arr.as_binary_opt::<i32>() {
400            arr.iter().flatten().all(|s| !s.is_empty())
401        } else if let Some(arr) = arr.as_string_opt::<i64>() {
402            arr.iter().flatten().all(|s| !s.is_empty())
403        } else if let Some(arr) = arr.as_binary_opt::<i64>() {
404            arr.iter().flatten().all(|s| !s.is_empty())
405        } else {
406            panic!("wrong dtype");
407        }
408    }) {
409        return None;
410    }
411
412    let lengths = arrays
413        .iter()
414        .flat_map(|arr| {
415            if let Some(arr) = arr.as_string_opt::<i32>() {
416                let offsets = arr.offsets().inner();
417                offsets
418                    .windows(2)
419                    .map(|w| (w[1] - w[0]) as u64)
420                    .collect::<Vec<_>>()
421            } else if let Some(arr) = arr.as_binary_opt::<i32>() {
422                let offsets = arr.offsets().inner();
423                offsets
424                    .windows(2)
425                    .map(|w| (w[1] - w[0]) as u64)
426                    .collect::<Vec<_>>()
427            } else if let Some(arr) = arr.as_string_opt::<i64>() {
428                let offsets = arr.offsets().inner();
429                offsets
430                    .windows(2)
431                    .map(|w| (w[1] - w[0]) as u64)
432                    .collect::<Vec<_>>()
433            } else if let Some(arr) = arr.as_binary_opt::<i64>() {
434                let offsets = arr.offsets().inner();
435                offsets
436                    .windows(2)
437                    .map(|w| (w[1] - w[0]) as u64)
438                    .collect::<Vec<_>>()
439            } else {
440                panic!("wrong dtype");
441            }
442        })
443        .collect::<Vec<_>>();
444
445    // find first non-zero value in lengths
446    let first_non_zero = lengths.iter().position(|&x| x != 0);
447    if let Some(first_non_zero) = first_non_zero {
448        // make sure all lengths are equal to first_non_zero length or zero
449        if !lengths
450            .iter()
451            .all(|&x| x == 0 || x == lengths[first_non_zero])
452        {
453            return None;
454        }
455
456        // set the byte width
457        Some(lengths[first_non_zero])
458    } else {
459        None
460    }
461}
462
463impl ArrayEncodingStrategy for ArrayStrategy {
464    fn create_array_encoder(
465        &self,
466        arrays: &[ArrayRef],
467        field: &Field,
468    ) -> Result<Box<dyn ArrayEncoder>> {
469        let data_size = arrays
470            .iter()
471            .map(|arr| arr.get_buffer_memory_size() as u64)
472            .sum::<u64>();
473        let data_type = arrays[0].data_type();
474
475        let use_dict_encoding = data_type == &DataType::Utf8
476            && check_dict_encoding(arrays, get_dict_encoding_threshold());
477
478        Self::choose_array_encoder(
479            arrays,
480            data_type,
481            data_size,
482            use_dict_encoding,
483            Some(&field.metadata),
484        )
485    }
486}
487
488#[cfg(test)]
489mod tests {
490    use super::{
491        ArrayEncodingStrategy, ArrayStrategy, check_dict_encoding, check_fixed_size_encoding,
492    };
493    use crate::constants::{COMPRESSION_LEVEL_META_KEY, COMPRESSION_META_KEY};
494    use arrow_array::{ArrayRef, StringArray};
495    use arrow_schema::Field;
496    use std::collections::HashMap;
497    use std::sync::Arc;
498
499    fn is_dict_encoding_applicable(arr: Vec<Option<&str>>, threshold: u64) -> bool {
500        let arr = StringArray::from(arr);
501        let arr = Arc::new(arr) as ArrayRef;
502        check_dict_encoding(&[arr], threshold)
503    }
504
505    #[test]
506    fn test_dict_encoding_should_be_applied_if_cardinality_less_than_threshold() {
507        assert!(is_dict_encoding_applicable(
508            vec![Some("a"), Some("b"), Some("a"), Some("b")],
509            3,
510        ));
511    }
512
513    #[test]
514    fn test_dict_encoding_should_not_be_applied_if_cardinality_larger_than_threshold() {
515        assert!(!is_dict_encoding_applicable(
516            vec![Some("a"), Some("b"), Some("c"), Some("d")],
517            3,
518        ));
519    }
520
521    #[test]
522    fn test_dict_encoding_should_not_be_applied_if_cardinality_equal_to_threshold() {
523        assert!(!is_dict_encoding_applicable(
524            vec![Some("a"), Some("b"), Some("c"), Some("a")],
525            3,
526        ));
527    }
528
529    #[test]
530    fn test_dict_encoding_should_not_be_applied_for_empty_arrays() {
531        assert!(!is_dict_encoding_applicable(vec![], 3));
532    }
533
534    #[test]
535    fn test_dict_encoding_should_not_be_applied_for_smaller_than_threshold_arrays() {
536        assert!(!is_dict_encoding_applicable(vec![Some("a"), Some("a")], 3));
537    }
538
539    fn is_fixed_size_encoding_applicable(arrays: Vec<Vec<Option<&str>>>) -> bool {
540        let mut final_arrays = Vec::new();
541        for arr in arrays {
542            let arr = StringArray::from(arr);
543            let arr = Arc::new(arr) as ArrayRef;
544            final_arrays.push(arr);
545        }
546
547        check_fixed_size_encoding(&final_arrays).is_some()
548    }
549
550    #[test]
551    fn test_fixed_size_binary_encoding_applicable() {
552        assert!(!is_fixed_size_encoding_applicable(vec![vec![]]));
553
554        assert!(is_fixed_size_encoding_applicable(vec![vec![
555            Some("a"),
556            Some("b")
557        ]]));
558
559        assert!(!is_fixed_size_encoding_applicable(vec![vec![
560            Some("abc"),
561            Some("de")
562        ]]));
563
564        assert!(is_fixed_size_encoding_applicable(vec![vec![
565            Some("pqr"),
566            None
567        ]]));
568
569        assert!(!is_fixed_size_encoding_applicable(vec![vec![
570            Some("pqr"),
571            Some("")
572        ]]));
573
574        assert!(!is_fixed_size_encoding_applicable(vec![vec![
575            Some(""),
576            Some("")
577        ]]));
578    }
579
580    #[test]
581    fn test_fixed_size_binary_encoding_applicable_multiple_arrays() {
582        assert!(is_fixed_size_encoding_applicable(vec![
583            vec![Some("a"), Some("b")],
584            vec![Some("c"), Some("d")]
585        ]));
586
587        assert!(!is_fixed_size_encoding_applicable(vec![
588            vec![Some("ab"), Some("bc")],
589            vec![Some("c"), Some("d")]
590        ]));
591
592        assert!(!is_fixed_size_encoding_applicable(vec![
593            vec![Some("ab"), None],
594            vec![None, Some("d")]
595        ]));
596
597        assert!(is_fixed_size_encoding_applicable(vec![
598            vec![Some("a"), None],
599            vec![None, Some("d")]
600        ]));
601
602        assert!(!is_fixed_size_encoding_applicable(vec![
603            vec![Some(""), None],
604            vec![None, Some("")]
605        ]));
606
607        assert!(!is_fixed_size_encoding_applicable(vec![
608            vec![None, None],
609            vec![None, None]
610        ]));
611    }
612
613    fn verify_array_encoder(
614        array: ArrayRef,
615        field_meta: Option<HashMap<String, String>>,
616        expected_encoder: &str,
617    ) {
618        let encoding_strategy = ArrayStrategy;
619        let mut field = Field::new("test_field", array.data_type().clone(), true);
620        if let Some(field_meta) = field_meta {
621            field.set_metadata(field_meta);
622        }
623        let lance_field = lance_core::datatypes::Field::try_from(field).unwrap();
624        let encoder_result = encoding_strategy.create_array_encoder(&[array], &lance_field);
625        assert!(encoder_result.is_ok());
626        let encoder = encoder_result.unwrap();
627        assert_eq!(format!("{:?}", encoder).as_str(), expected_encoder);
628    }
629
630    #[test]
631    fn test_choose_encoder_for_zstd_compressed_string_field() {
632        verify_array_encoder(
633            Arc::new(StringArray::from(vec!["a", "bb", "ccc"])),
634            Some(HashMap::from([(
635                COMPRESSION_META_KEY.to_string(),
636                "zstd".to_string(),
637            )])),
638            "BinaryEncoder { indices_encoder: BasicEncoder { values_encoder: ValueEncoder }, compression_config: Some(CompressionConfig { scheme: Zstd, level: None }), buffer_compressor: Some(ZstdBufferCompressor { compression_level: 0 }) }",
639        );
640    }
641
642    #[test]
643    fn test_choose_encoder_for_zstd_compression_level() {
644        verify_array_encoder(
645            Arc::new(StringArray::from(vec!["a", "bb", "ccc"])),
646            Some(HashMap::from([
647                (COMPRESSION_META_KEY.to_string(), "zstd".to_string()),
648                (COMPRESSION_LEVEL_META_KEY.to_string(), "22".to_string()),
649            ])),
650            "BinaryEncoder { indices_encoder: BasicEncoder { values_encoder: ValueEncoder }, compression_config: Some(CompressionConfig { scheme: Zstd, level: Some(22) }), buffer_compressor: Some(ZstdBufferCompressor { compression_level: 22 }) }",
651        );
652    }
653}