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