This is an automated email from the ASF dual-hosted git repository.
jerry-024 pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/paimon-vector-index.git
The following commit(s) were added to refs/heads/main by this push:
new cf0506d ivf: incrementally scan expanded batch probe ranges (#73)
cf0506d is described below
commit cf0506dac7fc1700d5a9898c00a41cc42a7c4868
Author: shyjsarah <[email protected]>
AuthorDate: Wed Aug 12 15:44:15 2026 +0800
ivf: incrementally scan expanded batch probe ranges (#73)
Co-authored-by: shaoyijie <[email protected]>
---
core/src/index.rs | 340 ++++++++++++++++++++++++++++++++++++-------------
core/src/ivfflat_io.rs | 102 ++++++++++++++-
core/src/ivfpq.rs | 304 ++++++++++++++++++++++++++++++++++++++++---
core/src/ivfrq_io.rs | 107 +++++++++++++++-
core/src/ivfsq_io.rs | 103 ++++++++++++++-
5 files changed, 835 insertions(+), 121 deletions(-)
diff --git a/core/src/index.rs b/core/src/index.rs
index 6454bd1..60c7762 100644
--- a/core/src/index.rs
+++ b/core/src/index.rs
@@ -30,25 +30,25 @@ use crate::distance::MetricType;
use crate::io::{write_index, IVFPQIndexReader, ReadRequest, SeekRead,
SeekWrite, MAGIC};
use crate::ivfflat::IVFFlatIndex;
use crate::ivfflat_io::{
- search_batch_ivfflat_reader, search_batch_ivfflat_reader_roaring_filter,
write_ivfflat_index,
- IVFFlatIndexReader, IVFFLAT_MAGIC,
+ search_batch_ivfflat_reader_filter_range,
search_batch_ivfflat_reader_roaring_filter_range,
+ write_ivfflat_index, IVFFlatIndexReader, IVFFLAT_MAGIC,
};
use crate::ivfpq::{
- search_batch_reader_roaring_filter_with_reuse_mode_and_budget,
- search_batch_reader_with_reuse_mode_and_budget, search_with_reader,
+ search_batch_reader_roaring_filter_with_reuse_mode_and_budget_range,
+ search_batch_reader_with_reuse_mode_and_budget_range, search_with_reader,
search_with_reader_roaring_filter, IVFPQIndex,
};
pub use crate::ivfpq::{IvfPqBatchTableReuseMode,
DEFAULT_IVFPQ_BATCH_TABLE_REUSE_MAX_BYTES};
use crate::ivfrq::IVFRQIndex;
pub use crate::ivfrq_io::IVFRQSearchStats;
use crate::ivfrq_io::{
- search_batch_ivfrq_reader, search_batch_ivfrq_reader_roaring_filter,
write_ivfrq_index,
- IVFRQIndexReader, IVF_RQ_MAGIC,
+ search_batch_ivfrq_reader_filter_range,
search_batch_ivfrq_reader_roaring_filter_range,
+ write_ivfrq_index, IVFRQIndexReader, IVF_RQ_MAGIC,
};
use crate::ivfsq::IVFSQIndex;
use crate::ivfsq_io::{
- search_batch_ivfsq_reader, search_batch_ivfsq_reader_roaring_filter,
write_ivfsq_index,
- IVFSQIndexReader, IVF_SQ_MAGIC,
+ search_batch_ivfsq_reader_filter_range,
search_batch_ivfsq_reader_roaring_filter_range,
+ write_ivfsq_index, IVFSQIndexReader, IVF_SQ_MAGIC,
};
pub use crate::read_options::{DeploymentProfile, VectorIndexReadPlan,
VectorIndexReaderOptions};
use crate::rq::{is_supported_rq_bits, padded_dimension, DEFAULT_RQ_BITS};
@@ -1784,13 +1784,22 @@ impl<R: SeekRead> VectorIndexReader<R> {
query_count,
params.top_k,
total_vectors,
- |active_queries, active_query_count, nprobe| {
- search_batch_ivfflat_reader(
+ |active_queries,
+ active_query_count,
+ probe_start,
+ probe_end,
+ seed_ids,
+ seed_distances| {
+ search_batch_ivfflat_reader_filter_range(
reader,
active_queries,
active_query_count,
params.top_k,
- nprobe,
+ probe_start,
+ probe_end,
+ seed_ids,
+ seed_distances,
+ None,
)
},
)
@@ -1807,13 +1816,22 @@ impl<R: SeekRead> VectorIndexReader<R> {
query_count,
params.top_k,
total_vectors,
- |active_queries, active_query_count, nprobe| {
- search_batch_ivfsq_reader(
+ |active_queries,
+ active_query_count,
+ probe_start,
+ probe_end,
+ seed_ids,
+ seed_distances| {
+ search_batch_ivfsq_reader_filter_range(
reader,
active_queries,
active_query_count,
params.top_k,
- nprobe,
+ probe_start,
+ probe_end,
+ seed_ids,
+ seed_distances,
+ None,
)
},
)
@@ -1830,13 +1848,21 @@ impl<R: SeekRead> VectorIndexReader<R> {
query_count,
params.top_k,
total_vectors,
- |active_queries, active_query_count, nprobe| {
- search_batch_reader_with_reuse_mode_and_budget(
+ |active_queries,
+ active_query_count,
+ probe_start,
+ probe_end,
+ seed_ids,
+ seed_distances| {
+ search_batch_reader_with_reuse_mode_and_budget_range(
reader,
active_queries,
active_query_count,
params.top_k,
- nprobe,
+ probe_start,
+ probe_end,
+ seed_ids,
+ seed_distances,
params.ivfpq_batch_table_reuse,
params.ivfpq_batch_table_reuse_max_bytes,
)
@@ -1856,13 +1882,22 @@ impl<R: SeekRead> VectorIndexReader<R> {
query_count,
params.top_k,
total_vectors,
- |active_queries, active_query_count, nprobe| {
- let result = search_batch_ivfrq_reader(
+ |active_queries,
+ active_query_count,
+ probe_start,
+ probe_end,
+ seed_ids,
+ seed_distances| {
+ let result = search_batch_ivfrq_reader_filter_range(
reader,
active_queries,
active_query_count,
params.top_k,
- nprobe,
+ probe_start,
+ probe_end,
+ seed_ids,
+ seed_distances,
+ None,
);
if result.is_ok() {
aggregate_stats.merge(reader.last_search_stats());
@@ -1912,13 +1947,21 @@ impl<R: SeekRead> VectorIndexReader<R> {
query_count,
params.top_k,
matching_count.unwrap_or(total_vectors),
- |active_queries, active_query_count, nprobe| {
- search_batch_ivfflat_reader_roaring_filter(
+ |active_queries,
+ active_query_count,
+ probe_start,
+ probe_end,
+ seed_ids,
+ seed_distances| {
+ search_batch_ivfflat_reader_roaring_filter_range(
reader,
active_queries,
active_query_count,
params.top_k,
- nprobe,
+ probe_start,
+ probe_end,
+ seed_ids,
+ seed_distances,
roaring_filter_bytes,
)
},
@@ -1937,13 +1980,21 @@ impl<R: SeekRead> VectorIndexReader<R> {
query_count,
params.top_k,
matching_count.unwrap_or(total_vectors),
- |active_queries, active_query_count, nprobe| {
- search_batch_ivfsq_reader_roaring_filter(
+ |active_queries,
+ active_query_count,
+ probe_start,
+ probe_end,
+ seed_ids,
+ seed_distances| {
+ search_batch_ivfsq_reader_roaring_filter_range(
reader,
active_queries,
active_query_count,
params.top_k,
- nprobe,
+ probe_start,
+ probe_end,
+ seed_ids,
+ seed_distances,
roaring_filter_bytes,
)
},
@@ -1962,13 +2013,21 @@ impl<R: SeekRead> VectorIndexReader<R> {
query_count,
params.top_k,
matching_count.unwrap_or(total_vectors),
- |active_queries, active_query_count, nprobe| {
-
search_batch_reader_roaring_filter_with_reuse_mode_and_budget(
+ |active_queries,
+ active_query_count,
+ probe_start,
+ probe_end,
+ seed_ids,
+ seed_distances| {
+
search_batch_reader_roaring_filter_with_reuse_mode_and_budget_range(
reader,
active_queries,
active_query_count,
params.top_k,
- nprobe,
+ probe_start,
+ probe_end,
+ seed_ids,
+ seed_distances,
roaring_filter_bytes,
params.ivfpq_batch_table_reuse,
params.ivfpq_batch_table_reuse_max_bytes,
@@ -1990,13 +2049,21 @@ impl<R: SeekRead> VectorIndexReader<R> {
query_count,
params.top_k,
matching_count.unwrap_or(total_vectors),
- |active_queries, active_query_count, nprobe| {
- let result = search_batch_ivfrq_reader_roaring_filter(
+ |active_queries,
+ active_query_count,
+ probe_start,
+ probe_end,
+ seed_ids,
+ seed_distances| {
+ let result =
search_batch_ivfrq_reader_roaring_filter_range(
reader,
active_queries,
active_query_count,
params.top_k,
- nprobe,
+ probe_start,
+ probe_end,
+ seed_ids,
+ seed_distances,
roaring_filter_bytes,
);
if result.is_ok() {
@@ -2060,8 +2127,8 @@ fn progressive_ivf_search(
/// Runs automatic IVF batch expansion independently for each query.
///
/// Queries which already produced the required number of results are removed
from later rounds.
-/// Retried queries are still searched from the first probed list; reusing
work across rounds is
-/// intentionally left to the incremental-search implementation.
+/// Each callback scans only the half-open probe range passed to it. In
expansion rounds it receives
+/// the active queries' accumulated Top-K as a seed and returns the updated
Top-K.
fn progressive_ivf_batch_search(
params: VectorSearchParams,
nlist: usize,
@@ -2070,7 +2137,7 @@ fn progressive_ivf_batch_search(
query_count: usize,
top_k: usize,
available_matches: usize,
- search: impl FnMut(&[f32], usize, usize) -> io::Result<(Vec<i64>,
Vec<f32>)>,
+ search: impl FnMut(&[f32], usize, usize, usize, &[i64], &[f32]) ->
io::Result<(Vec<i64>, Vec<f32>)>,
) -> io::Result<(Vec<i64>, Vec<f32>)> {
progressive_ivf_batch_search_with_retry_buffer_limit(
params,
@@ -2095,16 +2162,23 @@ fn progressive_ivf_batch_search_with_retry_buffer_limit(
top_k: usize,
available_matches: usize,
retry_buffer_limit_bytes: usize,
- mut search: impl FnMut(&[f32], usize, usize) -> io::Result<(Vec<i64>,
Vec<f32>)>,
+ mut search: impl FnMut(
+ &[f32],
+ usize,
+ usize,
+ usize,
+ &[i64],
+ &[f32],
+ ) -> io::Result<(Vec<i64>, Vec<f32>)>,
) -> io::Result<(Vec<i64>, Vec<f32>)> {
if params.search_width != SearchWidth::Auto {
- return search(queries, query_count, initial_nprobe);
+ return search(queries, query_count, 0, initial_nprobe, &[], &[]);
}
let dimension = queries.len() / query_count;
let required_per_query = top_k.min(available_matches);
let mut nprobe = initial_nprobe;
- let (mut result_ids, mut result_distances) = search(queries, query_count,
nprobe)?;
+ let (mut result_ids, mut result_distances) = search(queries, query_count,
0, nprobe, &[], &[])?;
let mut active_queries = result_distances
.chunks_exact(top_k)
.take(query_count)
@@ -2119,23 +2193,8 @@ fn progressive_ivf_batch_search_with_retry_buffer_limit(
return Ok((result_ids, result_distances));
}
+ let previous_nprobe = nprobe;
nprobe = nprobe.saturating_mul(2).min(nlist);
- if active_queries.len() == query_count {
- drop(result_ids);
- drop(result_distances);
- (result_ids, result_distances) = search(queries, query_count,
nprobe)?;
- active_queries = result_distances
- .chunks_exact(top_k)
- .take(query_count)
- .enumerate()
- .filter_map(|(query_index, distances)| {
- (!ivf_search_result_is_complete(distances,
required_per_query))
- .then_some(query_index)
- })
- .collect();
- continue;
- }
-
let bytes_per_retry_query = dimension
.saturating_mul(std::mem::size_of::<f32>())
.saturating_add(
@@ -2148,12 +2207,25 @@ fn progressive_ivf_batch_search_with_retry_buffer_limit(
let mut next_active_queries = Vec::new();
for active_chunk in active_queries.chunks(retry_chunk_size) {
let mut packed_queries = Vec::with_capacity(active_chunk.len() *
dimension);
+ let mut seed_ids = Vec::with_capacity(active_chunk.len() * top_k);
+ let mut seed_distances = Vec::with_capacity(active_chunk.len() *
top_k);
for &query_index in active_chunk {
- let start = query_index * dimension;
- packed_queries.extend_from_slice(&queries[start..start +
dimension]);
+ let query_start = query_index * dimension;
+
packed_queries.extend_from_slice(&queries[query_start..query_start +
dimension]);
+ let result_start = query_index * top_k;
+
seed_ids.extend_from_slice(&result_ids[result_start..result_start + top_k]);
+ seed_distances
+
.extend_from_slice(&result_distances[result_start..result_start + top_k]);
}
- let (round_ids, round_distances) = search(&packed_queries,
active_chunk.len(), nprobe)?;
+ let (round_ids, round_distances) = search(
+ &packed_queries,
+ active_chunk.len(),
+ previous_nprobe,
+ nprobe,
+ &seed_ids,
+ &seed_distances,
+ )?;
for (round_index, &query_index) in active_chunk.iter().enumerate()
{
let round_start = round_index * top_k;
let result_start = query_index * top_k;
@@ -3253,27 +3325,39 @@ mod tests {
}
#[test]
- fn automatic_batch_search_retries_only_incomplete_queries() {
+ fn automatic_batch_search_scans_only_new_probe_ranges_and_merges_results()
{
let queries = vec![10.0, 20.0, 30.0];
let mut observed = Vec::new();
let result = progressive_ivf_batch_search(
- VectorSearchParams::automatic(2),
+ VectorSearchParams::automatic(3),
8,
2,
&queries,
3,
- 2,
+ 3,
10,
- |active_queries, active_query_count, nprobe| {
- observed.push((nprobe, active_query_count,
active_queries.to_vec()));
- match nprobe {
- 2 => Ok((
- vec![100, 101, 200, -1, 300, 301],
- vec![1.0, 2.0, 1.0, f32::MAX, 1.0, 2.0],
+ |active_queries,
+ active_query_count,
+ probe_start,
+ probe_end,
+ seed_ids,
+ seed_distances| {
+ observed.push((
+ probe_start,
+ probe_end,
+ active_query_count,
+ active_queries.to_vec(),
+ seed_ids.to_vec(),
+ seed_distances.to_vec(),
+ ));
+ match (probe_start, probe_end) {
+ (0, 2) => Ok((
+ vec![100, 101, 102, 200, -1, -1, 300, 301, 302],
+ vec![1.0, 2.0, 3.0, 5.0, f32::MAX, f32::MAX, 1.0, 2.0,
3.0],
)),
- 4 => Ok((vec![200, -1], vec![1.0, f32::MAX])),
- 8 => Ok((vec![200, 201], vec![1.0, 2.0])),
- _ => unreachable!("unexpected nprobe {nprobe}"),
+ (2, 4) => Ok((vec![201, 200, -1], vec![4.0, 5.0,
f32::MAX])),
+ (4, 8) => Ok((vec![202, 201, 200], vec![3.0, 4.0, 5.0])),
+ _ => unreachable!("unexpected probe range
{probe_start}..{probe_end}"),
}
},
)
@@ -3282,17 +3366,31 @@ mod tests {
assert_eq!(
observed,
vec![
- (2, 3, vec![10.0, 20.0, 30.0]),
- (4, 1, vec![20.0]),
- (8, 1, vec![20.0]),
+ (0, 2, 3, vec![10.0, 20.0, 30.0], vec![], vec![]),
+ (
+ 2,
+ 4,
+ 1,
+ vec![20.0],
+ vec![200, -1, -1],
+ vec![5.0, f32::MAX, f32::MAX]
+ ),
+ (
+ 4,
+ 8,
+ 1,
+ vec![20.0],
+ vec![201, 200, -1],
+ vec![4.0, 5.0, f32::MAX]
+ ),
]
);
- assert_eq!(result.0, vec![100, 101, 200, 201, 300, 301]);
- assert_eq!(result.1, vec![1.0, 2.0, 1.0, 2.0, 1.0, 2.0]);
+ assert_eq!(result.0, vec![100, 101, 102, 202, 201, 200, 300, 301,
302]);
+ assert_eq!(result.1, vec![1.0, 2.0, 3.0, 3.0, 4.0, 5.0, 1.0, 2.0,
3.0]);
}
#[test]
- fn automatic_batch_search_chunks_partial_retries_to_bound_memory() {
+ fn automatic_batch_search_chunks_queries_and_seeds_to_bound_retry_memory()
{
let queries = vec![10.0, 11.0, 20.0, 21.0, 30.0, 31.0, 40.0, 41.0];
let mut observed = Vec::new();
let result = progressive_ivf_batch_search_with_retry_buffer_limit(
@@ -3304,9 +3402,21 @@ mod tests {
2,
10,
32,
- |active_queries, active_query_count, nprobe| {
- observed.push((nprobe, active_query_count,
active_queries.to_vec()));
- if nprobe == 2 {
+ |active_queries,
+ active_query_count,
+ probe_start,
+ probe_end,
+ seed_ids,
+ seed_distances| {
+ observed.push((
+ probe_start,
+ probe_end,
+ active_query_count,
+ active_queries.to_vec(),
+ seed_ids.to_vec(),
+ seed_distances.to_vec(),
+ ));
+ if probe_start == 0 {
return Ok((
vec![100, 101, 200, -1, 300, -1, 400, -1],
vec![1.0, 2.0, 1.0, f32::MAX, 1.0, f32::MAX, 1.0,
f32::MAX],
@@ -3322,10 +3432,31 @@ mod tests {
assert_eq!(
observed,
vec![
- (2, 4, queries),
- (4, 1, vec![20.0, 21.0]),
- (4, 1, vec![30.0, 31.0]),
- (4, 1, vec![40.0, 41.0]),
+ (0, 2, 4, queries, vec![], vec![]),
+ (
+ 2,
+ 4,
+ 1,
+ vec![20.0, 21.0],
+ vec![200, -1],
+ vec![1.0, f32::MAX],
+ ),
+ (
+ 2,
+ 4,
+ 1,
+ vec![30.0, 31.0],
+ vec![300, -1],
+ vec![1.0, f32::MAX],
+ ),
+ (
+ 2,
+ 4,
+ 1,
+ vec![40.0, 41.0],
+ vec![400, -1],
+ vec![1.0, f32::MAX],
+ ),
]
);
assert_eq!(result.0, vec![100, 101, 20, 21, 30, 31, 40, 41]);
@@ -3343,8 +3474,20 @@ mod tests {
3,
2,
10,
- |active_queries, active_query_count, nprobe| {
- observed.push((nprobe, active_query_count,
active_queries.to_vec()));
+ |active_queries,
+ active_query_count,
+ probe_start,
+ probe_end,
+ seed_ids,
+ seed_distances| {
+ observed.push((
+ probe_start,
+ probe_end,
+ active_query_count,
+ active_queries.to_vec(),
+ seed_ids.to_vec(),
+ seed_distances.to_vec(),
+ ));
Ok((
vec![100, 101, 200, 201, 300, 301],
vec![1.0, 2.0, 1.0, 2.0, 1.0, 2.0],
@@ -3353,11 +3496,38 @@ mod tests {
)
.unwrap();
- assert_eq!(observed, vec![(4, 3, queries)]);
+ assert_eq!(observed, vec![(0, 4, 3, queries, vec![], vec![])]);
assert_eq!(result.0, vec![100, 101, 200, 201, 300, 301]);
assert_eq!(result.1, vec![1.0, 2.0, 1.0, 2.0, 1.0, 2.0]);
}
+ #[test]
+ fn progressive_batch_seed_preserves_negative_one_row_ids() {
+ let result = progressive_ivf_batch_search(
+ VectorSearchParams::automatic(2),
+ 4,
+ 2,
+ &[1.0],
+ 1,
+ 2,
+ 10,
+ |_, _, probe_start, probe_end, seed_ids, seed_distances| {
+ if probe_start == 0 {
+ Ok((vec![-1, -1], vec![0.5, f32::MAX]))
+ } else {
+ assert_eq!((probe_start, probe_end), (2, 4));
+ assert_eq!(seed_ids, &[-1, -1]);
+ assert_eq!(seed_distances, &[0.5, f32::MAX]);
+ Ok((vec![-1, 8], vec![0.5, 2.0]))
+ }
+ },
+ )
+ .unwrap();
+
+ assert_eq!(result.0, vec![-1, 8]);
+ assert_eq!(result.1, vec![0.5, 2.0]);
+ }
+
#[test]
fn capped_automatic_filtered_search_can_expand_past_the_initial_cap() {
let params =
VectorSearchParams::automatic(2).with_max_initial_filter_expansion_factor(4);
diff --git a/core/src/ivfflat_io.rs b/core/src/ivfflat_io.rs
index 1409caa..10ddb5c 100644
--- a/core/src/ivfflat_io.rs
+++ b/core/src/ivfflat_io.rs
@@ -767,6 +767,20 @@ pub fn search_batch_ivfflat_reader_filter<R: SeekRead>(
k: usize,
nprobe: usize,
filter: Option<&dyn RowIdFilter>,
+) -> io::Result<(Vec<i64>, Vec<f32>)> {
+ search_batch_ivfflat_reader_filter_range(reader, queries, nq, k, 0,
nprobe, &[], &[], filter)
+}
+
+pub(crate) fn search_batch_ivfflat_reader_filter_range<R: SeekRead>(
+ reader: &mut IVFFlatIndexReader<R>,
+ queries: &[f32],
+ nq: usize,
+ k: usize,
+ probe_start: usize,
+ probe_end: usize,
+ seed_ids: &[i64],
+ seed_distances: &[f32],
+ filter: Option<&dyn RowIdFilter>,
) -> io::Result<(Vec<i64>, Vec<f32>)> {
reader.ensure_loaded()?;
let d = reader.d;
@@ -799,12 +813,13 @@ pub fn search_batch_ivfflat_reader_filter<R: SeekRead>(
"k must be greater than 0",
));
}
- if nprobe == 0 {
+ if probe_start >= probe_end {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
- "nprobe must be greater than 0",
+ "probe range must be non-empty",
));
}
+ validate_batch_seed(seed_ids, seed_distances, nq, k)?;
let mut processed = queries[..expected_query_len].to_vec();
if reader.metric == MetricType::Cosine {
@@ -819,13 +834,13 @@ pub fn search_batch_ivfflat_reader_filter<R: SeekRead>(
&reader.quantizer_centroids,
reader.nlist,
d,
- nprobe,
+ probe_end,
);
let mut list_to_queries = vec![Vec::new(); reader.nlist];
let mut unique_lists = Vec::new();
for (qi, probe_indices) in all_probe_indices.iter().enumerate() {
- for &list_id in probe_indices {
+ for &list_id in probe_indices.iter().skip(probe_start) {
if list_to_queries[list_id].is_empty() {
unique_lists.push(list_id);
}
@@ -834,6 +849,7 @@ pub fn search_batch_ivfflat_reader_filter<R: SeekRead>(
}
let mut heaps: Vec<ReaderTopKHeap> = (0..nq).map(|_|
ReaderTopKHeap::new(k)).collect();
+ seed_flat_heaps(&mut heaps, seed_ids, seed_distances, k);
let mut batch_start = 0usize;
while batch_start < unique_lists.len() {
let first_list = unique_lists[batch_start];
@@ -914,9 +930,85 @@ pub fn search_batch_ivfflat_reader_roaring_filter<R:
SeekRead>(
k: usize,
nprobe: usize,
roaring_filter_bytes: &[u8],
+) -> io::Result<(Vec<i64>, Vec<f32>)> {
+ search_batch_ivfflat_reader_roaring_filter_range(
+ reader,
+ queries,
+ nq,
+ k,
+ 0,
+ nprobe,
+ &[],
+ &[],
+ roaring_filter_bytes,
+ )
+}
+
+pub(crate) fn search_batch_ivfflat_reader_roaring_filter_range<R: SeekRead>(
+ reader: &mut IVFFlatIndexReader<R>,
+ queries: &[f32],
+ nq: usize,
+ k: usize,
+ probe_start: usize,
+ probe_end: usize,
+ seed_ids: &[i64],
+ seed_distances: &[f32],
+ roaring_filter_bytes: &[u8],
) -> io::Result<(Vec<i64>, Vec<f32>)> {
let filter = decode_roaring_filter(roaring_filter_bytes)?;
- search_batch_ivfflat_reader_filter(reader, queries, nq, k, nprobe,
Some(&filter))
+ search_batch_ivfflat_reader_filter_range(
+ reader,
+ queries,
+ nq,
+ k,
+ probe_start,
+ probe_end,
+ seed_ids,
+ seed_distances,
+ Some(&filter),
+ )
+}
+
+fn validate_batch_seed(
+ seed_ids: &[i64],
+ seed_distances: &[f32],
+ nq: usize,
+ k: usize,
+) -> io::Result<()> {
+ let expected = nq
+ .checked_mul(k)
+ .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "nq * k
overflows usize"))?;
+ if (seed_ids.is_empty() && seed_distances.is_empty())
+ || (seed_ids.len() == expected && seed_distances.len() == expected)
+ {
+ Ok(())
+ } else {
+ Err(io::Error::new(
+ io::ErrorKind::InvalidInput,
+ "seed result lengths must both equal nq * k",
+ ))
+ }
+}
+
+fn seed_flat_heaps(
+ heaps: &mut [ReaderTopKHeap],
+ seed_ids: &[i64],
+ seed_distances: &[f32],
+ k: usize,
+) {
+ for (query_index, heap) in heaps.iter_mut().enumerate() {
+ let start = query_index * k;
+ for (&id, &distance) in seed_ids
+ .get(start..start + k)
+ .unwrap_or_default()
+ .iter()
+ .zip(seed_distances.get(start..start + k).unwrap_or_default())
+ {
+ if distance != f32::MAX {
+ heap.push(distance, id);
+ }
+ }
+ }
}
fn scan_flat_list(
diff --git a/core/src/ivfpq.rs b/core/src/ivfpq.rs
index 75afcf1..7cfd70f 100644
--- a/core/src/ivfpq.rs
+++ b/core/src/ivfpq.rs
@@ -1769,18 +1769,78 @@ pub fn
search_batch_reader_with_reuse_mode_and_budget<R: SeekRead>(
reuse_mode: IvfPqBatchTableReuseMode,
reuse_max_bytes: usize,
) -> io::Result<(Vec<i64>, Vec<f32>)> {
- search_batch_reader_filter_with_reuse_mode_and_budget(
+ search_batch_reader_with_reuse_mode_and_budget_range(
reader,
queries,
nq,
k,
+ 0,
nprobe,
+ &[],
+ &[],
+ reuse_mode,
+ reuse_max_bytes,
+ )
+}
+
+pub(crate) fn search_batch_reader_with_reuse_mode_and_budget_range<R:
SeekRead>(
+ reader: &mut IVFPQIndexReader<R>,
+ queries: &[f32],
+ nq: usize,
+ k: usize,
+ probe_start: usize,
+ probe_end: usize,
+ seed_ids: &[i64],
+ seed_distances: &[f32],
+ reuse_mode: IvfPqBatchTableReuseMode,
+ reuse_max_bytes: usize,
+) -> io::Result<(Vec<i64>, Vec<f32>)> {
+ search_batch_reader_filter_with_reuse_mode_and_budget_range(
+ reader,
+ queries,
+ nq,
+ k,
+ probe_start,
+ probe_end,
+ seed_ids,
+ seed_distances,
None,
reuse_mode,
reuse_max_bytes,
)
}
+fn search_batch_reader_filter_with_reuse_mode_and_budget_range<R: SeekRead>(
+ reader: &mut IVFPQIndexReader<R>,
+ queries: &[f32],
+ nq: usize,
+ k: usize,
+ probe_start: usize,
+ probe_end: usize,
+ seed_ids: &[i64],
+ seed_distances: &[f32],
+ filter: Option<&dyn RowIdFilter>,
+ reuse_mode: IvfPqBatchTableReuseMode,
+ reuse_max_bytes: usize,
+) -> io::Result<(Vec<i64>, Vec<f32>)> {
+ search_batch_reader_filter_with_reuse_mode_and_observer(
+ reader,
+ queries,
+ nq,
+ k,
+ probe_start,
+ probe_end,
+ seed_ids,
+ seed_distances,
+ filter,
+ reuse_mode,
+ reuse_max_bytes,
+ |_| {},
+ #[cfg(test)]
+ None,
+ )
+}
+
/// Big batch search with an optional row-id filter.
pub fn search_batch_reader_filter<R: SeekRead>(
reader: &mut IVFPQIndexReader<R>,
@@ -1832,18 +1892,18 @@ pub fn
search_batch_reader_filter_with_reuse_mode_and_budget<R: SeekRead>(
reuse_mode: IvfPqBatchTableReuseMode,
reuse_max_bytes: usize,
) -> io::Result<(Vec<i64>, Vec<f32>)> {
- search_batch_reader_filter_with_reuse_mode_and_observer(
+ search_batch_reader_filter_with_reuse_mode_and_budget_range(
reader,
queries,
nq,
k,
+ 0,
nprobe,
+ &[],
+ &[],
filter,
reuse_mode,
reuse_max_bytes,
- |_| {},
- #[cfg(test)]
- None,
)
}
@@ -1862,7 +1922,10 @@ fn search_batch_reader_filter_with_observer<R: SeekRead>(
queries,
nq,
k,
+ 0,
nprobe,
+ &[],
+ &[],
filter,
IvfPqBatchTableReuseMode::Auto,
DEFAULT_IVFPQ_BATCH_TABLE_REUSE_MAX_BYTES,
@@ -1876,7 +1939,10 @@ fn
search_batch_reader_filter_with_reuse_mode_and_observer<R: SeekRead>(
queries: &[f32],
nq: usize,
k: usize,
- nprobe: usize,
+ probe_start: usize,
+ probe_end: usize,
+ seed_ids: &[i64],
+ seed_distances: &[f32],
filter: Option<&dyn RowIdFilter>,
reuse_mode: IvfPqBatchTableReuseMode,
reuse_max_bytes: usize,
@@ -1913,12 +1979,13 @@ fn
search_batch_reader_filter_with_reuse_mode_and_observer<R: SeekRead>(
"k must be greater than 0",
));
}
- if nprobe == 0 {
+ if probe_start >= probe_end {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
- "nprobe must be greater than 0",
+ "probe range must be non-empty",
));
}
+ validate_batch_seed(seed_ids, seed_distances, nq, k)?;
let m = reader.m;
let ksub = reader.ksub;
@@ -1945,7 +2012,7 @@ fn
search_batch_reader_filter_with_reuse_mode_and_observer<R: SeekRead>(
&reader.quantizer_centroids,
reader.nlist,
d,
- nprobe,
+ probe_end,
);
// Step 3: Read every probed list once. Queries share the decoded list
@@ -1953,7 +2020,7 @@ fn
search_batch_reader_filter_with_reuse_mode_and_observer<R: SeekRead>(
let mut seen = vec![false; reader.nlist];
let mut unique_lists = Vec::new();
for probe_indices in &all_probe_indices {
- for &list_id in probe_indices {
+ for &list_id in probe_indices.iter().skip(probe_start) {
if !seen[list_id] && reader.list_counts[list_id] > 0 {
seen[list_id] = true;
unique_lists.push(list_id);
@@ -2000,7 +2067,7 @@ fn
search_batch_reader_filter_with_reuse_mode_and_observer<R: SeekRead>(
// probed list for that query can share one table.
let reuse_non_residual_tables = reader.pq.nbits == 8
&& !by_residual
- && nprobe > 1
+ && probe_end - probe_start > 1
&& match reuse_mode {
IvfPqBatchTableReuseMode::Off => false,
IvfPqBatchTableReuseMode::On => true,
@@ -2029,6 +2096,7 @@ fn
search_batch_reader_filter_with_reuse_mode_and_observer<R: SeekRead>(
let mut stable_pq_norms = None;
let mut heaps = (0..nq).map(|_| TopKHeap::new(k)).collect::<Vec<_>>();
+ seed_heaps(&mut heaps, seed_ids, seed_distances, k);
let mut batch_start = 0usize;
while batch_start < unique_lists.len() {
let first_list = unique_lists[batch_start];
@@ -2037,7 +2105,11 @@ fn
search_batch_reader_filter_with_reuse_mode_and_observer<R: SeekRead>(
.filter_map(|query_index| {
all_probe_indices[query_index]
.iter()
- .position(|&list_id| list_id == first_list)
+ .enumerate()
+ .skip(probe_start)
+ .find_map(|(probe_rank, &list_id)| {
+ (list_id == first_list).then_some(probe_rank)
+ })
.map(|probe_rank| {
let query = &processed[query_index *
d..(query_index + 1) * d];
let sim_table =
(!reuse_non_residual_tables).then(|| {
@@ -2128,6 +2200,7 @@ fn
search_batch_reader_filter_with_reuse_mode_and_observer<R: SeekRead>(
for probe_indices in &all_probe_indices {
let matching_probe_count = probe_indices
.iter()
+ .skip(probe_start)
.filter(|&&list_id| {
let position = list_positions[list_id];
position != usize::MAX
@@ -2213,14 +2286,20 @@ fn
search_batch_reader_filter_with_reuse_mode_and_observer<R: SeekRead>(
let mut heap = TopKHeap::new(k);
let mut scratch = ReaderScanScratch::default();
let query_uses_ephemeral_precomputed =
use_ephemeral_precomputed
- && all_probe_indices[qi].iter().any(|&list_id| {
- let position = list_positions[list_id];
- position != usize::MAX &&
!ephemeral_precomputed_tables[position].is_empty()
- });
+ && all_probe_indices[qi]
+ .iter()
+ .skip(probe_start)
+ .any(|&list_id| {
+ let position = list_positions[list_id];
+ position != usize::MAX
+ &&
!ephemeral_precomputed_tables[position].is_empty()
+ });
if query_uses_ephemeral_precomputed {
fill_stable_ephemeral_query_table(query, &reader.pq, &mut
scratch.ip_table);
}
- for (probe_rank, &list_id) in
all_probe_indices[qi].iter().enumerate() {
+ for (probe_rank, &list_id) in
+ all_probe_indices[qi].iter().enumerate().skip(probe_start)
+ {
let position = list_positions[list_id];
if position == usize::MAX {
continue;
@@ -2300,7 +2379,7 @@ fn
search_batch_reader_filter_with_reuse_mode_and_observer<R: SeekRead>(
std::io::stderr().lock(),
"[paimon-vindex] ivfpq_batch_table_reuse
strategy=non_residual_query_table \
mode={reuse_mode:?} enabled={reuse_non_residual_tables} used={}
metric={} \
- pq_bits={} nq={nq} nprobe={nprobe} unique_lists={} filtered={}
required_bytes={:?} \
+ pq_bits={} nq={nq} nprobe={probe_end} unique_lists={} filtered={}
required_bytes={:?} \
budget_bytes={reuse_max_bytes} tables_built={tables_built}",
tables_built > 0,
metric.as_str(),
@@ -2365,13 +2444,44 @@ pub fn
search_batch_reader_roaring_filter_with_reuse_mode_and_budget<R: SeekRead
reuse_mode: IvfPqBatchTableReuseMode,
reuse_max_bytes: usize,
) -> io::Result<(Vec<i64>, Vec<f32>)> {
- let filter = decode_roaring_filter(roaring_filter_bytes)?;
- search_batch_reader_filter_with_reuse_mode_and_budget(
+ search_batch_reader_roaring_filter_with_reuse_mode_and_budget_range(
reader,
queries,
nq,
k,
+ 0,
nprobe,
+ &[],
+ &[],
+ roaring_filter_bytes,
+ reuse_mode,
+ reuse_max_bytes,
+ )
+}
+
+pub(crate) fn
search_batch_reader_roaring_filter_with_reuse_mode_and_budget_range<R:
SeekRead>(
+ reader: &mut IVFPQIndexReader<R>,
+ queries: &[f32],
+ nq: usize,
+ k: usize,
+ probe_start: usize,
+ probe_end: usize,
+ seed_ids: &[i64],
+ seed_distances: &[f32],
+ roaring_filter_bytes: &[u8],
+ reuse_mode: IvfPqBatchTableReuseMode,
+ reuse_max_bytes: usize,
+) -> io::Result<(Vec<i64>, Vec<f32>)> {
+ let filter = decode_roaring_filter(roaring_filter_bytes)?;
+ search_batch_reader_filter_with_reuse_mode_and_budget_range(
+ reader,
+ queries,
+ nq,
+ k,
+ probe_start,
+ probe_end,
+ seed_ids,
+ seed_distances,
Some(&filter),
reuse_mode,
reuse_max_bytes,
@@ -2418,6 +2528,43 @@ impl TopKHeap {
}
}
+fn validate_batch_seed(
+ seed_ids: &[i64],
+ seed_distances: &[f32],
+ nq: usize,
+ k: usize,
+) -> io::Result<()> {
+ let expected = nq
+ .checked_mul(k)
+ .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "nq * k
overflows usize"))?;
+ if (seed_ids.is_empty() && seed_distances.is_empty())
+ || (seed_ids.len() == expected && seed_distances.len() == expected)
+ {
+ Ok(())
+ } else {
+ Err(io::Error::new(
+ io::ErrorKind::InvalidInput,
+ "seed result lengths must both equal nq * k",
+ ))
+ }
+}
+
+fn seed_heaps(heaps: &mut [TopKHeap], seed_ids: &[i64], seed_distances:
&[f32], k: usize) {
+ for (query_index, heap) in heaps.iter_mut().enumerate() {
+ let start = query_index * k;
+ for (&id, &distance) in seed_ids
+ .get(start..start + k)
+ .unwrap_or_default()
+ .iter()
+ .zip(seed_distances.get(start..start + k).unwrap_or_default())
+ {
+ if distance != f32::MAX {
+ heap.push(distance, id);
+ }
+ }
+ }
+}
+
// --- Utilities ---
fn compute_residuals(
@@ -2622,7 +2769,10 @@ mod tests {
&data[..nq * d],
nq,
k,
+ 0,
nprobe,
+ &[],
+ &[],
filter,
reuse_mode,
DEFAULT_IVFPQ_BATCH_TABLE_REUSE_MAX_BYTES,
@@ -3793,6 +3943,102 @@ mod tests {
}
}
+ #[test]
+ fn incremental_batch_probe_ranges_partition_the_one_shot_lists() {
+ use crate::io::{write_index, PosWriter};
+
+ let d = 16;
+ let nlist = 4;
+ let m = 4;
+ let n = 128;
+ let nq = 2;
+ let k = n;
+ let data = generate_clustered_data(n, d, nlist, 2_026);
+ let ids = (0..n as i64).collect::<Vec<_>>();
+ let queries = &data[..nq * d];
+
+ let mut index = IVFPQIndex::new(d, nlist, m, MetricType::L2, false);
+ index.train(&data, n);
+ index.add(&data, &ids, n);
+ let mut bytes = Vec::new();
+ write_index(&index, &mut PosWriter::new(&mut bytes)).unwrap();
+
+ let mut full_reader =
IVFPQIndexReader::open(Cursor::new(bytes.clone())).unwrap();
+ let (full_ids, _) = search_batch_reader(&mut full_reader, queries, nq,
k, nlist).unwrap();
+
+ let mut incremental_reader =
IVFPQIndexReader::open(Cursor::new(bytes)).unwrap();
+ let (first_ids, first_distances) =
search_batch_reader_with_reuse_mode_and_budget_range(
+ &mut incremental_reader,
+ queries,
+ nq,
+ k,
+ 0,
+ 2,
+ &[],
+ &[],
+ IvfPqBatchTableReuseMode::Auto,
+ DEFAULT_IVFPQ_BATCH_TABLE_REUSE_MAX_BYTES,
+ )
+ .unwrap();
+ let (second_delta_ids, _) =
search_batch_reader_with_reuse_mode_and_budget_range(
+ &mut incremental_reader,
+ queries,
+ nq,
+ k,
+ 2,
+ nlist,
+ &[],
+ &[],
+ IvfPqBatchTableReuseMode::Auto,
+ DEFAULT_IVFPQ_BATCH_TABLE_REUSE_MAX_BYTES,
+ )
+ .unwrap();
+ let (second_ids, _) =
search_batch_reader_with_reuse_mode_and_budget_range(
+ &mut incremental_reader,
+ queries,
+ nq,
+ k,
+ 2,
+ nlist,
+ &first_ids,
+ &first_distances,
+ IvfPqBatchTableReuseMode::Auto,
+ DEFAULT_IVFPQ_BATCH_TABLE_REUSE_MAX_BYTES,
+ )
+ .unwrap();
+
+ for query_index in 0..nq {
+ let result = query_index * k..(query_index + 1) * k;
+ let full = full_ids[result.clone()]
+ .iter()
+ .copied()
+ .filter(|&id| id != -1)
+ .collect::<HashSet<_>>();
+ let first = first_ids[result.clone()]
+ .iter()
+ .copied()
+ .filter(|&id| id != -1)
+ .collect::<HashSet<_>>();
+ let second_delta = second_delta_ids[result.clone()]
+ .iter()
+ .copied()
+ .filter(|&id| id != -1)
+ .collect::<HashSet<_>>();
+ let second = second_ids[result]
+ .iter()
+ .copied()
+ .filter(|&id| id != -1)
+ .collect::<HashSet<_>>();
+
+ assert!(first.is_disjoint(&second_delta));
+ assert_eq!(
+ first.union(&second_delta).copied().collect::<HashSet<_>>(),
+ full
+ );
+ assert_eq!(second, full);
+ }
+ }
+
#[test]
fn
inner_product_batch_table_reuse_modes_preserve_results_and_control_table_builds()
{
use crate::io::{write_index, IVFPQIndexReader, PosWriter};
@@ -3820,7 +4066,10 @@ mod tests {
&data[..nq * d],
nq,
k,
+ 0,
nprobe,
+ &[],
+ &[],
None,
IvfPqBatchTableReuseMode::On,
DEFAULT_IVFPQ_BATCH_TABLE_REUSE_MAX_BYTES,
@@ -3838,7 +4087,10 @@ mod tests {
&data[..nq * d],
nq,
k,
+ 0,
nprobe,
+ &[],
+ &[],
None,
IvfPqBatchTableReuseMode::On,
nq * m * 256 * std::mem::size_of::<f32>() - 1,
@@ -3883,7 +4135,10 @@ mod tests {
&data[..large_nq * d],
large_nq,
k,
+ 0,
nprobe,
+ &[],
+ &[],
None,
IvfPqBatchTableReuseMode::Auto,
DEFAULT_IVFPQ_BATCH_TABLE_REUSE_MAX_BYTES,
@@ -3904,7 +4159,10 @@ mod tests {
&data[..nq * d],
nq,
k,
+ 0,
nprobe,
+ &[],
+ &[],
None,
IvfPqBatchTableReuseMode::Auto,
DEFAULT_IVFPQ_BATCH_TABLE_REUSE_MAX_BYTES,
@@ -3926,7 +4184,10 @@ mod tests {
&data[..nq * d],
nq,
k,
+ 0,
nprobe,
+ &[],
+ &[],
Some(&empty_filter),
IvfPqBatchTableReuseMode::On,
DEFAULT_IVFPQ_BATCH_TABLE_REUSE_MAX_BYTES,
@@ -4406,7 +4667,10 @@ mod tests {
queries,
nq,
k,
+ 0,
nlist,
+ &[],
+ &[],
None,
IvfPqBatchTableReuseMode::On,
1,
diff --git a/core/src/ivfrq_io.rs b/core/src/ivfrq_io.rs
index 328a332..fc8d6d5 100644
--- a/core/src/ivfrq_io.rs
+++ b/core/src/ivfrq_io.rs
@@ -636,9 +636,30 @@ pub fn search_batch_ivfrq_reader_filter<R: SeekRead>(
k: usize,
nprobe: usize,
filter: Option<&dyn RowIdFilter>,
+) -> io::Result<(Vec<i64>, Vec<f32>)> {
+ search_batch_ivfrq_reader_filter_range(reader, queries, nq, k, 0, nprobe,
&[], &[], filter)
+}
+
+pub(crate) fn search_batch_ivfrq_reader_filter_range<R: SeekRead>(
+ reader: &mut IVFRQIndexReader<R>,
+ queries: &[f32],
+ nq: usize,
+ k: usize,
+ probe_start: usize,
+ probe_end: usize,
+ seed_ids: &[i64],
+ seed_distances: &[f32],
+ filter: Option<&dyn RowIdFilter>,
) -> io::Result<(Vec<i64>, Vec<f32>)> {
reader.ensure_loaded()?;
- validate_search_inputs(queries, nq, reader.d, k, nprobe)?;
+ validate_search_inputs(queries, nq, reader.d, k, probe_end)?;
+ if probe_start >= probe_end {
+ return Err(io::Error::new(
+ io::ErrorKind::InvalidInput,
+ "probe range must be non-empty",
+ ));
+ }
+ validate_batch_seed(seed_ids, seed_distances, nq, k)?;
let processed = preprocess_vectors(queries, nq, reader.d, reader.metric);
let (all_probe_indices, all_probe_distances) =
kmeans::find_topk_batch_with_centroid_norms(
&processed,
@@ -647,7 +668,7 @@ pub fn search_batch_ivfrq_reader_filter<R: SeekRead>(
&reader.quantizer_centroid_norms,
reader.nlist,
reader.d,
- nprobe,
+ probe_end,
);
let query_norms = processed
.chunks_exact(reader.d)
@@ -684,7 +705,7 @@ pub fn search_batch_ivfrq_reader_filter<R: SeekRead>(
let mut seen_lists = vec![false; reader.nlist];
let mut unique_lists = Vec::new();
for probes in &all_probe_indices {
- for &list_id in probes {
+ for &list_id in probes.iter().skip(probe_start) {
if !seen_lists[list_id] {
seen_lists[list_id] = true;
unique_lists.push(list_id);
@@ -693,6 +714,7 @@ pub fn search_batch_ivfrq_reader_filter<R: SeekRead>(
}
let mut heaps: Vec<TopKHeap> = (0..nq).map(|_| TopKHeap::new(k)).collect();
+ seed_heaps(&mut heaps, seed_ids, seed_distances, k);
let mut query_stats = vec![IVFRQSearchStats::default(); nq];
let mut aggregate_stats = IVFRQSearchStats {
query_count: nq,
@@ -778,8 +800,10 @@ pub fn search_batch_ivfrq_reader_filter<R: SeekRead>(
.zip(query_stats.par_iter_mut())
.enumerate()
.for_each(|(query_index, (heap, stats))| {
- for (probe_position, &list_id) in
- all_probe_indices[query_index].iter().enumerate()
+ for (probe_position, &list_id) in
all_probe_indices[query_index]
+ .iter()
+ .enumerate()
+ .skip(probe_start)
{
let position = list_positions[list_id];
if position == usize::MAX {
@@ -824,9 +848,80 @@ pub fn search_batch_ivfrq_reader_roaring_filter<R:
SeekRead>(
k: usize,
nprobe: usize,
roaring_filter_bytes: &[u8],
+) -> io::Result<(Vec<i64>, Vec<f32>)> {
+ search_batch_ivfrq_reader_roaring_filter_range(
+ reader,
+ queries,
+ nq,
+ k,
+ 0,
+ nprobe,
+ &[],
+ &[],
+ roaring_filter_bytes,
+ )
+}
+
+pub(crate) fn search_batch_ivfrq_reader_roaring_filter_range<R: SeekRead>(
+ reader: &mut IVFRQIndexReader<R>,
+ queries: &[f32],
+ nq: usize,
+ k: usize,
+ probe_start: usize,
+ probe_end: usize,
+ seed_ids: &[i64],
+ seed_distances: &[f32],
+ roaring_filter_bytes: &[u8],
) -> io::Result<(Vec<i64>, Vec<f32>)> {
let filter = decode_roaring_filter(roaring_filter_bytes)?;
- search_batch_ivfrq_reader_filter(reader, queries, nq, k, nprobe,
Some(&filter))
+ search_batch_ivfrq_reader_filter_range(
+ reader,
+ queries,
+ nq,
+ k,
+ probe_start,
+ probe_end,
+ seed_ids,
+ seed_distances,
+ Some(&filter),
+ )
+}
+
+fn validate_batch_seed(
+ seed_ids: &[i64],
+ seed_distances: &[f32],
+ nq: usize,
+ k: usize,
+) -> io::Result<()> {
+ let expected = nq
+ .checked_mul(k)
+ .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "nq * k
overflows usize"))?;
+ if (seed_ids.is_empty() && seed_distances.is_empty())
+ || (seed_ids.len() == expected && seed_distances.len() == expected)
+ {
+ Ok(())
+ } else {
+ Err(io::Error::new(
+ io::ErrorKind::InvalidInput,
+ "seed result lengths must both equal nq * k",
+ ))
+ }
+}
+
+fn seed_heaps(heaps: &mut [TopKHeap], seed_ids: &[i64], seed_distances:
&[f32], k: usize) {
+ for (query_index, heap) in heaps.iter_mut().enumerate() {
+ let start = query_index * k;
+ for (&id, &distance) in seed_ids
+ .get(start..start + k)
+ .unwrap_or_default()
+ .iter()
+ .zip(seed_distances.get(start..start + k).unwrap_or_default())
+ {
+ if distance != f32::MAX {
+ heap.push(distance, id);
+ }
+ }
+ }
}
fn scan_blocked_list(
diff --git a/core/src/ivfsq_io.rs b/core/src/ivfsq_io.rs
index 541abee..0bc5819 100644
--- a/core/src/ivfsq_io.rs
+++ b/core/src/ivfsq_io.rs
@@ -642,9 +642,30 @@ pub fn search_batch_ivfsq_reader_filter<R: SeekRead>(
k: usize,
nprobe: usize,
filter: Option<&dyn RowIdFilter>,
+) -> io::Result<(Vec<i64>, Vec<f32>)> {
+ search_batch_ivfsq_reader_filter_range(reader, queries, nq, k, 0, nprobe,
&[], &[], filter)
+}
+
+pub(crate) fn search_batch_ivfsq_reader_filter_range<R: SeekRead>(
+ reader: &mut IVFSQIndexReader<R>,
+ queries: &[f32],
+ nq: usize,
+ k: usize,
+ probe_start: usize,
+ probe_end: usize,
+ seed_ids: &[i64],
+ seed_distances: &[f32],
+ filter: Option<&dyn RowIdFilter>,
) -> io::Result<(Vec<i64>, Vec<f32>)> {
reader.ensure_loaded()?;
- validate_search_inputs(queries, nq, reader.d, k, nprobe)?;
+ validate_search_inputs(queries, nq, reader.d, k, probe_end)?;
+ if probe_start >= probe_end {
+ return Err(io::Error::new(
+ io::ErrorKind::InvalidInput,
+ "probe range must be non-empty",
+ ));
+ }
+ validate_batch_seed(seed_ids, seed_distances, nq, k)?;
let processed = preprocess_vectors(queries, nq, reader.d, reader.metric);
let (all_probe_indices, _) = kmeans::find_topk_batch(
&processed,
@@ -652,12 +673,12 @@ pub fn search_batch_ivfsq_reader_filter<R: SeekRead>(
&reader.quantizer_centroids,
reader.nlist,
reader.d,
- nprobe,
+ probe_end,
);
let mut seen = vec![false; reader.nlist];
let mut unique_lists = Vec::new();
for list_ids in &all_probe_indices {
- for &list_id in list_ids {
+ for &list_id in list_ids.iter().skip(probe_start) {
if !seen[list_id] {
seen[list_id] = true;
unique_lists.push(list_id);
@@ -666,13 +687,14 @@ pub fn search_batch_ivfsq_reader_filter<R: SeekRead>(
}
let mut list_to_queries = vec![Vec::new(); reader.nlist];
for (query_index, list_ids) in all_probe_indices.iter().enumerate() {
- for &list_id in list_ids {
+ for &list_id in list_ids.iter().skip(probe_start) {
list_to_queries[list_id].push(query_index);
}
}
let d = reader.d;
let metric = reader.metric;
let mut heaps = (0..nq).map(|_| TopKHeap::new(k)).collect::<Vec<_>>();
+ seed_heaps(&mut heaps, seed_ids, seed_distances, k);
// Oversized-list chunks are scanned query-by-query, so one reusable
// distance buffer is sufficient regardless of the batch width.
let mut stream_scratch = SqScanScratch::default();
@@ -764,9 +786,80 @@ pub fn search_batch_ivfsq_reader_roaring_filter<R:
SeekRead>(
k: usize,
nprobe: usize,
roaring_filter_bytes: &[u8],
+) -> io::Result<(Vec<i64>, Vec<f32>)> {
+ search_batch_ivfsq_reader_roaring_filter_range(
+ reader,
+ queries,
+ nq,
+ k,
+ 0,
+ nprobe,
+ &[],
+ &[],
+ roaring_filter_bytes,
+ )
+}
+
+pub(crate) fn search_batch_ivfsq_reader_roaring_filter_range<R: SeekRead>(
+ reader: &mut IVFSQIndexReader<R>,
+ queries: &[f32],
+ nq: usize,
+ k: usize,
+ probe_start: usize,
+ probe_end: usize,
+ seed_ids: &[i64],
+ seed_distances: &[f32],
+ roaring_filter_bytes: &[u8],
) -> io::Result<(Vec<i64>, Vec<f32>)> {
let filter = decode_roaring_filter(roaring_filter_bytes)?;
- search_batch_ivfsq_reader_filter(reader, queries, nq, k, nprobe,
Some(&filter))
+ search_batch_ivfsq_reader_filter_range(
+ reader,
+ queries,
+ nq,
+ k,
+ probe_start,
+ probe_end,
+ seed_ids,
+ seed_distances,
+ Some(&filter),
+ )
+}
+
+fn validate_batch_seed(
+ seed_ids: &[i64],
+ seed_distances: &[f32],
+ nq: usize,
+ k: usize,
+) -> io::Result<()> {
+ let expected = nq
+ .checked_mul(k)
+ .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "nq * k
overflows usize"))?;
+ if (seed_ids.is_empty() && seed_distances.is_empty())
+ || (seed_ids.len() == expected && seed_distances.len() == expected)
+ {
+ Ok(())
+ } else {
+ Err(io::Error::new(
+ io::ErrorKind::InvalidInput,
+ "seed result lengths must both equal nq * k",
+ ))
+ }
+}
+
+fn seed_heaps(heaps: &mut [TopKHeap], seed_ids: &[i64], seed_distances:
&[f32], k: usize) {
+ for (query_index, heap) in heaps.iter_mut().enumerate() {
+ let start = query_index * k;
+ for (&id, &distance) in seed_ids
+ .get(start..start + k)
+ .unwrap_or_default()
+ .iter()
+ .zip(seed_distances.get(start..start + k).unwrap_or_default())
+ {
+ if distance != f32::MAX {
+ heap.push(distance, id);
+ }
+ }
+ }
}
pub struct SqListData {