pub mod arena_skiplist;
mod btree;
mod filter;
mod fts;
mod hnsw;
mod pk;
mod pk_key;
mod plugin;
mod query;
#[cfg(test)]
pub(crate) mod test_plugin;
#[cfg(test)]
mod tests;
use std::collections::{BTreeMap, HashMap};
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use arrow_array::RecordBatch;
use arrow_schema::Schema as ArrowSchema;
use lance_core::datatypes::Schema as LanceSchema;
use lance_core::{Error, Result};
use lance_index::scalar::InvertedIndexParams;
use lance_index::scalar::inverted::DocumentGranularity;
use lance_linalg::distance::DistanceType;
use tracing::instrument;
use super::memtable::batch_store::StoredBatch;
use super::wal::WriterCursors;
use pk::{OwnedPk, PkIndex};
pub use btree::{BTreeMemIndex, BTreeMemIndexPlugin};
pub(crate) use filter::{
IndexedFilter, MemIndexCatalog, evaluate as evaluate_index_filter, plan_filter,
};
pub use fts::{FtsEntry, FtsMemIndex, FtsMemIndexPlugin, FtsParams, FtsQueryExpr, SearchOptions};
pub(crate) use fts::{QueryLocalFtsIndex, QueryLocalFtsStats, search_cross_column};
pub use hnsw::{HnswMemIndex, HnswMemIndexPlugin, HnswParams};
pub use pk_key::encode_pk_tuple;
pub use plugin::{
FlushContext, FlushOutcome, GenerationWrite, MemIndex, MemIndexBuildContext, MemIndexParams,
MemIndexPlugin, MemIndexRegistry, MemIndexSpec, PrimaryKeyIndex, ResolveContext, ResolvedIndex,
};
pub use query::{
FtsMemQuery, MemMatches, MemQuery, MemSearchResult, PositionSet, RankedMatch, ScalarQuery,
SearchContext, VectorMemQuery,
};
pub type RowPosition = u64;
const PARALLEL_INDEX_MIN_ROWS: usize = 64;
impl Default for MemIndexRegistry {
fn default() -> Self {
let mut registry = Self::empty();
for plugin in [
Arc::new(BTreeMemIndexPlugin) as Arc<dyn MemIndexPlugin>,
Arc::new(HnswMemIndexPlugin),
Arc::new(FtsMemIndexPlugin),
] {
registry
.add_plugin(plugin)
.expect("the built-in kinds claim distinct details messages");
}
registry
}
}
pub fn validate_index_specs(
specs: &[MemIndexSpec],
schema: &ArrowSchema,
lance_schema: &LanceSchema,
pk_columns: &[String],
) -> Result<()> {
let mut seen = HashMap::new();
for spec in specs {
if !first_under_its_name(&mut seen, spec)? {
continue;
}
spec.validate(lance_schema)?;
}
for column in pk_columns {
let field = schema.field_with_name(column).map_err(|_| {
Error::invalid_input(format!(
"primary-key column '{column}' is not in the shard schema"
))
})?;
if pk_columns.len() > 1 && !pk_key::is_encodable(field.data_type()) {
return Err(Error::invalid_input(format!(
"composite primary-key column '{column}' has type {:?}, which has no \
order-preserving key encoding",
field.data_type()
)));
}
}
Ok(())
}
fn first_under_its_name<'a>(
seen: &mut HashMap<&'a str, &'a MemIndexSpec>,
spec: &'a MemIndexSpec,
) -> Result<bool> {
match seen.insert(spec.name.as_str(), spec) {
None => Ok(true),
Some(first) if first.same_index(spec) => Ok(false),
Some(_) => Err(Error::invalid_input(format!(
"index '{}' is configured twice, as two different indexes",
spec.name
))),
}
}
pub(crate) fn unsupported_index_type(
index_name: &str,
type_url: &str,
registry: &MemIndexRegistry,
) -> Error {
let supported = registry
.plugins()
.iter()
.map(|plugin| plugin.details_message())
.collect::<Vec<_>>()
.join(", ");
Error::invalid_input(format!(
"index '{index_name}' has type {type_url}, which no registered plugin maintains. \
Supported: [{supported}]"
))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum MemTableVisibility {
#[default]
Published,
Indexed,
}
#[derive(Default)]
pub struct IndexStore {
indexes: BTreeMap<String, Arc<dyn MemIndex>>,
filter_catalog: MemIndexCatalog,
pk_index: Option<PkIndex>,
indexed_count: AtomicUsize,
durability: Option<(Arc<WriterCursors>, usize)>,
pk_has_overrides: AtomicBool,
has_rows: AtomicBool,
}
impl std::fmt::Debug for IndexStore {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("IndexStore")
.field("indexes", &self.indexes.keys().collect::<Vec<_>>())
.field("pk_index", &self.pk_index.as_ref().map(PkIndex::describe))
.field("indexed_count", &self.indexed_count())
.field("pk_has_overrides", &self.pk_has_overrides())
.finish()
}
}
#[derive(Clone, Copy)]
enum Rows<'a> {
One(&'a RecordBatch, RowPosition),
Batches(&'a [StoredBatch]),
}
impl Rows<'_> {
fn num_rows(&self) -> usize {
match self {
Self::One(batch, _) => batch.num_rows(),
Self::Batches(batches) => batches.iter().map(|stored| stored.num_rows).sum(),
}
}
fn each(
&self,
mut insert: impl FnMut(&RecordBatch, RowPosition) -> Result<bool>,
) -> Result<bool> {
match self {
Self::One(batch, row_offset) => insert(batch, *row_offset),
Self::Batches(batches) => batches.iter().try_fold(false, |seen, stored| {
Ok(insert(&stored.data, stored.row_offset)? || seen)
}),
}
}
fn insert_into(&self, index: &dyn MemIndex) -> Result<()> {
match self {
Self::One(batch, row_offset) => index.insert(batch, *row_offset),
Self::Batches(batches) => index.insert_batches(batches),
}
}
}
type IndexTask<'a> = Box<dyn Fn() -> Result<bool> + Send + Sync + 'a>;
impl IndexStore {
pub fn new() -> Self {
Self::default()
}
pub fn from_specs(
specs: &[MemIndexSpec],
schema: &LanceSchema,
max_rows: usize,
max_batches: usize,
) -> Result<Self> {
let mut store = Self::new();
let mut seen = HashMap::new();
for spec in specs {
if !first_under_its_name(&mut seen, spec)? {
continue;
}
store.add_index(
spec.name.clone(),
spec.build(schema, max_rows, max_batches)?,
);
}
store.filter_catalog = MemIndexCatalog::new(specs, schema);
Ok(store)
}
pub fn add_index(&mut self, name: String, index: Arc<dyn MemIndex>) {
assert!(
!self.has_rows.load(Ordering::Acquire),
"indexes must be added before any row is inserted"
);
self.indexes.insert(name, index);
}
pub fn add_btree(&mut self, name: String, field_id: i32, column: String) {
self.add_index(name, Arc::new(BTreeMemIndex::new(field_id, column)));
}
pub fn add_hnsw(
&mut self,
name: String,
field_id: i32,
column: String,
distance_type: DistanceType,
capacity: usize,
max_batches: usize,
) {
self.add_index(
name,
Arc::new(HnswMemIndex::with_capacity(
field_id,
column,
distance_type,
Default::default(),
capacity,
max_batches,
)),
);
}
pub fn add_fts(&mut self, name: String, field_id: i32, column: String) {
self.add_index(name, Arc::new(FtsMemIndex::new(field_id, column)));
}
pub fn add_fts_with_params(
&mut self,
name: String,
field_id: i32,
column: String,
params: InvertedIndexParams,
) -> Result<()> {
self.add_index(
name,
Arc::new(FtsMemIndex::try_with_params(field_id, column, params)?),
);
Ok(())
}
pub fn insert(&self, batch: &RecordBatch, row_offset: RowPosition) -> Result<()> {
self.insert_with_batch_position(batch, row_offset, None)
}
#[instrument(
name = "idx_insert_batch",
level = "debug",
skip_all,
fields(num_rows = batch.num_rows(), row_offset, batch_position = ?batch_position)
)]
pub fn insert_with_batch_position(
&self,
batch: &RecordBatch,
row_offset: RowPosition,
batch_position: Option<usize>,
) -> Result<()> {
self.index_rows(Rows::One(batch, row_offset))?;
if let Some(position) = batch_position {
self.advance_indexed_count(position + 1);
}
Ok(())
}
#[instrument(name = "idx_insert_batches", level = "debug", skip_all, fields(batch_count = batches.len()))]
pub fn insert_batches(&self, batches: &[StoredBatch]) -> Result<()> {
let Some(last) = batches.iter().map(|stored| stored.batch_position).max() else {
return Ok(());
};
self.index_rows(Rows::Batches(batches))?;
self.advance_indexed_count(last + 1);
Ok(())
}
fn index_rows(&self, rows: Rows<'_>) -> Result<()> {
self.has_rows.store(true, Ordering::Release);
let track_overrides = self.should_track_pk_overrides();
let mut tasks: Vec<(&str, IndexTask<'_>)> = Vec::with_capacity(self.indexes.len() + 1);
for (name, index) in &self.indexes {
let task: IndexTask<'_> = match self.shared_pk_named(name).filter(|_| track_overrides) {
Some(pk) => Box::new(move || {
rows.each(|batch, row_offset| pk.insert_and_report_existing(batch, row_offset))
}),
None => Box::new(move || rows.insert_into(index.as_ref()).map(|()| false)),
};
tasks.push((name, task));
}
if let Some(pk) = self.pk_index.as_ref().and_then(PkIndex::owned) {
tasks.push((
"primary key",
Box::new(move || {
rows.each(|batch, row_offset| pk.insert(batch, row_offset, track_overrides))
}),
));
}
let inline = matches!(rows, Rows::One(..))
|| tasks.len() < 2
|| rows.num_rows() <= PARALLEL_INDEX_MIN_ROWS;
let results: Vec<Result<bool>> = if inline {
tasks.iter().map(|(_, task)| task()).collect()
} else {
std::thread::scope(|scope| {
let handles: Vec<_> = tasks
.iter()
.map(|(name, task)| (*name, scope.spawn(task)))
.collect();
handles
.into_iter()
.map(|(name, handle)| {
handle.join().unwrap_or_else(|panic| {
Err(Error::internal(format!(
"index '{name}' panicked: {}",
panic_message(panic.as_ref())
)))
})
})
.collect()
})
};
let mut had_existing = false;
for result in results {
had_existing |= result?;
}
self.mark_pk_overrides(had_existing);
Ok(())
}
pub(crate) fn advance_indexed_count(&self, count: usize) {
self.indexed_count.fetch_max(count, Ordering::AcqRel);
}
pub(crate) fn filter_catalog(&self) -> &MemIndexCatalog {
&self.filter_catalog
}
pub fn get_index(&self, name: &str) -> Option<&Arc<dyn MemIndex>> {
self.indexes.get(name)
}
pub(crate) fn own_key_index(&self) -> Option<Arc<dyn MemIndex>> {
match &self.pk_index {
Some(PkIndex::Owned(OwnedPk::Single(index))) => {
Some(index.clone() as Arc<dyn MemIndex>)
}
_ => None,
}
}
pub fn index_answering(&self, column: &str, query: &dyn MemQuery) -> Option<Arc<dyn MemIndex>> {
self.indexes
.values()
.cloned()
.chain(self.own_key_index())
.find(|index| {
index.columns().iter().any(|covered| covered == column) && index.can_answer(query)
})
}
pub fn fts_granularities_on(&self, column: &str) -> Vec<DocumentGranularity> {
[DocumentGranularity::Row, DocumentGranularity::ListElement]
.into_iter()
.filter(|granularity| {
self.index_answering(column, &FtsMemQuery::probe(*granularity))
.is_some()
})
.collect()
}
pub fn is_empty(&self) -> bool {
self.indexes.is_empty()
}
pub fn index_names(&self) -> Vec<String> {
self.indexes.keys().cloned().collect()
}
pub fn len(&self) -> usize {
self.indexes.len()
}
pub fn resident_bytes(&self) -> usize {
let named: usize = self
.indexes
.values()
.map(|index| index.resident_bytes())
.sum();
let owned_pk = self
.pk_index
.as_ref()
.and_then(PkIndex::owned)
.map_or(0, |pk| pk.btree().resident_bytes());
named + owned_pk
}
pub fn indexed_count(&self) -> usize {
self.indexed_count.load(Ordering::Acquire)
}
pub fn visible_count(&self) -> usize {
let indexed = self.indexed_count();
match &self.durability {
Some((cursors, global_offset)) => cursors.visible_count(indexed, *global_offset),
None => indexed,
}
}
pub fn prefix_count(&self, visibility: MemTableVisibility) -> usize {
match visibility {
MemTableVisibility::Published => self.visible_count(),
MemTableVisibility::Indexed => self.indexed_count(),
}
}
pub(crate) fn set_durability(&mut self, cursors: Arc<WriterCursors>, global_offset: usize) {
self.durability = Some((cursors, global_offset));
}
}
fn panic_message(panic: &(dyn std::any::Any + Send)) -> &str {
panic
.downcast_ref::<&str>()
.copied()
.or_else(|| panic.downcast_ref::<String>().map(String::as_str))
.unwrap_or("no message")
}