Skip to content

Commit 40b199c

Browse files
authored
[core] Add primary-key vector search read integration (#526)
Pass the ANN segment to the scorer seam so a real scorer can select per-segment index bytes, and add a fast-mode flag to bucket_search that skips the exact data-file fallback (ANN-only search) while leaving the default behavior unchanged.
1 parent c722f56 commit 40b199c

15 files changed

Lines changed: 3665 additions & 306 deletions

crates/paimon/src/spec/core_options.rs

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ pub(crate) const BLOB_FIELD_OPTION: &str = "blob-field";
117117
pub(crate) const BLOB_DESCRIPTOR_FIELD_OPTION: &str = "blob-descriptor-field";
118118
pub(crate) const BLOB_VIEW_FIELD_OPTION: &str = "blob-view-field";
119119
pub const BLOB_VIEW_RESOLVE_ENABLED_OPTION: &str = "blob-view.resolve.enabled";
120+
const PK_VECTOR_INDEX_COLUMNS_OPTION: &str = "pk-vector.index.columns";
120121

121122
/// Merge engine for primary-key tables.
122123
///
@@ -1067,6 +1068,72 @@ impl<'a> CoreOptions<'a> {
10671068
})
10681069
.unwrap_or_default()
10691070
}
1071+
1072+
/// True when the PK-vector index column option key is present (regardless of value).
1073+
pub fn primary_key_vector_index_enabled(&self) -> bool {
1074+
self.options.contains_key(PK_VECTOR_INDEX_COLUMNS_OPTION)
1075+
}
1076+
1077+
/// The configured PK-vector index columns, split on ',' and trimmed. Errors when
1078+
/// the key is present but resolves to no non-blank column.
1079+
pub fn primary_key_vector_index_columns(&self) -> crate::Result<Vec<String>> {
1080+
let raw = self
1081+
.options
1082+
.get(PK_VECTOR_INDEX_COLUMNS_OPTION)
1083+
.ok_or_else(|| crate::Error::ConfigInvalid {
1084+
message: "pk-vector.index.columns is not set".to_string(),
1085+
})?;
1086+
let columns: Vec<String> = raw
1087+
.split(',')
1088+
.map(|c| c.trim().to_string())
1089+
.filter(|c| !c.is_empty())
1090+
.collect();
1091+
if columns.is_empty() {
1092+
return Err(crate::Error::ConfigInvalid {
1093+
message: "pk-vector.index.columns is set but names no column".to_string(),
1094+
});
1095+
}
1096+
Ok(columns)
1097+
}
1098+
1099+
/// The single PK-vector index column. The first release supports exactly one.
1100+
pub fn primary_key_vector_index_column(&self) -> crate::Result<String> {
1101+
let mut columns = self.primary_key_vector_index_columns()?;
1102+
if columns.len() != 1 {
1103+
return Err(crate::Error::ConfigInvalid {
1104+
message: format!(
1105+
"pk-vector.index.columns must name exactly one column, got {}",
1106+
columns.len()
1107+
),
1108+
});
1109+
}
1110+
Ok(columns.remove(0))
1111+
}
1112+
1113+
/// The index type for a PK-vector column. Required — planning and the index reader
1114+
/// both need it, so an absent value is a hard error rather than a guessed default.
1115+
pub fn primary_key_vector_index_type(&self, col: &str) -> crate::Result<String> {
1116+
self.options
1117+
.get(&format!("fields.{col}.pk-vector.index.type"))
1118+
.map(|v| v.trim().to_string())
1119+
.filter(|v| !v.is_empty())
1120+
.ok_or_else(|| crate::Error::ConfigInvalid {
1121+
message: format!("fields.{col}.pk-vector.index.type is required but not set"),
1122+
})
1123+
}
1124+
1125+
/// The distance metric name for a PK-vector column, defaulting to inner_product.
1126+
/// Validated against the supported metrics; an unknown value is a hard error.
1127+
pub fn primary_key_vector_distance_metric(&self, col: &str) -> crate::Result<String> {
1128+
let raw = self
1129+
.options
1130+
.get(&format!("fields.{col}.pk-vector.distance.metric"))
1131+
.map(|v| v.trim().to_string())
1132+
.unwrap_or_else(|| "inner_product".to_string());
1133+
// Validate now (fail-loud) without exposing the crate-private metric enum.
1134+
crate::vindex::pkvector::metric::VectorSearchMetric::parse(&raw)?;
1135+
Ok(raw)
1136+
}
10701137
}
10711138

