use std::sync::Arc;
use futures::TryStreamExt;
use arrow_array::cast::AsArray;
use arrow_array::types::Int32Type;
use arrow_array::{ArrayRef, Int32Array, RecordBatch, RecordBatchIterator, StringArray};
use arrow_schema::{DataType, Field as ArrowField, Schema as ArrowSchema};
use lance_index::IndexType;
use lance_index::optimize::OptimizeOptions;
use lance_index::scalar::BuiltinIndexType;
use lance_index::scalar::FullTextSearchQuery;
use lance_index::scalar::ScalarIndexParams;
use lance_index::scalar::inverted::InvertedIndexParams;
use lance_io::utils::CachedFileSize;
use lance_linalg::distance::MetricType;
use lance_table::format::DataFile;
use lance_table::format::overlay::{DataOverlayFile, OverlayCoverage};
use roaring::RoaringBitmap;
use rstest::rstest;
use lance_file::writer::{FileWriter, FileWriterOptions};
use crate::Dataset;
use crate::dataset::optimize::{CompactionOptions, compact_files, remapping};
use crate::dataset::transaction::{DataOverlayGroup, Operation};
use crate::dataset::{WriteDestination, WriteParams};
use crate::index::vector::VectorIndexParams;
use crate::index::{CreateIndexBuilder, DatasetIndexExt};
async fn create_base_dataset() -> Dataset {
create_base_dataset_with(false).await
}
async fn create_base_dataset_with(stable_row_ids: bool) -> Dataset {
let schema = Arc::new(ArrowSchema::new(vec![
ArrowField::new("id", DataType::Int32, true),
ArrowField::new("age", DataType::Int32, true),
]));
let batch = RecordBatch::try_new(
schema.clone(),
vec![
Arc::new(Int32Array::from_iter_values(0..12)),
Arc::new(Int32Array::from_iter_values((0..12).map(|v| v * 10))),
],
)
.unwrap();
let write_params = WriteParams {
max_rows_per_file: 6,
enable_stable_row_ids: stable_row_ids,
..Default::default()
};
let reader = RecordBatchIterator::new(vec![Ok(batch)], schema.clone());
Dataset::write(reader, "memory://", Some(write_params))
.await
.unwrap()
}
async fn build_age_index(dataset: &mut Dataset) {
dataset
.create_index(
&["age"],
IndexType::BTree,
None,
&ScalarIndexParams::default(),
true,
)
.await
.unwrap();
}
async fn commit_overlay(
dataset: Dataset,
name: &str,
fragment_id: u64,
fields: &[i32],
coverage: OverlayCoverage,
columns: Vec<ArrayRef>,
) -> Dataset {
let read_version = dataset.version().version;
let overlay_schema = dataset.schema().project_by_ids(fields, true);
let filename = format!("{name}.lance");
let path = dataset.base.clone().join("data").join(filename.as_str());
let obj_writer = dataset.object_store.create(&path).await.unwrap();
let mut writer =
FileWriter::try_new(obj_writer, overlay_schema, FileWriterOptions::default()).unwrap();
let file_version = writer.version().into();
for (i, array) in columns.into_iter().enumerate() {
writer.write_column(i, array).await.unwrap();
}
let summary = writer.finish().await.unwrap();
let mut data_file = DataFile::new_unstarted(filename, file_version);
data_file.fields = writer
.field_id_to_column_indices()
.iter()
.map(|(field_id, _)| *field_id as i32)
.collect::<Vec<_>>()
.into();
data_file.column_indices = writer
.field_id_to_column_indices()
.iter()
.map(|(_, column_index)| *column_index as i32)
.collect::<Vec<_>>()
.into();
data_file.file_size_bytes = CachedFileSize::new(summary.size_bytes);
let overlay = DataOverlayFile {
data_file,
coverage,
committed_version: 0,
};
Dataset::commit(
WriteDestination::Dataset(Arc::new(dataset)),
Operation::DataOverlay {
groups: vec![DataOverlayGroup {
fragment_id,
overlays: vec![overlay],
}],
},
Some(read_version),
None,
None,
Arc::new(Default::default()),
false,
)
.await
.unwrap()
}
async fn ids_matching(dataset: &Dataset, filter: &str) -> Vec<i32> {
ids_matching_opts(dataset, filter, false).await
}
async fn ids_matching_opts(dataset: &Dataset, filter: &str, fast_search: bool) -> Vec<i32> {
let mut scanner = dataset.scan();
scanner.filter(filter).unwrap().project(&["id"]).unwrap();
if fast_search {
scanner.fast_search();
}
let batch = scanner.try_into_batch().await.unwrap();
let mut ids = ids_from_batches(std::slice::from_ref(&batch));
ids.sort_unstable();
ids
}
fn ids_from_batches(batches: &[RecordBatch]) -> Vec<i32> {
batches
.iter()
.flat_map(|b| {
b.column_by_name("id")
.unwrap()
.as_primitive::<Int32Type>()
.values()
.to_vec()
})
.collect()
}
fn i32_array(values: impl IntoIterator<Item = Option<i32>>) -> ArrayRef {
Arc::new(Int32Array::from_iter(values))
}
fn fsl(rows: Vec<Vec<f32>>, dim: i32) -> ArrayRef {
let flat: Vec<f32> = rows.into_iter().flatten().collect();
let item = Arc::new(ArrowField::new("item", DataType::Float32, true));
Arc::new(
arrow_array::FixedSizeListArray::try_new(
item,
dim,
Arc::new(arrow_array::Float32Array::from(flat)),
None,
)
.unwrap(),
)
}
#[rstest]
#[tokio::test]
async fn test_overlay_stale_drop_and_new_match(#[values(false, true)] stable_row_ids: bool) {
let mut dataset = create_base_dataset_with(stable_row_ids).await;
build_age_index(&mut dataset).await;
let dataset = commit_overlay(
dataset,
"age_overlay",
0,
&[1],
OverlayCoverage::dense(RoaringBitmap::from_iter([1])),
vec![i32_array([Some(999)])],
)
.await;
assert_eq!(ids_matching(&dataset, "age = 10").await, Vec::<i32>::new());
assert_eq!(ids_matching(&dataset, "age = 999").await, vec![1]);
assert_eq!(ids_matching(&dataset, "age = 20").await, vec![2]);
}
#[rstest]
#[tokio::test]
async fn test_btree_overlay_row_level_precision(#[values(false, true)] stable_row_ids: bool) {
let mut dataset = create_base_dataset_with(stable_row_ids).await;
build_age_index(&mut dataset).await;
let dataset = commit_overlay(
dataset,
"age_row_level",
0,
&[1],
OverlayCoverage::dense(RoaringBitmap::from_iter([1])),
vec![i32_array([Some(50)])],
)
.await;
assert_eq!(ids_matching(&dataset, "age = 10").await, Vec::<i32>::new());
assert_eq!(ids_matching(&dataset, "age = 50").await, vec![1, 5]);
assert_eq!(ids_matching(&dataset, "age = 20").await, vec![2]);
assert_eq!(ids_matching(&dataset, "age = 30").await, vec![3]);
}
#[tokio::test]
async fn test_btree_overlay_masked_under_fast_search() {
let mut dataset = create_base_dataset().await;
build_age_index(&mut dataset).await;
let dataset = commit_overlay(
dataset,
"age_fast_search",
0,
&[1],
OverlayCoverage::dense(RoaringBitmap::from_iter([1])),
vec![i32_array([Some(999)])],
)
.await;
assert_eq!(
ids_matching_opts(&dataset, "age = 10", true).await,
Vec::<i32>::new()
);
assert_eq!(
ids_matching_opts(&dataset, "age = 999", true).await,
vec![1]
);
assert_eq!(ids_matching_opts(&dataset, "age = 20", true).await, vec![2]);
}
#[tokio::test]
async fn test_overlay_on_unrelated_field_excludes_nothing() {
let mut dataset = create_base_dataset().await;
build_age_index(&mut dataset).await;
let dataset = commit_overlay(
dataset,
"id_overlay",
0,
&[0],
OverlayCoverage::dense(RoaringBitmap::from_iter([1])),
vec![i32_array([Some(777)])],
)
.await;
assert_eq!(ids_matching(&dataset, "age = 10").await, vec![777]);
assert_eq!(ids_matching(&dataset, "age = 20").await, vec![2]);
assert_eq!(ids_matching(&dataset, "id = 777").await, vec![777]);
assert_eq!(ids_matching(&dataset, "id = 1").await, Vec::<i32>::new());
}
#[tokio::test]
async fn test_overlay_older_than_index_not_excluded() {
let dataset = create_base_dataset().await;
let mut dataset = commit_overlay(
dataset,
"age_overlay_old",
0,
&[1],
OverlayCoverage::dense(RoaringBitmap::from_iter([1])),
vec![i32_array([Some(999)])],
)
.await;
build_age_index(&mut dataset).await;
assert_eq!(ids_matching(&dataset, "age = 999").await, vec![1]);
assert_eq!(ids_matching(&dataset, "age = 10").await, Vec::<i32>::new());
}
#[tokio::test]
async fn test_overlay_null_override() {
let mut dataset = create_base_dataset().await;
build_age_index(&mut dataset).await;
let dataset = commit_overlay(
dataset,
"age_overlay_null",
0,
&[1],
OverlayCoverage::dense(RoaringBitmap::from_iter([1])),
vec![i32_array([None])],
)
.await;
assert_eq!(ids_matching(&dataset, "age = 10").await, Vec::<i32>::new());
assert_eq!(ids_matching(&dataset, "age IS NULL").await, vec![1]);
}
#[rstest]
#[tokio::test]
async fn test_overlay_multi_fragment(#[values(false, true)] stable_row_ids: bool) {
let mut dataset = create_base_dataset_with(stable_row_ids).await;
build_age_index(&mut dataset).await;
let dataset = commit_overlay(
dataset,
"age_overlay_frag1",
1,
&[1],
OverlayCoverage::dense(RoaringBitmap::from_iter([2])),
vec![i32_array([Some(60)])],
)
.await;
assert_eq!(ids_matching(&dataset, "age = 80").await, Vec::<i32>::new());
assert_eq!(ids_matching(&dataset, "age = 60").await, vec![6, 8]);
assert_eq!(ids_matching(&dataset, "age = 30").await, vec![3]);
}
#[rstest]
#[tokio::test]
async fn test_btree_overlay_stale_row_with_prior_deletion(
#[values(false, true)] stable_row_ids: bool,
) {
let mut dataset = create_base_dataset_with(stable_row_ids).await;
build_age_index(&mut dataset).await;
dataset.delete("id = 6").await.unwrap();
let dataset = commit_overlay(
dataset,
"age_overlay_del",
1,
&[1],
OverlayCoverage::dense(RoaringBitmap::from_iter([2])),
vec![i32_array([Some(999)])],
)
.await;
assert_eq!(ids_matching(&dataset, "age = 80").await, Vec::<i32>::new());
assert_eq!(ids_matching(&dataset, "age = 999").await, vec![8]);
assert_eq!(ids_matching(&dataset, "age = 70").await, vec![7]);
assert_eq!(ids_matching(&dataset, "age = 60").await, Vec::<i32>::new());
}
const VEC_DIM: i32 = 8;
fn vec_query() -> Vec<f32> {
vec![1.0_f32, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
}
async fn create_vector_overlay_dataset(stable_row_ids: bool) -> Dataset {
let query = vec_query();
let far = vec![0.0_f32, 100.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0];
let mut vectors: Vec<Vec<f32>> = Vec::with_capacity(64);
for i in 0..64 {
if i == 35 {
vectors.push(query.clone());
} else {
let mut v = vec![0.0_f32; VEC_DIM as usize];
v[1] = (i + 2) as f32; vectors.push(v);
}
}
let schema = Arc::new(ArrowSchema::new(vec![
ArrowField::new("id", DataType::Int32, true),
ArrowField::new(
"vec",
DataType::FixedSizeList(
Arc::new(ArrowField::new("item", DataType::Float32, true)),
VEC_DIM,
),
true,
),
]));
let batch = RecordBatch::try_new(
schema.clone(),
vec![
Arc::new(Int32Array::from_iter_values(0..64)),
fsl(vectors, VEC_DIM),
],
)
.unwrap();
let write_params = WriteParams {
max_rows_per_file: 32,
enable_stable_row_ids: stable_row_ids,
..Default::default()
};
let reader = RecordBatchIterator::new(vec![Ok(batch)], schema.clone());
let mut dataset = Dataset::write(reader, "memory://", Some(write_params))
.await
.unwrap();
let params = VectorIndexParams::ivf_flat(1, MetricType::L2);
dataset
.create_index(&["vec"], IndexType::Vector, None, ¶ms, true)
.await
.unwrap();
commit_overlay(
dataset,
"vec_overlay",
1,
&[1],
OverlayCoverage::dense(RoaringBitmap::from_iter([3, 8])),
vec![fsl(vec![far, query], VEC_DIM)],
)
.await
}
async fn vector_query_ids(dataset: &Dataset, k: usize, fast_search: bool) -> Vec<i32> {
let mut scanner = dataset.scan();
scanner
.nearest("vec", &arrow_array::Float32Array::from(vec_query()), k)
.unwrap()
.minimum_nprobes(1)
.project(&["id"])
.unwrap();
if fast_search {
scanner.fast_search();
}
let results = scanner
.try_into_stream()
.await
.unwrap()
.try_collect::<Vec<_>>()
.await
.unwrap();
ids_from_batches(&results)
}
#[rstest]
#[tokio::test]
async fn test_vector_index_rescore_on_overlay(#[values(false, true)] stable_row_ids: bool) {
let dataset = create_vector_overlay_dataset(stable_row_ids).await;
let ids = vector_query_ids(&dataset, 3, false).await;
assert!(
ids.contains(&40),
"expected id=40 (re-scored to query) in {ids:?}"
);
assert!(
!ids.contains(&35),
"stale vector for id=35 should be dropped, got {ids:?}"
);
}
#[tokio::test]
async fn test_vector_overlay_stale_dropped_under_fast_search() {
let dataset = create_vector_overlay_dataset(false).await;
let ids = vector_query_ids(&dataset, 3, true).await;
assert!(
!ids.contains(&35),
"stale vector for id=35 must be dropped under fast_search, got {ids:?}"
);
assert!(
!ids.contains(&40),
"fast_search skips re-score, so id=40 should be absent, got {ids:?}"
);
}
#[tokio::test]
async fn test_overlay_stale_with_compound_index_expression() {
let mut dataset = create_base_dataset().await;
build_age_index(&mut dataset).await;
dataset
.create_index(
&["id"],
IndexType::BTree,
None,
&ScalarIndexParams::default(),
true,
)
.await
.unwrap();
let dataset = commit_overlay(
dataset,
"age_compound",
0,
&[1],
OverlayCoverage::dense(RoaringBitmap::from_iter([1])),
vec![i32_array([Some(999)])],
)
.await;
assert_eq!(ids_matching(&dataset, "age = 10").await, Vec::<i32>::new());
assert_eq!(ids_matching(&dataset, "age = 999").await, vec![1]);
assert_eq!(ids_matching(&dataset, "id = 2").await, vec![2]);
}
#[tokio::test]
async fn test_update_nonindexed_column_preserves_overlay_masking() {
use crate::dataset::UpdateBuilder;
let mut dataset = create_base_dataset_with(true).await;
build_age_index(&mut dataset).await;
let dataset = commit_overlay(
dataset,
"age_update",
0,
&[1],
OverlayCoverage::dense(RoaringBitmap::from_iter([1])),
vec![i32_array([Some(999)])],
)
.await;
assert_eq!(ids_matching(&dataset, "age = 10").await, Vec::<i32>::new());
assert_eq!(ids_matching(&dataset, "age = 999").await, vec![1]);
let dataset = UpdateBuilder::new(Arc::new(dataset))
.update_where("id = 1")
.unwrap()
.set("id", "100")
.unwrap()
.build()
.unwrap()
.execute()
.await
.unwrap()
.new_dataset;
assert_eq!(
ids_matching(&dataset, "age = 10").await,
Vec::<i32>::new(),
"stale index entry age=10 resurfaced after updating a non-indexed column"
);
assert_eq!(
ids_matching(&dataset, "age = 999").await,
vec![100],
"overlaid value age=999 lost after updating a non-indexed column"
);
assert_eq!(ids_matching(&dataset, "age = 20").await, vec![2]);
}
async fn create_text_dataset() -> Dataset {
let schema = Arc::new(ArrowSchema::new(vec![
ArrowField::new("id", DataType::Int32, true),
ArrowField::new("text", DataType::Utf8, true),
]));
let texts: Vec<&str> = vec![
"apple pie",
"apple banana", "cherry cake",
"banana split",
"orange juice",
"grape vine",
"mango sorbet", "pear tart",
"lemon curd",
"peach cobbler",
"plum pudding",
"fig newton",
];
let batch = RecordBatch::try_new(
schema.clone(),
vec![
Arc::new(Int32Array::from_iter_values(0..12)),
Arc::new(StringArray::from(texts)),
],
)
.unwrap();
let write_params = WriteParams {
max_rows_per_file: 6,
..Default::default()
};
let reader = RecordBatchIterator::new(vec![Ok(batch)], schema.clone());
Dataset::write(reader, "memory://", Some(write_params))
.await
.unwrap()
}
async fn build_text_fts_index(dataset: &mut Dataset) {
dataset
.create_index(
&["text"],
IndexType::Inverted,
None,
&InvertedIndexParams::default(),
true,
)
.await
.unwrap();
}
async fn build_text_fts_index_with_positions(dataset: &mut Dataset) {
dataset
.create_index(
&["text"],
IndexType::Inverted,
None,
&InvertedIndexParams::default().with_position(true),
true,
)
.await
.unwrap();
}
async fn fts_ids(dataset: &Dataset, query: FullTextSearchQuery) -> Vec<i32> {
let results = dataset
.scan()
.full_text_search(query)
.unwrap()
.project(&["id"])
.unwrap()
.try_into_stream()
.await
.unwrap()
.try_collect::<Vec<_>>()
.await
.unwrap();
let mut ids = ids_from_batches(&results);
ids.sort_unstable();
ids
}
async fn fts_ids_matching(dataset: &Dataset, term: &str) -> Vec<i32> {
fts_ids(dataset, FullTextSearchQuery::new(term.to_owned())).await
}
#[tokio::test]
async fn test_ngram_optimize_preserves_overlay_staleness() {
let mut dataset = create_text_dataset().await;
let params = ScalarIndexParams::for_builtin(BuiltinIndexType::NGram);
let fragment_ids = dataset
.get_fragments()
.into_iter()
.map(|fragment| fragment.id() as u32)
.collect::<Vec<_>>();
let mut segments = Vec::with_capacity(fragment_ids.len());
for fragment_id in fragment_ids {
segments.push(
CreateIndexBuilder::new(&mut dataset, &["text"], IndexType::NGram, ¶ms)
.name("text_ngram".to_string())
.fragments(vec![fragment_id])
.execute_uncommitted()
.await
.unwrap(),
);
}
let source_version = segments[0].dataset_version;
dataset
.commit_existing_index_segments("text_ngram", "text", segments)
.await
.unwrap();
let mut dataset = commit_overlay(
dataset,
"ngram_text_overlay",
0,
&[1],
OverlayCoverage::dense(RoaringBitmap::from_iter([1])),
vec![Arc::new(StringArray::from(vec![Some("cherry mango")]))],
)
.await;
dataset
.optimize_indices(&OptimizeOptions::merge(2))
.await
.unwrap();
let committed = dataset.load_indices_by_name("text_ngram").await.unwrap();
assert_eq!(committed.len(), 1);
assert_eq!(committed[0].dataset_version, source_version);
assert_eq!(
ids_matching(&dataset, "contains(text, 'apple')").await,
vec![0]
);
assert_eq!(
ids_matching(&dataset, "contains(text, 'mango')").await,
vec![1, 6]
);
}
#[tokio::test]
async fn test_btree_physical_merge_preserves_overlay_staleness() {
let mut dataset = create_base_dataset().await;
let params = ScalarIndexParams::default();
let mut segments = Vec::new();
for fragment in dataset.get_fragments() {
segments.push(
CreateIndexBuilder::new(&mut dataset, &["age"], IndexType::BTree, ¶ms)
.name("age_btree".to_string())
.fragments(vec![fragment.id() as u32])
.execute_uncommitted()
.await
.unwrap(),
);
}
let source_version = segments[0].dataset_version;
let mut dataset = commit_overlay(
dataset,
"btree_before_merge",
0,
&[1],
OverlayCoverage::dense(RoaringBitmap::from_iter([1])),
vec![i32_array([Some(999)])],
)
.await;
let merged = dataset
.merge_existing_index_segments(segments)
.await
.unwrap();
assert_eq!(merged.dataset_version, source_version);
dataset
.commit_existing_index_segments("age_btree", "age", vec![merged])
.await
.unwrap();
assert_eq!(ids_matching(&dataset, "age = 10").await, Vec::<i32>::new());
assert_eq!(ids_matching(&dataset, "age = 999").await, vec![1]);
}
#[tokio::test]
async fn test_ngram_remap_excludes_newer_overlay_fragments() {
let mut dataset = create_text_dataset().await;
let params = ScalarIndexParams::for_builtin(BuiltinIndexType::NGram);
dataset
.create_index(
&["text"],
IndexType::NGram,
Some("text_ngram".to_string()),
¶ms,
false,
)
.await
.unwrap();
let source_version =
dataset.load_indices_by_name("text_ngram").await.unwrap()[0].dataset_version;
compact_files(
&mut dataset,
CompactionOptions {
target_rows_per_fragment: 12,
defer_index_remap: true,
..Default::default()
},
None,
)
.await
.unwrap();
let compacted_fragment_id = dataset.get_fragments()[0].id();
let mut dataset = commit_overlay(
dataset,
"ngram_after_compaction",
compacted_fragment_id as u64,
&[1],
OverlayCoverage::dense(RoaringBitmap::from_iter([1])),
vec![Arc::new(StringArray::from(vec![Some("cherry mango")]))],
)
.await;
remapping::remap_column_index(&mut dataset, &["text"], Some("text_ngram".to_string()))
.await
.unwrap();
let committed = dataset.load_indices_by_name("text_ngram").await.unwrap();
assert_eq!(committed.len(), 1);
assert!(committed[0].dataset_version > source_version);
assert!(
!committed[0]
.fragment_bitmap
.as_ref()
.unwrap()
.contains(compacted_fragment_id as u32)
);
assert_eq!(
ids_matching(&dataset, "contains(text, 'apple')").await,
vec![0]
);
assert_eq!(
ids_matching(&dataset, "contains(text, 'mango')").await,
vec![1, 6]
);
}
async fn fts_phrase_ids_matching(dataset: &Dataset, phrase: &str) -> Vec<i32> {
use lance_index::scalar::inverted::query::{FtsQuery, PhraseQuery};
let query = FullTextSearchQuery::new_query(FtsQuery::Phrase(
PhraseQuery::new(phrase.to_owned()).with_column(Some("text".to_owned())),
));
fts_ids(dataset, query).await
}
#[tokio::test]
async fn test_fts_overlay_stale_drop_and_new_match() {
let mut dataset = create_text_dataset().await;
build_text_fts_index(&mut dataset).await;
let dataset = commit_overlay(
dataset,
"text_overlay",
0,
&[1],
OverlayCoverage::dense(RoaringBitmap::from_iter([1])),
vec![Arc::new(StringArray::from(vec![Some("cherry mango")]))],
)
.await;
assert_eq!(fts_ids_matching(&dataset, "apple").await, vec![0]);
assert_eq!(fts_ids_matching(&dataset, "banana").await, vec![3]);
let cherry_ids = fts_ids_matching(&dataset, "cherry").await;
assert!(
cherry_ids.contains(&1),
"id=1 overlay→cherry mango should be found: {cherry_ids:?}"
);
assert!(
cherry_ids.contains(&2),
"id=2 cherry cake should still be found: {cherry_ids:?}"
);
let mango_ids = fts_ids_matching(&dataset, "mango").await;
assert!(
mango_ids.contains(&1),
"id=1 overlay→cherry mango should be found: {mango_ids:?}"
);
assert!(
mango_ids.contains(&6),
"id=6 mango sorbet should still be found: {mango_ids:?}"
);
}
#[tokio::test]
async fn test_fts_phrase_overlay_stale_drop() {
let mut dataset = create_text_dataset().await;
build_text_fts_index_with_positions(&mut dataset).await;
assert_eq!(
fts_phrase_ids_matching(&dataset, "apple banana").await,
vec![1]
);
let dataset = commit_overlay(
dataset,
"phrase_overlay",
0,
&[1],
OverlayCoverage::dense(RoaringBitmap::from_iter([1])),
vec![Arc::new(StringArray::from(vec![Some("cherry mango")]))],
)
.await;
assert_eq!(
fts_phrase_ids_matching(&dataset, "apple banana").await,
Vec::<i32>::new()
);
}
#[tokio::test]
async fn test_fts_phrase_overlay_unrelated_field_not_excluded() {
let mut dataset = create_text_dataset().await;
build_text_fts_index_with_positions(&mut dataset).await;
let dataset = commit_overlay(
dataset,
"id_overlay",
0,
&[0],
OverlayCoverage::dense(RoaringBitmap::from_iter([1])),
vec![i32_array([Some(777)])],
)
.await;
assert_eq!(
fts_phrase_ids_matching(&dataset, "apple banana").await,
vec![777]
);
}
#[tokio::test]
async fn test_fts_overlay_unrelated_field_not_excluded() {
let mut dataset = create_text_dataset().await;
build_text_fts_index(&mut dataset).await;
let dataset = commit_overlay(
dataset,
"id_overlay_for_fts",
0,
&[0],
OverlayCoverage::dense(RoaringBitmap::from_iter([1])),
vec![i32_array([Some(999)])],
)
.await;
assert_eq!(fts_ids_matching(&dataset, "apple").await, vec![0, 999]);
assert_eq!(fts_ids_matching(&dataset, "banana").await, vec![3, 999]);
}
#[tokio::test]
#[ignore = "benchmark"]
#[allow(clippy::print_stdout)]
async fn bench_index_query_overlay_overhead() {
use std::time::Instant;
use arrow_array::Float32Array;
const DIM: i32 = 32;
const ROWS: i32 = 1_000_000;
const ROWS_PER_FRAG: i32 = 100_000; const ITERS: u32 = 10;
let uri = "/tmp/lance-bench-overlay-oss1325";
if std::path::Path::new(uri).exists() {
std::fs::remove_dir_all(uri).unwrap();
}
println!("Building {ROWS}-row dataset at {uri} (this takes ~30 s)...");
let schema = Arc::new(ArrowSchema::new(vec![
ArrowField::new("id", DataType::Int32, false),
ArrowField::new("age", DataType::Int32, false),
ArrowField::new(
"vec",
DataType::FixedSizeList(
Arc::new(ArrowField::new("item", DataType::Float32, true)),
DIM,
),
false,
),
]));
let row_ids: Vec<i32> = (0..ROWS).collect();
let ages: Vec<i32> = row_ids.iter().map(|&i| i * 10).collect();
let flat_vecs: Vec<f32> = (0..(ROWS as usize * DIM as usize))
.map(|j| (j / DIM as usize) as f32 % 1000.0)
.collect();
let vec_col = Arc::new(
arrow_array::FixedSizeListArray::try_new(
Arc::new(ArrowField::new("item", DataType::Float32, true)),
DIM,
Arc::new(Float32Array::from(flat_vecs)),
None,
)
.unwrap(),
);
let batch = RecordBatch::try_new(
schema.clone(),
vec![
Arc::new(Int32Array::from(row_ids)),
Arc::new(Int32Array::from(ages)),
vec_col,
],
)
.unwrap();
let write_params = WriteParams {
max_rows_per_file: ROWS_PER_FRAG as usize,
..Default::default()
};
let reader = RecordBatchIterator::new(vec![Ok(batch)], schema.clone());
let mut dataset = Dataset::write(reader, uri, Some(write_params))
.await
.unwrap();
println!("Building BTree index on age...");
dataset
.create_index(
&["age"],
IndexType::BTree,
None,
&ScalarIndexParams::default(),
true,
)
.await
.unwrap();
println!("Building IVF_FLAT(1 partition) index on vec...");
dataset
.create_index(
&["vec"],
IndexType::Vector,
None,
&VectorIndexParams::ivf_flat(1, MetricType::L2),
true,
)
.await
.unwrap();
println!("Indexes built.\n");
async fn timeit<F, Fut>(iters: u32, mut f: F) -> f64
where
F: FnMut() -> Fut,
Fut: std::future::Future<Output = ()>,
{
f().await; let t0 = Instant::now();
for _ in 0..iters {
f().await;
}
t0.elapsed().as_secs_f64() * 1000.0 / iters as f64
}
println!("=== Scenario A: BTree (overlay on `age`, fragment 0 becomes stale) ===");
println!(
"{:>10} {:>14} {:>14}",
"overlays", "cold_frag0_ms", "warm_frag1_ms"
);
let mut committed_a = 0u32;
for num_overlays in [0u32, 1, 4, 16] {
for layer in committed_a..num_overlays {
dataset = commit_overlay(
dataset,
&format!("age_ol{layer}"),
0, &[1], OverlayCoverage::dense(RoaringBitmap::from_iter([0u32])),
vec![i32_array([Some(999)])],
)
.await;
}
committed_a = num_overlays;
let ds = Arc::new(dataset.clone());
let ds2 = ds.clone();
let cold_ms = timeit(ITERS, || {
let ds = ds2.clone();
async move {
ds.scan()
.filter("age = 420")
.unwrap()
.project(&["age"])
.unwrap()
.try_into_batch()
.await
.unwrap();
}
})
.await;
let ds2 = ds.clone();
let warm_ms = timeit(ITERS, || {
let ds = ds2.clone();
async move {
ds.scan()
.filter("age = 1000420")
.unwrap()
.project(&["age"])
.unwrap()
.try_into_batch()
.await
.unwrap();
}
})
.await;
println!("{num_overlays:>10} {cold_ms:>14.1} {warm_ms:>14.1}");
}
println!("\n=== Scenario B: Vector ANN (overlay on `vec`, 100k rows brute-forced) ===");
println!("{:>12} {:>10}", "vec_overlays", "ann_ms");
let query_vec = Float32Array::from(vec![0.5f32; DIM as usize]);
for num_vec_overlays in [0u32, 1] {
if num_vec_overlays == 1 {
dataset = commit_overlay(
dataset,
"vec_ol0",
0, &[2], OverlayCoverage::dense(RoaringBitmap::from_iter([0u32])),
vec![fsl(vec![vec![0.0f32; DIM as usize]], DIM)],
)
.await;
}
let ds = Arc::new(dataset.clone());
let ds2 = ds.clone();
let qv = query_vec.clone();
let ann_ms = timeit(ITERS, || {
let ds = ds2.clone();
let q = qv.clone();
async move {
ds.scan()
.nearest("vec", &q, 10)
.unwrap()
.minimum_nprobes(1)
.project(&["id"])
.unwrap()
.try_into_batch()
.await
.unwrap();
}
})
.await;
println!("{num_vec_overlays:>12} {ann_ms:>10.1}");
}
}