use arrow_array::{BooleanArray, RecordBatch, UInt64Array};
use arrow_schema::Schema;
use async_trait::async_trait;
use bytes::Bytes;
use datafusion::physical_plan::SendableRecordBatchStream;
use datafusion_common::scalar::ScalarValue;
use datafusion_expr::Expr;
use lance_core::deepsize::DeepSizeOf;
use lance_core::utils::row_addr_remap::RowAddrRemap;
use lance_core::{Error, Result};
use lance_io::stream::{RecordBatchStream, RecordBatchStreamAdapter};
use lance_select::{NullableRowAddrSet, RowAddrTreeMap, RowSetOps};
use roaring::{RoaringBitmap, RoaringTreemap};
use serde::Serialize;
use std::collections::HashMap;
use std::fmt::Debug;
use std::pin::Pin;
use std::{any::Any, sync::Arc};
use crate::metrics::MetricsCollector;
use crate::{Index, IndexParams, IndexType};
#[derive(Debug, Clone, PartialEq, DeepSizeOf)]
pub struct IndexFile {
pub path: String,
pub size_bytes: u64,
}
pub const LANCE_SCALAR_INDEX: &str = "__lance_scalar_index";
#[derive(Debug, Clone, PartialEq, Eq, DeepSizeOf)]
pub enum BuiltinIndexType {
BTree,
Bitmap,
LabelList,
NGram,
ZoneMap,
BloomFilter,
RTree,
Inverted,
Fm,
}
impl BuiltinIndexType {
pub fn as_str(&self) -> &str {
match self {
Self::BTree => "btree",
Self::Bitmap => "bitmap",
Self::LabelList => "labellist",
Self::NGram => "ngram",
Self::ZoneMap => "zonemap",
Self::Inverted => "inverted",
Self::BloomFilter => "bloomfilter",
Self::RTree => "rtree",
Self::Fm => "fm",
}
}
}
impl TryFrom<IndexType> for BuiltinIndexType {
type Error = Error;
fn try_from(value: IndexType) -> Result<Self> {
match value {
IndexType::BTree => Ok(Self::BTree),
IndexType::Bitmap => Ok(Self::Bitmap),
IndexType::LabelList => Ok(Self::LabelList),
IndexType::NGram => Ok(Self::NGram),
IndexType::ZoneMap => Ok(Self::ZoneMap),
IndexType::Inverted => Ok(Self::Inverted),
IndexType::BloomFilter => Ok(Self::BloomFilter),
IndexType::RTree => Ok(Self::RTree),
IndexType::Fm => Ok(Self::Fm),
_ => Err(Error::index("Invalid index type".to_string())),
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct ScalarIndexParams {
pub index_type: String,
pub params: Option<String>,
}
impl Default for ScalarIndexParams {
fn default() -> Self {
Self {
index_type: BuiltinIndexType::BTree.as_str().to_string(),
params: None,
}
}
}
impl ScalarIndexParams {
pub fn for_builtin(index_type: BuiltinIndexType) -> Self {
Self {
index_type: index_type.as_str().to_string(),
params: None,
}
}
pub fn new(index_type: String) -> Self {
Self {
index_type,
params: None,
}
}
pub fn with_params<ParamsType: Serialize>(mut self, params: &ParamsType) -> Self {
self.params = Some(serde_json::to_string(params).unwrap());
self
}
}
impl IndexParams for ScalarIndexParams {
fn as_any(&self) -> &dyn std::any::Any {
self
}
fn index_name(&self) -> &str {
LANCE_SCALAR_INDEX
}
}
#[async_trait]
pub trait IndexWriter: Send {
async fn write_record_batch(&mut self, batch: RecordBatch) -> Result<u64>;
async fn add_global_buffer(&mut self, _data: Bytes) -> Result<u32> {
Err(Error::not_supported(
"global buffers are not supported by this index writer",
))
}
async fn finish(&mut self) -> Result<IndexFile>;
async fn finish_with_metadata(
&mut self,
metadata: HashMap<String, String>,
) -> Result<IndexFile>;
}
#[async_trait]
pub trait IndexReader: Send + Sync {
async fn read_record_batch(&self, n: u64, batch_size: u64) -> Result<RecordBatch>;
async fn read_global_buffer(&self, _index: u32) -> Result<Bytes> {
Err(Error::not_supported(
"global buffers are not supported by this index reader",
))
}
async fn read_range(
&self,
range: std::ops::Range<usize>,
projection: Option<&[&str]>,
) -> Result<RecordBatch>;
async fn read_ranges(
&self,
ranges: &[std::ops::Range<usize>],
projection: Option<&[&str]>,
) -> Result<RecordBatch> {
if ranges.is_empty() {
return self.read_range(0..0, projection).await;
}
let futures = ranges
.iter()
.map(|r| self.read_range(r.clone(), projection));
let batches = futures::future::try_join_all(futures).await?;
let schema = batches[0].schema();
Ok(arrow_select::concat::concat_batches(&schema, &batches)?)
}
async fn read_range_stream(
&self,
range: std::ops::Range<usize>,
projection: Option<&[&str]>,
) -> Result<Pin<Box<dyn RecordBatchStream>>> {
let batch = self.read_range(range, projection).await?;
let schema = batch.schema();
Ok(Box::pin(RecordBatchStreamAdapter::new(
schema,
futures::stream::once(async move { Ok(batch) }),
)))
}
async fn num_batches(&self, batch_size: u64) -> u32;
fn num_rows(&self) -> usize;
fn schema(&self) -> &lance_core::datatypes::Schema;
fn file_size_bytes(&self) -> Option<u64> {
None
}
}
#[async_trait]
pub trait IndexStore: std::fmt::Debug + Send + Sync + DeepSizeOf {
fn as_any(&self) -> &dyn Any;
fn clone_arc(&self) -> Arc<dyn IndexStore>;
fn io_parallelism(&self) -> usize;
async fn new_index_file(&self, name: &str, schema: Arc<Schema>)
-> Result<Box<dyn IndexWriter>>;
async fn open_index_file(&self, name: &str) -> Result<Arc<dyn IndexReader>>;
fn with_io_priority(&self, io_priority: u64) -> Arc<dyn IndexStore>;
async fn copy_index_file(&self, name: &str, dest_store: &dyn IndexStore) -> Result<IndexFile>;
async fn copy_index_file_to(
&self,
name: &str,
new_name: &str,
dest_store: &dyn IndexStore,
) -> Result<IndexFile> {
if name == new_name {
self.copy_index_file(name, dest_store).await
} else {
Err(Error::not_supported(format!(
"copying index file {name} to {new_name} is not supported by this index store"
)))
}
}
async fn rename_index_file(&self, name: &str, new_name: &str) -> Result<IndexFile>;
async fn delete_index_file(&self, name: &str) -> Result<()>;
async fn list_files_with_sizes(&self) -> Result<Vec<IndexFile>>;
}
pub trait AnyQuery: std::fmt::Debug + Any + Send + Sync {
fn as_any(&self) -> &dyn Any;
fn format(&self, col: &str) -> String;
fn to_expr(&self, col: String) -> Expr;
fn dyn_eq(&self, other: &dyn AnyQuery) -> bool;
}
impl PartialEq for dyn AnyQuery {
fn eq(&self, other: &Self) -> bool {
self.dyn_eq(other)
}
}
#[derive(Debug, PartialEq)]
pub enum SearchResult {
Exact(NullableRowAddrSet),
AtMost(NullableRowAddrSet),
AtLeast(NullableRowAddrSet),
}
impl SearchResult {
pub fn exact(row_ids: impl Into<RowAddrTreeMap>) -> Self {
Self::Exact(NullableRowAddrSet::new(row_ids.into(), Default::default()))
}
pub fn at_most(row_ids: impl Into<RowAddrTreeMap>) -> Self {
Self::AtMost(NullableRowAddrSet::new(row_ids.into(), Default::default()))
}
pub fn at_least(row_ids: impl Into<RowAddrTreeMap>) -> Self {
Self::AtLeast(NullableRowAddrSet::new(row_ids.into(), Default::default()))
}
pub fn with_nulls(self, nulls: impl Into<RowAddrTreeMap>) -> Self {
match self {
Self::Exact(row_ids) => Self::Exact(row_ids.with_nulls(nulls.into())),
Self::AtMost(row_ids) => Self::AtMost(row_ids.with_nulls(nulls.into())),
Self::AtLeast(row_ids) => Self::AtLeast(row_ids.with_nulls(nulls.into())),
}
}
pub fn row_addrs(&self) -> &NullableRowAddrSet {
match self {
Self::Exact(row_addrs) => row_addrs,
Self::AtMost(row_addrs) => row_addrs,
Self::AtLeast(row_addrs) => row_addrs,
}
}
pub fn is_exact(&self) -> bool {
matches!(self, Self::Exact(_))
}
}
pub struct CreatedIndex {
pub index_details: prost_types::Any,
pub index_version: u32,
pub files: Vec<IndexFile>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TrainingOrdering {
Values,
Addresses,
None,
}
#[derive(Debug, Clone)]
pub struct TrainingCriteria {
pub ordering: TrainingOrdering,
pub needs_row_ids: bool,
pub needs_row_addrs: bool,
}
impl TrainingCriteria {
pub fn new(ordering: TrainingOrdering) -> Self {
Self {
ordering,
needs_row_ids: false,
needs_row_addrs: false,
}
}
pub fn with_row_id(mut self) -> Self {
self.needs_row_ids = true;
self
}
pub fn with_row_addr(mut self) -> Self {
self.needs_row_addrs = true;
self
}
}
pub struct UpdateCriteria {
pub requires_old_data: bool,
pub data_criteria: TrainingCriteria,
}
#[derive(Debug, Clone)]
pub enum OldIndexDataFilter {
Fragments {
to_keep: RoaringBitmap,
to_remove: RoaringBitmap,
},
RowIds(RowAddrTreeMap),
}
impl OldIndexDataFilter {
pub fn filter_row_ids(&self, row_ids: &UInt64Array) -> BooleanArray {
match self {
Self::Fragments { to_keep, .. } => row_ids
.iter()
.map(|id| id.map(|id| to_keep.contains((id >> 32) as u32)))
.collect(),
Self::RowIds(valid_row_ids) => row_ids
.iter()
.map(|id| id.map(|id| valid_row_ids.contains(id)))
.collect(),
}
}
pub fn retain_old_rows(&self, rows: &mut RowAddrTreeMap) {
match self {
Self::Fragments { to_keep, .. } => rows.retain_fragments(to_keep.iter()),
Self::RowIds(valid_row_ids) => *rows &= valid_row_ids,
}
}
}
impl UpdateCriteria {
pub fn requires_old_data(data_criteria: TrainingCriteria) -> Self {
Self {
requires_old_data: true,
data_criteria,
}
}
pub fn only_new_data(data_criteria: TrainingCriteria) -> Self {
Self {
requires_old_data: false,
data_criteria,
}
}
}
#[async_trait]
pub trait ScalarIndex: Send + Sync + std::fmt::Debug + Index + DeepSizeOf {
async fn search(
&self,
query: &dyn AnyQuery,
metrics: &dyn MetricsCollector,
) -> Result<SearchResult>;
fn results_are_row_addresses(&self) -> bool {
false
}
fn can_remap(&self) -> bool;
async fn remap(
&self,
mapping: &RowAddrRemap,
dest_store: &dyn IndexStore,
) -> Result<CreatedIndex>;
async fn update(
&self,
new_data: SendableRecordBatchStream,
dest_store: &dyn IndexStore,
old_data_filter: Option<OldIndexDataFilter>,
) -> Result<CreatedIndex>;
fn update_criteria(&self) -> UpdateCriteria;
fn derive_index_params(&self) -> Result<ScalarIndexParams>;
fn value_range(&self) -> Option<(ScalarValue, ScalarValue)> {
None
}
}
pub trait RowIdRemapper: Send + Sync + std::fmt::Debug {
fn remap_row_id(&self, row_id: u64) -> Option<u64>;
fn remap_row_addrs_tree_map(&self, row_addrs: &RowAddrTreeMap) -> RowAddrTreeMap;
fn remap_row_ids_roaring_tree_map(&self, row_ids: &RoaringTreemap) -> RoaringTreemap;
fn remap_row_ids_record_batch(
&self,
batch: RecordBatch,
row_id_idx: usize,
) -> Result<RecordBatch>;
}