10721139
/// Parse a memory size string to bytes using binary (1024-based) semantics.
@@ -1856,4 +1923,88 @@ mod tests {
18561923
let opts = CoreOptions::new(&options);
18571924
assert!(!opts.ignore_update_before());
18581925
}
1926+
1927+
#[test]
1928+
fn test_pk_vector_index_disabled_by_default() {
1929+
let opts = HashMap::new();
1930+
assert!(!CoreOptions::new(&opts).primary_key_vector_index_enabled());
1931+
}
1932+
1933+
#[test]
1934+
fn test_pk_vector_single_column_and_type_and_metric() {
1935+
let opts = HashMap::from([
1936+
(
1937+
"pk-vector.index.columns".to_string(),
1938+
" embedding ".to_string(),
1939+
),
1940+
(
1941+
"fields.embedding.pk-vector.index.type".to_string(),
1942+
"ivf-flat".to_string(),
1943+
),
1944+
(
1945+
"fields.embedding.pk-vector.distance.metric".to_string(),
1946+
"Inner-Product".to_string(),
1947+
),
1948+
]);
1949+
let co = CoreOptions::new(&opts);
1950+
assert!(co.primary_key_vector_index_enabled());
1951+
assert_eq!(co.primary_key_vector_index_column().unwrap(), "embedding");
1952+
assert_eq!(
1953+
co.primary_key_vector_index_type("embedding").unwrap(),
1954+
"ivf-flat"
1955+
);
1956+
assert_eq!(
1957+
co.primary_key_vector_distance_metric("embedding").unwrap(),
1958+
"Inner-Product"
1959+
);
1960+
}
1961+
1962+
#[test]
1963+
fn test_pk_vector_metric_defaults_to_inner_product() {
1964+
let opts = HashMap::from([("pk-vector.index.columns".to_string(), "e".to_string())]);
1965+
assert_eq!(
1966+
CoreOptions::new(&opts)
1967+
.primary_key_vector_distance_metric("e")
1968+
.unwrap(),
1969+
"inner_product"
1970+
);
1971+
}
1972+
1973+
#[test]
1974+
fn test_pk_vector_unknown_metric_errors() {
1975+
let opts = HashMap::from([
1976+
("pk-vector.index.columns".to_string(), "e".to_string()),
1977+
(
1978+
"fields.e.pk-vector.distance.metric".to_string(),
1979+
"manhattan".to_string(),
1980+
),
1981+
]);
1982+
assert!(CoreOptions::new(&opts)
1983+
.primary_key_vector_distance_metric("e")
1984+
.is_err());
1985+
}
1986+
1987+
#[test]
1988+
fn test_pk_vector_empty_columns_errors() {
1989+
let opts = HashMap::from([("pk-vector.index.columns".to_string(), " , ".to_string())]);
1990+
let co = CoreOptions::new(&opts);
1991+
assert!(co.primary_key_vector_index_enabled()); // key present
1992+
assert!(co.primary_key_vector_index_columns().is_err());
1993+
}
1994+
1995+
#[test]
1996+
fn test_pk_vector_multiple_columns_unsupported() {
1997+
let opts = HashMap::from([("pk-vector.index.columns".to_string(), "a,b".to_string())]);
1998+
assert!(CoreOptions::new(&opts)
1999+
.primary_key_vector_index_column()
2000+
.is_err());
2001+
}
2002+
2003+
#[test]
2004+
fn test_pk_vector_index_type_absent_errors() {
2005+
let opts = HashMap::from([("pk-vector.index.columns".to_string(), "e".to_string())]);
2006+
assert!(CoreOptions::new(&opts)
2007+
.primary_key_vector_index_type("e")
2008+
.is_err());
2009+
}
18592010
}

crates/paimon/src/table/data_file_reader.rs

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -71,29 +71,24 @@ impl DataFileReader {
7171
self
7272
}
7373

74-
// These three accessors exist for the sibling `pk_vector_position_read`,
75-
// `pk_vector_indexed_split_read`, and `pk_vector_orchestrator` modules. The
76-
// read path that drives that chain lands in a later change, so under clippy
77-
// -D warnings they read as dead_code until then.
7874
/// Return a copy with a replaced read-type. Used by `pk_vector_position_read`
7975
/// to inject the internal `_ROW_ID` column for physical-position recovery.
80-
#[allow(dead_code)]
8176
pub(super) fn with_read_type(mut self, read_type: Vec<DataField>) -> Self {
8277
self.read_type = read_type;
8378
self
8479
}
8580

8681
/// The effective read-type (requested output fields) of this reader.
87-
/// Exposed for the sibling `pk_vector_position_read` module.
88-
#[allow(dead_code)]
82+
/// Exposed for the sibling `pk_vector_position_read` module, which drives the
83+
/// PK-vector materialization read path.
8984
pub(super) fn read_type(&self) -> &[DataField] {
9085
&self.read_type
9186
}
9287

9388
/// True if any configured predicate can actually drop rows. A lone
9489
/// `Predicate::AlwaysTrue` keeps every row in order and is not row-filtering,
95-
/// matching `reject_row_id_with_predicate`'s notion.
96-
#[allow(dead_code)]
90+
/// matching `reject_row_id_with_predicate`'s notion. Consumed by
91+
/// `pk_vector_position_read` (materialization read path).
9792
pub(super) fn has_row_filtering_predicate(&self) -> bool {
9893
self.predicates
9994
.iter()

crates/paimon/src/table/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,9 +56,11 @@ mod lumina_index_build_builder;
5656
pub(crate) mod merge_tree_split_generator;
5757
mod partition_filter;
5858
mod partition_stat;
59+
mod pk_vector_data_file_reader;
5960
mod pk_vector_indexed_split_read;
6061
mod pk_vector_orchestrator;
6162
mod pk_vector_position_read;
63+
mod pk_vector_scan;
6264
mod postpone_file_writer;
6365
mod prepared_files;
6466
mod read_builder;

0 commit comments

Comments
 (0)