Skip to main content

lance_index/scalar/
label_list.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright The Lance Authors
3
4use std::{
5    any::Any,
6    collections::HashMap,
7    fmt::Debug,
8    pin::Pin,
9    sync::{Arc, Mutex},
10};
11
12use arrow::array::AsArray;
13use arrow_array::{Array, RecordBatch, UInt64Array};
14use arrow_schema::{DataType, Field, Fields, Schema, SchemaRef};
15use async_trait::async_trait;
16use bytes::Bytes;
17use datafusion::execution::RecordBatchStream;
18use datafusion::physical_plan::{SendableRecordBatchStream, stream::RecordBatchStreamAdapter};
19use datafusion_common::ScalarValue;
20use deepsize::DeepSizeOf;
21use futures::{StreamExt, TryStream, TryStreamExt, stream::BoxStream};
22use lance_core::cache::LanceCache;
23use lance_core::error::LanceOptionExt;
24use lance_core::utils::mask::{NullableRowAddrSet, RowAddrTreeMap, RowSetOps};
25use lance_core::{Error, ROW_ID, Result};
26use roaring::RoaringBitmap;
27use tracing::instrument;
28
29use super::{AnyQuery, IndexStore, LabelListQuery, ScalarIndex, bitmap::BitmapIndex};
30use super::{BuiltinIndexType, SargableQuery, ScalarIndexParams};
31use super::{MetricsCollector, SearchResult};
32use crate::frag_reuse::FragReuseIndex;
33use crate::pbold;
34use crate::scalar::bitmap::BitmapIndexPlugin;
35use crate::scalar::expression::{LabelListQueryParser, ScalarQueryParser};
36use crate::scalar::registry::{
37    DefaultTrainingRequest, ScalarIndexPlugin, TrainingCriteria, TrainingOrdering, TrainingRequest,
38    VALUE_COLUMN_NAME,
39};
40use crate::scalar::{CreatedIndex, UpdateCriteria};
41use crate::{Index, IndexType};
42
43pub const BITMAP_LOOKUP_NAME: &str = "bitmap_page_lookup.lance";
44pub const LABEL_LIST_NULLS_METADATA_KEY: &str = "lance:label_list_nulls";
45pub const LABEL_LIST_NULLS_MIN_VERSION: i32 = 1;
46const LABEL_LIST_INDEX_VERSION: u32 = 1;
47
48#[async_trait]
49trait LabelListSubIndex: ScalarIndex + DeepSizeOf {
50    async fn search_exact(
51        &self,
52        query: &dyn AnyQuery,
53        metrics: &dyn MetricsCollector,
54    ) -> Result<NullableRowAddrSet> {
55        let result = self.search(query, metrics).await?;
56        match result {
57            SearchResult::Exact(row_ids) => {
58                // Label list semantics treat NULL elements as non-matches, so only TRUE/FALSE
59                // results should remain for array_has_any/array_has_all when the list itself
60                // is non-NULL. Clear nulls to avoid propagating element-level NULLs.
61                Ok(row_ids.with_nulls(RowAddrTreeMap::new()))
62            }
63            _ => Err(Error::internal(
64                "Label list sub-index should return exact results".to_string(),
65            )),
66        }
67    }
68}
69
70impl<T: ScalarIndex + DeepSizeOf> LabelListSubIndex for T {}
71
72/// A scalar index that can be used on `List<T>` columns to
73/// accelerate list membership filters such as `array_has_all`, `array_has_any`,
74/// and `array_has` / `array_contains`, using an underlying bitmap index.
75#[derive(Clone, Debug, DeepSizeOf)]
76pub struct LabelListIndex {
77    values_index: Arc<BitmapIndex>,
78    list_nulls: Arc<RowAddrTreeMap>,
79}
80
81impl LabelListIndex {
82    fn new(values_index: Arc<BitmapIndex>, list_nulls: Arc<RowAddrTreeMap>) -> Self {
83        Self {
84            values_index,
85            list_nulls,
86        }
87    }
88
89    async fn load(
90        store: Arc<dyn IndexStore>,
91        frag_reuse_index: Option<Arc<FragReuseIndex>>,
92        index_cache: &LanceCache,
93    ) -> Result<Arc<Self>> {
94        let values_index =
95            BitmapIndex::load(store.clone(), frag_reuse_index.clone(), index_cache).await?;
96        let list_nulls = read_list_nulls(store, frag_reuse_index).await?;
97        Ok(Arc::new(Self::new(values_index, Arc::new(list_nulls))))
98    }
99}
100
101#[async_trait]
102impl Index for LabelListIndex {
103    fn as_any(&self) -> &dyn Any {
104        self
105    }
106
107    fn as_index(self: Arc<Self>) -> Arc<dyn Index> {
108        self
109    }
110
111    fn as_vector_index(self: Arc<Self>) -> Result<Arc<dyn crate::vector::VectorIndex>> {
112        Err(Error::not_supported_source(
113            "LabeListIndex is not a vector index".into(),
114        ))
115    }
116
117    async fn prewarm(&self) -> Result<()> {
118        self.values_index.prewarm().await
119    }
120
121    fn index_type(&self) -> IndexType {
122        IndexType::LabelList
123    }
124
125    fn statistics(&self) -> Result<serde_json::Value> {
126        self.values_index.statistics()
127    }
128
129    async fn calculate_included_frags(&self) -> Result<RoaringBitmap> {
130        unimplemented!()
131    }
132}
133
134impl LabelListIndex {
135    fn search_values<'a>(
136        &'a self,
137        values: &'a Vec<ScalarValue>,
138        metrics: &'a dyn MetricsCollector,
139    ) -> BoxStream<'a, Result<NullableRowAddrSet>> {
140        futures::stream::iter(values)
141            .then(move |value| {
142                let value_query = SargableQuery::Equals(value.clone());
143                async move { self.values_index.search_exact(&value_query, metrics).await }
144            })
145            .boxed()
146    }
147
148    async fn set_union<'a>(
149        &'a self,
150        mut sets: impl TryStream<Ok = NullableRowAddrSet, Error = Error> + 'a + Unpin,
151        single_set: bool,
152    ) -> Result<NullableRowAddrSet> {
153        let mut union_bitmap = sets.try_next().await?.unwrap();
154        if single_set {
155            return Ok(union_bitmap);
156        }
157        while let Some(next) = sets.try_next().await? {
158            union_bitmap |= &next;
159        }
160        Ok(union_bitmap)
161    }
162
163    async fn set_intersection<'a>(
164        &'a self,
165        mut sets: impl TryStream<Ok = NullableRowAddrSet, Error = Error> + 'a + Unpin,
166        single_set: bool,
167    ) -> Result<NullableRowAddrSet> {
168        let mut intersect_bitmap = sets.try_next().await?.unwrap();
169        if single_set {
170            return Ok(intersect_bitmap);
171        }
172        while let Some(next) = sets.try_next().await? {
173            intersect_bitmap &= &next;
174        }
175        Ok(intersect_bitmap)
176    }
177}
178
179#[async_trait]
180impl ScalarIndex for LabelListIndex {
181    #[instrument(skip_all, level = "debug")]
182    async fn search(
183        &self,
184        query: &dyn AnyQuery,
185        metrics: &dyn MetricsCollector,
186    ) -> Result<SearchResult> {
187        let query = query.as_any().downcast_ref::<LabelListQuery>().unwrap();
188
189        let row_ids = match query {
190            LabelListQuery::HasAllLabels(labels) => {
191                let values_results = self.search_values(labels, metrics);
192                self.set_intersection(values_results, labels.len() == 1)
193                    .await
194            }
195            LabelListQuery::HasAnyLabel(labels) => {
196                let values_results = self.search_values(labels, metrics);
197                self.set_union(values_results, labels.len() == 1).await
198            }
199        }?;
200        let row_ids = if self.list_nulls.as_ref().is_empty() {
201            row_ids
202        } else {
203            let mut nulls = row_ids.null_rows().clone();
204            nulls |= self.list_nulls.as_ref();
205            row_ids.with_nulls(nulls)
206        };
207        Ok(SearchResult::Exact(row_ids))
208    }
209
210    fn can_remap(&self) -> bool {
211        true
212    }
213
214    /// Remap the row ids, creating a new remapped version of this index in `dest_store`
215    async fn remap(
216        &self,
217        mapping: &HashMap<u64, Option<u64>>,
218        dest_store: &dyn IndexStore,
219    ) -> Result<CreatedIndex> {
220        let state = self.values_index.load_bitmap_index_state().await?;
221        let remapped_state = BitmapIndexPlugin::remap_bitmap_state(state, mapping);
222        let remapped_nulls =
223            RowAddrTreeMap::from_iter(self.list_nulls.row_addrs().unwrap().filter_map(|addr| {
224                let addr_as_u64 = u64::from(addr);
225                mapping
226                    .get(&addr_as_u64)
227                    .copied()
228                    .unwrap_or(Some(addr_as_u64))
229            }));
230        write_label_list_bitmap_index(
231            remapped_state,
232            dest_store,
233            self.values_index.value_type(),
234            &remapped_nulls,
235        )
236        .await?;
237
238        Ok(CreatedIndex {
239            index_details: prost_types::Any::from_msg(&pbold::LabelListIndexDetails::default())
240                .unwrap(),
241            index_version: LABEL_LIST_INDEX_VERSION,
242            files: Some(dest_store.list_files_with_sizes().await?),
243        })
244    }
245
246    /// Add the new data into the index, creating an updated version of the index in `dest_store`
247    async fn update(
248        &self,
249        new_data: SendableRecordBatchStream,
250        dest_store: &dyn IndexStore,
251        old_data_filter: Option<super::OldIndexDataFilter>,
252    ) -> Result<CreatedIndex> {
253        let state = self.values_index.load_bitmap_index_state().await?;
254        let list_nulls = Arc::new(Mutex::new(RowAddrTreeMap::new()));
255        let new_data = track_list_nulls(new_data, list_nulls.clone());
256        let (merged_state, value_type) =
257            BitmapIndexPlugin::build_bitmap_index_state(unnest_chunks(new_data)?, state).await?;
258        let _ = old_data_filter;
259        let mut merged_nulls = (*self.list_nulls).clone();
260        let new_nulls = list_nulls.lock().unwrap().clone();
261        if !new_nulls.is_empty() {
262            merged_nulls |= &new_nulls;
263        }
264        write_label_list_bitmap_index(merged_state, dest_store, &value_type, &merged_nulls).await?;
265
266        Ok(CreatedIndex {
267            index_details: prost_types::Any::from_msg(&pbold::LabelListIndexDetails::default())
268                .unwrap(),
269            index_version: LABEL_LIST_INDEX_VERSION,
270            files: Some(dest_store.list_files_with_sizes().await?),
271        })
272    }
273
274    fn update_criteria(&self) -> UpdateCriteria {
275        UpdateCriteria::only_new_data(TrainingCriteria::new(TrainingOrdering::None).with_row_id())
276    }
277
278    fn derive_index_params(&self) -> Result<ScalarIndexParams> {
279        Ok(ScalarIndexParams::for_builtin(BuiltinIndexType::LabelList))
280    }
281}
282
283fn extract_flatten_indices(list_arr: &dyn Array) -> UInt64Array {
284    if let Some(list_arr) = list_arr.as_list_opt::<i32>() {
285        let mut indices = Vec::with_capacity(list_arr.values().len());
286        let offsets = list_arr.value_offsets();
287        for (offset_idx, w) in offsets.windows(2).enumerate() {
288            let size = (w[1] - w[0]) as u64;
289            indices.extend((0..size).map(|_| offset_idx as u64));
290        }
291        UInt64Array::from(indices)
292    } else if let Some(list_arr) = list_arr.as_list_opt::<i64>() {
293        let mut indices = Vec::with_capacity(list_arr.values().len());
294        let offsets = list_arr.value_offsets();
295        for (offset_idx, w) in offsets.windows(2).enumerate() {
296            let size = (w[1] - w[0]) as u64;
297            indices.extend((0..size).map(|_| offset_idx as u64));
298        }
299        UInt64Array::from(indices)
300    } else {
301        unreachable!(
302            "Should verify that the first column is a list earlier. Got array of type: {}",
303            list_arr.data_type()
304        )
305    }
306}
307
308/// Collect row_ids for list-level NULLs before unnest; unnest drops NULL lists entirely.
309fn track_list_nulls(
310    source: SendableRecordBatchStream,
311    list_nulls: Arc<Mutex<RowAddrTreeMap>>,
312) -> SendableRecordBatchStream {
313    let schema = source.schema();
314    let stream = source.try_filter_map(move |batch| {
315        let list_nulls = list_nulls.clone();
316        async move {
317            record_list_nulls(&batch, &list_nulls)?;
318            Ok(Some(batch))
319        }
320    });
321
322    Box::pin(RecordBatchStreamAdapter::new(schema, stream))
323}
324
325fn record_list_nulls(
326    batch: &RecordBatch,
327    list_nulls: &Arc<Mutex<RowAddrTreeMap>>,
328) -> datafusion_common::Result<()> {
329    let values = batch.column_by_name(VALUE_COLUMN_NAME).expect_ok()?;
330    let row_ids = batch.column_by_name(ROW_ID).expect_ok()?;
331    let row_ids = row_ids.as_any().downcast_ref::<UInt64Array>().unwrap();
332
333    let mut local_nulls = RowAddrTreeMap::new();
334    for i in 0..values.len() {
335        if values.is_null(i) {
336            local_nulls.insert(row_ids.value(i));
337        }
338    }
339    if !local_nulls.is_empty() {
340        let mut guard = list_nulls.lock().unwrap();
341        *guard |= &local_nulls;
342    }
343    Ok(())
344}
345
346fn unnest_schema(schema: &Schema) -> SchemaRef {
347    let mut fields_iter = schema.fields.iter().cloned();
348    let key_field = fields_iter.next().unwrap();
349    let remaining_fields = fields_iter.collect::<Vec<_>>();
350
351    let new_key_field = match key_field.data_type() {
352        DataType::List(item_field) | DataType::LargeList(item_field) => Field::new(
353            key_field.name(),
354            item_field.data_type().clone(),
355            item_field.is_nullable() || key_field.is_nullable(),
356        ),
357        other_type => {
358            unreachable!(
359                "The first field in the schema must be a List or LargeList type. \
360                Found: {}. This should have been verified earlier in the code.",
361                other_type
362            )
363        }
364    };
365
366    let all_fields = vec![Arc::new(new_key_field)]
367        .into_iter()
368        .chain(remaining_fields)
369        .collect::<Vec<_>>();
370
371    Arc::new(Schema::new(Fields::from(all_fields)))
372}
373
374fn unnest_batch(
375    batch: arrow::record_batch::RecordBatch,
376    unnest_schema: SchemaRef,
377) -> datafusion_common::Result<RecordBatch> {
378    let mut columns_iter = batch.columns().iter().cloned();
379    let key_col = columns_iter.next().unwrap();
380    let remaining_cols = columns_iter.collect::<Vec<_>>();
381
382    let remaining_fields = unnest_schema
383        .fields
384        .iter()
385        .skip(1)
386        .cloned()
387        .collect::<Vec<_>>();
388
389    let remaining_batch = RecordBatch::try_new(
390        Arc::new(Schema::new(Fields::from(remaining_fields))),
391        remaining_cols,
392    )?;
393
394    let flatten_indices = extract_flatten_indices(key_col.as_ref());
395
396    let flattened_remaining =
397        arrow_select::take::take_record_batch(&remaining_batch, &flatten_indices)?;
398
399    let new_key_values = if let Some(key_list) = key_col.as_list_opt::<i32>() {
400        let value_start = key_list.value_offsets()[key_list.offset()] as usize;
401        let value_stop = key_list.value_offsets()[key_list.len()] as usize;
402        key_list
403            .values()
404            .slice(value_start, value_stop - value_start)
405            .clone()
406    } else if let Some(key_list) = key_col.as_list_opt::<i64>() {
407        let value_start = key_list.value_offsets()[key_list.offset()] as usize;
408        let value_stop = key_list.value_offsets()[key_list.len()] as usize;
409        key_list
410            .values()
411            .slice(value_start, value_stop - value_start)
412            .clone()
413    } else {
414        unreachable!("Should verify that the first column is a list earlier")
415    };
416
417    let all_columns = vec![new_key_values]
418        .into_iter()
419        .chain(flattened_remaining.columns().iter().cloned())
420        .collect::<Vec<_>>();
421
422    datafusion_common::Result::Ok(arrow::record_batch::RecordBatch::try_new(
423        unnest_schema,
424        all_columns,
425    )?)
426}
427
428fn unnest_chunks(
429    source: Pin<Box<dyn RecordBatchStream + Send>>,
430) -> Result<SendableRecordBatchStream> {
431    let unnest_schema = unnest_schema(source.schema().as_ref());
432    let unnest_schema_copy = unnest_schema.clone();
433    let source = source.try_filter_map(move |batch| {
434        std::future::ready(Some(unnest_batch(batch, unnest_schema.clone())).transpose())
435    });
436
437    Ok(Box::pin(RecordBatchStreamAdapter::new(
438        unnest_schema_copy,
439        source,
440    )))
441}
442
443async fn read_list_nulls(
444    store: Arc<dyn IndexStore>,
445    frag_reuse_index: Option<Arc<FragReuseIndex>>,
446) -> Result<RowAddrTreeMap> {
447    let reader = store.open_index_file(BITMAP_LOOKUP_NAME).await?;
448    if let Some(buffer_idx_str) = reader.schema().metadata.get(LABEL_LIST_NULLS_METADATA_KEY) {
449        let buffer_idx = buffer_idx_str.parse::<u32>().map_err(|err| {
450            Error::internal(format!(
451                "LabelList metadata key {} had invalid global buffer index {}: {}",
452                LABEL_LIST_NULLS_METADATA_KEY, buffer_idx_str, err
453            ))
454        })?;
455        let bytes = reader.read_global_buffer(buffer_idx).await?;
456        let null_map = RowAddrTreeMap::deserialize_from(bytes.as_ref())?;
457        return if let Some(frag_reuse_index) = frag_reuse_index {
458            Ok(frag_reuse_index.remap_row_addrs_tree_map(&null_map))
459        } else {
460            Ok(null_map)
461        };
462    }
463    Ok(RowAddrTreeMap::default())
464}
465
466fn serialize_list_nulls(null_map: &RowAddrTreeMap) -> Result<Bytes> {
467    let mut bytes = Vec::new();
468    null_map.serialize_into(&mut bytes)?;
469    Ok(Bytes::from(bytes))
470}
471
472async fn write_label_list_bitmap_index(
473    state: HashMap<ScalarValue, RowAddrTreeMap>,
474    store: &dyn IndexStore,
475    value_type: &DataType,
476    list_nulls: &RowAddrTreeMap,
477) -> Result<()> {
478    BitmapIndexPlugin::write_bitmap_index_with_extras(
479        state,
480        store,
481        value_type,
482        HashMap::new(),
483        vec![(
484            LABEL_LIST_NULLS_METADATA_KEY.to_string(),
485            serialize_list_nulls(list_nulls)?,
486        )],
487    )
488    .await
489}
490
491#[derive(Debug, Default)]
492pub struct LabelListIndexPlugin;
493
494#[async_trait]
495impl ScalarIndexPlugin for LabelListIndexPlugin {
496    fn name(&self) -> &str {
497        "LabelList"
498    }
499
500    fn new_training_request(
501        &self,
502        _params: &str,
503        field: &Field,
504    ) -> Result<Box<dyn TrainingRequest>> {
505        if !matches!(
506            field.data_type(),
507            DataType::List(_) | DataType::LargeList(_)
508        ) {
509            return Err(Error::invalid_input_source(format!(
510                "LabelList index can only be created on List or LargeList type columns. Column has type {:?}",
511                field.data_type()
512            )
513            .into()));
514        }
515
516        Ok(Box::new(DefaultTrainingRequest::new(
517            TrainingCriteria::new(TrainingOrdering::None).with_row_id(),
518        )))
519    }
520
521    fn provides_exact_answer(&self) -> bool {
522        true
523    }
524
525    fn version(&self) -> u32 {
526        LABEL_LIST_INDEX_VERSION
527    }
528
529    fn new_query_parser(
530        &self,
531        index_name: String,
532        _index_details: &prost_types::Any,
533    ) -> Option<Box<dyn ScalarQueryParser>> {
534        Some(Box::new(LabelListQueryParser::new(index_name)))
535    }
536
537    /// Train a new index
538    ///
539    /// The provided data must fulfill all the criteria returned by `training_criteria`
540    /// and the plugin can rely on this fact.
541    async fn train_index(
542        &self,
543        data: SendableRecordBatchStream,
544        index_store: &dyn IndexStore,
545        _request: Box<dyn TrainingRequest>,
546        fragment_ids: Option<Vec<u32>>,
547        _progress: Arc<dyn crate::progress::IndexBuildProgress>,
548    ) -> Result<CreatedIndex> {
549        if fragment_ids.is_some() {
550            return Err(Error::invalid_input_source(
551                "LabelList index does not support fragment training".into(),
552            ));
553        }
554
555        let schema = data.schema();
556        let field = schema
557            .column_with_name(VALUE_COLUMN_NAME)
558            .ok_or_else(|| {
559                Error::invalid_input_source(
560                    "Index training data missing value column"
561                        .to_string()
562                        .into(),
563                )
564            })?
565            .1;
566
567        if !matches!(
568            field.data_type(),
569            DataType::List(_) | DataType::LargeList(_)
570        ) {
571            return Err(Error::invalid_input_source(format!(
572                "LabelList index can only be created on List or LargeList type columns. Column has type {:?}",
573                field.data_type()
574            )
575            .into()));
576        }
577
578        let list_nulls = Arc::new(Mutex::new(RowAddrTreeMap::new()));
579        let data = track_list_nulls(data, list_nulls.clone());
580        let data = unnest_chunks(data)?;
581        let (state, value_type) =
582            BitmapIndexPlugin::build_bitmap_index_state(data, HashMap::new()).await?;
583        let list_nulls = list_nulls.lock().unwrap().clone();
584        write_label_list_bitmap_index(state, index_store, &value_type, &list_nulls).await?;
585        Ok(CreatedIndex {
586            index_details: prost_types::Any::from_msg(&pbold::LabelListIndexDetails::default())
587                .unwrap(),
588            index_version: LABEL_LIST_INDEX_VERSION,
589            files: Some(index_store.list_files_with_sizes().await?),
590        })
591    }
592
593    /// Load an index from storage
594    async fn load_index(
595        &self,
596        index_store: Arc<dyn IndexStore>,
597        _index_details: &prost_types::Any,
598        frag_reuse_index: Option<Arc<FragReuseIndex>>,
599        cache: &LanceCache,
600    ) -> Result<Arc<dyn ScalarIndex>> {
601        Ok(
602            LabelListIndex::load(index_store, frag_reuse_index, cache).await?
603                as Arc<dyn ScalarIndex>,
604        )
605    }
606}