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 lance_core::utils::row_addr_remap::RowAddrRemap;
5use std::{
6    any::Any,
7    collections::HashMap,
8    fmt::Debug,
9    pin::Pin,
10    sync::{Arc, Mutex},
11};
12
13use arrow::array::AsArray;
14use arrow_array::{Array, RecordBatch, UInt64Array};
15use arrow_schema::{DataType, Field, Fields, Schema, SchemaRef};
16use async_trait::async_trait;
17use bytes::Bytes;
18use datafusion::execution::RecordBatchStream;
19use datafusion::physical_plan::{SendableRecordBatchStream, stream::RecordBatchStreamAdapter};
20use datafusion_common::ScalarValue;
21use futures::{StreamExt, TryStream, TryStreamExt, stream::BoxStream};
22use lance_core::cache::{
23    CacheCodec, CacheCodecImpl, CacheEntryReader, CacheEntryWriter, CacheKey, CacheKeySchema,
24    KeyBuilder, LanceCache,
25};
26use lance_core::deepsize::DeepSizeOf;
27use lance_core::error::LanceOptionExt;
28use lance_core::{Error, ROW_ID, Result};
29use lance_select::{NullableRowAddrSet, RowAddrTreeMap, RowSetOps};
30use roaring::RoaringBitmap;
31use tracing::instrument;
32
33use super::{
34    AnyQuery, IndexFile, IndexStore, LabelListQuery, OldIndexDataFilter, ScalarIndex,
35    bitmap::BitmapIndex,
36};
37use super::{BuiltinIndexType, SargableQuery, ScalarIndexParams};
38use super::{MetricsCollector, SearchResult};
39use crate::pbold;
40use crate::scalar::bitmap::{BitmapIndexPlugin, BitmapIndexState};
41use crate::scalar::expression::{LabelListQueryParser, ScalarQueryParser};
42use crate::scalar::registry::{
43    BasicTrainer, DefaultTrainingRequest, ScalarIndexLoad, ScalarIndexPlugin, TrainingCriteria,
44    TrainingOrdering, TrainingRequest, VALUE_COLUMN_NAME, single_flight_open,
45};
46use crate::scalar::{CreatedIndex, RowIdRemapper, UpdateCriteria};
47use crate::{Index, IndexType};
48
49pub const BITMAP_LOOKUP_NAME: &str = "bitmap_page_lookup.lance";
50pub const LABEL_LIST_NULLS_METADATA_KEY: &str = "lance:label_list_nulls";
51pub const LABEL_LIST_NULLS_MIN_VERSION: i32 = 1;
52const LABEL_LIST_INDEX_VERSION: u32 = 1;
53
54#[async_trait]
55trait LabelListSubIndex: ScalarIndex + DeepSizeOf {
56    async fn search_exact(
57        &self,
58        query: &dyn AnyQuery,
59        metrics: &dyn MetricsCollector,
60    ) -> Result<NullableRowAddrSet> {
61        let result = self.search(query, metrics).await?;
62        match result {
63            SearchResult::Exact(row_ids) => {
64                // Label list semantics treat NULL elements as non-matches, so only TRUE/FALSE
65                // results should remain for array_has_any/array_has_all when the list itself
66                // is non-NULL. Clear nulls to avoid propagating element-level NULLs.
67                Ok(row_ids.with_nulls(RowAddrTreeMap::new()))
68            }
69            _ => Err(Error::internal(
70                "Label list sub-index should return exact results".to_string(),
71            )),
72        }
73    }
74}
75
76impl<T: ScalarIndex + DeepSizeOf> LabelListSubIndex for T {}
77
78/// A scalar index that can be used on `List<T>` columns to
79/// accelerate list membership filters such as `array_has_all`, `array_has_any`,
80/// and `array_has` / `array_contains`, using an underlying bitmap index.
81#[derive(Clone, Debug, DeepSizeOf)]
82pub struct LabelListIndex {
83    values_index: Arc<BitmapIndex>,
84    list_nulls: Arc<RowAddrTreeMap>,
85}
86
87impl LabelListIndex {
88    fn new(values_index: Arc<BitmapIndex>, list_nulls: Arc<RowAddrTreeMap>) -> Self {
89        Self {
90            values_index,
91            list_nulls,
92        }
93    }
94
95    async fn load(
96        store: Arc<dyn IndexStore>,
97        frag_reuse_index: Option<Arc<dyn RowIdRemapper>>,
98        index_cache: &LanceCache,
99    ) -> Result<Arc<Self>> {
100        let values_index =
101            BitmapIndex::load(store.clone(), frag_reuse_index.clone(), index_cache).await?;
102        let list_nulls = read_list_nulls(store, frag_reuse_index).await?;
103        Ok(Arc::new(Self::new(values_index, Arc::new(list_nulls))))
104    }
105}
106
107#[async_trait]
108impl Index for LabelListIndex {
109    fn as_any(&self) -> &dyn Any {
110        self
111    }
112
113    fn as_index(self: Arc<Self>) -> Arc<dyn Index> {
114        self
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: &RowAddrRemap,
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.get(addr_as_u64).unwrap_or(Some(addr_as_u64))
226            }));
227        let file = write_label_list_bitmap_index(
228            remapped_state,
229            dest_store,
230            self.values_index.value_type(),
231            &remapped_nulls,
232        )
233        .await?;
234
235        Ok(CreatedIndex {
236            index_details: prost_types::Any::from_msg(&pbold::LabelListIndexDetails::default())
237                .unwrap(),
238            index_version: LABEL_LIST_INDEX_VERSION,
239            files: vec![file],
240        })
241    }
242
243    /// Add the new data into the index, creating an updated version of the index in `dest_store`
244    async fn update(
245        &self,
246        new_data: SendableRecordBatchStream,
247        dest_store: &dyn IndexStore,
248        old_data_filter: Option<super::OldIndexDataFilter>,
249    ) -> Result<CreatedIndex> {
250        let state = self.values_index.load_bitmap_index_state().await?;
251        let list_nulls = Arc::new(Mutex::new(RowAddrTreeMap::new()));
252        let new_data = track_list_nulls(new_data, list_nulls.clone());
253        let (merged_state, value_type) =
254            BitmapIndexPlugin::build_bitmap_index_state(unnest_chunks(new_data)?, state).await?;
255        let _ = old_data_filter;
256        let mut merged_nulls = (*self.list_nulls).clone();
257        let new_nulls = list_nulls.lock().unwrap().clone();
258        if !new_nulls.is_empty() {
259            merged_nulls |= &new_nulls;
260        }
261        let file =
262            write_label_list_bitmap_index(merged_state, dest_store, &value_type, &merged_nulls)
263                .await?;
264
265        Ok(CreatedIndex {
266            index_details: prost_types::Any::from_msg(&pbold::LabelListIndexDetails::default())
267                .unwrap(),
268            index_version: LABEL_LIST_INDEX_VERSION,
269            files: vec![file],
270        })
271    }
272
273    fn update_criteria(&self) -> UpdateCriteria {
274        UpdateCriteria::only_new_data(TrainingCriteria::new(TrainingOrdering::None).with_row_id())
275    }
276
277    fn derive_index_params(&self) -> Result<ScalarIndexParams> {
278        Ok(ScalarIndexParams::for_builtin(BuiltinIndexType::LabelList))
279    }
280}
281
282fn extract_flatten_indices(list_arr: &dyn Array) -> UInt64Array {
283    if let Some(list_arr) = list_arr.as_list_opt::<i32>() {
284        let mut indices = Vec::with_capacity(list_arr.values().len());
285        let offsets = list_arr.value_offsets();
286        for (offset_idx, w) in offsets.windows(2).enumerate() {
287            let size = (w[1] - w[0]) as u64;
288            indices.extend((0..size).map(|_| offset_idx as u64));
289        }
290        UInt64Array::from(indices)
291    } else if let Some(list_arr) = list_arr.as_list_opt::<i64>() {
292        let mut indices = Vec::with_capacity(list_arr.values().len());
293        let offsets = list_arr.value_offsets();
294        for (offset_idx, w) in offsets.windows(2).enumerate() {
295            let size = (w[1] - w[0]) as u64;
296            indices.extend((0..size).map(|_| offset_idx as u64));
297        }
298        UInt64Array::from(indices)
299    } else {
300        unreachable!(
301            "Should verify that the first column is a list earlier. Got array of type: {}",
302            list_arr.data_type()
303        )
304    }
305}
306
307/// Collect row_ids for list-level NULLs before unnest; unnest drops NULL lists entirely.
308fn track_list_nulls(
309    source: SendableRecordBatchStream,
310    list_nulls: Arc<Mutex<RowAddrTreeMap>>,
311) -> SendableRecordBatchStream {
312    let schema = source.schema();
313    let stream = source.try_filter_map(move |batch| {
314        let list_nulls = list_nulls.clone();
315        async move {
316            record_list_nulls(&batch, &list_nulls)?;
317            Ok(Some(batch))
318        }
319    });
320
321    Box::pin(RecordBatchStreamAdapter::new(schema, stream))
322}
323
324fn record_list_nulls(
325    batch: &RecordBatch,
326    list_nulls: &Arc<Mutex<RowAddrTreeMap>>,
327) -> datafusion_common::Result<()> {
328    let values = batch.column_by_name(VALUE_COLUMN_NAME).expect_ok()?;
329    let row_ids = batch.column_by_name(ROW_ID).expect_ok()?;
330    let row_ids = row_ids.as_any().downcast_ref::<UInt64Array>().unwrap();
331
332    let mut local_nulls = RowAddrTreeMap::new();
333    for i in 0..values.len() {
334        if values.is_null(i) {
335            local_nulls.insert(row_ids.value(i));
336        }
337    }
338    if !local_nulls.is_empty() {
339        let mut guard = list_nulls.lock().unwrap();
340        *guard |= &local_nulls;
341    }
342    Ok(())
343}
344
345fn unnest_schema(schema: &Schema) -> SchemaRef {
346    let mut fields_iter = schema.fields.iter().cloned();
347    let key_field = fields_iter.next().unwrap();
348    let remaining_fields = fields_iter.collect::<Vec<_>>();
349
350    let new_key_field = match key_field.data_type() {
351        DataType::List(item_field) | DataType::LargeList(item_field) => Field::new(
352            key_field.name(),
353            item_field.data_type().clone(),
354            item_field.is_nullable() || key_field.is_nullable(),
355        ),
356        other_type => {
357            unreachable!(
358                "The first field in the schema must be a List or LargeList type. \
359                Found: {}. This should have been verified earlier in the code.",
360                other_type
361            )
362        }
363    };
364
365    let all_fields = vec![Arc::new(new_key_field)]
366        .into_iter()
367        .chain(remaining_fields)
368        .collect::<Vec<_>>();
369
370    Arc::new(Schema::new(Fields::from(all_fields)))
371}
372
373fn unnest_batch(
374    batch: arrow::record_batch::RecordBatch,
375    unnest_schema: SchemaRef,
376) -> datafusion_common::Result<RecordBatch> {
377    let mut columns_iter = batch.columns().iter().cloned();
378    let key_col = columns_iter.next().unwrap();
379    let remaining_cols = columns_iter.collect::<Vec<_>>();
380
381    let remaining_fields = unnest_schema
382        .fields
383        .iter()
384        .skip(1)
385        .cloned()
386        .collect::<Vec<_>>();
387
388    let remaining_batch = RecordBatch::try_new(
389        Arc::new(Schema::new(Fields::from(remaining_fields))),
390        remaining_cols,
391    )?;
392
393    let flatten_indices = extract_flatten_indices(key_col.as_ref());
394
395    let flattened_remaining =
396        arrow_select::take::take_record_batch(&remaining_batch, &flatten_indices)?;
397
398    let new_key_values = if let Some(key_list) = key_col.as_list_opt::<i32>() {
399        let value_start = key_list.value_offsets()[key_list.offset()] as usize;
400        let value_stop = key_list.value_offsets()[key_list.len()] as usize;
401        key_list
402            .values()
403            .slice(value_start, value_stop - value_start)
404            .clone()
405    } else if let Some(key_list) = key_col.as_list_opt::<i64>() {
406        let value_start = key_list.value_offsets()[key_list.offset()] as usize;
407        let value_stop = key_list.value_offsets()[key_list.len()] as usize;
408        key_list
409            .values()
410            .slice(value_start, value_stop - value_start)
411            .clone()
412    } else {
413        unreachable!("Should verify that the first column is a list earlier")
414    };
415
416    let all_columns = vec![new_key_values]
417        .into_iter()
418        .chain(flattened_remaining.columns().iter().cloned())
419        .collect::<Vec<_>>();
420
421    datafusion_common::Result::Ok(arrow::record_batch::RecordBatch::try_new(
422        unnest_schema,
423        all_columns,
424    )?)
425}
426
427fn unnest_chunks(
428    source: Pin<Box<dyn RecordBatchStream + Send>>,
429) -> Result<SendableRecordBatchStream> {
430    let unnest_schema = unnest_schema(source.schema().as_ref());
431    let unnest_schema_copy = unnest_schema.clone();
432    let source = source.try_filter_map(move |batch| {
433        std::future::ready(Some(unnest_batch(batch, unnest_schema.clone())).transpose())
434    });
435
436    Ok(Box::pin(RecordBatchStreamAdapter::new(
437        unnest_schema_copy,
438        source,
439    )))
440}
441
442async fn read_list_nulls(
443    store: Arc<dyn IndexStore>,
444    frag_reuse_index: Option<Arc<dyn RowIdRemapper>>,
445) -> Result<RowAddrTreeMap> {
446    let reader = store.open_index_file(BITMAP_LOOKUP_NAME).await?;
447    if let Some(buffer_idx_str) = reader.schema().metadata.get(LABEL_LIST_NULLS_METADATA_KEY) {
448        let buffer_idx = buffer_idx_str.parse::<u32>().map_err(|err| {
449            Error::internal(format!(
450                "LabelList metadata key {} had invalid global buffer index {}: {}",
451                LABEL_LIST_NULLS_METADATA_KEY, buffer_idx_str, err
452            ))
453        })?;
454        let bytes = reader.read_global_buffer(buffer_idx).await?;
455        let null_map = RowAddrTreeMap::deserialize_from(bytes.as_ref())?;
456        return if let Some(frag_reuse_index) = frag_reuse_index {
457            Ok(frag_reuse_index.remap_row_addrs_tree_map(&null_map))
458        } else {
459            Ok(null_map)
460        };
461    }
462    Ok(RowAddrTreeMap::default())
463}
464
465fn serialize_list_nulls(null_map: &RowAddrTreeMap) -> Result<Bytes> {
466    let mut bytes = Vec::new();
467    null_map.serialize_into(&mut bytes)?;
468    Ok(Bytes::from(bytes))
469}
470
471async fn write_label_list_bitmap_index(
472    state: HashMap<ScalarValue, RowAddrTreeMap>,
473    store: &dyn IndexStore,
474    value_type: &DataType,
475    list_nulls: &RowAddrTreeMap,
476) -> Result<IndexFile> {
477    BitmapIndexPlugin::write_bitmap_index_with_extras(
478        state,
479        store,
480        value_type,
481        HashMap::new(),
482        vec![(
483            LABEL_LIST_NULLS_METADATA_KEY.to_string(),
484            serialize_list_nulls(list_nulls)?,
485        )],
486    )
487    .await
488}
489
490/// Merge multiple LabelList index segments into a single index.
491///
492/// A [`LabelListIndex`] is a [`BitmapIndex`] over the unnested list values plus a
493/// separate `list_nulls` row set. Because distributed segments cover disjoint rows
494/// (distinct fragments), merging is a cheap union of the underlying bitmap states
495/// and of the `list_nulls` sets — no re-scan of source data is required. This
496/// mirrors [`crate::scalar::bitmap::merge_bitmap_indices`] but also carries the
497/// per-segment `list_nulls`. When `old_data_filter` is provided, rows from
498/// retired fragments are removed from both the value bitmaps and `list_nulls`.
499pub async fn merge_label_list_indices(
500    source_indices: &[Arc<LabelListIndex>],
501    dest_store: &dyn IndexStore,
502    old_data_filter: Option<OldIndexDataFilter>,
503    progress: Arc<dyn crate::progress::IndexBuildProgress>,
504) -> Result<CreatedIndex> {
505    if source_indices.is_empty() {
506        return Err(Error::invalid_input(
507            "LabelList segment merge requires at least one source segment".to_string(),
508        ));
509    }
510
511    let value_type = source_indices[0].values_index.value_type().clone();
512    let mut merged_state = HashMap::<ScalarValue, RowAddrTreeMap>::new();
513    let mut merged_nulls = RowAddrTreeMap::new();
514
515    progress
516        .stage_start(
517            "merge_label_list_segments",
518            Some(source_indices.len() as u64),
519            "segments",
520        )
521        .await?;
522    for (idx, source_index) in source_indices.iter().enumerate() {
523        if source_index.values_index.value_type() != &value_type {
524            return Err(Error::invalid_input(format!(
525                "LabelList segment has value type {:?}, expected {:?}",
526                source_index.values_index.value_type(),
527                value_type
528            )));
529        }
530
531        let state = source_index.values_index.load_bitmap_index_state().await?;
532        for (key, mut bitmap) in state {
533            if let Some(filter) = old_data_filter.as_ref() {
534                filter.retain_old_rows(&mut bitmap);
535            }
536            if bitmap.is_empty() {
537                continue;
538            }
539            merged_state
540                .entry(key)
541                .and_modify(|existing| *existing |= &bitmap)
542                .or_insert(bitmap);
543        }
544        let mut list_nulls = source_index.list_nulls.as_ref().clone();
545        if let Some(filter) = old_data_filter.as_ref() {
546            filter.retain_old_rows(&mut list_nulls);
547        }
548        merged_nulls |= &list_nulls;
549        progress
550            .stage_progress("merge_label_list_segments", (idx + 1) as u64)
551            .await?;
552    }
553    progress.stage_complete("merge_label_list_segments").await?;
554
555    progress
556        .stage_start("write_label_list_index", Some(1), "files")
557        .await?;
558    let file =
559        write_label_list_bitmap_index(merged_state, dest_store, &value_type, &merged_nulls).await?;
560    progress.stage_progress("write_label_list_index", 1).await?;
561    progress.stage_complete("write_label_list_index").await?;
562
563    Ok(CreatedIndex {
564        index_details: prost_types::Any::from_msg(&pbold::LabelListIndexDetails::default())
565            .unwrap(),
566        index_version: LABEL_LIST_INDEX_VERSION,
567        files: vec![file],
568    })
569}
570
571/// The serializable state of a [`LabelListIndex`].
572///
573/// `LabelListIndex` is a thin wrapper around a [`BitmapIndex`] plus a separate
574/// row bitmap tracking which list values were `NULL` (lost by unnest at build
575/// time). Its cache state is the corresponding [`BitmapIndexState`] plus the
576/// already-loaded `list_nulls`.
577#[derive(Debug, Clone)]
578pub struct LabelListIndexState {
579    bitmap_state: BitmapIndexState,
580    list_nulls: Arc<RowAddrTreeMap>,
581}
582
583impl DeepSizeOf for LabelListIndexState {
584    fn deep_size_of_children(&self, context: &mut lance_core::deepsize::Context) -> usize {
585        self.bitmap_state.deep_size_of_children(context)
586            + self.list_nulls.deep_size_of_children(context)
587    }
588}
589
590impl LabelListIndexState {
591    fn from_index(index: &LabelListIndex) -> Result<Self> {
592        Ok(Self {
593            bitmap_state: BitmapIndexState::from_index(&index.values_index)?,
594            list_nulls: index.list_nulls.clone(),
595        })
596    }
597
598    fn from_scalar_index(index: &dyn ScalarIndex) -> Result<Self> {
599        let label_list = index
600            .as_any()
601            .downcast_ref::<LabelListIndex>()
602            .ok_or_else(|| {
603                Error::internal(
604                    "LabelListIndexState::from_scalar_index called with a non-label-list index",
605                )
606            })?;
607        Self::from_index(label_list)
608    }
609
610    fn into_label_list_index(
611        self,
612        store: Arc<dyn IndexStore>,
613        index_cache: &LanceCache,
614        frag_reuse_index: Option<Arc<dyn RowIdRemapper>>,
615    ) -> Result<Arc<LabelListIndex>> {
616        let bitmap = self
617            .bitmap_state
618            .to_bitmap_index(store, index_cache, frag_reuse_index)?;
619        Ok(Arc::new(LabelListIndex::new(bitmap, self.list_nulls)))
620    }
621}
622
623impl CacheCodecImpl for LabelListIndexState {
624    const TYPE_ID: &'static str = "lance.scalar.LabelListIndexState";
625    const CURRENT_VERSION: u32 = 1;
626
627    /// Wire format:
628    /// ```text
629    /// RAW_BLOB : list_nulls (roaring tree map, portable encoding)
630    /// <nested BitmapIndexState body (self-delimiting)>
631    /// ```
632    fn serialize(&self, w: &mut CacheEntryWriter<'_>) -> Result<()> {
633        let mut nulls_bytes = Vec::with_capacity(self.list_nulls.serialized_size());
634        self.list_nulls.serialize_into(&mut nulls_bytes)?;
635        w.write_raw(&nulls_bytes)?;
636        // The bitmap state writes its own self-delimiting body inline.
637        self.bitmap_state.serialize(w)?;
638        Ok(())
639    }
640
641    fn deserialize(r: &mut CacheEntryReader<'_>) -> Result<Self> {
642        let nulls_bytes = r.read_raw()?;
643        let list_nulls = Arc::new(RowAddrTreeMap::deserialize_from(nulls_bytes.as_ref())?);
644        // The bitmap state is self-delimiting (length-prefixed null map +
645        // Arrow IPC stream with EOS marker); it continues reading the body
646        // from where the null map left off.
647        let bitmap_state = BitmapIndexState::deserialize(r)?;
648        Ok(Self {
649            bitmap_state,
650            list_nulls,
651        })
652    }
653}
654
655struct LabelListIndexStateKey;
656
657impl CacheKey for LabelListIndexStateKey {
658    type ValueType = LabelListIndexState;
659
660    fn key(&self) -> std::borrow::Cow<'_, str> {
661        "state".into()
662    }
663
664    fn type_name() -> &'static str {
665        "LabelListIndexState"
666    }
667
668    fn schema() -> CacheKeySchema {
669        CacheKeySchema::new("lance.scalar.label-list-index-state-key", 1)
670    }
671
672    fn write_key(&self, builder: &mut KeyBuilder) {
673        builder.write_variant(0);
674    }
675
676    fn codec() -> Option<CacheCodec> {
677        Some(CacheCodec::from_impl::<LabelListIndexState>())
678    }
679}
680
681#[derive(Debug, Default)]
682pub struct LabelListIndexPlugin;
683
684pub(super) fn validate_label_list_data_type(data_type: &DataType) -> Result<()> {
685    let item_type = match data_type {
686        DataType::List(item_field) | DataType::LargeList(item_field) => item_field.data_type(),
687        _ => {
688            return Err(Error::invalid_input_source(
689                format!(
690                    "LabelList index can only be created on List or LargeList type columns. Column has type {:?}",
691                    data_type
692                )
693                .into(),
694            ));
695        }
696    };
697
698    if item_type.is_nested() {
699        return Err(Error::invalid_input_source(
700            format!(
701                "LabelList index item type must be non-nested. Column has type {:?}",
702                data_type
703            )
704            .into(),
705        ));
706    }
707
708    Ok(())
709}
710
711#[async_trait]
712impl BasicTrainer for LabelListIndexPlugin {
713    fn new_training_request(
714        &self,
715        _params: &str,
716        field: &Field,
717    ) -> Result<Box<dyn TrainingRequest>> {
718        validate_label_list_data_type(field.data_type())?;
719
720        Ok(Box::new(DefaultTrainingRequest::new(
721            TrainingCriteria::new(TrainingOrdering::None).with_row_id(),
722        )))
723    }
724
725    /// Train a new index
726    ///
727    /// The provided data must fulfill all the criteria returned by `training_criteria`
728    /// and the plugin can rely on this fact.
729    async fn train_index(
730        &self,
731        data: SendableRecordBatchStream,
732        index_store: &dyn IndexStore,
733        _request: Box<dyn TrainingRequest>,
734        // Training over a fragment subset is supported for distributed builds: the
735        // provided `data` stream is already scoped to those fragments, so a partial
736        // index covering exactly those rows is produced. Segments are recombined by
737        // `merge_label_list_indices`.
738        _fragment_ids: Option<Vec<u32>>,
739        _progress: Arc<dyn crate::progress::IndexBuildProgress>,
740    ) -> Result<CreatedIndex> {
741        let schema = data.schema();
742        let field = schema
743            .column_with_name(VALUE_COLUMN_NAME)
744            .ok_or_else(|| {
745                Error::invalid_input_source(
746                    "Index training data missing value column"
747                        .to_string()
748                        .into(),
749                )
750            })?
751            .1;
752
753        validate_label_list_data_type(field.data_type())?;
754
755        let list_nulls = Arc::new(Mutex::new(RowAddrTreeMap::new()));
756        let data = track_list_nulls(data, list_nulls.clone());
757        let data = unnest_chunks(data)?;
758        let (state, value_type) =
759            BitmapIndexPlugin::build_bitmap_index_state(data, HashMap::new()).await?;
760        let list_nulls = list_nulls.lock().unwrap().clone();
761        let file =
762            write_label_list_bitmap_index(state, index_store, &value_type, &list_nulls).await?;
763        Ok(CreatedIndex {
764            index_details: prost_types::Any::from_msg(&pbold::LabelListIndexDetails::default())
765                .unwrap(),
766            index_version: LABEL_LIST_INDEX_VERSION,
767            files: vec![file],
768        })
769    }
770}
771
772#[async_trait]
773impl ScalarIndexPlugin for LabelListIndexPlugin {
774    fn basic_trainer(&self) -> Option<&dyn BasicTrainer> {
775        Some(self)
776    }
777
778    fn name(&self) -> &str {
779        "LabelList"
780    }
781
782    fn provides_exact_answer(&self) -> bool {
783        true
784    }
785
786    fn version(&self) -> u32 {
787        LABEL_LIST_INDEX_VERSION
788    }
789
790    fn new_query_parser(
791        &self,
792        index_name: String,
793        _index_details: &prost_types::Any,
794    ) -> Option<Box<dyn ScalarQueryParser>> {
795        Some(Box::new(LabelListQueryParser::new(
796            index_name,
797            self.name().to_string(),
798        )))
799    }
800
801    /// Load an index from storage
802    async fn load_index(
803        &self,
804        index_store: Arc<dyn IndexStore>,
805        _index_details: &prost_types::Any,
806        frag_reuse_index: Option<Arc<dyn RowIdRemapper>>,
807        cache: &LanceCache,
808    ) -> Result<Arc<dyn ScalarIndex>> {
809        Ok(
810            LabelListIndex::load(index_store, frag_reuse_index, cache).await?
811                as Arc<dyn ScalarIndex>,
812        )
813    }
814
815    async fn get_from_cache(
816        &self,
817        index_store: Arc<dyn IndexStore>,
818        frag_reuse_index: Option<Arc<dyn RowIdRemapper>>,
819        cache: &LanceCache,
820    ) -> Result<Option<Arc<dyn ScalarIndex>>> {
821        let Some(state) = cache.get_with_key(&LabelListIndexStateKey).await else {
822            return Ok(None);
823        };
824        let state = (*state).clone();
825        let index = state.into_label_list_index(index_store, cache, frag_reuse_index)?;
826        Ok(Some(index as Arc<dyn ScalarIndex>))
827    }
828
829    async fn put_in_cache(&self, cache: &LanceCache, index: Arc<dyn ScalarIndex>) -> Result<()> {
830        let state = LabelListIndexState::from_scalar_index(index.as_ref())?;
831        cache
832            .insert_with_key(&LabelListIndexStateKey, Arc::new(state))
833            .await;
834        Ok(())
835    }
836
837    async fn get_or_insert_in_cache(
838        &self,
839        index_store: Arc<dyn IndexStore>,
840        frag_reuse_index: Option<Arc<dyn RowIdRemapper>>,
841        cache: &LanceCache,
842        load: ScalarIndexLoad<'_>,
843    ) -> Result<Arc<dyn ScalarIndex>> {
844        single_flight_open(
845            cache,
846            LabelListIndexStateKey,
847            load,
848            LabelListIndexState::from_scalar_index,
849            move |state| {
850                Ok((*state)
851                    .clone()
852                    .into_label_list_index(index_store, cache, frag_reuse_index)?
853                    as Arc<dyn ScalarIndex>)
854            },
855        )
856        .await
857    }
858}
859
860#[cfg(test)]
861mod tests {
862    use std::collections::BTreeMap;
863
864    use datafusion_common::ScalarValue;
865    use lance_core::cache::CacheCodec;
866    use lance_core::utils::address::RowAddress;
867    use rstest::rstest;
868
869    use super::super::bitmap::BitmapIndexState;
870    use super::super::btree::OrderableScalarValue;
871    use super::*;
872
873    #[rstest]
874    #[case::list(DataType::List(Arc::new(Field::new(
875        "item",
876        DataType::List(Arc::new(Field::new("item", DataType::Int64, true))),
877        true,
878    ))))]
879    #[case::large_list(DataType::LargeList(Arc::new(Field::new(
880        "item",
881        DataType::List(Arc::new(Field::new("item", DataType::Int64, true))),
882        true,
883    ))))]
884    fn test_rejects_nested_item_type(#[case] data_type: DataType) {
885        let field = Field::new(VALUE_COLUMN_NAME, data_type, true);
886        let error = LabelListIndexPlugin
887            .new_training_request("", &field)
888            .err()
889            .expect("nested item type should be rejected");
890
891        assert!(
892            matches!(error, Error::InvalidInput { .. }),
893            "expected invalid input error, got: {error}"
894        );
895        assert!(
896            error
897                .to_string()
898                .contains("LabelList index item type must be non-nested"),
899            "unexpected error: {error}"
900        );
901    }
902
903    fn sample_state() -> LabelListIndexState {
904        let mut index_map = BTreeMap::new();
905        for k in 0..32i32 {
906            index_map.insert(
907                OrderableScalarValue(ScalarValue::Int32(Some(k))),
908                k as usize,
909            );
910        }
911        let mut bitmap_nulls = RowAddrTreeMap::new();
912        bitmap_nulls.insert(RowAddress::new_from_parts(0, 3).into());
913        let bitmap_state =
914            BitmapIndexState::new_for_test(index_map, bitmap_nulls, DataType::Int32).unwrap();
915
916        let mut list_nulls = RowAddrTreeMap::new();
917        list_nulls.insert(RowAddress::new_from_parts(0, 9).into());
918        LabelListIndexState {
919            bitmap_state,
920            list_nulls: Arc::new(list_nulls),
921        }
922    }
923
924    #[test]
925    fn test_label_list_state_codec_roundtrip() {
926        let state = sample_state();
927        let mut buf = Vec::new();
928        state
929            .serialize(&mut CacheEntryWriter::new(&mut buf))
930            .unwrap();
931        let data = Bytes::from(buf);
932        let mut reader = CacheEntryReader::new(&data, 0, LabelListIndexState::CURRENT_VERSION);
933        let restored = LabelListIndexState::deserialize(&mut reader).unwrap();
934
935        assert_eq!(&*restored.list_nulls, &*state.list_nulls);
936        assert_eq!(
937            restored.bitmap_state.lookup_batch(),
938            state.bitmap_state.lookup_batch()
939        );
940        assert_eq!(
941            restored.bitmap_state.null_map(),
942            state.bitmap_state.null_map()
943        );
944    }
945
946    /// The nested bitmap lookup batch must decode zero-copy through the full
947    /// envelope, proving the leading `list_nulls` RAW_BLOB does not knock the
948    /// nested IPC section off its 64-byte boundary.
949    #[test]
950    fn test_label_list_nested_lookup_is_zero_copy() {
951        const ALIGN: usize = 64;
952        let codec = CacheCodec::from_impl::<LabelListIndexState>();
953        let any: Arc<dyn std::any::Any + Send + Sync> = Arc::new(sample_state());
954        let mut buf = Vec::new();
955        codec.serialize(&any, &mut buf).unwrap();
956
957        let mut v = vec![0u8; buf.len() + ALIGN];
958        let pad = (ALIGN - (v.as_ptr() as usize % ALIGN)) % ALIGN;
959        v[pad..pad + buf.len()].copy_from_slice(&buf);
960        let data = Bytes::from(v).slice(pad..pad + buf.len());
961
962        let restored = codec.deserialize(&data).hit().unwrap();
963        let restored = restored.downcast::<LabelListIndexState>().unwrap();
964
965        let base = data.as_ptr() as usize;
966        let end = base + data.len();
967        for col in restored.bitmap_state.lookup_batch().columns() {
968            for buffer in col.to_data().buffers() {
969                let ptr = buffer.as_ptr() as usize;
970                assert!(
971                    ptr >= base && ptr < end,
972                    "nested bitmap lookup buffer was realigned — misaligned IPC section",
973                );
974            }
975        }
976    }
977}