Skip to content

Commit eb6bc77

Browse files
committed
perf(table): search primary-key vector buckets and files concurrently
Add a global-index.thread-num option (default 32, rejected when non-positive) and thread its value as a concurrency limit through the primary-key vector search. The per-bucket loop in the orchestrator and the exact-fallback per-file loop in the bucket search now fan out with buffer_unordered up to that limit; a limit of 1 takes a plain sequential loop so file and bucket visit order stay deterministic. Results are accumulated by fixed query and split index and merged through the existing order-independent bounded heaps and global top-k, so the output does not depend on completion order. The ANN segment scan stays sequential because it is synchronous CPU work.
1 parent 2a3ff24 commit eb6bc77

4 files changed

Lines changed: 717 additions & 131 deletions

File tree

crates/paimon/src/spec/core_options.rs

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ const DATA_EVOLUTION_ENABLED_OPTION: &str = "data-evolution.enabled";
2424
const GLOBAL_INDEX_ENABLED_OPTION: &str = "global-index.enabled";
2525
const GLOBAL_INDEX_SEARCH_MODE_OPTION: &str = "global-index.search-mode";
2626
const GLOBAL_INDEX_ROW_COUNT_PER_SHARD_OPTION: &str = "global-index.row-count-per-shard";
27+
const GLOBAL_INDEX_THREAD_NUM_OPTION: &str = "global-index.thread-num";
2728
const GLOBAL_INDEX_COLUMN_UPDATE_ACTION_OPTION: &str = "global-index.column-update-action";
2829
const SORTED_INDEX_RECORDS_PER_RANGE_OPTION: &str = "sorted-index.records-per-range";
2930
const BTREE_INDEX_FALLBACK_SCAN_MAX_SIZE_OPTION: &str = "btree-index.fallback-scan-max-size";
@@ -114,6 +115,7 @@ const DEFAULT_READ_BATCH_SIZE: usize = 1024;
114115
const DYNAMIC_BUCKET_TARGET_ROW_NUM_OPTION: &str = "dynamic-bucket.target-row-num";
115116
const DEFAULT_DYNAMIC_BUCKET_TARGET_ROW_NUM: i64 = 200_000;
116117
const DEFAULT_GLOBAL_INDEX_ROW_COUNT_PER_SHARD: i64 = 100_000;
118+
const DEFAULT_GLOBAL_INDEX_THREAD_NUM: i64 = 32;
117119
const DEFAULT_GLOBAL_INDEX_FALLBACK_SCAN_MAX_SIZE: i64 = 256 * 1024 * 1024;
118120
const BLOB_AS_DESCRIPTOR_OPTION: &str = "blob-as-descriptor";
119121
pub(crate) const BLOB_FIELD_OPTION: &str = "blob-field";
@@ -574,6 +576,28 @@ impl<'a> CoreOptions<'a> {
574576
Ok(value)
575577
}
576578

579+
/// Maximum number of concurrent tasks for global-index I/O, mirroring Java
580+
/// `CoreOptions.GLOBAL_INDEX_THREAD_NUM` (key `global-index.thread-num`,
581+
/// default 32). Used as the fan-out limit for the primary-key vector search
582+
/// (per-bucket and per-exact-file). A value of `1` reproduces strict
583+
/// sequential execution. A non-positive value is a misconfiguration and fails
584+
/// loud rather than being silently clamped.
585+
pub fn global_index_thread_num(&self) -> crate::Result<usize> {
586+
let value = self
587+
.parse_i64_option(GLOBAL_INDEX_THREAD_NUM_OPTION)?
588+
.unwrap_or(DEFAULT_GLOBAL_INDEX_THREAD_NUM);
589+
if value <= 0 {
590+
return Err(crate::Error::DataInvalid {
591+
message: format!(
592+
"Option '{}' must be greater than 0, got: {}",
593+
GLOBAL_INDEX_THREAD_NUM_OPTION, value
594+
),
595+
source: None,
596+
});
597+
}
598+
Ok(value as usize)
599+
}
600+
577601
pub fn sorted_index_records_per_range(&self) -> crate::Result<i64> {
578602
let value = self
579603
.parse_i64_option(SORTED_INDEX_RECORDS_PER_RANGE_OPTION)?
@@ -1235,6 +1259,7 @@ mod tests {
12351259
core_options.global_index_row_count_per_shard().unwrap(),
12361260
100_000
12371261
);
1262+
assert_eq!(core_options.global_index_thread_num().unwrap(), 32);
12381263
assert_eq!(
12391264
core_options.sorted_index_records_per_range().unwrap(),
12401265
100_000
@@ -1365,6 +1390,39 @@ mod tests {
13651390
}
13661391
}
13671392

1393+
#[test]
1394+
fn test_global_index_thread_num_default_and_custom() {
1395+
// Default mirrors Java (32) when unset.
1396+
let empty = HashMap::new();
1397+
let core = CoreOptions::new(&empty);
1398+
assert_eq!(core.global_index_thread_num().unwrap(), 32);
1399+
1400+
// Explicit value is read back verbatim.
1401+
let options =
1402+
HashMap::from([(GLOBAL_INDEX_THREAD_NUM_OPTION.to_string(), "8".to_string())]);
1403+
let core = CoreOptions::new(&options);
1404+
assert_eq!(core.global_index_thread_num().unwrap(), 8);
1405+
}
1406+
1407+
#[test]
1408+
fn test_global_index_thread_num_rejects_invalid_values() {
1409+
// Non-positive values are a misconfiguration and must fail loud (never
1410+
// clamped to 1); an unparsable value is likewise rejected.
1411+
for value in ["0", "-1", "abc"] {
1412+
let options = HashMap::from([(
1413+
GLOBAL_INDEX_THREAD_NUM_OPTION.to_string(),
1414+
value.to_string(),
1415+
)]);
1416+
let core = CoreOptions::new(&options);
1417+
1418+
let err = core
1419+
.global_index_thread_num()
1420+
.expect_err("invalid thread-num should fail");
1421+
assert!(matches!(err, crate::Error::DataInvalid { message, .. }
1422+
if message.contains(GLOBAL_INDEX_THREAD_NUM_OPTION)));
1423+
}
1424+
}
1425+
13681426
#[test]
13691427
fn test_sorted_index_records_per_range_rejects_invalid_values() {
13701428
for value in ["0", "-1", "abc"] {

0 commit comments

Comments
 (0)