This is an automated email from the ASF dual-hosted git repository.
JingsongLi pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/paimon-rust.git
The following commit(s) were added to refs/heads/main by this push:
new 017d1a48 perf(table): create pk-vector exact-fallback readers on
demand (#535)
017d1a48 is described below
commit 017d1a4819a9935ebf2a3927c0458bc788e772ec
Author: Junrui Lee <[email protected]>
AuthorDate: Sun Jul 19 09:00:35 2026 +0800
perf(table): create pk-vector exact-fallback readers on demand (#535)
---
crates/paimon/src/table/pk_vector_orchestrator.rs | 215 ++++++++++------
crates/paimon/src/table/vector_search_builder.rs | 117 +++------
crates/paimon/src/vindex/pkvector/bucket.rs | 294 +++++++++++++---------
3 files changed, 341 insertions(+), 285 deletions(-)
diff --git a/crates/paimon/src/table/pk_vector_orchestrator.rs
b/crates/paimon/src/table/pk_vector_orchestrator.rs
index 6004a98c..e8da709e 100644
--- a/crates/paimon/src/table/pk_vector_orchestrator.rs
+++ b/crates/paimon/src/table/pk_vector_orchestrator.rs
@@ -34,9 +34,10 @@ use crate::table::data_file_reader::DataFileReader;
use crate::table::pk_vector_indexed_split_read::PkVectorIndexedSplit;
use crate::table::source::{DataSplit, DataSplitBuilder, RowRange};
use crate::vindex::pkvector::ann::PkVectorAnnSearcher;
-use crate::vindex::pkvector::bucket::{bucket_search, BucketActiveFile,
BucketAnnSegment};
+use crate::vindex::pkvector::bucket::{
+ bucket_search, BucketActiveFile, BucketAnnSegment, ExactReaderFuture,
+};
use crate::vindex::pkvector::metric::{java_float_compare, VectorSearchMetric};
-use crate::vindex::pkvector::reader::PkVectorReader;
use crate::vindex::pkvector::result::PkVectorSearchResult;
fn data_invalid(message: impl Into<String>) -> crate::Error {
@@ -46,6 +47,33 @@ fn data_invalid(message: impl Into<String>) -> crate::Error {
}
}
+/// Coerce a closure into the higher-ranked per-file exact-reader factory
shape so
+/// its returned future borrows for exactly the file argument's lifetime.
Closure
+/// return types cannot express this borrow through inference alone, so the
bound
+/// is supplied here.
+fn as_bucket_factory<F>(f: F) -> F
+where
+ F: for<'f> FnMut(&'f BucketActiveFile) -> ExactReaderFuture<'f> + Send,
+{
+ f
+}
+
+/// Coerce a closure into the split-scoped exact-reader factory shape expected
by
+/// [`PkVectorOrchestrator::search_candidates`], binding the returned future's
+/// borrow to the file argument's lifetime. Callers building a factory closure
use
+/// this so the higher-ranked bound is supplied where inference cannot.
+pub(crate) fn as_split_exact_reader_factory<F>(f: F) -> F
+where
+ F: for<'s, 'f> FnMut(
+ usize,
+ &'s PkVectorSearchSplit,
+ &'f BucketActiveFile,
+ ) -> ExactReaderFuture<'f>
+ + Send,
+{
+ f
+}
+
/// Validate a hit's physical row position against its data file, mirroring the
/// bounds Java `PrimaryKeyVectorResult.splits()` enforces per candidate: the
/// position must be non-negative, within the file's row count, and fit in an
@@ -293,11 +321,11 @@ impl PkVectorOrchestrator {
metric: VectorSearchMetric,
limit: usize,
ann_searcher: Option<&dyn PkVectorAnnSearcher>,
- exact_reader_factory: &mut (dyn FnMut(
+ exact_reader_factory: &mut (dyn for<'s, 'f> FnMut(
usize,
- &PkVectorSearchSplit,
- &BucketActiveFile,
- ) -> crate::Result<Box<dyn PkVectorReader>>
+ &'s PkVectorSearchSplit,
+ &'f BucketActiveFile,
+ ) -> ExactReaderFuture<'f>
+ Send),
search_options: &HashMap<String, String>,
skip_exact_fallback: bool,
@@ -323,8 +351,11 @@ impl PkVectorOrchestrator {
for (split_index, split) in splits.iter().enumerate() {
let dvs = build_bucket_dv_map(&self.reader, split).await?;
// Wrap the split-scoped factory into bucket_search's per-file
signature.
- let mut bucket_factory =
- |file: &BucketActiveFile| exact_reader_factory(split_index,
split, file);
+ // The coercion helper ties the produced future's borrow to the
file
+ // argument, which closure inference cannot express on its own.
+ let mut bucket_factory = as_bucket_factory(|file:
&BucketActiveFile| {
+ exact_reader_factory(split_index, split, file)
+ });
let residual_ranges = residual_by_split.map(|per_split|
&per_split[split_index]);
let results = bucket_search(
ann_searcher,
@@ -338,7 +369,8 @@ impl PkVectorOrchestrator {
search_options,
skip_exact_fallback,
residual_ranges,
- )?;
+ )
+ .await?;
for PkVectorSearchResult {
data_file_name,
row_position,
@@ -634,6 +666,7 @@ mod e2e_tests {
use crate::table::schema_manager::SchemaManager;
use crate::table::source::DeletionFile;
use crate::vindex::pkvector::reader::test_support::ArrayReader;
+ use crate::vindex::pkvector::reader::PkVectorReader;
use arrow_array::{Array, Float32Array, Int32Array, Int64Array,
RecordBatch};
use bytes::Bytes;
use futures::TryStreamExt;
@@ -800,6 +833,30 @@ mod e2e_tests {
}
}
+ /// Coerce a closure into the higher-ranked split-scoped exact-reader
factory
+ /// shape so its returned future borrows for exactly the file argument's
+ /// lifetime. Closure return types cannot express this borrow through
inference
+ /// alone, so the bound is supplied here.
+ fn as_split_factory<F>(f: F) -> F
+ where
+ F: for<'s, 'f> FnMut(
+ usize,
+ &'s PkVectorSearchSplit,
+ &'f BucketActiveFile,
+ ) -> ExactReaderFuture<'f>
+ + Send,
+ {
+ as_split_exact_reader_factory(f)
+ }
+
+ /// Same coercion for the per-file factory used by
`materialize_via_splits`.
+ fn as_file_factory<F>(f: F) -> F
+ where
+ F: for<'f> FnMut(&'f BucketActiveFile) -> ExactReaderFuture<'f> + Send,
+ {
+ f
+ }
+
fn column_by_name<'a>(batch: &'a RecordBatch, name: &str) -> Option<&'a
Arc<dyn Array>> {
batch
.schema()
@@ -886,14 +943,14 @@ mod e2e_tests {
metric: VectorSearchMetric,
limit: usize,
ann: Option<&dyn PkVectorAnnSearcher>,
- factory: &mut (dyn FnMut(&BucketActiveFile) -> crate::Result<Box<dyn
PkVectorReader>>
- + Send),
+ factory: &mut (dyn for<'f> FnMut(&'f BucketActiveFile) ->
ExactReaderFuture<'f> + Send),
opts: &HashMap<String, String>,
) -> crate::Result<Vec<RecordBatch>> {
let orch = PkVectorOrchestrator::new(reader.clone());
// Wrap the per-file factory into the split-scoped shape
search_candidates
// expects; the split index/split are unused here.
- let mut wrapped = |_: usize, _: &PkVectorSearchSplit, f:
&BucketActiveFile| factory(f);
+ let mut wrapped =
+ as_split_factory(|_: usize, _: &PkVectorSearchSplit, f:
&BucketActiveFile| factory(f));
let survivors = orch
.search_candidates(
splits,
@@ -924,12 +981,11 @@ mod e2e_tests {
let file_io = FileIOBuilder::new("memory").build().unwrap();
let reader = make_reader(file_io, "memory:/pkvo_zero");
let splits: Vec<PkVectorSearchSplit> = Vec::new();
- let mut factory = |_: usize,
- _: &PkVectorSearchSplit,
- _: &BucketActiveFile|
- -> crate::Result<Box<dyn PkVectorReader>> {
- unreachable!("no bucket search on eager-rejected input")
- };
+ let mut factory = as_split_factory(
+ |_: usize, _: &PkVectorSearchSplit, _: &BucketActiveFile| ->
ExactReaderFuture<'_> {
+ Box::pin(async { unreachable!("no bucket search on
eager-rejected input") })
+ },
+ );
let opts = HashMap::new();
let err = PkVectorOrchestrator::new(reader)
.search_candidates(
@@ -954,12 +1010,11 @@ mod e2e_tests {
let file_io = FileIOBuilder::new("memory").build().unwrap();
let reader = make_reader(file_io, "memory:/pkvo_empty_query");
let splits: Vec<PkVectorSearchSplit> = Vec::new();
- let mut factory = |_: usize,
- _: &PkVectorSearchSplit,
- _: &BucketActiveFile|
- -> crate::Result<Box<dyn PkVectorReader>> {
- unreachable!("no bucket search on eager-rejected input")
- };
+ let mut factory = as_split_factory(
+ |_: usize, _: &PkVectorSearchSplit, _: &BucketActiveFile| ->
ExactReaderFuture<'_> {
+ Box::pin(async { unreachable!("no bucket search on
eager-rejected input") })
+ },
+ );
let opts = HashMap::new();
let err = PkVectorOrchestrator::new(reader)
.search_candidates(
@@ -1013,13 +1068,15 @@ mod e2e_tests {
}],
};
// Exact fallback scans only "exact.mosaic": pos0 {1,0} d=1.0, pos1
{2,0} d=4.0.
- let mut factory = |f: &BucketActiveFile| -> crate::Result<Box<dyn
PkVectorReader>> {
+ let mut factory = as_file_factory(|f: &BucketActiveFile| ->
ExactReaderFuture<'_> {
let vectors = match f.file_name.as_str() {
"exact.mosaic" => vec![Some(vec![1.0, 0.0]), Some(vec![2.0,
0.0])],
other => panic!("unexpected exact scan of covered file
{other}"),
};
- Ok(Box::new(ArrayReader::new(2, vectors)))
- };
+ Box::pin(async move {
+ Ok(Box::new(ArrayReader::new(2, vectors)) as Box<dyn
PkVectorReader>)
+ })
+ });
let opts = HashMap::new();
let batches = materialize_via_splits(
make_reader(file_io, table_path),
@@ -1094,7 +1151,7 @@ mod e2e_tests {
// b0: x = 1,4,6 -> d = 1,16,36. b1: x = 2,3,5 -> d = 4,9,25.
// Global best 3: d1 (b0 pos0 id10), d4 (b1 pos0 id20), d9 (b1 pos1
id21).
- let mut factory = |f: &BucketActiveFile| -> crate::Result<Box<dyn
PkVectorReader>> {
+ let mut factory = as_file_factory(|f: &BucketActiveFile| ->
ExactReaderFuture<'_> {
let vectors = match f.file_name.as_str() {
"b0.mosaic" => vec![
Some(vec![1.0, 0.0]),
@@ -1108,8 +1165,10 @@ mod e2e_tests {
],
other => panic!("unexpected file {other}"),
};
- Ok(Box::new(ArrayReader::new(2, vectors)))
- };
+ Box::pin(async move {
+ Ok(Box::new(ArrayReader::new(2, vectors)) as Box<dyn
PkVectorReader>)
+ })
+ });
let opts = HashMap::new();
let batches = materialize_via_splits(
make_reader(file_io, table_path),
@@ -1163,7 +1222,7 @@ mod e2e_tests {
};
// pos0 {1,0} d=1, pos1 {2,0} d=4 (DELETED), pos2 {3,0} d=9, pos3
{0,0} d=0.
- let mut factory = |f: &BucketActiveFile| -> crate::Result<Box<dyn
PkVectorReader>> {
+ let mut factory = as_file_factory(|f: &BucketActiveFile| ->
ExactReaderFuture<'_> {
let vectors = match f.file_name.as_str() {
"d.mosaic" => vec![
Some(vec![1.0, 0.0]),
@@ -1173,8 +1232,10 @@ mod e2e_tests {
],
other => panic!("unexpected file {other}"),
};
- Ok(Box::new(ArrayReader::new(2, vectors)))
- };
+ Box::pin(async move {
+ Ok(Box::new(ArrayReader::new(2, vectors)) as Box<dyn
PkVectorReader>)
+ })
+ });
let opts = HashMap::new();
let batches = materialize_via_splits(
make_reader(file_io, table_path),
@@ -1224,7 +1285,7 @@ mod e2e_tests {
};
// pos0 {3,0} d=9, pos1 {1,0} d=1, pos2 {2,0} d=4. Best-first =
[1,2,0].
- let mut factory = |f: &BucketActiveFile| -> crate::Result<Box<dyn
PkVectorReader>> {
+ let mut factory = as_file_factory(|f: &BucketActiveFile| ->
ExactReaderFuture<'_> {
let vectors = match f.file_name.as_str() {
"o.mosaic" => vec![
Some(vec![3.0, 0.0]),
@@ -1233,8 +1294,10 @@ mod e2e_tests {
],
other => panic!("unexpected file {other}"),
};
- Ok(Box::new(ArrayReader::new(2, vectors)))
- };
+ Box::pin(async move {
+ Ok(Box::new(ArrayReader::new(2, vectors)) as Box<dyn
PkVectorReader>)
+ })
+ });
let opts = HashMap::new();
let batches = materialize_via_splits(
make_reader(file_io, table_path),
@@ -1283,20 +1346,21 @@ mod e2e_tests {
active_files: vec![active("c.mosaic", 3)],
};
// pos0 {3,0} d=9, pos1 {1,0} d=1, pos2 {2,0} d=4.
- let mut factory = |_: usize,
- _: &PkVectorSearchSplit,
- f: &BucketActiveFile|
- -> crate::Result<Box<dyn PkVectorReader>> {
- assert_eq!(f.file_name, "c.mosaic");
- Ok(Box::new(ArrayReader::new(
- 2,
- vec![
- Some(vec![3.0, 0.0]),
- Some(vec![1.0, 0.0]),
- Some(vec![2.0, 0.0]),
- ],
- )))
- };
+ let mut factory = as_split_factory(
+ |_: usize, _: &PkVectorSearchSplit, f: &BucketActiveFile| ->
ExactReaderFuture<'_> {
+ assert_eq!(f.file_name, "c.mosaic");
+ Box::pin(async {
+ Ok(Box::new(ArrayReader::new(
+ 2,
+ vec![
+ Some(vec![3.0, 0.0]),
+ Some(vec![1.0, 0.0]),
+ Some(vec![2.0, 0.0]),
+ ],
+ )) as Box<dyn PkVectorReader>)
+ })
+ },
+ );
let opts = HashMap::new();
let cands = PkVectorOrchestrator::new(make_reader(file_io, table_path))
.search_candidates(
@@ -1348,20 +1412,21 @@ mod e2e_tests {
active_files: vec![active("r.mosaic", 3)],
};
// pos0 {3,0} d=9, pos1 {1,0} d=1, pos2 {2,0} d=4.
- let mut factory = |_: usize,
- _: &PkVectorSearchSplit,
- f: &BucketActiveFile|
- -> crate::Result<Box<dyn PkVectorReader>> {
- assert_eq!(f.file_name, "r.mosaic");
- Ok(Box::new(ArrayReader::new(
- 2,
- vec![
- Some(vec![3.0, 0.0]),
- Some(vec![1.0, 0.0]),
- Some(vec![2.0, 0.0]),
- ],
- )))
- };
+ let mut factory = as_split_factory(
+ |_: usize, _: &PkVectorSearchSplit, f: &BucketActiveFile| ->
ExactReaderFuture<'_> {
+ assert_eq!(f.file_name, "r.mosaic");
+ Box::pin(async {
+ Ok(Box::new(ArrayReader::new(
+ 2,
+ vec![
+ Some(vec![3.0, 0.0]),
+ Some(vec![1.0, 0.0]),
+ Some(vec![2.0, 0.0]),
+ ],
+ )) as Box<dyn PkVectorReader>)
+ })
+ },
+ );
// Allow only positions 0 and 2 for "r.mosaic"; pos1 (the best hit) is
// excluded by the residual.
let mut allowed = RoaringTreemap::new();
@@ -1415,12 +1480,11 @@ mod e2e_tests {
ann_segments: Vec::new(),
active_files: vec![active("m.mosaic", 2)],
};
- let mut factory = |_: usize,
- _: &PkVectorSearchSplit,
- _: &BucketActiveFile|
- -> crate::Result<Box<dyn PkVectorReader>> {
- unreachable!("length guard must fire before any bucket search")
- };
+ let mut factory = as_split_factory(
+ |_: usize, _: &PkVectorSearchSplit, _: &BucketActiveFile| ->
ExactReaderFuture<'_> {
+ Box::pin(async { unreachable!("length guard must fire before
any bucket search") })
+ },
+ );
// Two residual maps for a single split.
let residual_by_split: Vec<HashMap<String, RoaringTreemap>> =
vec![HashMap::new(), HashMap::new()];
@@ -1465,12 +1529,11 @@ mod e2e_tests {
ann_segments: Vec::new(),
active_files: vec![active("f.mosaic", 2)],
};
- let mut factory = |_: usize,
- _: &PkVectorSearchSplit,
- _: &BucketActiveFile|
- -> crate::Result<Box<dyn PkVectorReader>> {
- unreachable!("fast mode must not read exact")
- };
+ let mut factory = as_split_factory(
+ |_: usize, _: &PkVectorSearchSplit, _: &BucketActiveFile| ->
ExactReaderFuture<'_> {
+ Box::pin(async { unreachable!("fast mode must not read exact")
})
+ },
+ );
let opts = HashMap::new();
let cands = PkVectorOrchestrator::new(make_reader(file_io, table_path))
.search_candidates(
diff --git a/crates/paimon/src/table/vector_search_builder.rs
b/crates/paimon/src/table/vector_search_builder.rs
index facc1f99..2ce2f8df 100644
--- a/crates/paimon/src/table/vector_search_builder.rs
+++ b/crates/paimon/src/table/vector_search_builder.rs
@@ -32,8 +32,8 @@ use crate::table::global_index_scanner::{
use crate::table::pk_vector_data_file_reader::DataFilePkVectorReaderFactory;
use crate::table::pk_vector_indexed_split_read::PkVectorIndexedSplitRead;
use crate::table::pk_vector_orchestrator::{
- build_indexed_splits, validate_row_position, PkVectorCandidate,
PkVectorOrchestrator,
- PkVectorSearchSplit,
+ as_split_exact_reader_factory, build_indexed_splits,
validate_row_position, PkVectorCandidate,
+ PkVectorOrchestrator, PkVectorSearchSplit,
};
use crate::table::pk_vector_position_read::{
PKEY_VECTOR_POSITION_COLUMN, PKEY_VECTOR_SCORE_COLUMN,
@@ -47,9 +47,8 @@ use crate::table::{
use crate::vector_search::{GlobalIndexIOMeta, SearchResult, VectorSearch};
use crate::vindex::is_vindex_index_type;
use crate::vindex::pkvector::ann::VindexAnnSearcher;
-use crate::vindex::pkvector::bucket::{covered_source_files, BucketActiveFile,
BucketAnnSegment};
+use crate::vindex::pkvector::bucket::{BucketActiveFile, BucketAnnSegment,
ExactReaderFuture};
use crate::vindex::pkvector::metric::VectorSearchMetric;
-use crate::vindex::pkvector::reader::PkVectorReader;
use crate::vindex::reader::VindexVectorGlobalIndexReader;
use arrow_array::{Array, FixedSizeListArray, Float32Array, Int64Array,
ListArray, RecordBatch};
use arrow_select::interleave::interleave_record_batch;
@@ -425,8 +424,8 @@ impl<'a> VectorSearchBuilder<'a> {
// when a filter is set; otherwise `None` leaves the search
unfiltered. The
// residual reader projects the predicate columns plus `_ROW_ID` (used
to
// recover file-local physical positions) and carries no pushdown,
matching
- // `residual_positions_by_file`. Computed before the exact-reader
preload so
- // the preload can skip files the residual allow-list leaves empty.
+ // `residual_positions_by_file`. A file the allow-list leaves empty is
+ // skipped by the bucket search without opening an exact reader.
let residual_by_split: Option<Vec<HashMap<String, RoaringTreemap>>> =
match &self.filter {
Some(filter) => {
let file_predicates = FilePredicates {
@@ -465,50 +464,32 @@ impl<'a> VectorSearchBuilder<'a> {
None => None,
};
- // Exact-fallback readers, keyed by (split_index, file_name). In FAST
mode
- // the kernel never invokes the factory, so skip the in-memory column
read
- // entirely. Otherwise preload only the *uncovered* active files:
files an
- // ANN segment already covers never reach the exact fallback, so
reading
- // their vector column here would be wasted IO/memory. Mirrors Java,
which
- // creates a `PkVectorReader` lazily only for uncovered files. When a
- // residual filter leaves a file's allow-list empty (or absent) the
bucket
- // search skips it, so its reader is not preloaded either.
- let mut exact_readers: HashMap<(usize, String), Box<dyn
PkVectorReader>> = HashMap::new();
- if !skip_exact_fallback {
- for (split_index, split) in plan.splits.iter().enumerate() {
- let covered = covered_source_files(&split.ann_segments,
&split.active_files);
- let factory = DataFilePkVectorReaderFactory::new(
- reader.clone(),
- split.data_split.clone(),
- vector_field.clone(),
- )?;
- for active in &split.active_files {
- if covered.contains(&active.file_name) {
- continue;
- }
- if !should_preload_exact_reader(
- residual_by_split.as_deref(),
- split_index,
- &active.file_name,
- ) {
- continue;
- }
- let r = factory.create(active).await?;
- exact_readers.insert((split_index,
active.file_name.clone()), r);
- }
- }
- }
- let mut factory = |split_index: usize,
- _split: &PkVectorSearchSplit,
- file: &BucketActiveFile|
- -> crate::Result<Box<dyn PkVectorReader>> {
- exact_readers
- .remove(&(split_index, file.file_name.clone()))
- .ok_or_else(|| crate::Error::DataInvalid {
- message: format!("no preloaded exact reader for {}",
file.file_name),
- source: None,
+ // Build the exact-fallback vector reader on demand: the kernel calls
this
+ // only for a file it actually searches (uncovered by ANN,
residual-allowed,
+ // and only when the search mode is not FAST). Everything the future
needs is
+ // cloned/owned up front so it borrows neither the split nor the file
across
+ // the await.
+ let reader_for_factory = reader.clone();
+ let vector_field_for_factory = vector_field.clone();
+ let mut factory = as_split_exact_reader_factory(
+ move |_split_index: usize,
+ split: &PkVectorSearchSplit,
+ file: &BucketActiveFile|
+ -> ExactReaderFuture<'_> {
+ let reader = reader_for_factory.clone();
+ let vector_field = vector_field_for_factory.clone();
+ let data_split = split.data_split.clone();
+ let active = BucketActiveFile {
+ file_name: file.file_name.clone(),
+ row_count: file.row_count,
+ };
+ Box::pin(async move {
+ let factory =
+ DataFilePkVectorReaderFactory::new(reader, data_split,
vector_field)?;
+ factory.create(&active).await
})
- };
+ },
+ );
let candidates = PkVectorOrchestrator::new(reader)
.search_candidates(
@@ -1022,24 +1003,6 @@ fn is_vector_global_index_file(index_file:
&IndexFileMeta) -> bool {
VectorIndexBackend::from_index_type(&index_file.index_type).is_some()
}
-/// Whether the exact-fallback reader for `file_name` in split `split_index`
-/// should be preloaded. With a residual filter, a file absent from the split's
-/// allow-list or with an empty allow-list has no candidate rows, so the bucket
-/// search skips it and preloading its vector column would be wasted IO.
-fn should_preload_exact_reader(
- residual_by_split: Option<&[HashMap<String, RoaringTreemap>]>,
- split_index: usize,
- file_name: &str,
-) -> bool {
- match residual_by_split {
- None => true,
- Some(per_split) => per_split
- .get(split_index)
- .and_then(|m| m.get(file_name))
- .is_some_and(|allowed| !allowed.is_empty()),
- }
-}
-
/// Compute, per data file in `split`, the set of physical row positions whose
/// rows satisfy the residual predicate. Mirrors the row-collecting half of
Java
/// `PrimaryKeyVectorRead`'s `executeFilter`: because
@@ -2232,26 +2195,6 @@ mod tests {
assert_eq!(find_field_id_by_name(&fields, "nonexistent"), None);
}
- #[test]
- fn should_preload_skips_empty_or_absent_residual_files() {
- use roaring::RoaringTreemap;
- use std::collections::HashMap;
- // No filter -> always preload.
- assert!(should_preload_exact_reader(None, 0, "f0"));
- // Filter present: file with a non-empty allow-list -> preload.
- let mut m0: HashMap<String, RoaringTreemap> = HashMap::new();
- m0.insert("f0".to_string(), RoaringTreemap::from_iter([0u64]));
- let per_split = vec![m0];
- assert!(should_preload_exact_reader(Some(&per_split), 0, "f0"));
- // Filter present: file absent -> skip.
- assert!(!should_preload_exact_reader(Some(&per_split), 0, "missing"));
- // Filter present: file with empty allow-list -> skip.
- let mut m1: HashMap<String, RoaringTreemap> = HashMap::new();
- m1.insert("f1".to_string(), RoaringTreemap::new());
- let per_split2 = vec![m1];
- assert!(!should_preload_exact_reader(Some(&per_split2), 0, "f1"));
- }
-
#[test]
fn test_raw_vector_score_matches_java_metric_semantics() {
let l2 = compute_raw_vector_score(&[1.0, 2.0], &[1.0, 4.0],
RawVectorMetric::L2);
diff --git a/crates/paimon/src/vindex/pkvector/bucket.rs
b/crates/paimon/src/vindex/pkvector/bucket.rs
index 604ac7dc..137d7fc0 100644
--- a/crates/paimon/src/vindex/pkvector/bucket.rs
+++ b/crates/paimon/src/vindex/pkvector/bucket.rs
@@ -19,6 +19,8 @@ use std::cmp::Ordering;
use std::collections::{BinaryHeap, HashMap, HashSet};
use std::sync::Arc;
+use futures::future::BoxFuture;
+
use super::ann::PkVectorAnnSearcher;
use super::data_invalid;
use super::exact::exact_search;
@@ -28,6 +30,10 @@ use super::result::PkVectorSearchResult;
use crate::deletion_vector::DeletionVector;
use crate::spec::PkVectorSourceMeta;
+/// Future returned by the exact-fallback reader factory: builds the sequential
+/// vector reader for one data file on demand.
+pub(crate) type ExactReaderFuture<'a> = BoxFuture<'a, crate::Result<Box<dyn
PkVectorReader>>>;
+
/// One ANN segment to be searched by the bucket kernel. `source_meta` resolves
/// segment ordinals back to physical `(data file, position)` and drives
live-row
/// masking; the remaining fields address the segment's index file for the ANN
@@ -110,11 +116,10 @@ fn add_candidate(heap: &mut BinaryHeap<WorstFirst>,
candidate: PkVectorSearchRes
/// Active data files whose rows are already covered by an ANN segment's source
/// metadata, matched by both file name AND row count. The bucket exact
fallback
-/// skips these files, so a caller that preloads exact readers should preload
-/// only the *uncovered* active files (`active_files` minus this set) rather
than
-/// reading every active file's vector column up front. A source naming an
-/// inactive file, or one whose row count disagrees with the active file, is
not
-/// covered here; `bucket_search` rejects the row-count mismatch separately.
+/// skips these files (an ANN segment already covers their rows), so they never
+/// need an exact reader. A source naming an inactive file, or one whose row
+/// count disagrees with the active file, is not covered here; `bucket_search`
+/// rejects the row-count mismatch separately.
pub(crate) fn covered_source_files(
ann_segments: &[BucketAnnSegment],
active_files: &[BucketActiveFile],
@@ -150,14 +155,13 @@ pub(crate) fn covered_source_files(
/// with an empty set) has no allowed rows and produces no candidates. Mirrors
Java
/// `rowRangesByFile`.
#[allow(clippy::too_many_arguments)]
-pub(crate) fn bucket_search(
+pub(crate) async fn bucket_search(
ann_searcher: Option<&dyn PkVectorAnnSearcher>,
ann_segments: &[BucketAnnSegment],
active_files: &[BucketActiveFile],
deletion_vectors: &HashMap<String, Arc<DeletionVector>>,
- exact_reader_factory: &mut dyn FnMut(
- &BucketActiveFile,
- ) -> crate::Result<Box<dyn PkVectorReader>>,
+ exact_reader_factory: &mut (dyn for<'a> FnMut(&'a BucketActiveFile) ->
ExactReaderFuture<'a>
+ + Send),
query: &[f32],
metric: VectorSearchMetric,
limit: usize,
@@ -219,8 +223,7 @@ pub(crate) fn bucket_search(
let active_source_files: HashSet<String> =
files_by_name.keys().map(|name| name.to_string()).collect();
// Active files whose rows an ANN segment already covers; the exact
fallback
- // skips them. Same rule the caller's exact-reader preload uses, so both
agree
- // on which files still need an exact reader.
+ // skips them, so the lazy exact-reader factory is never invoked for those
files.
let covered = covered_source_files(ann_segments, active_files);
for segment in ann_segments {
@@ -290,7 +293,7 @@ pub(crate) fn bucket_search(
},
}
};
- let mut reader = exact_reader_factory(file)?;
+ let mut reader = exact_reader_factory(file).await?;
for result in exact_search(
&file.file_name,
reader.as_mut(),
@@ -316,7 +319,6 @@ mod tests {
use crate::vindex::pkvector::ann::PkVectorAnnSearcher;
use crate::vindex::pkvector::reader::test_support::ArrayReader;
use roaring::RoaringBitmap;
- use std::cell::RefCell;
fn meta(files: &[(&str, i64)]) -> PkVectorSourceMeta {
PkVectorSourceMeta::new(
@@ -336,6 +338,17 @@ mod tests {
}
}
+ /// Coerce a closure into the higher-ranked exact-reader factory shape so
its
+ /// returned future borrows for exactly the argument's lifetime. Closure
return
+ /// types cannot express this borrow through inference alone, so the bound
is
+ /// supplied here.
+ fn as_factory<F>(f: F) -> F
+ where
+ F: for<'a> FnMut(&'a BucketActiveFile) -> ExactReaderFuture<'a> + Send,
+ {
+ f
+ }
+
/// Fake ANN searcher returning preset results and recording calls.
struct FakeAnnSearcher {
result: Vec<PkVectorSearchResult>,
@@ -356,10 +369,11 @@ mod tests {
}
}
- #[test]
- fn test_rejects_non_positive_limit() {
- let mut factory =
- |_: &BucketActiveFile| -> crate::Result<Box<dyn PkVectorReader>> {
unreachable!() };
+ #[tokio::test]
+ async fn test_rejects_non_positive_limit() {
+ let mut factory = as_factory(|_: &BucketActiveFile| ->
ExactReaderFuture<'_> {
+ Box::pin(async { unreachable!() })
+ });
let err = bucket_search(
None,
&[],
@@ -373,12 +387,13 @@ mod tests {
false,
None,
)
+ .await
.unwrap_err();
assert!(err.to_string().contains("positive"));
}
- #[test]
- fn test_bounded_heap_evicts_by_best_first_tiebreak_over_limit() {
+ #[tokio::test]
+ async fn test_bounded_heap_evicts_by_best_first_tiebreak_over_limit() {
// All candidates share distance 1.0, so eviction is decided purely by
the
// BEST_FIRST tie-break (data_file_name ASC, then row_position ASC).
Feed
// more than `limit` ANN hits and assert the kept set is the smallest
@@ -399,8 +414,9 @@ mod tests {
hit("data-1", 1),
],
};
- let mut factory =
- |_: &BucketActiveFile| -> crate::Result<Box<dyn PkVectorReader>> {
unreachable!() };
+ let mut factory = as_factory(|_: &BucketActiveFile| ->
ExactReaderFuture<'_> {
+ Box::pin(async { unreachable!() })
+ });
let results = bucket_search(
Some(&ann),
&[segment],
@@ -414,6 +430,7 @@ mod tests {
false,
None,
)
+ .await
.unwrap();
// Top-3 BEST_FIRST: (data-1,0), (data-1,1), (data-1,2) — the larger
// data_file_name "data-2" entries are evicted despite equal distance.
@@ -426,8 +443,8 @@ mod tests {
);
}
- #[test]
- fn nan_ann_hit_never_evicts_finite_candidate_from_top1() {
+ #[tokio::test]
+ async fn nan_ann_hit_never_evicts_finite_candidate_from_top1() {
// The core failure mode: an ANN hit with a negative-NaN distance must
not
// win the single bucket Top-1 slot over a finite hit. Under
f32::total_cmp
// the -NaN would rank best and evict the finite candidate here in the
@@ -449,8 +466,9 @@ mod tests {
},
],
};
- let mut factory =
- |_: &BucketActiveFile| -> crate::Result<Box<dyn PkVectorReader>> {
unreachable!() };
+ let mut factory = as_factory(|_: &BucketActiveFile| ->
ExactReaderFuture<'_> {
+ Box::pin(async { unreachable!() })
+ });
let results = bucket_search(
Some(&ann),
&[segment],
@@ -464,14 +482,15 @@ mod tests {
false,
None,
)
+ .await
.unwrap();
assert_eq!(results.len(), 1);
assert_eq!(results[0].row_position, 1);
assert_eq!(results[0].distance, -1.0);
}
- #[test]
- fn test_merges_ann_and_exact_without_rescanning_covered_files() {
+ #[tokio::test]
+ async fn test_merges_ann_and_exact_without_rescanning_covered_files() {
// data-1 is ANN-covered; data-2 is exact fallback. Factory must never
be
// called for data-1.
let segment = BucketAnnSegment::for_test(meta(&[("data-1", 2)]));
@@ -482,15 +501,17 @@ mod tests {
distance: 0.5,
}],
};
- let calls = RefCell::new(Vec::<String>::new());
- let mut factory = |f: &BucketActiveFile| -> crate::Result<Box<dyn
PkVectorReader>> {
- calls.borrow_mut().push(f.file_name.clone());
+ let calls = std::sync::Mutex::new(Vec::<String>::new());
+ let mut factory = as_factory(|f: &BucketActiveFile| ->
ExactReaderFuture<'_> {
+ calls.lock().unwrap().push(f.file_name.clone());
// data-2 vectors: pos0 {1,0} dist 1.0, pos1 {3,0} dist 9.0
- Ok(Box::new(ArrayReader::new(
- 2,
- vec![Some(vec![1.0, 0.0]), Some(vec![3.0, 0.0])],
- )))
- };
+ Box::pin(async {
+ Ok(Box::new(ArrayReader::new(
+ 2,
+ vec![Some(vec![1.0, 0.0]), Some(vec![3.0, 0.0])],
+ )) as Box<dyn PkVectorReader>)
+ })
+ });
let results = bucket_search(
Some(&ann),
&[segment],
@@ -504,6 +525,7 @@ mod tests {
false,
None,
)
+ .await
.unwrap();
assert_eq!(
results,
@@ -520,22 +542,24 @@ mod tests {
},
]
);
- assert_eq!(calls.borrow().as_slice(), &["data-2".to_string()]);
+ assert_eq!(calls.lock().unwrap().as_slice(), &["data-2".to_string()]);
}
- #[test]
- fn test_exact_fallback_merges_files_and_applies_deletion_vectors() {
+ #[tokio::test]
+ async fn test_exact_fallback_merges_files_and_applies_deletion_vectors() {
// No ANN. data-1 pos0 {0,0} deleted; remaining candidates merge
across files.
- let calls = RefCell::new(0);
- let mut factory = |f: &BucketActiveFile| -> crate::Result<Box<dyn
PkVectorReader>> {
- *calls.borrow_mut() += 1;
+ let calls = std::sync::Mutex::new(0usize);
+ let mut factory = as_factory(|f: &BucketActiveFile| ->
ExactReaderFuture<'_> {
+ *calls.lock().unwrap() += 1;
let vectors = match f.file_name.as_str() {
"data-1" => vec![Some(vec![0.0, 0.0]), Some(vec![2.0, 0.0])],
"data-2" => vec![Some(vec![1.0, 0.0]), None],
_ => unreachable!(),
};
- Ok(Box::new(ArrayReader::new(2, vectors)))
- };
+ Box::pin(async move {
+ Ok(Box::new(ArrayReader::new(2, vectors)) as Box<dyn
PkVectorReader>)
+ })
+ });
let mut dvs: HashMap<String, Arc<DeletionVector>> = HashMap::new();
let mut bm = RoaringBitmap::new();
bm.insert(0); // data-1 position 0 deleted
@@ -554,6 +578,7 @@ mod tests {
false,
None,
)
+ .await
.unwrap();
// Candidates: data-2 pos0 {1,0} dist 1.0; data-1 pos1 {2,0} dist 4.0.
// (data-1 pos0 deleted, data-2 pos1 null.)
@@ -574,10 +599,11 @@ mod tests {
);
}
- #[test]
- fn test_rejects_duplicate_active_file_name() {
- let mut factory =
- |_: &BucketActiveFile| -> crate::Result<Box<dyn PkVectorReader>> {
unreachable!() };
+ #[tokio::test]
+ async fn test_rejects_duplicate_active_file_name() {
+ let mut factory = as_factory(|_: &BucketActiveFile| ->
ExactReaderFuture<'_> {
+ Box::pin(async { unreachable!() })
+ });
let err = bucket_search(
None,
&[],
@@ -591,18 +617,20 @@ mod tests {
false,
None,
)
+ .await
.unwrap_err();
assert!(err.to_string().contains("duplicate") ||
err.to_string().contains("Duplicate"));
}
- #[test]
- fn test_rejects_ann_source_row_count_mismatch_for_active_file() {
+ #[tokio::test]
+ async fn test_rejects_ann_source_row_count_mismatch_for_active_file() {
let ann = FakeAnnSearcher { result: vec![] };
// Segment references data-1 with 2 rows, but the active file has 3
rows.
// An active source with a mismatched row count is still a hard error.
let segment = BucketAnnSegment::for_test(meta(&[("data-1", 2)]));
- let mut factory =
- |_: &BucketActiveFile| -> crate::Result<Box<dyn PkVectorReader>> {
unreachable!() };
+ let mut factory = as_factory(|_: &BucketActiveFile| ->
ExactReaderFuture<'_> {
+ Box::pin(async { unreachable!() })
+ });
let err = bucket_search(
Some(&ann),
&[segment],
@@ -616,14 +644,15 @@ mod tests {
false,
None,
)
+ .await
.unwrap_err();
assert!(
err.to_string().contains("does not match") ||
err.to_string().contains("ANN source")
);
}
- #[test]
- fn test_skips_inactive_ann_source_and_searches_active_ones() {
+ #[tokio::test]
+ async fn test_skips_inactive_ann_source_and_searches_active_ones() {
// Segment covers [data-1, data-2] but only data-1 is still active
// (data-2 was compacted away). Java master skips the inactive source
// instead of failing the whole query; data-2 is neither covered (so it
@@ -637,11 +666,11 @@ mod tests {
distance: 0.5,
}],
};
- let calls = RefCell::new(Vec::<String>::new());
- let mut factory = |f: &BucketActiveFile| -> crate::Result<Box<dyn
PkVectorReader>> {
- calls.borrow_mut().push(f.file_name.clone());
- unreachable!("only data-1 is active and it is ANN-covered")
- };
+ let calls = std::sync::Mutex::new(Vec::<String>::new());
+ let mut factory = as_factory(|f: &BucketActiveFile| ->
ExactReaderFuture<'_> {
+ calls.lock().unwrap().push(f.file_name.clone());
+ Box::pin(async { unreachable!("only data-1 is active and it is
ANN-covered") })
+ });
let results = bucket_search(
Some(&ann),
&[segment],
@@ -655,6 +684,7 @@ mod tests {
false,
None,
)
+ .await
.unwrap();
assert_eq!(
results,
@@ -665,14 +695,15 @@ mod tests {
}]
);
// No exact fallback ran: data-1 is ANN-covered, data-2 is not active.
- assert!(calls.borrow().is_empty());
+ assert!(calls.lock().unwrap().is_empty());
}
- #[test]
- fn test_rejects_segments_without_ann_searcher() {
+ #[tokio::test]
+ async fn test_rejects_segments_without_ann_searcher() {
let segment = BucketAnnSegment::for_test(meta(&[("data-1", 2)]));
- let mut factory =
- |_: &BucketActiveFile| -> crate::Result<Box<dyn PkVectorReader>> {
unreachable!() };
+ let mut factory = as_factory(|_: &BucketActiveFile| ->
ExactReaderFuture<'_> {
+ Box::pin(async { unreachable!() })
+ });
let err = bucket_search(
None,
&[segment],
@@ -686,6 +717,7 @@ mod tests {
false,
None,
)
+ .await
.unwrap_err();
assert!(
err.to_string().contains("ANN search is not configured")
@@ -693,12 +725,13 @@ mod tests {
);
}
- #[test]
- fn test_skip_exact_fallback_does_not_call_factory() {
+ #[tokio::test]
+ async fn test_skip_exact_fallback_does_not_call_factory() {
// No ANN segments, two active files. With skip_exact_fallback = true
the
// factory must never be called and the result is empty.
- let mut factory =
- |_: &BucketActiveFile| -> crate::Result<Box<dyn PkVectorReader>> {
unreachable!() };
+ let mut factory = as_factory(|_: &BucketActiveFile| ->
ExactReaderFuture<'_> {
+ Box::pin(async { unreachable!() })
+ });
let results = bucket_search(
None,
&[],
@@ -712,12 +745,13 @@ mod tests {
true, // skip_exact_fallback
None,
)
+ .await
.unwrap();
assert!(results.is_empty());
}
- #[test]
- fn test_rejects_duplicate_ann_segment_path() {
+ #[tokio::test]
+ async fn test_rejects_duplicate_ann_segment_path() {
let seg1 = BucketAnnSegment {
source_meta: meta(&[("data-1", 2)]),
path: "duplicate-path".to_string(),
@@ -731,8 +765,9 @@ mod tests {
index_meta: vec![4, 5, 6],
};
let ann = FakeAnnSearcher { result: vec![] };
- let mut factory =
- |_: &BucketActiveFile| -> crate::Result<Box<dyn PkVectorReader>> {
unreachable!() };
+ let mut factory = as_factory(|_: &BucketActiveFile| ->
ExactReaderFuture<'_> {
+ Box::pin(async { unreachable!() })
+ });
let err = bucket_search(
Some(&ann),
&[seg1, seg2],
@@ -746,6 +781,7 @@ mod tests {
false,
None,
)
+ .await
.unwrap_err();
assert!(
err.to_string().contains("duplicate-path")
@@ -753,8 +789,8 @@ mod tests {
);
}
- #[test]
- fn test_rejects_source_file_covered_by_multiple_segments() {
+ #[tokio::test]
+ async fn test_rejects_source_file_covered_by_multiple_segments() {
let seg1 = BucketAnnSegment {
source_meta: meta(&[("data-1", 2)]),
path: "segment-1".to_string(),
@@ -768,8 +804,9 @@ mod tests {
index_meta: vec![4, 5, 6],
};
let ann = FakeAnnSearcher { result: vec![] };
- let mut factory =
- |_: &BucketActiveFile| -> crate::Result<Box<dyn PkVectorReader>> {
unreachable!() };
+ let mut factory = as_factory(|_: &BucketActiveFile| ->
ExactReaderFuture<'_> {
+ Box::pin(async { unreachable!() })
+ });
let err = bucket_search(
Some(&ann),
&[seg1, seg2],
@@ -783,6 +820,7 @@ mod tests {
false,
None,
)
+ .await
.unwrap_err();
assert!(
err.to_string().contains("data-1")
@@ -792,10 +830,11 @@ mod tests {
);
}
- #[test]
- fn test_negative_active_row_count_rejected() {
- let mut factory =
- |_: &BucketActiveFile| -> crate::Result<Box<dyn PkVectorReader>> {
unreachable!() };
+ #[tokio::test]
+ async fn test_negative_active_row_count_rejected() {
+ let mut factory = as_factory(|_: &BucketActiveFile| ->
ExactReaderFuture<'_> {
+ Box::pin(async { unreachable!() })
+ });
let err = bucket_search(
None,
&[],
@@ -809,6 +848,7 @@ mod tests {
false,
None,
)
+ .await
.unwrap_err();
assert!(err.to_string().contains("row count") ||
err.to_string().contains("-1"));
}
@@ -850,21 +890,23 @@ mod tests {
t
}
- #[test]
- fn test_exact_residual_allow_list_restricts_positions() {
+ #[tokio::test]
+ async fn test_exact_residual_allow_list_restricts_positions() {
// No ANN. data-1 has 3 rows: pos0 {1,0} dist 1.0, pos1 {2,0} dist 4.0,
// pos2 {3,0} dist 9.0. residual allows only {0, 2} -> pos1 excluded
even
// though it is not deletion-vector deleted.
- let mut factory = |_: &BucketActiveFile| -> crate::Result<Box<dyn
PkVectorReader>> {
- Ok(Box::new(ArrayReader::new(
- 2,
- vec![
- Some(vec![1.0, 0.0]),
- Some(vec![2.0, 0.0]),
- Some(vec![3.0, 0.0]),
- ],
- )))
- };
+ let mut factory = as_factory(|_: &BucketActiveFile| ->
ExactReaderFuture<'_> {
+ Box::pin(async {
+ Ok(Box::new(ArrayReader::new(
+ 2,
+ vec![
+ Some(vec![1.0, 0.0]),
+ Some(vec![2.0, 0.0]),
+ Some(vec![3.0, 0.0]),
+ ],
+ )) as Box<dyn PkVectorReader>)
+ })
+ });
let mut residual: HashMap<String, roaring::RoaringTreemap> =
HashMap::new();
residual.insert("data-1".into(), treemap(&[0, 2]));
let results = bucket_search(
@@ -880,6 +922,7 @@ mod tests {
false,
Some(&residual),
)
+ .await
.unwrap();
assert_eq!(
results,
@@ -898,18 +941,20 @@ mod tests {
);
}
- #[test]
- fn test_exact_residual_file_absent_from_map_is_skipped_without_reading() {
+ #[tokio::test]
+ async fn
test_exact_residual_file_absent_from_map_is_skipped_without_reading() {
// residual covers only data-1; data-2 has no entry -> no allowed
rows, so
// data-2 is skipped entirely (its factory reader is never built).
- let calls = RefCell::new(Vec::<String>::new());
- let mut factory = |f: &BucketActiveFile| -> crate::Result<Box<dyn
PkVectorReader>> {
- calls.borrow_mut().push(f.file_name.clone());
- Ok(Box::new(ArrayReader::new(
- 2,
- vec![Some(vec![1.0, 0.0]), Some(vec![2.0, 0.0])],
- )))
- };
+ let calls = std::sync::Mutex::new(Vec::<String>::new());
+ let mut factory = as_factory(|f: &BucketActiveFile| ->
ExactReaderFuture<'_> {
+ calls.lock().unwrap().push(f.file_name.clone());
+ Box::pin(async {
+ Ok(Box::new(ArrayReader::new(
+ 2,
+ vec![Some(vec![1.0, 0.0]), Some(vec![2.0, 0.0])],
+ )) as Box<dyn PkVectorReader>)
+ })
+ });
let mut residual: HashMap<String, roaring::RoaringTreemap> =
HashMap::new();
residual.insert("data-1".into(), treemap(&[0, 1]));
let results = bucket_search(
@@ -925,21 +970,22 @@ mod tests {
false,
Some(&residual),
)
+ .await
.unwrap();
// Only data-1 rows appear; data-2 was never read.
assert!(results.iter().all(|r| r.data_file_name == "data-1"));
- assert_eq!(calls.borrow().as_slice(), &["data-1".to_string()]);
+ assert_eq!(calls.lock().unwrap().as_slice(), &["data-1".to_string()]);
}
- #[test]
- fn test_exact_residual_empty_set_file_is_skipped_without_reading() {
+ #[tokio::test]
+ async fn test_exact_residual_empty_set_file_is_skipped_without_reading() {
// data-1 has an entry but it is empty -> no allowed rows, skipped
without
// reading. Mirrors a file with no residual matches.
- let calls = RefCell::new(0);
- let mut factory = |_: &BucketActiveFile| -> crate::Result<Box<dyn
PkVectorReader>> {
- *calls.borrow_mut() += 1;
- unreachable!("data-1 has an empty allow set and must not be read")
- };
+ let calls = std::sync::Mutex::new(0usize);
+ let mut factory = as_factory(|_: &BucketActiveFile| ->
ExactReaderFuture<'_> {
+ *calls.lock().unwrap() += 1;
+ Box::pin(async { unreachable!("data-1 has an empty allow set and
must not be read") })
+ });
let mut residual: HashMap<String, roaring::RoaringTreemap> =
HashMap::new();
residual.insert("data-1".into(), treemap(&[]));
let results = bucket_search(
@@ -955,25 +1001,28 @@ mod tests {
false,
Some(&residual),
)
+ .await
.unwrap();
assert!(results.is_empty());
- assert_eq!(*calls.borrow(), 0);
+ assert_eq!(*calls.lock().unwrap(), 0);
}
- #[test]
- fn test_exact_residual_intersects_with_deletion_vector() {
+ #[tokio::test]
+ async fn test_exact_residual_intersects_with_deletion_vector() {
// residual allows {0, 1, 2} but the deletion vector deletes pos0; the
// surviving candidates are the residual-allowed AND not-deleted rows.
- let mut factory = |_: &BucketActiveFile| -> crate::Result<Box<dyn
PkVectorReader>> {
- Ok(Box::new(ArrayReader::new(
- 2,
- vec![
- Some(vec![1.0, 0.0]),
- Some(vec![2.0, 0.0]),
- Some(vec![3.0, 0.0]),
- ],
- )))
- };
+ let mut factory = as_factory(|_: &BucketActiveFile| ->
ExactReaderFuture<'_> {
+ Box::pin(async {
+ Ok(Box::new(ArrayReader::new(
+ 2,
+ vec![
+ Some(vec![1.0, 0.0]),
+ Some(vec![2.0, 0.0]),
+ Some(vec![3.0, 0.0]),
+ ],
+ )) as Box<dyn PkVectorReader>)
+ })
+ });
let mut dvs: HashMap<String, Arc<DeletionVector>> = HashMap::new();
let mut bm = RoaringBitmap::new();
bm.insert(0); // pos0 deleted
@@ -993,6 +1042,7 @@ mod tests {
false,
Some(&residual),
)
+ .await
.unwrap();
assert_eq!(
results.iter().map(|r| r.row_position).collect::<Vec<_>>(),