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::{any::Any, collections::HashMap, fmt::Debug, pin::Pin, sync::Arc};
5
6use arrow::array::AsArray;
7use arrow_array::{Array, RecordBatch, UInt64Array};
8use arrow_schema::{DataType, Field, Fields, Schema, SchemaRef};
9use async_trait::async_trait;
10use datafusion::execution::RecordBatchStream;
11use datafusion::physical_plan::{SendableRecordBatchStream, stream::RecordBatchStreamAdapter};
12use datafusion_common::ScalarValue;
13use deepsize::DeepSizeOf;
14use futures::{StreamExt, TryStream, TryStreamExt, stream::BoxStream};
15use lance_core::cache::LanceCache;
16use lance_core::utils::mask::{NullableRowAddrSet, RowAddrTreeMap};
17use lance_core::{Error, Result};
18use roaring::RoaringBitmap;
19use tracing::instrument;
20
21use super::{AnyQuery, IndexStore, LabelListQuery, ScalarIndex, bitmap::BitmapIndex};
22use super::{BuiltinIndexType, SargableQuery, ScalarIndexParams};
23use super::{MetricsCollector, SearchResult};
24use crate::frag_reuse::FragReuseIndex;
25use crate::pbold;
26use crate::scalar::bitmap::BitmapIndexPlugin;
27use crate::scalar::expression::{LabelListQueryParser, ScalarQueryParser};
28use crate::scalar::registry::{
29    DefaultTrainingRequest, ScalarIndexPlugin, TrainingCriteria, TrainingOrdering, TrainingRequest,
30    VALUE_COLUMN_NAME,
31};
32use crate::scalar::{CreatedIndex, UpdateCriteria};
33use crate::{Index, IndexType};
34
35pub const BITMAP_LOOKUP_NAME: &str = "bitmap_page_lookup.lance";
36const LABEL_LIST_INDEX_VERSION: u32 = 0;
37
38#[async_trait]
39trait LabelListSubIndex: ScalarIndex + DeepSizeOf {
40    async fn search_exact(
41        &self,
42        query: &dyn AnyQuery,
43        metrics: &dyn MetricsCollector,
44    ) -> Result<NullableRowAddrSet> {
45        let result = self.search(query, metrics).await?;
46        match result {
47            SearchResult::Exact(row_ids) => {
48                // Label list semantics treat NULL elements as non-matches, so only TRUE/FALSE
49                // results should remain for array_has_any/array_has_all when the list itself
50                // is non-NULL. Clear nulls to avoid propagating element-level NULLs.
51                Ok(row_ids.with_nulls(RowAddrTreeMap::new()))
52            }
53            _ => Err(Error::internal(
54                "Label list sub-index should return exact results".to_string(),
55            )),
56        }
57    }
58}
59
60impl<T: ScalarIndex + DeepSizeOf> LabelListSubIndex for T {}
61
62/// A scalar index that can be used on `List<T>` columns to
63/// accelerate list membership filters such as `array_has_all`, `array_has_any`,
64/// and `array_has` / `array_contains`, using an underlying bitmap index.
65#[derive(Clone, Debug, DeepSizeOf)]
66pub struct LabelListIndex {
67    values_index: Arc<dyn LabelListSubIndex>,
68}
69
70impl LabelListIndex {
71    fn new(values_index: Arc<dyn LabelListSubIndex>) -> Self {
72        Self { values_index }
73    }
74
75    async fn load(
76        store: Arc<dyn IndexStore>,
77        frag_reuse_index: Option<Arc<FragReuseIndex>>,
78        index_cache: &LanceCache,
79    ) -> Result<Arc<Self>> {
80        BitmapIndex::load(store, frag_reuse_index, index_cache)
81            .await
82            .map(|index| Arc::new(Self::new(index)))
83    }
84}
85
86#[async_trait]
87impl Index for LabelListIndex {
88    fn as_any(&self) -> &dyn Any {
89        self
90    }
91
92    fn as_index(self: Arc<Self>) -> Arc<dyn Index> {
93        self
94    }
95
96    fn as_vector_index(self: Arc<Self>) -> Result<Arc<dyn crate::vector::VectorIndex>> {
97        Err(Error::not_supported_source(
98            "LabeListIndex is not a vector index".into(),
99        ))
100    }
101
102    async fn prewarm(&self) -> Result<()> {
103        self.values_index.prewarm().await
104    }
105
106    fn index_type(&self) -> IndexType {
107        IndexType::LabelList
108    }
109
110    fn statistics(&self) -> Result<serde_json::Value> {
111        self.values_index.statistics()
112    }
113
114    async fn calculate_included_frags(&self) -> Result<RoaringBitmap> {
115        unimplemented!()
116    }
117}
118
119impl LabelListIndex {
120    fn search_values<'a>(
121        &'a self,
122        values: &'a Vec<ScalarValue>,
123        metrics: &'a dyn MetricsCollector,
124    ) -> BoxStream<'a, Result<NullableRowAddrSet>> {
125        futures::stream::iter(values)
126            .then(move |value| {
127                let value_query = SargableQuery::Equals(value.clone());
128                async move { self.values_index.search_exact(&value_query, metrics).await }
129            })
130            .boxed()
131    }
132
133    async fn set_union<'a>(
134        &'a self,
135        mut sets: impl TryStream<Ok = NullableRowAddrSet, Error = Error> + 'a + Unpin,
136        single_set: bool,
137    ) -> Result<NullableRowAddrSet> {
138        let mut union_bitmap = sets.try_next().await?.unwrap();
139        if single_set {
140            return Ok(union_bitmap);
141        }
142        while let Some(next) = sets.try_next().await? {
143            union_bitmap |= &next;
144        }
145        Ok(union_bitmap)
146    }
147
148    async fn set_intersection<'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 intersect_bitmap = sets.try_next().await?.unwrap();
154        if single_set {
155            return Ok(intersect_bitmap);
156        }
157        while let Some(next) = sets.try_next().await? {
158            intersect_bitmap &= &next;
159        }
160        Ok(intersect_bitmap)
161    }
162}
163
164#[async_trait]
165impl ScalarIndex for LabelListIndex {
166    #[instrument(skip_all, level = "debug")]
167    async fn search(
168        &self,
169        query: &dyn AnyQuery,
170        metrics: &dyn MetricsCollector,
171    ) -> Result<SearchResult> {
172        let query = query.as_any().downcast_ref::<LabelListQuery>().unwrap();
173
174        let row_ids = match query {
175            LabelListQuery::HasAllLabels(labels) => {
176                let values_results = self.search_values(labels, metrics);
177                self.set_intersection(values_results, labels.len() == 1)
178                    .await
179            }
180            LabelListQuery::HasAnyLabel(labels) => {
181                let values_results = self.search_values(labels, metrics);
182                self.set_union(values_results, labels.len() == 1).await
183            }
184        }?;
185        Ok(SearchResult::Exact(row_ids))
186    }
187
188    fn can_remap(&self) -> bool {
189        true
190    }
191
192    /// Remap the row ids, creating a new remapped version of this index in `dest_store`
193    async fn remap(
194        &self,
195        mapping: &HashMap<u64, Option<u64>>,
196        dest_store: &dyn IndexStore,
197    ) -> Result<CreatedIndex> {
198        self.values_index.remap(mapping, dest_store).await?;
199
200        Ok(CreatedIndex {
201            index_details: prost_types::Any::from_msg(&pbold::LabelListIndexDetails::default())
202                .unwrap(),
203            index_version: LABEL_LIST_INDEX_VERSION,
204        })
205    }
206
207    /// Add the new data into the index, creating an updated version of the index in `dest_store`
208    async fn update(
209        &self,
210        new_data: SendableRecordBatchStream,
211        dest_store: &dyn IndexStore,
212        valid_old_fragments: Option<&RoaringBitmap>,
213    ) -> Result<CreatedIndex> {
214        self.values_index
215            .update(unnest_chunks(new_data)?, dest_store, valid_old_fragments)
216            .await?;
217
218        Ok(CreatedIndex {
219            index_details: prost_types::Any::from_msg(&pbold::LabelListIndexDetails::default())
220                .unwrap(),
221            index_version: LABEL_LIST_INDEX_VERSION,
222        })
223    }
224
225    fn update_criteria(&self) -> UpdateCriteria {
226        UpdateCriteria::only_new_data(TrainingCriteria::new(TrainingOrdering::None).with_row_id())
227    }
228
229    fn derive_index_params(&self) -> Result<ScalarIndexParams> {
230        Ok(ScalarIndexParams::for_builtin(BuiltinIndexType::LabelList))
231    }
232}
233
234fn extract_flatten_indices(list_arr: &dyn Array) -> UInt64Array {
235    if let Some(list_arr) = list_arr.as_list_opt::<i32>() {
236        let mut indices = Vec::with_capacity(list_arr.values().len());
237        let offsets = list_arr.value_offsets();
238        for (offset_idx, w) in offsets.windows(2).enumerate() {
239            let size = (w[1] - w[0]) as u64;
240            indices.extend((0..size).map(|_| offset_idx as u64));
241        }
242        UInt64Array::from(indices)
243    } else if let Some(list_arr) = list_arr.as_list_opt::<i64>() {
244        let mut indices = Vec::with_capacity(list_arr.values().len());
245        let offsets = list_arr.value_offsets();
246        for (offset_idx, w) in offsets.windows(2).enumerate() {
247            let size = (w[1] - w[0]) as u64;
248            indices.extend((0..size).map(|_| offset_idx as u64));
249        }
250        UInt64Array::from(indices)
251    } else {
252        unreachable!(
253            "Should verify that the first column is a list earlier. Got array of type: {}",
254            list_arr.data_type()
255        )
256    }
257}
258
259fn unnest_schema(schema: &Schema) -> SchemaRef {
260    let mut fields_iter = schema.fields.iter().cloned();
261    let key_field = fields_iter.next().unwrap();
262    let remaining_fields = fields_iter.collect::<Vec<_>>();
263
264    let new_key_field = match key_field.data_type() {
265        DataType::List(item_field) | DataType::LargeList(item_field) => Field::new(
266            key_field.name(),
267            item_field.data_type().clone(),
268            item_field.is_nullable() || key_field.is_nullable(),
269        ),
270        other_type => {
271            unreachable!(
272                "The first field in the schema must be a List or LargeList type. \
273                Found: {}. This should have been verified earlier in the code.",
274                other_type
275            )
276        }
277    };
278
279    let all_fields = vec![Arc::new(new_key_field)]
280        .into_iter()
281        .chain(remaining_fields)
282        .collect::<Vec<_>>();
283
284    Arc::new(Schema::new(Fields::from(all_fields)))
285}
286
287fn unnest_batch(
288    batch: arrow::record_batch::RecordBatch,
289    unnest_schema: SchemaRef,
290) -> datafusion_common::Result<RecordBatch> {
291    let mut columns_iter = batch.columns().iter().cloned();
292    let key_col = columns_iter.next().unwrap();
293    let remaining_cols = columns_iter.collect::<Vec<_>>();
294
295    let remaining_fields = unnest_schema
296        .fields
297        .iter()
298        .skip(1)
299        .cloned()
300        .collect::<Vec<_>>();
301
302    let remaining_batch = RecordBatch::try_new(
303        Arc::new(Schema::new(Fields::from(remaining_fields))),
304        remaining_cols,
305    )?;
306
307    let flatten_indices = extract_flatten_indices(key_col.as_ref());
308
309    let flattened_remaining =
310        arrow_select::take::take_record_batch(&remaining_batch, &flatten_indices)?;
311
312    let new_key_values = if let Some(key_list) = key_col.as_list_opt::<i32>() {
313        let value_start = key_list.value_offsets()[key_list.offset()] as usize;
314        let value_stop = key_list.value_offsets()[key_list.len()] as usize;
315        key_list
316            .values()
317            .slice(value_start, value_stop - value_start)
318            .clone()
319    } else if let Some(key_list) = key_col.as_list_opt::<i64>() {
320        let value_start = key_list.value_offsets()[key_list.offset()] as usize;
321        let value_stop = key_list.value_offsets()[key_list.len()] as usize;
322        key_list
323            .values()
324            .slice(value_start, value_stop - value_start)
325            .clone()
326    } else {
327        unreachable!("Should verify that the first column is a list earlier")
328    };
329
330    let all_columns = vec![new_key_values]
331        .into_iter()
332        .chain(flattened_remaining.columns().iter().cloned())
333        .collect::<Vec<_>>();
334
335    datafusion_common::Result::Ok(arrow::record_batch::RecordBatch::try_new(
336        unnest_schema,
337        all_columns,
338    )?)
339}
340
341fn unnest_chunks(
342    source: Pin<Box<dyn RecordBatchStream + Send>>,
343) -> Result<SendableRecordBatchStream> {
344    let unnest_schema = unnest_schema(source.schema().as_ref());
345    let unnest_schema_copy = unnest_schema.clone();
346    let source = source.try_filter_map(move |batch| {
347        std::future::ready(Some(unnest_batch(batch, unnest_schema.clone())).transpose())
348    });
349
350    Ok(Box::pin(RecordBatchStreamAdapter::new(
351        unnest_schema_copy,
352        source,
353    )))
354}
355
356#[derive(Debug, Default)]
357pub struct LabelListIndexPlugin;
358
359#[async_trait]
360impl ScalarIndexPlugin for LabelListIndexPlugin {
361    fn name(&self) -> &str {
362        "LabelList"
363    }
364
365    fn new_training_request(
366        &self,
367        _params: &str,
368        field: &Field,
369    ) -> Result<Box<dyn TrainingRequest>> {
370        if !matches!(
371            field.data_type(),
372            DataType::List(_) | DataType::LargeList(_)
373        ) {
374            return Err(Error::invalid_input_source(format!(
375                "LabelList index can only be created on List or LargeList type columns. Column has type {:?}",
376                field.data_type()
377            )
378            .into()));
379        }
380
381        Ok(Box::new(DefaultTrainingRequest::new(
382            TrainingCriteria::new(TrainingOrdering::None).with_row_id(),
383        )))
384    }
385
386    fn provides_exact_answer(&self) -> bool {
387        true
388    }
389
390    fn version(&self) -> u32 {
391        LABEL_LIST_INDEX_VERSION
392    }
393
394    fn new_query_parser(
395        &self,
396        index_name: String,
397        _index_details: &prost_types::Any,
398    ) -> Option<Box<dyn ScalarQueryParser>> {
399        Some(Box::new(LabelListQueryParser::new(index_name)))
400    }
401
402    /// Train a new index
403    ///
404    /// The provided data must fulfill all the criteria returned by `training_criteria`
405    /// and the plugin can rely on this fact.
406    async fn train_index(
407        &self,
408        data: SendableRecordBatchStream,
409        index_store: &dyn IndexStore,
410        request: Box<dyn TrainingRequest>,
411        fragment_ids: Option<Vec<u32>>,
412        progress: Arc<dyn crate::progress::IndexBuildProgress>,
413    ) -> Result<CreatedIndex> {
414        if fragment_ids.is_some() {
415            return Err(Error::invalid_input_source(
416                "LabelList index does not support fragment training".into(),
417            ));
418        }
419
420        let schema = data.schema();
421        let field = schema
422            .column_with_name(VALUE_COLUMN_NAME)
423            .ok_or_else(|| {
424                Error::invalid_input_source(
425                    "Index training data missing value column"
426                        .to_string()
427                        .into(),
428                )
429            })?
430            .1;
431
432        if !matches!(
433            field.data_type(),
434            DataType::List(_) | DataType::LargeList(_)
435        ) {
436            return Err(Error::invalid_input_source(format!(
437                "LabelList index can only be created on List or LargeList type columns. Column has type {:?}",
438                field.data_type()
439            )
440            .into()));
441        }
442
443        let data = unnest_chunks(data)?;
444        let bitmap_plugin = BitmapIndexPlugin;
445        bitmap_plugin
446            .train_index(data, index_store, request, fragment_ids, progress)
447            .await?;
448        Ok(CreatedIndex {
449            index_details: prost_types::Any::from_msg(&pbold::LabelListIndexDetails::default())
450                .unwrap(),
451            index_version: LABEL_LIST_INDEX_VERSION,
452        })
453    }
454
455    /// Load an index from storage
456    async fn load_index(
457        &self,
458        index_store: Arc<dyn IndexStore>,
459        _index_details: &prost_types::Any,
460        frag_reuse_index: Option<Arc<FragReuseIndex>>,
461        cache: &LanceCache,
462    ) -> Result<Arc<dyn ScalarIndex>> {
463        Ok(
464            LabelListIndex::load(index_store, frag_reuse_index, cache).await?
465                as Arc<dyn ScalarIndex>,
466        )
467    }
468}