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-rust.git
The following commit(s) were added to refs/heads/main by this push:
new b3624353 vindex: use automatic IVF search width by default (#840)
b3624353 is described below
commit b36243539a9fc2ad9a50f8b8e6641d76bf9514ab
Author: shyjsarah <[email protected]>
AuthorDate: Tue Sep 15 17:22:37 2026 +0800
vindex: use automatic IVF search width by default (#840)
Co-authored-by: shaoyijie <[email protected]>
---
crates/paimon/src/vindex/reader.rs | 123 ++++++++++++++++++++++++++++++-------
docs/src/sql.md | 5 +-
2 files changed, 106 insertions(+), 22 deletions(-)
diff --git a/crates/paimon/src/vindex/reader.rs
b/crates/paimon/src/vindex/reader.rs
index 418b4301..557c2ee9 100644
--- a/crates/paimon/src/vindex/reader.rs
+++ b/crates/paimon/src/vindex/reader.rs
@@ -29,7 +29,6 @@ use std::io;
use std::sync::{Arc, Condvar, Mutex};
use std::time::{Duration, Instant};
-const DEFAULT_NPROBE: usize = 16;
const NPROBE_PARAMETER: &str = "ivf.nprobe";
// Matches Java's NativeVectorGlobalIndexReader; l_search is intentionally
snake_case.
const L_SEARCH_PARAMETER: &str = "diskann.l_search";
@@ -244,7 +243,7 @@ impl VindexVectorGlobalIndexReader {
.options
.get(NPROBE_PARAMETER)
.cloned()
- .unwrap_or_else(|| DEFAULT_NPROBE.to_string());
+ .unwrap_or_else(|| "auto".to_string());
log::debug!(
target: "paimon::vector_search",
"event=paimon_vindex_reader file={} nq={} nprobe={}
batch_index_parallelism={} memory_budget_bytes={} max_chunk_size={}
native_chunk_count={} native_chunk_queries={} scalar_chunk_count={}
total_ms={:.3} vindex_open_ms={:.3} metadata_ms={:.3} optimize_ms={:.3}
native_search_wall_ms={:.3} unattributed_ms={:.3}",
@@ -489,10 +488,12 @@ fn prepare_search_with_shared_filter(
}
None => VectorSearchParams::automatic(top_k),
},
- _ => VectorSearchParams::new(
- top_k,
- int_parameter(options, NPROBE_PARAMETER, DEFAULT_NPROBE)?,
- ),
+ _ => match options.get(NPROBE_PARAMETER) {
+ Some(value) => {
+ VectorSearchParams::new(top_k,
parse_int_parameter(NPROBE_PARAMETER, value)?)
+ }
+ None => VectorSearchParams::automatic(top_k),
+ },
};
let filter_bytes = if let Some(include_ids) =
vector_search.effective_include_row_ids() {
@@ -738,10 +739,15 @@ fn native_batch_query_working_set_bytes(
.dimension
.saturating_mul(std::mem::size_of::<f32>() * 2);
let centroid_products =
metadata.nlist.saturating_mul(std::mem::size_of::<f32>());
- let probe_results = prepared
- .params
- .configured_ivf_nprobe()
- .unwrap_or(0)
+ let ivf_nprobe = prepared.params.configured_ivf_nprobe().unwrap_or_else(||
{
+ // Automatic IVF search can progressively expand across every list.
+ if metadata.index_type == IndexType::DiskAnn {
+ 0
+ } else {
+ metadata.nlist
+ }
+ });
+ let probe_results = ivf_nprobe
.min(metadata.nlist)
.saturating_mul(std::mem::size_of::<usize>() +
std::mem::size_of::<f32>());
let top_k_results = prepared.params.top_k.saturating_mul(
@@ -838,19 +844,23 @@ fn int_parameter(
default_value: usize,
) -> crate::Result<usize> {
match options.get(key) {
- Some(value) => value
- .parse::<usize>()
- .map_err(|_| crate::Error::DataInvalid {
- message: format!(
- "Invalid value for '{}': {}. Must be a non-negative
integer.",
- key, value
- ),
- source: None,
- }),
+ Some(value) => parse_int_parameter(key, value),
None => Ok(default_value),
}
}
+fn parse_int_parameter(key: &str, value: &str) -> crate::Result<usize> {
+ value
+ .parse::<usize>()
+ .map_err(|_| crate::Error::DataInvalid {
+ message: format!(
+ "Invalid value for '{}': {}. Must be a non-negative integer.",
+ key, value
+ ),
+ source: None,
+ })
+}
+
#[cfg(test)]
mod tests {
use super::*;
@@ -1091,6 +1101,34 @@ mod tests {
}
}
+ #[test]
+ fn automatic_ivf_batch_reserves_progressive_probe_memory() {
+ let metadata = VectorIndexMetadata {
+ index_type: IndexType::IvfFlat,
+ dimension: 128,
+ nlist: 256,
+ metric: MetricType::L2,
+ total_vectors: 8192,
+ pq_m: None,
+ pq_bits: None,
+ rq_bits: None,
+ diskann: None,
+ };
+ let automatic = PreparedSearch {
+ params: VectorSearchParams::automatic(10),
+ filter_bytes: None,
+ };
+ let widest_explicit = PreparedSearch {
+ params: VectorSearchParams::new(10, metadata.nlist),
+ filter_bytes: None,
+ };
+
+ assert_eq!(
+ native_batch_query_working_set_bytes(&metadata, &automatic),
+ native_batch_query_working_set_bytes(&metadata, &widest_explicit),
+ );
+ }
+
#[test]
fn native_batch_chunk_reservation_tracks_actual_chunk() {
let metadata = VectorIndexMetadata {
@@ -1207,15 +1245,22 @@ mod tests {
#[test]
fn test_int_parameter() {
+ const FALLBACK: usize = 7;
+
let mut options = HashMap::new();
options.insert(NPROBE_PARAMETER.to_string(), "32".to_string());
assert_eq!(
- int_parameter(&options, NPROBE_PARAMETER, DEFAULT_NPROBE).unwrap(),
+ int_parameter(&options, NPROBE_PARAMETER, FALLBACK).unwrap(),
32
);
options.insert(NPROBE_PARAMETER.to_string(), "abc".to_string());
- assert!(int_parameter(&options, NPROBE_PARAMETER,
DEFAULT_NPROBE).is_err());
+ assert!(int_parameter(&options, NPROBE_PARAMETER, FALLBACK).is_err());
+ options.remove(NPROBE_PARAMETER);
+ assert_eq!(
+ int_parameter(&options, NPROBE_PARAMETER, FALLBACK).unwrap(),
+ FALLBACK
+ );
}
#[test]
@@ -1297,6 +1342,42 @@ mod tests {
assert_eq!(ivf.params.width, 4);
}
+ #[test]
+ fn prepare_ivf_search_uses_automatic_or_explicit_nprobe() {
+ let metadata = VectorIndexMetadata {
+ index_type: IndexType::IvfFlat,
+ dimension: TEST_DIMENSION,
+ nlist: 128,
+ metric: MetricType::L2,
+ total_vectors: 10_000,
+ pq_m: None,
+ pq_bits: None,
+ rq_bits: None,
+ diskann: None,
+ };
+
+ let automatic = prepare_search(&metadata, &HashMap::new(), &query())
+ .unwrap()
+ .unwrap();
+ assert_eq!(
+ automatic.params.search_width,
+ paimon_vindex_core::index::SearchWidth::Auto
+ );
+
+ let explicit = prepare_search(
+ &metadata,
+ &HashMap::from([("ivf.nprobe".to_string(), "32".to_string())]),
+ &query(),
+ )
+ .unwrap()
+ .unwrap();
+ assert_eq!(
+ explicit.params.search_width,
+ paimon_vindex_core::index::SearchWidth::IvfNProbe
+ );
+ assert_eq!(explicit.params.width, 32);
+ }
+
#[test]
fn diskann_reader_enforces_configured_memory_budget_during_optimization() {
let header = DiskAnnHeader::for_layout(
diff --git a/docs/src/sql.md b/docs/src/sql.md
index e55a644b..0aa71fa5 100644
--- a/docs/src/sql.md
+++ b/docs/src/sql.md
@@ -1613,7 +1613,7 @@ With Paimon's `SQLContext`, set query-time vindex options
for the session before
calling `vector_search`, then reset them when they are no longer needed:
```sql
--- IVF only; defaults to 16.
+-- IVF only; omit it to use the automatic search width.
SET 'paimon.ivf.nprobe' = '32';
SELECT * FROM vector_search('paimon.my_db.items', 'embedding', '[1.0, 0.0,
0.0, 0.0]', 10);
RESET 'paimon.ivf.nprobe';
@@ -1627,6 +1627,9 @@ RESET 'paimon.diskann.l_search';
Query options are index-family specific and non-applicable options are ignored:
IVF readers consume only `ivf.nprobe`, while DiskANN readers consume only
`diskann.l_search`. Setting both options is allowed; each index uses its own.
+When the applicable option is omitted, both index families select a search
+width automatically. IVF searches may progressively expand `nprobe` when the
+initial width underfills the requested top-k.
`vindex.reader.memory-budget-bytes` sets the per-Reader resident-data and cache
budget (default 4 GiB). It is distinct from the DiskANN build option