Skip to main content

lance_index/
scalar.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright The Lance Authors
3
4//! Scalar indices for metadata search & filtering
5
6use arrow::buffer::{OffsetBuffer, ScalarBuffer};
7use arrow_array::{BooleanArray, ListArray, RecordBatch, UInt64Array};
8use arrow_schema::{Field, Schema};
9use async_trait::async_trait;
10use bytes::Bytes;
11use datafusion::functions::regex::regexplike::RegexpLikeFunc;
12use datafusion::functions::string::contains::ContainsFunc;
13use datafusion::functions_nested::array_has;
14use datafusion::physical_plan::SendableRecordBatchStream;
15use datafusion_common::{Column, scalar::ScalarValue};
16use std::collections::{HashMap, HashSet};
17use std::fmt::Debug;
18use std::pin::Pin;
19use std::{any::Any, ops::Bound, sync::Arc};
20
21use datafusion_expr::{Expr, expr::ScalarFunction};
22use inverted::query::{FtsQuery, FtsQueryNode, FtsSearchParams, MatchQuery, fill_fts_query_column};
23use lance_core::deepsize::DeepSizeOf;
24use lance_core::{Error, Result};
25use lance_io::stream::{RecordBatchStream, RecordBatchStreamAdapter};
26use lance_select::{NullableRowAddrSet, RowAddrTreeMap, RowSetOps};
27use roaring::RoaringBitmap;
28use serde::Serialize;
29
30use crate::metrics::MetricsCollector;
31use crate::scalar::registry::TrainingCriteria;
32use crate::{Index, IndexParams, IndexType};
33pub use lance_table::format::IndexFile;
34
35pub mod bitmap;
36pub mod bloomfilter;
37pub mod btree;
38pub mod expression;
39pub mod fmindex;
40pub mod inverted;
41pub mod json;
42pub mod label_list;
43pub mod lance_format;
44pub mod ngram;
45pub mod registry;
46#[cfg(feature = "geo")]
47pub mod rtree;
48pub mod zoned;
49pub mod zonemap;
50
51use crate::frag_reuse::FragReuseIndex;
52pub use inverted::tokenizer::InvertedIndexParams;
53use lance_datafusion::udf::CONTAINS_TOKENS_UDF;
54
55pub const LANCE_SCALAR_INDEX: &str = "__lance_scalar_index";
56
57/// Builtin index types supported by the Lance library
58///
59/// This is primarily for convenience to avoid a bunch of string
60/// constants and provide some auto-complete.  This type should not
61/// be used in the manifest as plugins cannot add new entries.
62#[derive(Debug, Clone, PartialEq, Eq, DeepSizeOf)]
63pub enum BuiltinIndexType {
64    BTree,
65    Bitmap,
66    LabelList,
67    NGram,
68    ZoneMap,
69    BloomFilter,
70    RTree,
71    Inverted,
72    Fm,
73}
74
75impl BuiltinIndexType {
76    pub fn as_str(&self) -> &str {
77        match self {
78            Self::BTree => "btree",
79            Self::Bitmap => "bitmap",
80            Self::LabelList => "labellist",
81            Self::NGram => "ngram",
82            Self::ZoneMap => "zonemap",
83            Self::Inverted => "inverted",
84            Self::BloomFilter => "bloomfilter",
85            Self::RTree => "rtree",
86            Self::Fm => "fm",
87        }
88    }
89}
90
91impl TryFrom<IndexType> for BuiltinIndexType {
92    type Error = Error;
93
94    fn try_from(value: IndexType) -> Result<Self> {
95        match value {
96            IndexType::BTree => Ok(Self::BTree),
97            IndexType::Bitmap => Ok(Self::Bitmap),
98            IndexType::LabelList => Ok(Self::LabelList),
99            IndexType::NGram => Ok(Self::NGram),
100            IndexType::ZoneMap => Ok(Self::ZoneMap),
101            IndexType::Inverted => Ok(Self::Inverted),
102            IndexType::BloomFilter => Ok(Self::BloomFilter),
103            IndexType::RTree => Ok(Self::RTree),
104            IndexType::Fm => Ok(Self::Fm),
105            _ => Err(Error::index("Invalid index type".to_string())),
106        }
107    }
108}
109
110#[derive(Debug, Clone, PartialEq)]
111pub struct ScalarIndexParams {
112    /// The type of index to create
113    ///
114    /// Plugins may add additional index types.  Index type lookup is case-insensitive.
115    pub index_type: String,
116    /// The parameters to train the index
117    ///
118    /// This should be a JSON string.  The contents of the JSON string will be specific to the
119    /// index type.  If not set, then default parameters will be used for the index type.
120    pub params: Option<String>,
121}
122
123impl Default for ScalarIndexParams {
124    fn default() -> Self {
125        Self {
126            index_type: BuiltinIndexType::BTree.as_str().to_string(),
127            params: None,
128        }
129    }
130}
131
132impl ScalarIndexParams {
133    /// Creates a new ScalarIndexParams from one of the builtin index types
134    pub fn for_builtin(index_type: BuiltinIndexType) -> Self {
135        Self {
136            index_type: index_type.as_str().to_string(),
137            params: None,
138        }
139    }
140
141    /// Create a new ScalarIndexParams with the given index type
142    pub fn new(index_type: String) -> Self {
143        Self {
144            index_type,
145            params: None,
146        }
147    }
148
149    /// Set the parameters for the index
150    pub fn with_params<ParamsType: Serialize>(mut self, params: &ParamsType) -> Self {
151        self.params = Some(serde_json::to_string(params).unwrap());
152        self
153    }
154}
155
156impl IndexParams for ScalarIndexParams {
157    fn as_any(&self) -> &dyn std::any::Any {
158        self
159    }
160
161    fn index_name(&self) -> &str {
162        LANCE_SCALAR_INDEX
163    }
164}
165
166impl IndexParams for InvertedIndexParams {
167    fn as_any(&self) -> &dyn std::any::Any {
168        self
169    }
170
171    fn index_name(&self) -> &str {
172        "INVERTED"
173    }
174}
175
176/// Trait for storing an index (or parts of an index) into storage
177#[async_trait]
178pub trait IndexWriter: Send {
179    /// Writes a record batch into the file, returning the 0-based index of the batch in the file
180    ///
181    /// E.g. if this is the third time this is called this method will return 2
182    async fn write_record_batch(&mut self, batch: RecordBatch) -> Result<u64>;
183    /// Adds a global buffer and returns its index.
184    async fn add_global_buffer(&mut self, _data: Bytes) -> Result<u32> {
185        Err(Error::not_supported(
186            "global buffers are not supported by this index writer",
187        ))
188    }
189    /// Finishes writing the file and closes the file
190    async fn finish(&mut self) -> Result<IndexFile>;
191    /// Finishes writing the file and closes the file with additional metadata
192    async fn finish_with_metadata(
193        &mut self,
194        metadata: HashMap<String, String>,
195    ) -> Result<IndexFile>;
196}
197
198/// Trait for reading an index (or parts of an index) from storage
199#[async_trait]
200pub trait IndexReader: Send + Sync {
201    /// Read the n-th record batch from the file
202    async fn read_record_batch(&self, n: u64, batch_size: u64) -> Result<RecordBatch>;
203    /// Reads a global buffer by index.
204    async fn read_global_buffer(&self, _index: u32) -> Result<Bytes> {
205        Err(Error::not_supported(
206            "global buffers are not supported by this index reader",
207        ))
208    }
209    /// Read the range of rows from the file.
210    /// If projection is Some, only return the columns in the projection,
211    /// nested columns like Some(&["x.y"]) are not supported.
212    /// If projection is None, return all columns.
213    async fn read_range(
214        &self,
215        range: std::ops::Range<usize>,
216        projection: Option<&[&str]>,
217    ) -> Result<RecordBatch>;
218    /// Read multiple ranges and concatenate into a single batch.
219    /// Default impl runs `read_range`s in parallel via `try_join_all`.
220    async fn read_ranges(
221        &self,
222        ranges: &[std::ops::Range<usize>],
223        projection: Option<&[&str]>,
224    ) -> Result<RecordBatch> {
225        if ranges.is_empty() {
226            return self.read_range(0..0, projection).await;
227        }
228        let futures = ranges
229            .iter()
230            .map(|r| self.read_range(r.clone(), projection));
231        let batches = futures::future::try_join_all(futures).await?;
232        let schema = batches[0].schema();
233        Ok(arrow_select::concat::concat_batches(&schema, &batches)?)
234    }
235    /// Read a range of rows as a stream of record batches.
236    ///
237    /// This allows the caller to process rows incrementally without loading the
238    /// entire range into memory at once.
239    ///
240    /// The default implementation falls back to [`Self::read_range`] and wraps
241    /// the result in a single-item stream.
242    async fn read_range_stream(
243        &self,
244        range: std::ops::Range<usize>,
245        projection: Option<&[&str]>,
246    ) -> Result<Pin<Box<dyn RecordBatchStream>>> {
247        let batch = self.read_range(range, projection).await?;
248        let schema = batch.schema();
249        Ok(Box::pin(RecordBatchStreamAdapter::new(
250            schema,
251            futures::stream::once(async move { Ok(batch) }),
252        )))
253    }
254    /// Return the number of batches in the file
255    async fn num_batches(&self, batch_size: u64) -> u32;
256    /// Return the number of rows in the file
257    fn num_rows(&self) -> usize;
258    /// Return the metadata of the file
259    fn schema(&self) -> &lance_core::datatypes::Schema;
260    /// Best-effort on-disk byte size of the file when the reader already knows it
261    /// without extra I/O, else `None`. Used to size prewarm chunks.
262    fn file_size_bytes(&self) -> Option<u64> {
263        None
264    }
265}
266
267/// Trait abstracting I/O away from index logic
268///
269/// Scalar indices are currently serialized as indexable arrow record batches stored in
270/// named "files".  The index store is responsible for serializing and deserializing
271/// these batches into file data (e.g. as .lance files or .parquet files, etc.)
272#[async_trait]
273pub trait IndexStore: std::fmt::Debug + Send + Sync + DeepSizeOf {
274    fn as_any(&self) -> &dyn Any;
275    fn clone_arc(&self) -> Arc<dyn IndexStore>;
276
277    /// Suggested I/O parallelism for the store
278    fn io_parallelism(&self) -> usize;
279
280    /// Create a new file and return a writer to store data in the file
281    async fn new_index_file(&self, name: &str, schema: Arc<Schema>)
282    -> Result<Box<dyn IndexWriter>>;
283
284    /// Open an existing file for retrieval
285    async fn open_index_file(&self, name: &str) -> Result<Arc<dyn IndexReader>>;
286
287    /// Copy a range of batches from an index file from this store to another
288    ///
289    /// This is often useful when remapping or updating
290    async fn copy_index_file(&self, name: &str, dest_store: &dyn IndexStore) -> Result<IndexFile>;
291
292    /// Copy an index file from this store to a new name in another store, leaving the source intact
293    async fn copy_index_file_to(
294        &self,
295        name: &str,
296        new_name: &str,
297        dest_store: &dyn IndexStore,
298    ) -> Result<IndexFile> {
299        if name == new_name {
300            self.copy_index_file(name, dest_store).await
301        } else {
302            Err(Error::not_supported(format!(
303                "copying index file {name} to {new_name} is not supported by this index store"
304            )))
305        }
306    }
307
308    /// Rename an index file
309    async fn rename_index_file(&self, name: &str, new_name: &str) -> Result<IndexFile>;
310
311    /// Delete an index file (used in the tmp spill store to keep tmp size down)
312    async fn delete_index_file(&self, name: &str) -> Result<()>;
313
314    /// List all files in the index directory with their sizes.
315    ///
316    /// Returns a list of (relative_path, size_bytes) tuples.
317    /// Used to capture file metadata after index creation/modification.
318    async fn list_files_with_sizes(&self) -> Result<Vec<IndexFile>>;
319}
320
321/// Different scalar indices may support different kinds of queries
322///
323/// For example, a btree index can support a wide range of queries (e.g. x > 7)
324/// while an index based on FTS only supports queries like "x LIKE 'foo'"
325///
326/// This trait is used when we need an object that can represent any kind of query
327///
328/// Note: if you are implementing this trait for a query type then you probably also
329/// need to implement the [crate::scalar::expression::ScalarQueryParser] trait to
330/// create instances of your query at parse time.
331pub trait AnyQuery: std::fmt::Debug + Any + Send + Sync {
332    /// Cast the query as Any to allow for downcasting
333    fn as_any(&self) -> &dyn Any;
334    /// Format the query as a string for display purposes
335    fn format(&self, col: &str) -> String;
336    /// Convert the query to a datafusion expression
337    fn to_expr(&self, col: String) -> Expr;
338    /// Compare this query to another query
339    fn dyn_eq(&self, other: &dyn AnyQuery) -> bool;
340}
341
342impl PartialEq for dyn AnyQuery {
343    fn eq(&self, other: &Self) -> bool {
344        self.dyn_eq(other)
345    }
346}
347/// A full text search query
348#[derive(Debug, Clone, PartialEq)]
349pub struct FullTextSearchQuery {
350    pub query: FtsQuery,
351
352    /// The maximum number of results to return
353    pub limit: Option<i64>,
354
355    /// The wand factor to use for ranking
356    /// if None, use the default value of 1.0
357    /// Increasing this value will reduce the recall and improve the performance
358    /// 1.0 is the value that would give the best performance without recall loss
359    pub wand_factor: Option<f32>,
360}
361
362impl FullTextSearchQuery {
363    /// Create a new terms query
364    pub fn new(query: String) -> Self {
365        let query = MatchQuery::new(query).into();
366        Self {
367            query,
368            limit: None,
369            wand_factor: None,
370        }
371    }
372
373    /// Create a new fuzzy query
374    pub fn new_fuzzy(term: String, max_distance: Option<u32>) -> Self {
375        let query = MatchQuery::new(term).with_fuzziness(max_distance).into();
376        Self {
377            query,
378            limit: None,
379            wand_factor: None,
380        }
381    }
382
383    /// Create a new compound query
384    pub fn new_query(query: FtsQuery) -> Self {
385        Self {
386            query,
387            limit: None,
388            wand_factor: None,
389        }
390    }
391
392    /// Set the column to search over
393    /// This is available for only MatchQuery and PhraseQuery
394    pub fn with_column(mut self, column: String) -> Result<Self> {
395        self.query = fill_fts_query_column(&self.query, &[column], true)?;
396        Ok(self)
397    }
398
399    /// Set the column to search over
400    /// This is available for only MatchQuery
401    pub fn with_columns(mut self, columns: &[String]) -> Result<Self> {
402        self.query = fill_fts_query_column(&self.query, columns, true)?;
403        Ok(self)
404    }
405
406    /// limit the number of results to return
407    /// if None, return all results
408    pub fn limit(mut self, limit: Option<i64>) -> Self {
409        self.limit = limit;
410        self
411    }
412
413    pub fn wand_factor(mut self, wand_factor: Option<f32>) -> Self {
414        self.wand_factor = wand_factor;
415        self
416    }
417
418    pub fn columns(&self) -> HashSet<String> {
419        self.query.columns()
420    }
421
422    pub fn params(&self) -> FtsSearchParams {
423        FtsSearchParams::new()
424            .with_limit(self.limit.map(|limit| limit as usize))
425            .with_wand_factor(self.wand_factor.unwrap_or(1.0))
426    }
427}
428
429/// A query that a basic scalar index (e.g. btree / bitmap) can satisfy
430///
431/// This is a subset of expression operators that is often referred to as the
432/// "sargable" operators
433///
434/// Note that negation is not included.  Negation should be applied later.  For
435/// example, to invert an equality query (e.g. all rows where the value is not 7)
436/// you can grab all rows where the value = 7 and then do an inverted take (or use
437/// a block list instead of an allow list for prefiltering)
438#[derive(Debug, Clone, PartialEq)]
439pub enum SargableQuery {
440    /// Retrieve all row ids where the value is in the given [min, max) range
441    Range(Bound<ScalarValue>, Bound<ScalarValue>),
442    /// Retrieve all row ids where the value is in the given set of values
443    IsIn(Vec<ScalarValue>),
444    /// Retrieve all row ids where the value is exactly the given value
445    Equals(ScalarValue),
446    /// Retrieve all row ids where the value matches the given full text search query
447    FullTextSearch(FullTextSearchQuery),
448    /// Retrieve all row ids where the value is null
449    IsNull(),
450    /// Retrieve all row ids where the value matches LIKE 'prefix%' pattern
451    /// This is used for both explicit LIKE expressions and starts_with() function calls
452    LikePrefix(ScalarValue),
453}
454
455impl AnyQuery for SargableQuery {
456    fn as_any(&self) -> &dyn Any {
457        self
458    }
459
460    fn format(&self, col: &str) -> String {
461        match self {
462            Self::Range(lower, upper) => match (lower, upper) {
463                (Bound::Unbounded, Bound::Unbounded) => "true".to_string(),
464                (Bound::Unbounded, Bound::Included(rhs)) => format!("{} <= {}", col, rhs),
465                (Bound::Unbounded, Bound::Excluded(rhs)) => format!("{} < {}", col, rhs),
466                (Bound::Included(lhs), Bound::Unbounded) => format!("{} >= {}", col, lhs),
467                (Bound::Included(lhs), Bound::Included(rhs)) => {
468                    format!("{} >= {} && {} <= {}", col, lhs, col, rhs)
469                }
470                (Bound::Included(lhs), Bound::Excluded(rhs)) => {
471                    format!("{} >= {} && {} < {}", col, lhs, col, rhs)
472                }
473                (Bound::Excluded(lhs), Bound::Unbounded) => format!("{} > {}", col, lhs),
474                (Bound::Excluded(lhs), Bound::Included(rhs)) => {
475                    format!("{} > {} && {} <= {}", col, lhs, col, rhs)
476                }
477                (Bound::Excluded(lhs), Bound::Excluded(rhs)) => {
478                    format!("{} > {} && {} < {}", col, lhs, col, rhs)
479                }
480            },
481            Self::IsIn(values) => {
482                format!(
483                    "{} IN [{}]",
484                    col,
485                    values
486                        .iter()
487                        .map(|val| val.to_string())
488                        .collect::<Vec<_>>()
489                        .join(",")
490                )
491            }
492            Self::FullTextSearch(query) => {
493                format!("fts({})", query.query)
494            }
495            Self::IsNull() => {
496                format!("{} IS NULL", col)
497            }
498            Self::Equals(val) => {
499                format!("{} = {}", col, val)
500            }
501            Self::LikePrefix(prefix) => {
502                format!("{} LIKE '{}%'", col, prefix)
503            }
504        }
505    }
506
507    fn to_expr(&self, col: String) -> Expr {
508        let col_expr = Expr::Column(Column::new_unqualified(col));
509        match self {
510            Self::Range(lower, upper) => match (lower, upper) {
511                (Bound::Unbounded, Bound::Unbounded) => {
512                    Expr::Literal(ScalarValue::Boolean(Some(true)), None)
513                }
514                (Bound::Unbounded, Bound::Included(rhs)) => {
515                    col_expr.lt_eq(Expr::Literal(rhs.clone(), None))
516                }
517                (Bound::Unbounded, Bound::Excluded(rhs)) => {
518                    col_expr.lt(Expr::Literal(rhs.clone(), None))
519                }
520                (Bound::Included(lhs), Bound::Unbounded) => {
521                    col_expr.gt_eq(Expr::Literal(lhs.clone(), None))
522                }
523                (Bound::Included(lhs), Bound::Included(rhs)) => col_expr.between(
524                    Expr::Literal(lhs.clone(), None),
525                    Expr::Literal(rhs.clone(), None),
526                ),
527                (Bound::Included(lhs), Bound::Excluded(rhs)) => col_expr
528                    .clone()
529                    .gt_eq(Expr::Literal(lhs.clone(), None))
530                    .and(col_expr.lt(Expr::Literal(rhs.clone(), None))),
531                (Bound::Excluded(lhs), Bound::Unbounded) => {
532                    col_expr.gt(Expr::Literal(lhs.clone(), None))
533                }
534                (Bound::Excluded(lhs), Bound::Included(rhs)) => col_expr
535                    .clone()
536                    .gt(Expr::Literal(lhs.clone(), None))
537                    .and(col_expr.lt_eq(Expr::Literal(rhs.clone(), None))),
538                (Bound::Excluded(lhs), Bound::Excluded(rhs)) => col_expr
539                    .clone()
540                    .gt(Expr::Literal(lhs.clone(), None))
541                    .and(col_expr.lt(Expr::Literal(rhs.clone(), None))),
542            },
543            Self::IsIn(values) => col_expr.in_list(
544                values
545                    .iter()
546                    .map(|val| Expr::Literal(val.clone(), None))
547                    .collect::<Vec<_>>(),
548                false,
549            ),
550            Self::FullTextSearch(query) => col_expr.like(Expr::Literal(
551                ScalarValue::Utf8(Some(query.query.to_string())),
552                None,
553            )),
554            Self::IsNull() => col_expr.is_null(),
555            Self::Equals(value) => col_expr.eq(Expr::Literal(value.clone(), None)),
556            Self::LikePrefix(prefix) => {
557                let pattern = match prefix {
558                    ScalarValue::Utf8(Some(s)) => ScalarValue::Utf8(Some(format!("{}%", s))),
559                    ScalarValue::LargeUtf8(Some(s)) => {
560                        ScalarValue::LargeUtf8(Some(format!("{}%", s)))
561                    }
562                    other => other.clone(),
563                };
564                col_expr.like(Expr::Literal(pattern, None))
565            }
566        }
567    }
568
569    fn dyn_eq(&self, other: &dyn AnyQuery) -> bool {
570        match other.as_any().downcast_ref::<Self>() {
571            Some(o) => self == o,
572            None => false,
573        }
574    }
575}
576
577/// A query that a LabelListIndex can satisfy
578#[derive(Debug, Clone, PartialEq)]
579pub enum LabelListQuery {
580    /// Retrieve all row ids where every label is in the list of values for the row
581    HasAllLabels(Vec<ScalarValue>),
582    /// Retrieve all row ids where at least one of the given labels is in the list of values for the row
583    HasAnyLabel(Vec<ScalarValue>),
584}
585
586impl AnyQuery for LabelListQuery {
587    fn as_any(&self) -> &dyn Any {
588        self
589    }
590
591    fn format(&self, col: &str) -> String {
592        format!("{}", self.to_expr(col.to_string()))
593    }
594
595    fn to_expr(&self, col: String) -> Expr {
596        match self {
597            Self::HasAllLabels(labels) => {
598                let labels_arr = ScalarValue::iter_to_array(labels.iter().cloned()).unwrap();
599                let offsets_buffer =
600                    OffsetBuffer::new(ScalarBuffer::<i32>::from(vec![0, labels_arr.len() as i32]));
601                let labels_list = ListArray::try_new(
602                    Arc::new(Field::new("item", labels_arr.data_type().clone(), true)),
603                    offsets_buffer,
604                    labels_arr,
605                    None,
606                )
607                .unwrap();
608                let labels_arr = Arc::new(labels_list);
609                Expr::ScalarFunction(ScalarFunction {
610                    func: Arc::new(array_has::ArrayHasAll::new().into()),
611                    args: vec![
612                        Expr::Column(Column::new_unqualified(col)),
613                        Expr::Literal(ScalarValue::List(labels_arr), None),
614                    ],
615                })
616            }
617            Self::HasAnyLabel(labels) => {
618                let labels_arr = ScalarValue::iter_to_array(labels.iter().cloned()).unwrap();
619                let offsets_buffer =
620                    OffsetBuffer::new(ScalarBuffer::<i32>::from(vec![0, labels_arr.len() as i32]));
621                let labels_list = ListArray::try_new(
622                    Arc::new(Field::new("item", labels_arr.data_type().clone(), true)),
623                    offsets_buffer,
624                    labels_arr,
625                    None,
626                )
627                .unwrap();
628                let labels_arr = Arc::new(labels_list);
629                Expr::ScalarFunction(ScalarFunction {
630                    func: Arc::new(array_has::ArrayHasAny::new().into()),
631                    args: vec![
632                        Expr::Column(Column::new_unqualified(col)),
633                        Expr::Literal(ScalarValue::List(labels_arr), None),
634                    ],
635                })
636            }
637        }
638    }
639
640    fn dyn_eq(&self, other: &dyn AnyQuery) -> bool {
641        match other.as_any().downcast_ref::<Self>() {
642            Some(o) => self == o,
643            None => false,
644        }
645    }
646}
647
648/// A query that a NGramIndex can satisfy
649#[derive(Debug, Clone, PartialEq)]
650pub enum TextQuery {
651    /// Retrieve all row ids where the text contains the given string
652    StringContains(String),
653    /// Retrieve all row ids whose text matches the given regular expression.
654    ///
655    /// The pattern is a full regular expression (as accepted by `regexp_like`).
656    /// The index returns a candidate superset that the scan rechecks, so any
657    /// pattern is sound; patterns with no usable trigram structure simply fall
658    /// back to rechecking every row.
659    Regex(String),
660    // TODO: In the future we should be able to do case-insensitive contains
661    // as well as partial matches (e.g. LIKE 'foo%').
662}
663
664impl AnyQuery for TextQuery {
665    fn as_any(&self) -> &dyn Any {
666        self
667    }
668
669    fn format(&self, col: &str) -> String {
670        format!("{}", self.to_expr(col.to_string()))
671    }
672
673    fn to_expr(&self, col: String) -> Expr {
674        match self {
675            Self::StringContains(substr) => Expr::ScalarFunction(ScalarFunction {
676                func: Arc::new(ContainsFunc::new().into()),
677                args: vec![
678                    Expr::Column(Column::new_unqualified(col)),
679                    Expr::Literal(ScalarValue::Utf8(Some(substr.clone())), None),
680                ],
681            }),
682            // `regexp_like` returns Boolean directly, so the reconstructed
683            // expression can be used as-is for the recheck filter (no IsNotNull
684            // wrapper, unlike `regexp_match`). It is the semantic equivalent of
685            // the original predicate for the "does it match" question.
686            Self::Regex(pattern) => Expr::ScalarFunction(ScalarFunction {
687                func: Arc::new(RegexpLikeFunc::new().into()),
688                args: vec![
689                    Expr::Column(Column::new_unqualified(col)),
690                    Expr::Literal(ScalarValue::Utf8(Some(pattern.clone())), None),
691                ],
692            }),
693        }
694    }
695
696    fn dyn_eq(&self, other: &dyn AnyQuery) -> bool {
697        match other.as_any().downcast_ref::<Self>() {
698            Some(o) => self == o,
699            None => false,
700        }
701    }
702}
703
704/// A query that a InvertedIndex can satisfy
705#[derive(Debug, Clone, PartialEq)]
706pub enum TokenQuery {
707    /// Retrieve all row ids where the text contains all tokens parsed from given string. The tokens
708    /// are separated by punctuations and white spaces.
709    TokensContains(String),
710}
711
712/// A query that a BloomFilter index can satisfy
713///
714/// This is a subset of SargableQuery that only includes operations that bloom filters
715/// can efficiently handle: equals, is_null, and is_in queries.
716#[derive(Debug, Clone, PartialEq)]
717pub enum BloomFilterQuery {
718    /// Retrieve all row ids where the value is exactly the given value
719    Equals(ScalarValue),
720    /// Retrieve all row ids where the value is null
721    IsNull(),
722    /// Retrieve all row ids where the value is in the given set of values
723    IsIn(Vec<ScalarValue>),
724}
725
726impl AnyQuery for BloomFilterQuery {
727    fn as_any(&self) -> &dyn Any {
728        self
729    }
730
731    fn format(&self, col: &str) -> String {
732        match self {
733            Self::Equals(val) => {
734                format!("{} = {}", col, val)
735            }
736            Self::IsNull() => {
737                format!("{} IS NULL", col)
738            }
739            Self::IsIn(values) => {
740                format!(
741                    "{} IN [{}]",
742                    col,
743                    values
744                        .iter()
745                        .map(|val| val.to_string())
746                        .collect::<Vec<_>>()
747                        .join(",")
748                )
749            }
750        }
751    }
752
753    fn to_expr(&self, col: String) -> Expr {
754        let col_expr = Expr::Column(Column::new_unqualified(col));
755        match self {
756            Self::Equals(value) => col_expr.eq(Expr::Literal(value.clone(), None)),
757            Self::IsNull() => col_expr.is_null(),
758            Self::IsIn(values) => col_expr.in_list(
759                values
760                    .iter()
761                    .map(|val| Expr::Literal(val.clone(), None))
762                    .collect::<Vec<_>>(),
763                false,
764            ),
765        }
766    }
767
768    fn dyn_eq(&self, other: &dyn AnyQuery) -> bool {
769        match other.as_any().downcast_ref::<Self>() {
770            Some(o) => self == o,
771            None => false,
772        }
773    }
774}
775
776impl AnyQuery for TokenQuery {
777    fn as_any(&self) -> &dyn Any {
778        self
779    }
780
781    fn format(&self, col: &str) -> String {
782        format!("{}", self.to_expr(col.to_string()))
783    }
784
785    fn to_expr(&self, col: String) -> Expr {
786        match self {
787            Self::TokensContains(substr) => Expr::ScalarFunction(ScalarFunction {
788                func: Arc::new(CONTAINS_TOKENS_UDF.clone()),
789                args: vec![
790                    Expr::Column(Column::new_unqualified(col)),
791                    Expr::Literal(ScalarValue::Utf8(Some(substr.clone())), None),
792                ],
793            }),
794        }
795    }
796
797    fn dyn_eq(&self, other: &dyn AnyQuery) -> bool {
798        match other.as_any().downcast_ref::<Self>() {
799            Some(o) => self == o,
800            None => false,
801        }
802    }
803}
804
805#[cfg(feature = "geo")]
806#[derive(Debug, Clone, PartialEq)]
807pub struct RelationQuery {
808    pub value: ScalarValue,
809    pub field: Field,
810}
811
812/// A query that a Geo index can satisfy
813#[cfg(feature = "geo")]
814#[derive(Debug, Clone, PartialEq)]
815pub enum GeoQuery {
816    IntersectQuery(RelationQuery),
817    IsNull,
818}
819
820#[cfg(feature = "geo")]
821impl AnyQuery for GeoQuery {
822    fn as_any(&self) -> &dyn Any {
823        self
824    }
825
826    fn format(&self, col: &str) -> String {
827        match self {
828            Self::IntersectQuery(query) => {
829                format!("Intersect({} {})", col, query.value)
830            }
831            Self::IsNull => {
832                format!("{} IS NULL", col)
833            }
834        }
835    }
836
837    fn to_expr(&self, _col: String) -> Expr {
838        todo!()
839    }
840
841    fn dyn_eq(&self, other: &dyn AnyQuery) -> bool {
842        match other.as_any().downcast_ref::<Self>() {
843            Some(o) => self == o,
844            None => false,
845        }
846    }
847}
848
849/// The result of a search operation against a scalar index
850#[derive(Debug, PartialEq)]
851pub enum SearchResult {
852    /// The exact row ids that satisfy the query
853    Exact(NullableRowAddrSet),
854    /// Any row id satisfying the query will be in this set but not every
855    /// row id in this set will satisfy the query, a further recheck step
856    /// is needed
857    AtMost(NullableRowAddrSet),
858    /// All of the given row ids satisfy the query but there may be more
859    ///
860    /// No scalar index actually returns this today but it can arise from
861    /// boolean operations (e.g. NOT(AtMost(x)) == AtLeast(NOT(x)))
862    AtLeast(NullableRowAddrSet),
863}
864
865impl SearchResult {
866    pub fn exact(row_ids: impl Into<RowAddrTreeMap>) -> Self {
867        Self::Exact(NullableRowAddrSet::new(row_ids.into(), Default::default()))
868    }
869
870    pub fn at_most(row_ids: impl Into<RowAddrTreeMap>) -> Self {
871        Self::AtMost(NullableRowAddrSet::new(row_ids.into(), Default::default()))
872    }
873
874    pub fn at_least(row_ids: impl Into<RowAddrTreeMap>) -> Self {
875        Self::AtLeast(NullableRowAddrSet::new(row_ids.into(), Default::default()))
876    }
877
878    pub fn with_nulls(self, nulls: impl Into<RowAddrTreeMap>) -> Self {
879        match self {
880            Self::Exact(row_ids) => Self::Exact(row_ids.with_nulls(nulls.into())),
881            Self::AtMost(row_ids) => Self::AtMost(row_ids.with_nulls(nulls.into())),
882            Self::AtLeast(row_ids) => Self::AtLeast(row_ids.with_nulls(nulls.into())),
883        }
884    }
885
886    pub fn row_addrs(&self) -> &NullableRowAddrSet {
887        match self {
888            Self::Exact(row_addrs) => row_addrs,
889            Self::AtMost(row_addrs) => row_addrs,
890            Self::AtLeast(row_addrs) => row_addrs,
891        }
892    }
893
894    pub fn is_exact(&self) -> bool {
895        matches!(self, Self::Exact(_))
896    }
897}
898
899/// Brief information about an index that was created
900pub struct CreatedIndex {
901    /// The details of the index that was created
902    ///
903    /// These should be stored somewhere as they will be needed to
904    /// load the index later.
905    pub index_details: prost_types::Any,
906    /// The version of the index that was created
907    ///
908    /// This can be used to determine if a reader is able to load the index.
909    pub index_version: u32,
910    /// List of files and their sizes for this index
911    ///
912    /// This enables skipping HEAD calls when opening indices and provides
913    /// visibility into index storage size via describe_indices().
914    pub files: Vec<IndexFile>,
915}
916
917/// The criteria that specifies how to update an index
918pub struct UpdateCriteria {
919    /// If true, then we need to read the old data to update the index
920    ///
921    /// This should be avoided if possible but is left in for some legacy paths
922    pub requires_old_data: bool,
923    /// The criteria required for data (both old and new)
924    pub data_criteria: TrainingCriteria,
925}
926
927/// Filter used when merging existing scalar-index rows during update.
928///
929/// The caller must pick a filter mode that matches the row-id semantics of the
930/// dataset:
931/// - address-style row IDs: fragment filtering is valid
932/// - stable row IDs: use exact row-id membership instead
933#[derive(Debug, Clone)]
934pub enum OldIndexDataFilter {
935    /// Keeps track of which fragments are still valid and which are no longer valid.
936    ///
937    /// This is valid for address-style row IDs.
938    Fragments {
939        to_keep: RoaringBitmap,
940        to_remove: RoaringBitmap,
941    },
942    /// Keep old rows whose row IDs are in this exact allow-list.
943    ///
944    /// This is required for stable row IDs, where row IDs are opaque and
945    /// should not be interpreted as encoded row addresses.
946    RowIds(RowAddrTreeMap),
947}
948
949impl OldIndexDataFilter {
950    /// Build a boolean mask that keeps only row IDs selected by this filter.
951    pub fn filter_row_ids(&self, row_ids: &UInt64Array) -> BooleanArray {
952        match self {
953            Self::Fragments { to_keep, .. } => row_ids
954                .iter()
955                .map(|id| id.map(|id| to_keep.contains((id >> 32) as u32)))
956                .collect(),
957            Self::RowIds(valid_row_ids) => row_ids
958                .iter()
959                .map(|id| id.map(|id| valid_row_ids.contains(id)))
960                .collect(),
961        }
962    }
963}
964
965impl UpdateCriteria {
966    pub fn requires_old_data(data_criteria: TrainingCriteria) -> Self {
967        Self {
968            requires_old_data: true,
969            data_criteria,
970        }
971    }
972
973    pub fn only_new_data(data_criteria: TrainingCriteria) -> Self {
974        Self {
975            requires_old_data: false,
976            data_criteria,
977        }
978    }
979}
980
981/// Compute the lexicographically next prefix by incrementing the last character's code point.
982/// Returns None if no valid upper bound exists.
983///
984/// This is used for LIKE prefix queries to convert `LIKE 'foo%'` to range `[foo, fop)`.
985///
986/// # UTF-8 and Unicode Handling
987///
988/// This function operates on Unicode code points (characters), not bytes. Since UTF-8
989/// byte ordering is identical to Unicode code point ordering, incrementing a character's
990/// code point produces the correct lexicographic successor for byte-wise string comparison.
991///
992/// If incrementing the last character would overflow or land in the surrogate range
993/// (U+D800-U+DFFF), we try incrementing the previous character, and so on.
994///
995/// Examples:
996/// - `"foo"` → `Some("fop")`
997/// - `"café"` → `Some("cafê")`  (é U+00E9 → ê U+00EA)
998/// - `"abc中"` → `Some("abc丮")` (中 U+4E2D → 丮 U+4E2E)
999/// - `"cafÿ"` → `Some("cafĀ")` (ÿ U+00FF → Ā U+0100)
1000pub fn compute_next_prefix(prefix: &str) -> Option<String> {
1001    if prefix.is_empty() {
1002        return None;
1003    }
1004
1005    let chars: Vec<char> = prefix.chars().collect();
1006
1007    // Try incrementing characters from right to left
1008    for i in (0..chars.len()).rev() {
1009        if let Some(next_char) = next_unicode_char(chars[i]) {
1010            let mut result: String = chars[..i].iter().collect();
1011            result.push(next_char);
1012            return Some(result);
1013        }
1014        // This character cannot be incremented (e.g., U+10FFFF), try previous
1015    }
1016
1017    // All characters were at maximum value
1018    None
1019}
1020
1021/// Get the next valid Unicode scalar value after the given character.
1022/// Skips the surrogate range (U+D800-U+DFFF) which is not valid in UTF-8.
1023fn next_unicode_char(c: char) -> Option<char> {
1024    let cp = c as u32;
1025    let next_cp = cp.checked_add(1)?;
1026
1027    // Skip surrogate range (U+D800-U+DFFF)
1028    let next_cp = if (0xD800..=0xDFFF).contains(&next_cp) {
1029        0xE000
1030    } else {
1031        next_cp
1032    };
1033
1034    char::from_u32(next_cp)
1035}
1036
1037/// A trait for a scalar index, a structure that can determine row ids that satisfy scalar queries
1038#[async_trait]
1039pub trait ScalarIndex: Send + Sync + std::fmt::Debug + Index + DeepSizeOf {
1040    /// Search the scalar index
1041    ///
1042    /// Returns all row ids that satisfy the query, these row ids are not necessarily ordered
1043    async fn search(
1044        &self,
1045        query: &dyn AnyQuery,
1046        metrics: &dyn MetricsCollector,
1047    ) -> Result<SearchResult>;
1048
1049    /// Returns true if the remap operation is supported
1050    fn can_remap(&self) -> bool;
1051
1052    /// Remap the row ids, creating a new remapped version of this index in `dest_store`
1053    async fn remap(
1054        &self,
1055        mapping: &HashMap<u64, Option<u64>>,
1056        dest_store: &dyn IndexStore,
1057    ) -> Result<CreatedIndex>;
1058
1059    /// Add the new data into the index, creating an updated version of the index in `dest_store`
1060    ///
1061    /// If `old_data_filter` is provided, old index data will be filtered before
1062    /// merge according to the chosen filter mode.
1063    async fn update(
1064        &self,
1065        new_data: SendableRecordBatchStream,
1066        dest_store: &dyn IndexStore,
1067        old_data_filter: Option<OldIndexDataFilter>,
1068    ) -> Result<CreatedIndex>;
1069
1070    /// Returns the criteria that will be used to update the index
1071    fn update_criteria(&self) -> UpdateCriteria;
1072
1073    /// Derive the index parameters from the current index
1074    ///
1075    /// This returns a ScalarIndexParams that can be used to recreate an index
1076    /// with the same configuration on another dataset.
1077    fn derive_index_params(&self) -> Result<ScalarIndexParams>;
1078}