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 878c33d6 Unify global index construction behind a common table builder
(#1091)
878c33d6 is described below
commit 878c33d6f56f9c3a040152774595f35548cb9bbf
Author: Jingsong Lee <[email protected]>
AuthorDate: Sat Oct 10 16:12:25 2026 +0800
Unify global index construction behind a common table builder (#1091)
---
bindings/c/src/tests.rs | 7 +-
.../python/python/pypaimon_rust/datafusion.pyi | 15 +-
bindings/python/src/context.rs | 2 +-
bindings/python/src/index_build.rs | 50 ++-
bindings/python/src/table.rs | 6 +-
crates/integrations/datafusion/src/procedures.rs | 82 +----
crates/integrations/datafusion/tests/procedures.rs | 4 +-
crates/paimon/examples/ivfpq_build_benchmark.rs | 3 +-
crates/paimon/src/table/data_evolution_reader.rs | 2 +-
crates/paimon/src/table/de_vector_read/tests.rs | 3 +-
crates/paimon/src/table/de_vector_scan/tests.rs | 3 +-
.../src/table/full_text_index_build_builder.rs | 58 ++--
.../table/full_text_index_build_builder/tests.rs | 24 +-
.../paimon/src/table/global_index_build_builder.rs | 348 +++++++++++++++++++++
.../paimon/src/table/global_index_build_common.rs | 23 ++
.../paimon/src/table/global_index_drop_builder.rs | 27 +-
crates/paimon/src/table/global_index_types.rs | 72 ++---
.../paimon/src/table/lumina_index_build_builder.rs | 63 ++--
.../src/table/lumina_index_build_builder/tests.rs | 15 +-
.../src/table/lumina_index_build_builder/writer.rs | 23 +-
crates/paimon/src/table/mod.rs | 38 +--
.../src/table/sorted_global_index_build_builder.rs | 39 +--
.../preparation_tests.rs | 20 +-
.../sorted_global_index_build_builder/tests.rs | 88 +++---
.../paimon/src/table/vector_search_test_utils.rs | 5 +-
.../paimon/src/table/vindex_index_build_builder.rs | 67 ++--
.../src/table/vindex_index_build_builder/tests.rs | 39 ++-
.../src/table/vindex_index_build_builder/timing.rs | 6 +-
.../src/table/vindex_index_build_builder/writer.rs | 24 +-
crates/paimon/src/table/write_builder.rs | 2 +-
crates/paimon/tests/scan_planning_parity_test.rs | 2 +-
31 files changed, 699 insertions(+), 461 deletions(-)
diff --git a/bindings/c/src/tests.rs b/bindings/c/src/tests.rs
index b8dbd785..6eb98979 100644
--- a/bindings/c/src/tests.rs
+++ b/bindings/c/src/tests.rs
@@ -3123,7 +3123,7 @@ fn test_two_commits_same_builder() {
// * A primary-key vector table backed by a real vindex IVF-flat ANN segment
// built in-process (bucket-local ANN search, residual filter supported).
// * A data-evolution (append) vector table whose global index is produced by
-// the public `new_vindex_index_build_builder(...).execute()` path.
+// the public
`new_global_index_build_builder().with_index_type(...).execute()` path.
//
// Both fixtures live entirely on the in-memory FileIO, so no temp dirs or
// on-disk schema files are needed: the written data file keeps `schema_id ==
0`,
@@ -3482,7 +3482,7 @@ fn append_vector_batch(ids: Vec<i32>, vectors: Vec<[f32;
2]>) -> RecordBatch {
}
/// Build a data-evolution vector table: write vectors via the public write
path,
-/// then build the global vindex index via `new_vindex_index_build_builder`.
+/// then build the global vindex index via `new_global_index_build_builder`.
fn build_append_vector_table(path: &str) -> Table {
let file_io = memory_file_io();
setup_table_dirs(&file_io, path);
@@ -3524,7 +3524,8 @@ fn build_append_vector_table(path: &str) -> Table {
write_builder.new_commit().commit(messages).await.unwrap();
let built = table
- .new_vindex_index_build_builder(INDEX_TYPE)
+ .new_global_index_build_builder()
+ .with_index_type(INDEX_TYPE)
.with_index_column("embedding")
.execute()
.await
diff --git a/bindings/python/python/pypaimon_rust/datafusion.pyi
b/bindings/python/python/pypaimon_rust/datafusion.pyi
index 0974cea2..e38d5e5e 100644
--- a/bindings/python/python/pypaimon_rust/datafusion.pyi
+++ b/bindings/python/python/pypaimon_rust/datafusion.pyi
@@ -223,7 +223,7 @@ class Table:
def new_stream_scan(self) -> StreamTableScan: ...
def new_vector_search_builder(self) -> "VectorSearchBuilder": ...
def new_batch_vector_search_builder(self) -> "BatchVectorSearchBuilder":
...
- def new_sorted_global_index_build_builder(self) ->
"SortedGlobalIndexBuildBuilder": ...
+ def new_global_index_build_builder(self) -> "GlobalIndexBuildBuilder": ...
def new_batch_write_builder(self) -> "BatchWriteBuilder": ...
def new_postpone_fixed_bucket_write_builder(self) ->
"PostponeFixedBucketWriteBuilder": ...
def new_stream_write_builder(self) -> "StreamWriteBuilder": ...
@@ -260,11 +260,14 @@ class Table:
"""
...
-class SortedGlobalIndexBuildBuilder:
- def with_index_column(self, column: str) ->
"SortedGlobalIndexBuildBuilder": ...
- def with_index_type(self, index_type: str) ->
"SortedGlobalIndexBuildBuilder": ...
- def with_options(self, options: Dict[str, str]) ->
"SortedGlobalIndexBuildBuilder": ...
- def with_partition_filter(self, predicate: Dict[str, Any]) ->
"SortedGlobalIndexBuildBuilder": ...
+class GlobalIndexBuildBuilder:
+ @staticmethod
+ def supports_index_type(index_type: str) -> bool: ...
+ def with_index_columns(self, columns: List[str]) ->
"GlobalIndexBuildBuilder": ...
+ def with_index_column(self, column: str) -> "GlobalIndexBuildBuilder": ...
+ def with_index_type(self, index_type: str) -> "GlobalIndexBuildBuilder":
...
+ def with_options(self, options: Dict[str, str]) ->
"GlobalIndexBuildBuilder": ...
+ def with_partition_filter(self, predicate: Dict[str, Any]) ->
"GlobalIndexBuildBuilder": ...
def build(self) -> List["CommitMessage"]:
"""Build unpublished index files; the caller decides whether to commit
or abort."""
...
diff --git a/bindings/python/src/context.rs b/bindings/python/src/context.rs
index 37a1f03e..f0baad1b 100644
--- a/bindings/python/src/context.rs
+++ b/bindings/python/src/context.rs
@@ -726,7 +726,7 @@ pub fn register_module(py: Python<'_>, m: &Bound<'_,
PyModule>) -> PyResult<()>
let this = PyModule::new(py, "datafusion")?;
this.add_class::<PaimonCatalog>()?;
this.add_class::<crate::table::PyTable>()?;
- this.add_class::<crate::index_build::PySortedGlobalIndexBuildBuilder>()?;
+ this.add_class::<crate::index_build::PyGlobalIndexBuildBuilder>()?;
this.add_class::<crate::vector_search::PyVectorSearchBuilder>()?;
this.add_class::<crate::vector_search::PyBatchVectorSearchBuilder>()?;
this.add_class::<crate::vector_search::PyVectorScan>()?;
diff --git a/bindings/python/src/index_build.rs
b/bindings/python/src/index_build.rs
index 2b216153..d81e7252 100644
--- a/bindings/python/src/index_build.rs
+++ b/bindings/python/src/index_build.rs
@@ -15,13 +15,13 @@
// specific language governing permissions and limitations
// under the License.
-//! Parameter and commit-message wrappers for the core sorted-index builder.
+//! Parameter and commit-message wrappers for the core global-index builder.
use std::collections::HashMap;
use std::sync::Arc;
use paimon::spec::Predicate;
-use paimon::table::{SortedGlobalIndexBuildBuilder, Table};
+use paimon::table::{GlobalIndexBuildBuilder, Table};
use paimon_datafusion::runtime::runtime;
use pyo3::prelude::*;
use pyo3::types::PyDict;
@@ -30,33 +30,41 @@ use crate::error::to_py_err;
use crate::predicate::dict_to_table_predicate;
use crate::write::PyCommitMessage;
-#[pyclass(
- name = "SortedGlobalIndexBuildBuilder",
- module = "pypaimon_rust.datafusion"
-)]
-pub struct PySortedGlobalIndexBuildBuilder {
+enum PyIndexColumns {
+ Column(String),
+ Columns(Vec<String>),
+}
+
+#[pyclass(name = "GlobalIndexBuildBuilder", module =
"pypaimon_rust.datafusion")]
+pub struct PyGlobalIndexBuildBuilder {
table: Arc<Table>,
- column: Option<String>,
+ columns: Option<PyIndexColumns>,
index_type: String,
options: HashMap<String, String>,
partition_filters: Vec<Predicate>,
}
-impl PySortedGlobalIndexBuildBuilder {
+impl PyGlobalIndexBuildBuilder {
pub(crate) fn new(table: Arc<Table>) -> Self {
Self {
table,
- column: None,
+ columns: None,
index_type: "btree".into(),
options: HashMap::new(),
partition_filters: vec![],
}
}
- fn builder(&self) -> paimon::Result<SortedGlobalIndexBuildBuilder<'_>> {
- let mut builder = self.table.new_sorted_global_index_build_builder();
- if let Some(column) = &self.column {
- builder.with_index_column(column);
+ fn builder(&self) -> paimon::Result<GlobalIndexBuildBuilder<'_>> {
+ let mut builder = self.table.new_global_index_build_builder();
+ match &self.columns {
+ Some(PyIndexColumns::Column(column)) => {
+ builder.with_index_column(column);
+ }
+ Some(PyIndexColumns::Columns(columns)) => {
+
builder.with_index_columns(&columns.iter().map(String::as_str).collect::<Vec<_>>());
+ }
+ None => {}
}
builder
.with_index_type(&self.index_type)
@@ -69,9 +77,19 @@ impl PySortedGlobalIndexBuildBuilder {
}
#[pymethods]
-impl PySortedGlobalIndexBuildBuilder {
+impl PyGlobalIndexBuildBuilder {
+ #[staticmethod]
+ fn supports_index_type(index_type: &str) -> bool {
+ GlobalIndexBuildBuilder::supports_index_type(index_type)
+ }
+
+ fn with_index_columns(mut slf: PyRefMut<'_, Self>, columns: Vec<String>)
-> PyRefMut<'_, Self> {
+ slf.columns = Some(PyIndexColumns::Columns(columns));
+ slf
+ }
+
fn with_index_column(mut slf: PyRefMut<'_, Self>, column: String) ->
PyRefMut<'_, Self> {
- slf.column = Some(column);
+ slf.columns = Some(PyIndexColumns::Column(column));
slf
}
diff --git a/bindings/python/src/table.rs b/bindings/python/src/table.rs
index a2771e41..363c4a07 100644
--- a/bindings/python/src/table.rs
+++ b/bindings/python/src/table.rs
@@ -206,10 +206,8 @@ impl PyTable {
crate::vector_search::PyBatchVectorSearchBuilder::new(Arc::clone(&self.inner))
}
- fn new_sorted_global_index_build_builder(
- &self,
- ) -> crate::index_build::PySortedGlobalIndexBuildBuilder {
-
crate::index_build::PySortedGlobalIndexBuildBuilder::new(Arc::clone(&self.inner))
+ fn new_global_index_build_builder(&self) ->
crate::index_build::PyGlobalIndexBuildBuilder {
+
crate::index_build::PyGlobalIndexBuildBuilder::new(Arc::clone(&self.inner))
}
fn new_batch_write_builder(&self) -> PyBatchWriteBuilder {
diff --git a/crates/integrations/datafusion/src/procedures.rs
b/crates/integrations/datafusion/src/procedures.rs
index 18416eec..b908da7d 100644
--- a/crates/integrations/datafusion/src/procedures.rs
+++ b/crates/integrations/datafusion/src/procedures.rs
@@ -64,21 +64,18 @@ use paimon::api::{
PermissionColumns, PermissionResource, PolicyType, ResourceType, RowFilter,
};
use paimon::catalog::{Catalog, Identifier, RESTCatalog};
-use paimon::lumina::LUMINA_IDENTIFIER;
+use paimon::lumina::{is_lumina_index_type, LUMINA_IDENTIFIER};
use paimon::spec::Snapshot;
use paimon::table::{
- normalize_global_index_type_for_drop, BranchManager, SnapshotManager,
Table, TagManager,
+ normalize_global_index_type, BranchManager, SnapshotManager, Table,
TagManager,
SUPPORTED_GLOBAL_INDEX_TYPES_FOR_DROP,
};
-use paimon::vindex::is_vindex_index_type;
use crate::error::to_datafusion_error;
/// Default `index_type` for the global index procedures when the argument is
/// omitted, matching Java's `CreateGlobalIndexProcedure`.
const DEFAULT_GLOBAL_INDEX_TYPE: &str = "btree";
-/// Java `NativeFullTextGlobalIndexerFactory.IDENTIFIER`.
-const FULL_TEXT_GLOBAL_INDEX_TYPE: &str = "full-text";
/// Resolve a snapshot by id: try live snapshot file first, then fall back to
tag metadata.
async fn resolve_snapshot_by_id(
@@ -716,13 +713,18 @@ async fn proc_create_lumina_index(
) -> DFResult<DataFrame> {
let table = get_table(catalog, catalog_name, args).await?;
let index_column = require_arg(args, "index_column")?;
- let mut builder = table.new_lumina_index_build_builder();
+ let mut builder = table.new_global_index_build_builder();
builder.with_index_column(index_column);
let index_type = normalize_index_type(
args.get("index_type")
.map(String::as_str)
.unwrap_or(LUMINA_IDENTIFIER),
);
+ if !is_lumina_index_type(&index_type) {
+ return Err(DataFusionError::NotImplemented(format!(
+ "Unsupported Lumina index type: {index_type}"
+ )));
+ }
builder.with_index_type(&index_type);
if let Some(options) = args.get("options") {
builder.with_options(parse_key_value_options(options)?);
@@ -743,45 +745,14 @@ async fn proc_create_global_index(
.get("index_type")
.map(String::as_str)
.unwrap_or(DEFAULT_GLOBAL_INDEX_TYPE);
- let index_type = normalize_index_type(index_type_arg);
- let index_type = index_type.as_str();
- if is_scalar_global_index_type(index_type) {
- let mut builder = table.new_sorted_global_index_build_builder();
- builder.with_index_column(index_column);
- builder.with_index_type(index_type);
- if let Some(options) = args.get("options") {
- builder.with_options(parse_key_value_options(options)?);
- }
- builder.execute().await.map_err(to_datafusion_error)?;
- } else if is_vindex_index_type(index_type) {
- let mut builder = table.new_vindex_index_build_builder(index_type);
- builder.with_index_column(index_column);
- if let Some(options) = args.get("options") {
- builder.with_options(parse_key_value_options(options)?);
- }
- builder.execute().await.map_err(to_datafusion_error)?;
- } else if index_type == FULL_TEXT_GLOBAL_INDEX_TYPE {
- #[cfg(feature = "fulltext")]
- {
- let mut builder = table.new_full_text_index_build_builder();
- builder.with_index_column(index_column);
- if let Some(options) = args.get("options") {
- builder.with_options(parse_key_value_options(options)?);
- }
- builder.execute().await.map_err(to_datafusion_error)?;
- }
- #[cfg(not(feature = "fulltext"))]
- return Err(DataFusionError::NotImplemented(
- "create_global_index with index_type => 'full-text' requires the
'fulltext' feature"
- .to_string(),
- ));
- } else {
- // Echo the raw argument, not the normalized one, so a typo stays
visible.
- return Err(DataFusionError::NotImplemented(format!(
- "create_global_index only supports index_type => 'btree',
'bitmap', 'multivalue', 'fm', 'full-text', \
- or vindex types ('ivf-flat', 'ivf-pq', 'ivf-sq', 'ivf-rq',
'diskann'), got '{index_type_arg}'"
- )));
+ let mut builder = table.new_global_index_build_builder();
+ builder
+ .with_index_column(index_column)
+ .with_index_type(index_type_arg);
+ if let Some(options) = args.get("options") {
+ builder.with_options(parse_key_value_options(options)?);
}
+ builder.execute().await.map_err(to_datafusion_error)?;
ok_result(ctx)
}
@@ -799,7 +770,7 @@ async fn proc_drop_global_index(
.unwrap_or(DEFAULT_GLOBAL_INDEX_TYPE);
let index_type = normalize_index_type(index_type_arg);
let index_type = index_type.as_str();
- if normalize_global_index_type_for_drop(index_type).is_none() {
+ if normalize_global_index_type(index_type).is_none() {
// Echo the raw argument, not the normalized one, so a typo stays
visible.
return Err(DataFusionError::NotImplemented(format!(
"unsupported global index type '{index_type_arg}'; supported:
{SUPPORTED_GLOBAL_INDEX_TYPES_FOR_DROP}"
@@ -829,17 +800,10 @@ async fn proc_drop_global_index(
ok_result(ctx)
}
-/// Precondition: `index_type` is already canonical (see
`normalize_index_type`).
-fn is_scalar_global_index_type(index_type: &str) -> bool {
- matches!(index_type, "btree" | "bitmap" | "multivalue" | "fm")
-}
-
/// Canonicalize a procedure's `index_type` argument: trim, then lowercase.
/// Mirrors `indexType.toLowerCase(Locale.ROOT).trim()` in Java's Flink and
Spark
/// `CreateGlobalIndexProcedure` / `DropGlobalIndexProcedure`. Normalizing at
this
-/// boundary keeps the core builders' exact matching intact -- they are the
analog
-/// of Java's `GlobalIndexer`, which likewise receives an already-canonical
value
-/// and persists it into index metadata.
+/// boundary and the core builder both accept procedure-style identifiers.
fn normalize_index_type(index_type: &str) -> String {
index_type.trim().to_ascii_lowercase()
}
@@ -1442,18 +1406,6 @@ mod tests {
assert_eq!(normalize_index_type(" Full-Text "), "full-text");
}
- #[test]
- fn test_scalar_global_index_type_predicate() {
- assert!(is_scalar_global_index_type("btree"));
- assert!(is_scalar_global_index_type("bitmap"));
- assert!(is_scalar_global_index_type("multivalue"));
- assert!(is_scalar_global_index_type("fm"));
- assert!(!is_scalar_global_index_type("ivf-flat"));
- assert!(!is_scalar_global_index_type("lumina"));
- // The predicate requires a canonical input; callers normalize first.
- assert!(!is_scalar_global_index_type("BTREE"));
- }
-
#[test]
fn test_reject_unknown_args_covers_tag_and_index_procedures() {
// A typo'd optional arg on a tag/index procedure used to be silently
dropped:
diff --git a/crates/integrations/datafusion/tests/procedures.rs
b/crates/integrations/datafusion/tests/procedures.rs
index d89c4547..abc06c63 100644
--- a/crates/integrations/datafusion/tests/procedures.rs
+++ b/crates/integrations/datafusion/tests/procedures.rs
@@ -544,7 +544,7 @@ async fn
test_create_global_index_rejects_unsupported_index_types() {
index_type => '{index_type}'\
)"
),
- "only supports index_type => 'btree', 'bitmap', 'multivalue',
'fm', 'full-text', or vindex types",
+ "Unsupported global index type",
)
.await;
}
@@ -675,7 +675,7 @@ async fn
test_global_index_procedures_echo_raw_unsupported_type() {
assert_sql_error(
&sql_context,
"CALL sys.create_global_index(table => 'test_db.btree_echo',
index_column => 'id', index_type => 'Ivf-Hnsw-Flat')",
- "got 'Ivf-Hnsw-Flat'",
+ "Unsupported global index type: 'Ivf-Hnsw-Flat'",
)
.await;
assert_sql_error(
diff --git a/crates/paimon/examples/ivfpq_build_benchmark.rs
b/crates/paimon/examples/ivfpq_build_benchmark.rs
index cbfe1f3f..84a7844e 100644
--- a/crates/paimon/examples/ivfpq_build_benchmark.rs
+++ b/crates/paimon/examples/ivfpq_build_benchmark.rs
@@ -62,7 +62,8 @@ async fn main() -> Result<(), Box<dyn Error>> {
]);
let started = Instant::now();
let built_shards = table
- .new_vindex_index_build_builder("ivf-pq")
+ .new_global_index_build_builder()
+ .with_index_type("ivf-pq")
.with_index_column(&column)
.with_options(options.clone())
.execute()
diff --git a/crates/paimon/src/table/data_evolution_reader.rs
b/crates/paimon/src/table/data_evolution_reader.rs
index b1dbbddb..a29c9241 100644
--- a/crates/paimon/src/table/data_evolution_reader.rs
+++ b/crates/paimon/src/table/data_evolution_reader.rs
@@ -7360,7 +7360,7 @@ mod tests {
.await
.unwrap();
table
- .new_btree_global_index_build_builder()
+ .new_global_index_build_builder()
.with_index_column("id")
.execute()
.await
diff --git a/crates/paimon/src/table/de_vector_read/tests.rs
b/crates/paimon/src/table/de_vector_read/tests.rs
index ed64c2cb..0d76e309 100644
--- a/crates/paimon/src/table/de_vector_read/tests.rs
+++ b/crates/paimon/src/table/de_vector_read/tests.rs
@@ -1516,7 +1516,8 @@ async fn
filtered_rerank_rejects_metric_from_a_skipped_shard() {
.await
.unwrap();
let count = table
- .new_vindex_index_build_builder(IVF_FLAT_IDENTIFIER)
+ .new_global_index_build_builder()
+ .with_index_type(IVF_FLAT_IDENTIFIER)
.with_index_column("embedding")
.with_options(HashMap::from([(
"ivf-flat.distance.metric".to_string(),
diff --git a/crates/paimon/src/table/de_vector_scan/tests.rs
b/crates/paimon/src/table/de_vector_scan/tests.rs
index aa1b14f9..310a442f 100644
--- a/crates/paimon/src/table/de_vector_scan/tests.rs
+++ b/crates/paimon/src/table/de_vector_scan/tests.rs
@@ -506,7 +506,8 @@ async fn
scalar_coverage_gaps_follow_scalar_and_vector_modes() {
.unwrap();
if vector_covers_tail {
table
-
.new_vindex_index_build_builder(crate::vindex::IVF_FLAT_IDENTIFIER)
+ .new_global_index_build_builder()
+ .with_index_type(crate::vindex::IVF_FLAT_IDENTIFIER)
.with_index_column("embedding")
.with_options(HashMap::from([(
"ivf-flat.nlist".to_string(),
diff --git a/crates/paimon/src/table/full_text_index_build_builder.rs
b/crates/paimon/src/table/full_text_index_build_builder.rs
index 1ae932bd..0ee0924f 100644
--- a/crates/paimon/src/table/full_text_index_build_builder.rs
+++ b/crates/paimon/src/table/full_text_index_build_builder.rs
@@ -21,12 +21,11 @@
//! `NativeFullTextGlobalIndexWriter`: the table's row-id space is cut into
//! `global-index.row-count-per-shard` shards, each shard's text column is fed
to
//! the shared `paimon-ftindex-core` writer with shard-relative row ids, and
every
-//! non-empty shard becomes one `full-text` index file committed in a single
-//! snapshot. Java writes the same archive through the same native core, so the
+//! non-empty shard becomes one unpublished `full-text` index file. Java
writes the same archive through the same native core, so the
//! files are readable by both `FullTextSearchBuilder` and Java.
use crate::spec::{
- CoreOptions, DataField, DataType, GlobalIndexMeta, IndexFileMeta,
ROW_ID_FIELD_NAME,
+ CoreOptions, DataField, DataType, GlobalIndexMeta, IndexFileMeta,
Predicate, ROW_ID_FIELD_NAME,
};
use crate::table::global_index_build_common::vector::{
find_index_field, plan_vector_index_shards, VectorIndexShard,
@@ -45,15 +44,15 @@ use paimon_ftindex_core::io::PosWriter;
use paimon_ftindex_core::{FullTextIndexConfig, FullTextIndexWriter};
use std::collections::{BTreeMap, HashMap};
-const INDEX_DIR: &str = "index";
/// Java `NativeFullTextIndexOptions.FULL_TEXT_PREFIX`: only these options
reach
/// the native writer, with the prefix removed.
const FULL_TEXT_OPTION_PREFIX: &str = "full-text.";
-pub struct FullTextIndexBuildBuilder<'a> {
+pub(super) struct FullTextIndexBuildBuilder<'a> {
table: &'a Table,
index_column: Option<String>,
options: HashMap<String, String>,
+ pub(super) partition_filter: Option<Predicate>,
}
impl<'a> FullTextIndexBuildBuilder<'a> {
@@ -62,6 +61,7 @@ impl<'a> FullTextIndexBuildBuilder<'a> {
table,
index_column: None,
options: HashMap::new(),
+ partition_filter: None,
}
}
@@ -77,10 +77,7 @@ impl<'a> FullTextIndexBuildBuilder<'a> {
self
}
- /// Build full-text index files for every row range of the latest snapshot
- /// that is not yet covered by a full-text index on the column. Returns the
- /// number of index files committed.
- pub async fn execute(&self) -> Result<usize> {
+ pub(super) async fn prepare(&self) -> Result<(Option<i64>,
Vec<CommitMessage>)> {
// Building the index scans the table's rows.
CoreOptions::new(self.table.schema().options()).ensure_read_authorized()?;
@@ -115,17 +112,15 @@ impl<'a> FullTextIndexBuildBuilder<'a> {
// The table's manager resolves a REST-managed table's latest snapshot
// through the catalog, like reads and commit validation do.
let snapshot_manager = self.table.snapshot_manager();
- let snapshot = snapshot_manager
- .get_latest_snapshot()
- .await?
- .ok_or_else(|| Error::DataInvalid {
- message: "Cannot build full-text index without a
snapshot".to_string(),
- source: None,
- })?;
+ let Some(snapshot) = snapshot_manager.get_latest_snapshot().await?
else {
+ return Ok((None, vec![]));
+ };
- let manifest_entries = self
- .table
- .new_read_builder()
+ let mut read_builder = self.table.new_read_builder();
+ if let Some(filter) = &self.partition_filter {
+ read_builder.with_filter(filter.clone());
+ }
+ let manifest_entries = read_builder
.new_scan()
.with_scan_all_files()
.plan_manifest_entries(&snapshot)
@@ -150,7 +145,7 @@ impl<'a> FullTextIndexBuildBuilder<'a> {
"full-text",
)?;
if shards.is_empty() {
- return Ok(0);
+ return Ok((Some(snapshot.id()), vec![]));
}
validate_existing_index_overlap(
@@ -201,14 +196,10 @@ impl<'a> FullTextIndexBuildBuilder<'a> {
messages.push(message);
}
if messages.is_empty() {
- return Ok(0);
+ return Ok((Some(snapshot.id()), vec![]));
}
- let file_count = messages.len();
- commit
- .commit_if_latest_snapshot(messages, snapshot.id())
- .await?;
- Ok(file_count)
+ Ok((Some(snapshot.id()), messages))
}
async fn build_shard(
@@ -272,16 +263,13 @@ impl<'a> FullTextIndexBuildBuilder<'a> {
})
.await?;
- let table_location = self.table.location().trim_end_matches('/');
let file_name = format!(
"{FULL_TEXT_GLOBAL_INDEX_TYPE}-global-index-{}.index",
uuid::Uuid::new_v4()
);
- let index_path = format!("{table_location}/{INDEX_DIR}/{file_name}");
- self.table
- .file_io()
- .mkdirs(&format!("{table_location}/{INDEX_DIR}/"))
- .await?;
+ let (index_path, external_path) =
+
super::global_index_build_common::prepare_index_file_path(self.table,
&file_name)
+ .await?;
let upload: Result<i64> = async {
copy_local_file_to_output(
local_index.path(),
@@ -312,13 +300,15 @@ impl<'a> FullTextIndexBuildBuilder<'a> {
file_size,
row_count: cursor.row_count,
deletion_vectors_ranges: None,
- external_path: None,
+ external_path,
global_index_meta: Some(GlobalIndexMeta {
row_range_start: shard.row_range_start,
row_range_end: shard.row_range_end,
index_field_id,
extra_field_ids: None,
- source_meta: None,
+ source_meta: Some(
+
crate::spec::DataEvolutionIndexSourceMeta::new(shard.snapshot_id)?.serialize(),
+ ),
index_meta: Some(index_meta.to_vec()),
}),
}))
diff --git a/crates/paimon/src/table/full_text_index_build_builder/tests.rs
b/crates/paimon/src/table/full_text_index_build_builder/tests.rs
index f59ccfc3..3dc16531 100644
--- a/crates/paimon/src/table/full_text_index_build_builder/tests.rs
+++ b/crates/paimon/src/table/full_text_index_build_builder/tests.rs
@@ -169,7 +169,8 @@ async fn
test_execute_builds_shards_readable_by_full_text_search() {
assert!(search(&table, "paimon").await.is_empty());
let file_count = table
- .new_full_text_index_build_builder()
+ .new_global_index_build_builder()
+ .with_index_type("full-text")
.with_index_column("name")
.execute()
.await
@@ -224,7 +225,8 @@ async fn test_execute_only_indexes_new_rows() {
write_rows(&table, vec![1, 2], vec![Some("paimon one"),
Some("other")]).await;
let builder = || {
- let mut builder = table.new_full_text_index_build_builder();
+ let mut builder = table.new_global_index_build_builder();
+ builder.with_index_type("full-text");
builder.with_index_column("name");
builder
};
@@ -259,7 +261,8 @@ async fn test_execute_indexes_all_null_shard() {
// Java's writer counts NULL rows, so an all-NULL shard still yields an
// (empty) index file that marks the range as indexed.
let file_count = table
- .new_full_text_index_build_builder()
+ .new_global_index_build_builder()
+ .with_index_type("full-text")
.with_index_column("name")
.execute()
.await
@@ -275,7 +278,8 @@ async fn
test_execute_passes_prefixed_options_to_native_writer() {
write_rows(&table, vec![1], vec![Some("Running Paimon")]).await;
let file_count = table
- .new_full_text_index_build_builder()
+ .new_global_index_build_builder()
+ .with_index_type("full-text")
.with_index_column("name")
.with_options(HashMap::from([
("full-text.stem".to_string(), "false".to_string()),
@@ -309,7 +313,8 @@ async fn
test_execute_rejects_invalid_native_options_before_reading() {
setup_dirs(&table).await;
let err = table
- .new_full_text_index_build_builder()
+ .new_global_index_build_builder()
+ .with_index_type("full-text")
.with_index_column("name")
.with_options(HashMap::from([(
"full-text.tokenizer".to_string(),
@@ -328,7 +333,8 @@ async fn
test_execute_rejects_invalid_native_options_before_reading() {
async fn test_execute_rejects_unsupported_tables_and_columns() {
let expect_err = |table: Table, column: &'static str| async move {
table
- .new_full_text_index_build_builder()
+ .new_global_index_build_builder()
+ .with_index_type("full-text")
.with_index_column(column)
.execute()
.await
@@ -394,7 +400,8 @@ async fn
test_execute_rejects_unsupported_tables_and_columns() {
async fn test_execute_requires_index_column() {
let table = test_table("memory:/ft_no_column", table_options("10"));
let err = table
- .new_full_text_index_build_builder()
+ .new_global_index_build_builder()
+ .with_index_type("full-text")
.execute()
.await
.unwrap_err();
@@ -611,7 +618,8 @@ async fn
test_build_search_and_drop_through_the_catalog_snapshot() {
);
let file_count = table
- .new_full_text_index_build_builder()
+ .new_global_index_build_builder()
+ .with_index_type("full-text")
.with_index_column("name")
.execute()
.await
diff --git a/crates/paimon/src/table/global_index_build_builder.rs
b/crates/paimon/src/table/global_index_build_builder.rs
new file mode 100644
index 00000000..428de822
--- /dev/null
+++ b/crates/paimon/src/table/global_index_build_builder.rs
@@ -0,0 +1,348 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+use std::collections::HashMap;
+use std::time::Instant;
+
+#[cfg(feature = "fulltext")]
+use super::full_text_index_build_builder::FullTextIndexBuildBuilder;
+use super::global_index_build_common::{resolve_index_fields, IndexColumns};
+use super::global_index_types::{
+ normalize_global_index_type, normalize_queryable_global_index_type,
BTREE_GLOBAL_INDEX_TYPE,
+ FULL_TEXT_GLOBAL_INDEX_TYPE,
+};
+use super::lumina_index_build_builder::LuminaIndexBuildBuilder;
+use super::sorted_global_index_build_builder::SortedGlobalIndexBuildBuilder;
+use super::vindex_index_build_builder::timing::{
+ vector_index_build_timing_enabled, VectorIndexBuildTiming,
+};
+use super::vindex_index_build_builder::VindexIndexBuildBuilder;
+use super::{CommitMessage, Table, TableCommit};
+use crate::lumina::LUMINA_IDENTIFIER;
+use crate::spec::{CoreOptions, Predicate};
+use crate::{Error, Result};
+
+/// Build global indexes by type, like Java's `GlobalIndexer.create`.
+/// Sorting and native writer selection are internal implementation details.
+pub struct GlobalIndexBuildBuilder<'a> {
+ table: &'a Table,
+ columns: Option<IndexColumns>,
+ index_type: String,
+ options: HashMap<String, String>,
+ partition_filter: Option<Predicate>,
+}
+
+struct PreparedBuild {
+ snapshot_id: Option<i64>,
+ messages: Vec<CommitMessage>,
+ vector_timings: Vec<VectorIndexBuildTiming>,
+}
+
+impl From<(Option<i64>, Vec<CommitMessage>)> for PreparedBuild {
+ fn from((snapshot_id, messages): (Option<i64>, Vec<CommitMessage>)) ->
Self {
+ Self {
+ snapshot_id,
+ messages,
+ vector_timings: vec![],
+ }
+ }
+}
+
+impl<'a> GlobalIndexBuildBuilder<'a> {
+ pub(crate) fn new(table: &'a Table) -> Self {
+ Self {
+ table,
+ columns: None,
+ index_type: BTREE_GLOBAL_INDEX_TYPE.into(),
+ options: HashMap::new(),
+ partition_filter: None,
+ }
+ }
+
+ /// Whether this build includes a writer for the requested index type.
+ pub fn supports_index_type(index_type: &str) -> bool {
+
normalize_global_index_type(index_type.trim()).is_some_and(|index_type| {
+ index_type != FULL_TEXT_GLOBAL_INDEX_TYPE || cfg!(feature =
"fulltext")
+ })
+ }
+
+ pub fn with_index_column(&mut self, column: &str) -> &mut Self {
+ self.columns = Some(IndexColumns::Column(column.into()));
+ self
+ }
+
+ /// Select columns in physical tuple-key order; only BTree supports
multiple columns.
+ pub fn with_index_columns(&mut self, columns: &[&str]) -> &mut Self {
+ self.columns = Some(IndexColumns::Columns(
+ columns.iter().map(|column| (*column).into()).collect(),
+ ));
+ self
+ }
+
+ pub fn with_index_type(&mut self, index_type: &str) -> &mut Self {
+ self.index_type = index_type.into();
+ self
+ }
+
+ pub fn with_options(&mut self, options: HashMap<String, String>) -> &mut
Self {
+ self.options = options;
+ self
+ }
+
+ /// Restrict the build to partitions. Repeated calls combine filters with
AND.
+ pub fn with_partition_filter(&mut self, filter: Predicate) -> Result<&mut
Self> {
+ super::partition_filter::validate_partition_filter(self.table,
&filter)?;
+ self.partition_filter = Some(match self.partition_filter.take() {
+ Some(previous) => Predicate::and(vec![previous, filter]),
+ None => filter,
+ });
+ Ok(self)
+ }
+
+ /// Prepare files without publishing a snapshot. The caller owns the
returned messages.
+ /// No snapshot or no uncovered rows produces an empty list.
+ pub async fn build(&self) -> Result<Vec<CommitMessage>> {
+ Ok(self.prepare().await?.messages)
+ }
+
+ /// Prepare and commit, rejecting changes to the snapshot used for the
build.
+ pub async fn execute(&self) -> Result<usize> {
+ let prepared = self.prepare().await?;
+ if prepared.messages.is_empty() {
+ return Ok(0);
+ }
+ let count = prepared
+ .messages
+ .iter()
+ .map(|message| message.new_index_files.len())
+ .sum();
+ let commit_start =
vector_index_build_timing_enabled().then(Instant::now);
+ TableCommit::new(
+ self.table.clone(),
+ format!("global-index-create-{}", uuid::Uuid::new_v4()),
+ )
+ .commit_if_latest_snapshot(
+ prepared.messages,
+ prepared
+ .snapshot_id
+ .expect("nonempty index build has a snapshot"),
+ )
+ .await?;
+ if let Some(start) = commit_start {
+ let elapsed = start.elapsed();
+ for timing in prepared.vector_timings {
+ timing.log(self.index_type.trim(), elapsed);
+ }
+ }
+ // Once submitted, messages may have been committed even if an error
is returned.
+ // Never abort their files on a failed or uncertain commit.
+ Ok(count)
+ }
+
+ async fn prepare(&self) -> Result<PreparedBuild> {
+
CoreOptions::new(self.table.schema().options()).ensure_read_authorized()?;
+ self.table.ensure_not_branch_reference_for_write()?;
+ let index_type =
normalize_global_index_type(self.index_type.trim()).ok_or_else(|| {
+ Error::Unsupported {
+ message: format!("Unsupported global index type: '{}'",
self.index_type),
+ }
+ })?;
+ let columns = self.columns.as_ref().ok_or_else(|| Error::DataInvalid {
+ message: "Global index column is required".into(),
+ source: None,
+ })?;
+ let fields = resolve_index_fields(self.table, columns, index_type)?;
+ let columns = fields.iter().map(|field|
field.name()).collect::<Vec<_>>();
+ if normalize_queryable_global_index_type(index_type).is_some() {
+ let mut builder = SortedGlobalIndexBuildBuilder::new(self.table);
+ builder
+ .with_index_columns(&columns)
+ .with_index_type(index_type)
+ .with_options(self.options.clone());
+ if let Some(filter) = &self.partition_filter {
+ builder.with_partition_filter(filter.clone())?;
+ }
+ return Ok(builder.prepare().await?.into());
+ }
+ if index_type == FULL_TEXT_GLOBAL_INDEX_TYPE {
+ #[cfg(feature = "fulltext")]
+ {
+ let mut builder = FullTextIndexBuildBuilder::new(self.table);
+ builder
+ .with_index_column(columns[0])
+ .with_options(self.options.clone());
+ builder.partition_filter = self.partition_filter.clone();
+ return Ok(builder.prepare().await?.into());
+ }
+ #[cfg(not(feature = "fulltext"))]
+ return Err(Error::Unsupported {
+ message: "Full-text global index build requires the 'fulltext'
feature".into(),
+ });
+ }
+ if index_type == LUMINA_IDENTIFIER {
+ let mut builder = LuminaIndexBuildBuilder::new(self.table);
+ builder
+ .with_index_column(columns[0])
+ .with_options(self.options.clone());
+ builder.partition_filter = self.partition_filter.clone();
+ return Ok(builder.prepare().await?.into());
+ }
+ let mut builder = VindexIndexBuildBuilder::new(self.table, index_type);
+ builder
+ .with_index_column(columns[0])
+ .with_options(self.options.clone());
+ builder.partition_filter = self.partition_filter.clone();
+ let (snapshot_id, messages, vector_timings) = builder.prepare().await?;
+ Ok(PreparedBuild {
+ snapshot_id,
+ messages,
+ vector_timings,
+ })
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::catalog::Identifier;
+ use crate::io::FileIOBuilder;
+ use crate::spec::{ArrayType, DataType, FloatType, IntType, Schema,
TableSchema, VarCharType};
+
+ fn empty_table() -> Table {
+ let schema = Schema::builder()
+ .column("id", DataType::Int(IntType::new()))
+ .column("text", DataType::VarChar(VarCharType::string_type()))
+ .column(
+ "vector",
+
DataType::Array(ArrayType::new(DataType::Float(FloatType::new()))),
+ )
+ .options(HashMap::from([
+ ("row-tracking.enabled".into(), "true".into()),
+ ("data-evolution.enabled".into(), "true".into()),
+ ("global-index.enabled".into(), "true".into()),
+ ]))
+ .build()
+ .unwrap();
+ Table::new(
+ FileIOBuilder::new("memory").build().unwrap(),
+ Identifier::new("default", "empty"),
+ "memory:/empty_global_index".into(),
+ TableSchema::new(0, &schema),
+ None,
+ )
+ }
+
+ #[tokio::test]
+ async fn all_index_families_share_empty_build_and_execute_contract() {
+ let table = empty_table();
+ for (kind, column) in [
+ ("btree", "id"),
+ ("bitmap", "id"),
+ ("multivalue", "vector"),
+ ("fm", "text"),
+ ("full-text", "text"),
+ ("lumina", "vector"),
+ ("lumina-vector-ann", "vector"),
+ ("ivf-flat", "vector"),
+ ("ivf-pq", "vector"),
+ ("ivf-sq", "vector"),
+ ("ivf-rq", "vector"),
+ ("diskann", "vector"),
+ ] {
+ if !GlobalIndexBuildBuilder::supports_index_type(kind) {
+ continue;
+ }
+ let mut options = HashMap::from([(
+ format!(
+ "{}.dimension",
+ if kind == "lumina-vector-ann" {
+ "lumina"
+ } else {
+ kind
+ }
+ ),
+ "2".into(),
+ )]);
+ if kind == "ivf-pq" {
+ options.insert("ivf-pq.pq.m".into(), "1".into());
+ }
+ options.insert("global-index.row-count-per-shard".into(),
"4".into());
+ let mut builder = table.new_global_index_build_builder();
+ builder
+ .with_index_column(column)
+ .with_index_type(&format!(" {} ", kind.to_uppercase()))
+ .with_options(options);
+ assert!(builder.build().await.unwrap().is_empty(), "{kind}");
+ assert_eq!(builder.execute().await.unwrap(), 0, "{kind}");
+ }
+ assert!(table
+ .snapshot_manager()
+ .get_latest_snapshot()
+ .await
+ .unwrap()
+ .is_none());
+ }
+
+ #[tokio::test]
+ async fn column_validation_precedes_any_index_io() {
+ let table = empty_table();
+ for kind in ["bitmap", "fm", "full-text", "ivf-flat", "lumina"] {
+ let mut builder = table.new_global_index_build_builder();
+ builder
+ .with_index_columns(&["id", "text"])
+ .with_index_type(kind);
+ assert!(
+ matches!(builder.build().await, Err(Error::Unsupported {
message }) if message.contains("multiple index columns"))
+ );
+ }
+ for columns in [vec![], vec![""], vec!["id", "id"]] {
+ assert!(table
+ .new_global_index_build_builder()
+ .with_index_columns(&columns)
+ .build()
+ .await
+ .is_err());
+ }
+ assert!(matches!(
+ table
+ .new_global_index_build_builder()
+ .with_index_column("missing")
+ .build()
+ .await,
+ Err(Error::ColumnNotExist { .. })
+ ));
+ assert!(matches!(
+ table
+ .new_global_index_build_builder()
+ .with_index_type("unknown")
+ .build()
+ .await,
+ Err(Error::Unsupported { .. })
+ ));
+ assert!(!GlobalIndexBuildBuilder::supports_index_type("unknown"));
+ }
+
+ #[cfg(not(feature = "fulltext"))]
+ #[tokio::test]
+ async fn full_text_without_feature_is_explicitly_unsupported() {
+ assert!(!GlobalIndexBuildBuilder::supports_index_type("full-text"));
+ assert!(
+
matches!(empty_table().new_global_index_build_builder().with_index_column("text")
+ .with_index_type("full-text").build().await,
Err(Error::Unsupported { message }) if message.contains("fulltext"))
+ );
+ }
+}
diff --git a/crates/paimon/src/table/global_index_build_common.rs
b/crates/paimon/src/table/global_index_build_common.rs
index aab1784f..77775b18 100644
--- a/crates/paimon/src/table/global_index_build_common.rs
+++ b/crates/paimon/src/table/global_index_build_common.rs
@@ -235,3 +235,26 @@ pub(crate) async fn validate_existing_index_overlap(
}
Ok(())
}
+
+/// Resolve and create the table's global index directory, including external
placement.
+pub(crate) async fn prepare_index_file_path(
+ table: &Table,
+ file_name: &str,
+) -> Result<(String, Option<String>)> {
+ let external_path = super::external_path::new_index_external_path(
+ table.schema().options(),
+ false,
+ "",
+ file_name,
+ )?;
+ let path = external_path.clone().unwrap_or_else(|| {
+ format!(
+ "{}/index/{file_name}",
+ table.location().trim_end_matches('/')
+ )
+ });
+ if let Some((directory, _)) = path.rsplit_once('/') {
+ table.file_io().mkdirs(&format!("{directory}/")).await?;
+ }
+ Ok((path, external_path))
+}
diff --git a/crates/paimon/src/table/global_index_drop_builder.rs
b/crates/paimon/src/table/global_index_drop_builder.rs
index 155a708e..7b2f811b 100644
--- a/crates/paimon/src/table/global_index_drop_builder.rs
+++ b/crates/paimon/src/table/global_index_drop_builder.rs
@@ -17,8 +17,7 @@
use super::global_index_build_common::IndexColumns;
use super::global_index_types::{
- normalize_global_index_type_for_drop, BTREE_GLOBAL_INDEX_TYPE,
- SUPPORTED_GLOBAL_INDEX_TYPES_FOR_DROP,
+ normalize_global_index_type, BTREE_GLOBAL_INDEX_TYPE,
SUPPORTED_GLOBAL_INDEX_TYPES_FOR_DROP,
};
use crate::spec::{FileKind, IndexFileMeta, IndexManifest};
use crate::table::{CommitMessage, Table, TableCommit};
@@ -73,13 +72,11 @@ impl<'a> GlobalIndexDropBuilder<'a> {
self.table.ensure_not_branch_reference_for_write()?;
let index_type =
-
normalize_global_index_type_for_drop(&self.index_type).ok_or_else(|| {
- Error::Unsupported {
- message: format!(
- "unsupported global index type '{}'; supported: {}",
- self.index_type, SUPPORTED_GLOBAL_INDEX_TYPES_FOR_DROP
- ),
- }
+ normalize_global_index_type(&self.index_type).ok_or_else(||
Error::Unsupported {
+ message: format!(
+ "unsupported global index type '{}'; supported: {}",
+ self.index_type, SUPPORTED_GLOBAL_INDEX_TYPES_FOR_DROP
+ ),
})?;
let index_column = self
.index_column
@@ -121,9 +118,7 @@ impl<'a> GlobalIndexDropBuilder<'a> {
if entry.kind != FileKind::Add {
continue;
}
- if
normalize_global_index_type_for_drop(&entry.index_file.index_type)
- != Some(index_type)
- {
+ if normalize_global_index_type(&entry.index_file.index_type) !=
Some(index_type) {
continue;
}
let Some(global_meta) =
entry.index_file.global_index_meta.as_ref() else {
@@ -218,7 +213,7 @@ mod tests {
}
refused(
table
- .new_btree_global_index_build_builder()
+ .new_global_index_build_builder()
.execute()
.await
.unwrap_err(),
@@ -226,7 +221,8 @@ mod tests {
);
refused(
table
- .new_vindex_index_build_builder("vector")
+ .new_global_index_build_builder()
+ .with_index_type("vector")
.execute()
.await
.unwrap_err(),
@@ -234,7 +230,8 @@ mod tests {
);
refused(
table
- .new_lumina_index_build_builder()
+ .new_global_index_build_builder()
+ .with_index_type("lumina")
.execute()
.await
.unwrap_err(),
diff --git a/crates/paimon/src/table/global_index_types.rs
b/crates/paimon/src/table/global_index_types.rs
index a7a03841..a51a54cc 100644
--- a/crates/paimon/src/table/global_index_types.rs
+++ b/crates/paimon/src/table/global_index_types.rs
@@ -55,14 +55,14 @@ pub const SUPPORTED_GLOBAL_INDEX_TYPES_FOR_DROP: &str =
"btree, bitmap, multivalue, fm, full-text, lumina, lumina-vector-ann,
ivf-flat, ivf-pq, ivf-sq, ivf-rq, diskann";
/// Canonicalize any supported global index type to a stable `&'static str`, or
-/// `None` if unsupported. Case-insensitive. Order: sorted -> full-text ->
lumina
-/// -> vindex.
+/// `None` if unsupported. Case-insensitive and shared by build and drop.
+/// Order: scalar -> full-text -> lumina -> vindex.
///
/// Both lumina aliases (`lumina`, `lumina-vector-ann`) canonicalize to
/// `"lumina"`; each vindex type keeps its own identity so dropping one vindex
/// type never matches another on the same column. Callers should compare the
/// canonical form of BOTH the request and each stored entry's `index_type`.
-pub fn normalize_global_index_type_for_drop(index_type: &str) ->
Option<&'static str> {
+pub fn normalize_global_index_type(index_type: &str) -> Option<&'static str> {
if let Some(queryable) = normalize_queryable_global_index_type(index_type)
{
return Some(queryable);
}
@@ -100,77 +100,47 @@ mod tests {
#[test]
fn sorted_types_canonicalize_case_insensitively() {
- assert_eq!(normalize_global_index_type_for_drop("BTREE"),
Some("btree"));
+ assert_eq!(normalize_global_index_type("BTREE"), Some("btree"));
+ assert_eq!(normalize_global_index_type("Bitmap"), Some("bitmap"));
assert_eq!(
- normalize_global_index_type_for_drop("Bitmap"),
- Some("bitmap")
- );
- assert_eq!(
- normalize_global_index_type_for_drop("MultiValue"),
+ normalize_global_index_type("MultiValue"),
Some("multivalue")
);
- assert_eq!(normalize_global_index_type_for_drop("FM"), Some("fm"));
+ assert_eq!(normalize_global_index_type("FM"), Some("fm"));
}
#[test]
fn full_text_canonicalizes_case_insensitively() {
- assert_eq!(
- normalize_global_index_type_for_drop("full-text"),
- Some("full-text")
- );
- assert_eq!(
- normalize_global_index_type_for_drop("Full-Text"),
- Some("full-text")
- );
+ assert_eq!(normalize_global_index_type("full-text"),
Some("full-text"));
+ assert_eq!(normalize_global_index_type("Full-Text"),
Some("full-text"));
// Full-text is dropped by its own identity, never as a queryable
scalar index.
assert_eq!(normalize_queryable_global_index_type("full-text"), None);
}
#[test]
fn lumina_aliases_canonicalize_to_lumina() {
+ assert_eq!(normalize_global_index_type("lumina"), Some("lumina"));
assert_eq!(
- normalize_global_index_type_for_drop("lumina"),
- Some("lumina")
- );
- assert_eq!(
- normalize_global_index_type_for_drop("lumina-vector-ann"),
- Some("lumina")
- );
- assert_eq!(
- normalize_global_index_type_for_drop("LUMINA"),
+ normalize_global_index_type("lumina-vector-ann"),
Some("lumina")
);
+ assert_eq!(normalize_global_index_type("LUMINA"), Some("lumina"));
}
#[test]
fn vindex_types_keep_distinct_identity() {
- assert_eq!(
- normalize_global_index_type_for_drop("ivf-flat"),
- Some("ivf-flat")
- );
- assert_eq!(
- normalize_global_index_type_for_drop("IVF-PQ"),
- Some("ivf-pq")
- );
- assert_eq!(
- normalize_global_index_type_for_drop("IVF-SQ"),
- Some("ivf-sq")
- );
- assert_eq!(
- normalize_global_index_type_for_drop("IVF-RQ"),
- Some("ivf-rq")
- );
- assert_eq!(
- normalize_global_index_type_for_drop("DiskANN"),
- Some("diskann")
- );
+ assert_eq!(normalize_global_index_type("ivf-flat"), Some("ivf-flat"));
+ assert_eq!(normalize_global_index_type("IVF-PQ"), Some("ivf-pq"));
+ assert_eq!(normalize_global_index_type("IVF-SQ"), Some("ivf-sq"));
+ assert_eq!(normalize_global_index_type("IVF-RQ"), Some("ivf-rq"));
+ assert_eq!(normalize_global_index_type("DiskANN"), Some("diskann"));
}
#[test]
fn unsupported_types_return_none() {
- assert_eq!(normalize_global_index_type_for_drop("hash"), None);
- assert_eq!(normalize_global_index_type_for_drop("ivf-hnsw-flat"),
None);
- assert_eq!(normalize_global_index_type_for_drop("ivf-hnsw-sq"), None);
- assert_eq!(normalize_global_index_type_for_drop(""), None);
+ assert_eq!(normalize_global_index_type("hash"), None);
+ assert_eq!(normalize_global_index_type("ivf-hnsw-flat"), None);
+ assert_eq!(normalize_global_index_type("ivf-hnsw-sq"), None);
+ assert_eq!(normalize_global_index_type(""), None);
}
}
diff --git a/crates/paimon/src/table/lumina_index_build_builder.rs
b/crates/paimon/src/table/lumina_index_build_builder.rs
index e50a7311..b96f43d9 100644
--- a/crates/paimon/src/table/lumina_index_build_builder.rs
+++ b/crates/paimon/src/table/lumina_index_build_builder.rs
@@ -28,19 +28,17 @@ use validation::{
};
use writer::abort_on_build_error;
-use crate::lumina::{
- is_lumina_index_type, LuminaIndexMeta, LuminaVectorIndexOptions,
LUMINA_IDENTIFIER,
-};
-use crate::spec::CoreOptions;
-use crate::table::{CommitMessage, RowRange, SnapshotManager, Table,
TableCommit};
+use crate::lumina::{LuminaIndexMeta, LuminaVectorIndexOptions,
LUMINA_IDENTIFIER};
+use crate::spec::{CoreOptions, Predicate};
+use crate::table::{CommitMessage, RowRange, Table, TableCommit};
use crate::{Error, Result};
use std::collections::HashMap;
-pub struct LuminaIndexBuildBuilder<'a> {
+pub(super) struct LuminaIndexBuildBuilder<'a> {
table: &'a Table,
index_column: Option<String>,
- index_type: String,
options: HashMap<String, String>,
+ pub(super) partition_filter: Option<Predicate>,
}
impl<'a> LuminaIndexBuildBuilder<'a> {
@@ -48,8 +46,8 @@ impl<'a> LuminaIndexBuildBuilder<'a> {
Self {
table,
index_column: None,
- index_type: LUMINA_IDENTIFIER.to_string(),
options: HashMap::new(),
+ partition_filter: None,
}
}
@@ -58,29 +56,17 @@ impl<'a> LuminaIndexBuildBuilder<'a> {
self
}
- pub fn with_index_type(&mut self, index_type: &str) -> &mut Self {
- self.index_type = index_type.to_string();
- self
- }
-
pub fn with_options(&mut self, options: HashMap<String, String>) -> &mut
Self {
self.options = options;
self
}
- pub async fn execute(&self) -> Result<usize> {
+ pub(super) async fn prepare(&self) -> Result<(Option<i64>,
Vec<CommitMessage>)> {
// Building the index scans the table's rows.
CoreOptions::new(self.table.schema().options()).ensure_read_authorized()?;
self.table.ensure_not_branch_reference_for_write()?;
- if !is_lumina_index_type(&self.index_type) {
- return Err(Error::DataInvalid {
- message: format!("Unsupported Lumina index type: {}",
self.index_type),
- source: None,
- });
- }
-
let index_column = self
.index_column
.as_deref()
@@ -89,7 +75,9 @@ impl<'a> LuminaIndexBuildBuilder<'a> {
source: None,
})?;
- let core_options = CoreOptions::new(self.table.schema().options());
+ let mut merged_options = self.table.schema().options().clone();
+ merged_options.extend(self.options.clone());
+ let core_options = CoreOptions::new(&merged_options);
validate_table_options(self.table, &core_options)?;
let rows_per_shard = core_options.global_index_row_count_per_shard()?;
@@ -103,21 +91,16 @@ impl<'a> LuminaIndexBuildBuilder<'a> {
let dimension = lumina_options.dimension;
let index_meta =
LuminaIndexMeta::new(lumina_options.to_lumina_options()).serialize()?;
- let snapshot_manager = SnapshotManager::new(
- self.table.file_io().clone(),
- self.table.location().to_string(),
- );
- let snapshot = snapshot_manager
- .get_latest_snapshot()
- .await?
- .ok_or_else(|| Error::DataInvalid {
- message: "Cannot build Lumina index without a
snapshot".to_string(),
- source: None,
- })?;
+ let snapshot_manager = self.table.snapshot_manager();
+ let Some(snapshot) = snapshot_manager.get_latest_snapshot().await?
else {
+ return Ok((None, vec![]));
+ };
- let manifest_entries = self
- .table
- .new_read_builder()
+ let mut read_builder = self.table.new_read_builder();
+ if let Some(filter) = &self.partition_filter {
+ read_builder.with_filter(filter.clone());
+ }
+ let manifest_entries = read_builder
.new_scan()
.with_scan_all_files()
.plan_manifest_entries(&snapshot)
@@ -141,7 +124,7 @@ impl<'a> LuminaIndexBuildBuilder<'a> {
&indexed,
)?;
if shards.is_empty() {
- return Ok(0);
+ return Ok((Some(snapshot.id()), vec![]));
}
crate::table::global_index_build_common::validate_existing_index_overlap(
@@ -186,11 +169,7 @@ impl<'a> LuminaIndexBuildBuilder<'a> {
messages.push(message);
}
- commit
- .commit_if_latest_snapshot(messages, snapshot.id())
- .await?;
-
- Ok(shard_count)
+ Ok((Some(snapshot.id()), messages))
}
}
diff --git a/crates/paimon/src/table/lumina_index_build_builder/tests.rs
b/crates/paimon/src/table/lumina_index_build_builder/tests.rs
index b4e59878..43e5a93a 100644
--- a/crates/paimon/src/table/lumina_index_build_builder/tests.rs
+++ b/crates/paimon/src/table/lumina_index_build_builder/tests.rs
@@ -393,7 +393,8 @@ async fn test_execute_rejects_primary_key_table() {
);
let err = table
- .new_lumina_index_build_builder()
+ .new_global_index_build_builder()
+ .with_index_type("lumina")
.with_index_column("embedding")
.execute()
.await
@@ -411,7 +412,8 @@ async fn test_execute_rejects_deletion_vectors_table() {
let table = test_table(options);
let err = table
- .new_lumina_index_build_builder()
+ .new_global_index_build_builder()
+ .with_index_type("lumina")
.with_index_column("embedding")
.execute()
.await
@@ -867,7 +869,8 @@ async fn test_execute_writes_lumina_index_manifest() {
.unwrap();
let shard_count = table
- .new_lumina_index_build_builder()
+ .new_global_index_build_builder()
+ .with_index_type("lumina")
.with_index_column("embedding")
.execute()
.await
@@ -1059,7 +1062,8 @@ async fn lumina_second_build_without_new_data_is_noop() {
assert!(!names_before.is_empty());
let built = table
- .new_lumina_index_build_builder()
+ .new_global_index_build_builder()
+ .with_index_type("lumina")
.with_index_column("embedding")
.execute()
.await
@@ -1167,7 +1171,8 @@ async fn lumina_incremental_build_indexes_only_new_rows()
{
// error. Without the native Lumina library the gap build surfaces a
// library-load error instead; with it present it succeeds.
let result = table
- .new_lumina_index_build_builder()
+ .new_global_index_build_builder()
+ .with_index_type("lumina")
.with_index_column("embedding")
.execute()
.await;
diff --git a/crates/paimon/src/table/lumina_index_build_builder/writer.rs
b/crates/paimon/src/table/lumina_index_build_builder/writer.rs
index df661f26..8ad84596 100644
--- a/crates/paimon/src/table/lumina_index_build_builder/writer.rs
+++ b/crates/paimon/src/table/lumina_index_build_builder/writer.rs
@@ -28,8 +28,6 @@ use crate::table::{CommitMessage, TableCommit};
use crate::{Error, Result};
use std::path::PathBuf;
-const INDEX_DIR: &str = "index";
-
impl LuminaIndexBuildBuilder<'_> {
pub(super) async fn build_index_file(
&self,
@@ -61,18 +59,11 @@ impl LuminaIndexBuildBuilder<'_> {
builder.dump(&temp_path_str)?;
let file_name = format!("lumina-global-index-{}.index",
uuid::Uuid::new_v4());
- self.table
- .file_io()
- .mkdirs(&format!(
- "{}/{INDEX_DIR}/",
- self.table.location().trim_end_matches('/')
- ))
+ let (index_path, external_path) =
+ crate::table::global_index_build_common::prepare_index_file_path(
+ self.table, &file_name,
+ )
.await?;
- let index_path = format!(
- "{}/{INDEX_DIR}/{}",
- self.table.location().trim_end_matches('/'),
- file_name
- );
let write_result: Result<i64> = async {
copy_local_file_to_output(&temp_path,
self.table.file_io().new_output(&index_path)?)
.await?;
@@ -97,13 +88,15 @@ impl LuminaIndexBuildBuilder<'_> {
file_size,
row_count,
deletion_vectors_ranges: None,
- external_path: None,
+ external_path,
global_index_meta: Some(GlobalIndexMeta {
row_range_start: shard.row_range_start,
row_range_end: shard.row_range_end,
index_field_id,
extra_field_ids: None,
- source_meta: None,
+ source_meta: Some(
+
crate::spec::DataEvolutionIndexSourceMeta::new(shard.snapshot_id)?.serialize(),
+ ),
index_meta: Some(index_meta),
}),
})
diff --git a/crates/paimon/src/table/mod.rs b/crates/paimon/src/table/mod.rs
index 5c577881..3877bd6f 100644
--- a/crates/paimon/src/table/mod.rs
+++ b/crates/paimon/src/table/mod.rs
@@ -74,6 +74,7 @@ mod full_text_index_adapter;
mod full_text_index_build_builder;
#[cfg(feature = "fulltext")]
mod full_text_search_builder;
+mod global_index_build_builder;
pub(crate) mod global_index_build_common;
mod global_index_drop_builder;
pub(crate) mod global_index_scanner;
@@ -188,21 +189,17 @@ pub use format_partition::{
pub use format_partition_stats::FormatTablePartitionStatsCollector;
pub use format_partition_truncate::FormatTableTruncator;
#[cfg(feature = "fulltext")]
-pub use full_text_index_build_builder::FullTextIndexBuildBuilder;
-#[cfg(feature = "fulltext")]
pub use full_text_search_builder::FullTextSearchBuilder;
use futures::stream::BoxStream;
+pub use global_index_build_builder::GlobalIndexBuildBuilder;
pub use global_index_drop_builder::GlobalIndexDropBuilder;
-pub use global_index_types::{
- normalize_global_index_type_for_drop,
SUPPORTED_GLOBAL_INDEX_TYPES_FOR_DROP,
-};
+pub use global_index_types::{normalize_global_index_type,
SUPPORTED_GLOBAL_INDEX_TYPES_FOR_DROP};
pub use hybrid_search_builder::{
HybridSearchBuilder, HybridSearchRanker, HybridSearchRoute,
HybridSearchRouteKind,
};
pub use incremental_scan::{
IncrementalPlan, IncrementalScan, IncrementalScanMode, IncrementalSplit,
};
-pub use lumina_index_build_builder::LuminaIndexBuildBuilder;
pub use object_table::{ObjectEntry, ObjectTable};
pub use partition_row_count::PartitionRowCount;
pub use partition_stat::PartitionStat;
@@ -218,9 +215,6 @@ pub use scan_trace::ScanTrace;
pub use schema_manager::SchemaManager;
pub use snapshot_commit::{RESTSnapshotCommit, RenamingSnapshotCommit,
SnapshotCommit};
pub use snapshot_manager::SnapshotManager;
-pub use sorted_global_index_build_builder::{
- BTreeGlobalIndexBuildBuilder, SortedGlobalIndexBuildBuilder,
-};
pub use source::{
merge_row_ranges, DataSplit, DataSplitBuilder, DeletionFile,
PartitionBucket, Plan, RowRange,
};
@@ -239,7 +233,6 @@ pub use update_assignment::UpdateAssignment;
pub use vector_read::{BatchVectorRead, VectorRead};
pub use vector_scan::{VectorScan, VectorScanPlan};
pub use vector_search_builder::VectorSearchBuilder;
-pub use vindex_index_build_builder::VindexIndexBuildBuilder;
pub use write_builder::WriteBuilder;
use crate::catalog::{validate_branch_name, Identifier, DEFAULT_MAIN_BRANCH};
@@ -555,34 +548,15 @@ impl Table {
BatchVectorSearchBuilder::new(self)
}
- /// Create a builder for `full-text` global index files.
- ///
- /// Reference:
[NativeFullTextGlobalIndexWriter](https://github.com/apache/paimon/blob/master/paimon-full-text/src/main/java/org/apache/paimon/fulltext/index/NativeFullTextGlobalIndexWriter.java)
- #[cfg(feature = "fulltext")]
- pub fn new_full_text_index_build_builder(&self) ->
FullTextIndexBuildBuilder<'_> {
- FullTextIndexBuildBuilder::new(self)
- }
-
- pub fn new_lumina_index_build_builder(&self) ->
LuminaIndexBuildBuilder<'_> {
- LuminaIndexBuildBuilder::new(self)
- }
-
- pub fn new_sorted_global_index_build_builder(&self) ->
SortedGlobalIndexBuildBuilder<'_> {
- SortedGlobalIndexBuildBuilder::new(self)
- }
-
- pub fn new_btree_global_index_build_builder(&self) ->
BTreeGlobalIndexBuildBuilder<'_> {
- self.new_sorted_global_index_build_builder()
+ /// Build global index files selected by index type, without publishing
them.
+ pub fn new_global_index_build_builder(&self) ->
GlobalIndexBuildBuilder<'_> {
+ GlobalIndexBuildBuilder::new(self)
}
pub fn new_global_index_drop_builder(&self) -> GlobalIndexDropBuilder<'_> {
GlobalIndexDropBuilder::new(self)
}
- pub fn new_vindex_index_build_builder(&self, index_type: &str) ->
VindexIndexBuildBuilder<'_> {
- VindexIndexBuildBuilder::new(self, index_type)
- }
-
/// Create a write builder for write/commit.
///
/// Reference: [pypaimon
FileStoreTable.new_write_builder](https://github.com/apache/paimon/blob/master/paimon-python/pypaimon/table/file_store_table.py).
diff --git a/crates/paimon/src/table/sorted_global_index_build_builder.rs
b/crates/paimon/src/table/sorted_global_index_build_builder.rs
index 7b8e08e0..1c116235 100644
--- a/crates/paimon/src/table/sorted_global_index_build_builder.rs
+++ b/crates/paimon/src/table/sorted_global_index_build_builder.rs
@@ -63,7 +63,7 @@ fn make_index_key_codec(index_type: &str, data_type:
&DataType) -> (KeyComparato
}
}
-pub struct SortedGlobalIndexBuildBuilder<'a> {
+pub(super) struct SortedGlobalIndexBuildBuilder<'a> {
table: &'a Table,
index_column: Option<IndexColumns>,
index_type: String,
@@ -71,10 +71,6 @@ pub struct SortedGlobalIndexBuildBuilder<'a> {
partition_filter: Option<Predicate>,
}
-/// Backward-compatible name retained for callers that used the original
-/// BTree-only builder API before it also supported bitmap and multivalue.
-pub type BTreeGlobalIndexBuildBuilder<'a> = SortedGlobalIndexBuildBuilder<'a>;
-
impl<'a> SortedGlobalIndexBuildBuilder<'a> {
pub(crate) fn new(table: &'a Table) -> Self {
Self {
@@ -86,11 +82,6 @@ impl<'a> SortedGlobalIndexBuildBuilder<'a> {
}
}
- pub fn with_index_column(&mut self, column: &str) -> &mut Self {
- self.index_column = Some(IndexColumns::Column(column.to_string()));
- self
- }
-
/// Select BTree columns in physical tuple-key order.
pub fn with_index_columns(&mut self, columns: &[&str]) -> &mut Self {
self.index_column = Some(IndexColumns::Columns(
@@ -120,32 +111,6 @@ impl<'a> SortedGlobalIndexBuildBuilder<'a> {
Ok(self)
}
- /// Build index files without publishing a snapshot, like Java's index
builders.
- /// The caller owns the returned messages and decides whether to commit or
abort.
- /// A build with no snapshot or uncovered rows returns an empty list.
- pub async fn build(&self) -> Result<Vec<CommitMessage>> {
- self.prepare().await.map(|(_, messages)| messages)
- }
-
- /// Build and commit, rejecting a concurrent change to the planned
snapshot.
- pub async fn execute(&self) -> Result<usize> {
- let (snapshot_id, messages) = self.prepare().await?;
- if messages.is_empty() {
- return Ok(0);
- }
- let count = messages
- .iter()
- .map(|message| message.new_index_files.len())
- .sum();
- self.new_commit()
- .commit_if_latest_snapshot(
- messages,
- snapshot_id.expect("nonempty build has a snapshot"),
- )
- .await?;
- Ok(count)
- }
-
fn new_commit(&self) -> TableCommit {
TableCommit::new(
self.table.clone(),
@@ -157,7 +122,7 @@ impl<'a> SortedGlobalIndexBuildBuilder<'a> {
)
}
- async fn prepare(&self) -> Result<(Option<i64>, Vec<CommitMessage>)> {
+ pub(super) async fn prepare(&self) -> Result<(Option<i64>,
Vec<CommitMessage>)> {
// Building the index scans the table's rows.
CoreOptions::new(self.table.schema().options()).ensure_read_authorized()?;
diff --git
a/crates/paimon/src/table/sorted_global_index_build_builder/preparation_tests.rs
b/crates/paimon/src/table/sorted_global_index_build_builder/preparation_tests.rs
index b23a7a49..c50ebc2e 100644
---
a/crates/paimon/src/table/sorted_global_index_build_builder/preparation_tests.rs
+++
b/crates/paimon/src/table/sorted_global_index_build_builder/preparation_tests.rs
@@ -144,7 +144,7 @@ async fn
staged_indexes_reject_target_column_changes_and_preserve_files() {
vec![None; 2],
)
.await;
- let mut builder = table.new_sorted_global_index_build_builder();
+ let mut builder = table.new_global_index_build_builder();
builder.with_index_column("name").with_index_type(kind);
let mut messages = builder.build().await.unwrap();
let file = messages[0].new_index_files[0].clone();
@@ -202,7 +202,7 @@ async fn
staged_indexes_reject_deleted_partial_columns_and_removed_source_ranges
let updates = update_name(&table, "changed").await;
let commit = TableCommit::new(table.clone(), "update".into());
commit.commit(updates.clone()).await.unwrap();
- let mut builder = table.new_sorted_global_index_build_builder();
+ let mut builder = table.new_global_index_build_builder();
builder.with_index_column("name").with_index_type(kind);
let messages = builder.build().await.unwrap();
if remove_all {
@@ -242,7 +242,7 @@ async fn
staged_index_survives_append_and_unrelated_column_update() {
vec![None; 2],
)
.await;
- let mut builder = table.new_sorted_global_index_build_builder();
+ let mut builder = table.new_global_index_build_builder();
builder.with_index_column("name");
let messages = builder.build().await.unwrap();
assert_eq!(
@@ -297,7 +297,7 @@ async fn
equivalent_empty_partition_encodings_share_source_checks() {
for changed in [false, true] {
let table = table(false, false);
append(&table, vec![1], vec![Some("a")], vec![None]).await;
- let mut builder = table.new_sorted_global_index_build_builder();
+ let mut builder = table.new_global_index_build_builder();
builder.with_index_column("name");
let mut messages = builder.build().await.unwrap();
for message in &mut messages {
@@ -343,7 +343,7 @@ async fn
prepared_files_are_invisible_until_explicit_commit() {
.await
.unwrap()
.unwrap();
- let mut builder = table.new_sorted_global_index_build_builder();
+ let mut builder = table.new_global_index_build_builder();
builder.with_index_column("name").with_index_type(kind);
let messages = builder.build().await.unwrap();
assert_eq!(messages.len(), 2);
@@ -410,7 +410,7 @@ async fn
caller_can_abort_prepared_files_without_changing_snapshot() {
for kind in ["btree", "bitmap"] {
let table = table(false, true);
append(&table, vec![1, 2], vec![Some("a"), None], vec![None; 2]).await;
- let mut builder = table.new_sorted_global_index_build_builder();
+ let mut builder = table.new_global_index_build_builder();
builder.with_index_column("name").with_index_type(kind);
let messages = builder.build().await.unwrap();
let path = messages[0].new_index_files[0]
@@ -474,7 +474,7 @@ async fn
partition_builds_only_cover_selected_partitions_and_new_rows() {
.await;
let predicates = PredicateBuilder::new(table.schema().fields());
let selected = predicates.equal("pt", Datum::Int(1)).unwrap();
- let mut builder = table.new_sorted_global_index_build_builder();
+ let mut builder = table.new_global_index_build_builder();
builder
.with_index_column("id")
.with_index_type(kind)
@@ -503,7 +503,7 @@ async fn
partition_builds_only_cover_selected_partitions_and_new_rows() {
assert_eq!(builder.execute().await.unwrap(), 1);
assert!(builder.build().await.unwrap().is_empty());
- let mut all = table.new_sorted_global_index_build_builder();
+ let mut all = table.new_global_index_build_builder();
all.with_index_column("id").with_index_type(kind);
let remaining = all.build().await.unwrap();
assert_eq!(
@@ -526,7 +526,7 @@ async fn
partition_builds_only_cover_selected_partitions_and_new_rows() {
#[tokio::test]
async fn empty_and_disjoint_partition_builds_publish_nothing() {
let table = table(true, false);
- let mut builder = table.new_sorted_global_index_build_builder();
+ let mut builder = table.new_global_index_build_builder();
builder.with_index_column("id");
assert!(builder.build().await.unwrap().is_empty());
assert_eq!(builder.execute().await.unwrap(), 0);
@@ -562,7 +562,7 @@ async fn
empty_and_disjoint_partition_builds_publish_nothing() {
.is_null("pt")
.unwrap();
assert!(unpartitioned
- .new_sorted_global_index_build_builder()
+ .new_global_index_build_builder()
.with_partition_filter(filter)
.is_err());
}
diff --git a/crates/paimon/src/table/sorted_global_index_build_builder/tests.rs
b/crates/paimon/src/table/sorted_global_index_build_builder/tests.rs
index 61122928..8f9008a5 100644
--- a/crates/paimon/src/table/sorted_global_index_build_builder/tests.rs
+++ b/crates/paimon/src/table/sorted_global_index_build_builder/tests.rs
@@ -720,7 +720,7 @@ async fn assert_btree_build_version(version: u32) {
.unwrap();
let shard_count = table
- .new_btree_global_index_build_builder()
+ .new_global_index_build_builder()
.with_index_column("name")
.execute()
.await
@@ -932,7 +932,7 @@ async fn column_group_btree_table(rows: i32, version: u32)
-> Table {
.any(|file| file.write_cols.as_deref() == Some(&["payload".into()])));
for column in ["id", "name"] {
table
- .new_btree_global_index_build_builder()
+ .new_global_index_build_builder()
.with_index_column(column)
.execute()
.await
@@ -1081,7 +1081,7 @@ async fn test_execute_writes_and_queries_fm_index() {
assert_eq!(
table
- .new_sorted_global_index_build_builder()
+ .new_global_index_build_builder()
.with_index_column("name")
.with_index_type(FM_GLOBAL_INDEX_TYPE)
.execute()
@@ -1192,7 +1192,7 @@ async fn
test_failed_fm_build_removes_partial_index_file() {
options.insert("fm-index.partition-row-count".to_string(),
"1".to_string());
options.insert("fm-index.compression".to_string(), "none".to_string());
let error = table
- .new_sorted_global_index_build_builder()
+ .new_global_index_build_builder()
.with_index_column("name")
.with_index_type(FM_GLOBAL_INDEX_TYPE)
.with_options(options)
@@ -1244,7 +1244,7 @@ async fn test_global_index_prunes_during_manifest_read() {
}
table
- .new_btree_global_index_build_builder()
+ .new_global_index_build_builder()
.with_index_column("name")
.execute()
.await
@@ -1297,7 +1297,7 @@ async fn
test_scalar_full_search_includes_unindexed_rows() {
.await
.unwrap();
table
- .new_btree_global_index_build_builder()
+ .new_global_index_build_builder()
.with_index_column("name")
.execute()
.await
@@ -1365,7 +1365,7 @@ async fn
test_empty_global_index_ranges_skip_legacy_manifests() {
.await
.unwrap();
table
- .new_btree_global_index_build_builder()
+ .new_global_index_build_builder()
.with_index_column("name")
.execute()
.await
@@ -1441,7 +1441,7 @@ async fn
test_detail_mode_defers_manifest_pruning_for_unindexed_ranges() {
.await
.unwrap();
table
- .new_btree_global_index_build_builder()
+ .new_global_index_build_builder()
.with_index_column("name")
.execute()
.await
@@ -1504,7 +1504,7 @@ async fn
test_execute_writes_bitmap_index_manifest_and_java_file() {
.unwrap();
let shard_count = table
- .new_btree_global_index_build_builder()
+ .new_global_index_build_builder()
.with_index_column("name")
.with_index_type(BITMAP_GLOBAL_INDEX_TYPE)
.execute()
@@ -1606,7 +1606,7 @@ async fn
test_execute_multivalue_index_and_array_queries_end_to_end() {
.unwrap();
let shard_count = table
- .new_btree_global_index_build_builder()
+ .new_global_index_build_builder()
.with_index_column("items")
.with_index_type(MULTIVALUE_GLOBAL_INDEX_TYPE)
.execute()
@@ -1778,7 +1778,7 @@ async fn
test_bitmap_floating_candidates_preserve_residual_results() {
for column in ["f", "d"] {
let shard_count = table
- .new_btree_global_index_build_builder()
+ .new_global_index_build_builder()
.with_index_column(column)
.with_index_type(BITMAP_GLOBAL_INDEX_TYPE)
.execute()
@@ -1931,7 +1931,7 @@ async fn bitmap_second_build_without_new_data_is_noop() {
.unwrap();
let first_built = table
- .new_btree_global_index_build_builder()
+ .new_global_index_build_builder()
.with_index_column("name")
.with_index_type(BITMAP_GLOBAL_INDEX_TYPE)
.execute()
@@ -1946,7 +1946,7 @@ async fn bitmap_second_build_without_new_data_is_noop() {
assert!(!files_after_first.is_empty());
let built = table
- .new_btree_global_index_build_builder()
+ .new_global_index_build_builder()
.with_index_column("name")
.with_index_type(BITMAP_GLOBAL_INDEX_TYPE)
.execute()
@@ -1992,7 +1992,7 @@ async fn
bitmap_build_after_btree_on_same_field_still_indexes() {
.unwrap();
let btree_built = table
- .new_btree_global_index_build_builder()
+ .new_global_index_build_builder()
.with_index_column("name")
.execute()
.await
@@ -2000,7 +2000,7 @@ async fn
bitmap_build_after_btree_on_same_field_still_indexes() {
assert!(btree_built > 0);
let bitmap_built = table
- .new_btree_global_index_build_builder()
+ .new_global_index_build_builder()
.with_index_column("name")
.with_index_type(BITMAP_GLOBAL_INDEX_TYPE)
.execute()
@@ -2050,7 +2050,7 @@ async fn bitmap_incremental_build_indexes_only_new_rows()
{
.unwrap();
let first_built = table
- .new_btree_global_index_build_builder()
+ .new_global_index_build_builder()
.with_index_column("name")
.with_index_type(BITMAP_GLOBAL_INDEX_TYPE)
.execute()
@@ -2076,7 +2076,7 @@ async fn bitmap_incremental_build_indexes_only_new_rows()
{
.unwrap();
let second_built = table
- .new_btree_global_index_build_builder()
+ .new_global_index_build_builder()
.with_index_column("name")
.with_index_type(BITMAP_GLOBAL_INDEX_TYPE)
.execute()
@@ -2215,7 +2215,7 @@ async fn second_build_without_new_data_is_noop() {
.unwrap();
let first_built = table
- .new_btree_global_index_build_builder()
+ .new_global_index_build_builder()
.with_index_column("name")
.execute()
.await
@@ -2226,7 +2226,7 @@ async fn second_build_without_new_data_is_noop() {
assert!(!files_after_first.is_empty());
let built = table
- .new_btree_global_index_build_builder()
+ .new_global_index_build_builder()
.with_index_column("name")
.execute()
.await
@@ -2269,7 +2269,7 @@ async fn incremental_build_indexes_only_new_rows() {
.unwrap();
let first_built = table
- .new_btree_global_index_build_builder()
+ .new_global_index_build_builder()
.with_index_column("name")
.execute()
.await
@@ -2296,7 +2296,7 @@ async fn incremental_build_indexes_only_new_rows() {
.unwrap();
let second_built = table
- .new_btree_global_index_build_builder()
+ .new_global_index_build_builder()
.with_index_column("name")
.execute()
.await
@@ -2356,7 +2356,7 @@ async fn first_build_indexes_full_coverage() {
.unwrap();
let built = table
- .new_btree_global_index_build_builder()
+ .new_global_index_build_builder()
.with_index_column("name")
.execute()
.await
@@ -2401,7 +2401,7 @@ async fn
incremental_build_splits_gap_across_records_per_range_grid() {
.unwrap();
let first_built = table
- .new_btree_global_index_build_builder()
+ .new_global_index_build_builder()
.with_index_column("name")
.execute()
.await
@@ -2457,7 +2457,7 @@ async fn
incremental_build_splits_gap_across_records_per_range_grid() {
);
let second_built = table
- .new_btree_global_index_build_builder()
+ .new_global_index_build_builder()
.with_index_column("name")
.execute()
.await
@@ -2578,7 +2578,7 @@ async fn
incremental_build_splits_gap_around_mid_coverage_indexed_hole() {
// Build: gap = coverage minus the hole = [0, hole_start-1] and
// [hole_end+1, last_row]; two shards since the grid does not split here.
let built = table
- .new_btree_global_index_build_builder()
+ .new_global_index_build_builder()
.with_index_column("name")
.execute()
.await
@@ -2673,7 +2673,7 @@ async fn
test_composite_btree_build_query_coverage_and_drop() {
.unwrap();
assert_eq!(
table
- .new_btree_global_index_build_builder()
+ .new_global_index_build_builder()
.with_index_columns(&["name", "id"])
.execute()
.await
@@ -2682,7 +2682,7 @@ async fn
test_composite_btree_build_query_coverage_and_drop() {
);
assert_eq!(
table
- .new_btree_global_index_build_builder()
+ .new_global_index_build_builder()
.with_index_column(" name , id ")
.execute()
.await
@@ -2800,7 +2800,7 @@ async fn
test_composite_btree_build_query_coverage_and_drop() {
assert_eq!(scan_ids(&table, point).await, vec![1, 1]);
assert_eq!(
table
- .new_btree_global_index_build_builder()
+ .new_global_index_build_builder()
.with_index_columns(&["name", "id"])
.execute()
.await
@@ -2810,7 +2810,7 @@ async fn
test_composite_btree_build_query_coverage_and_drop() {
// Independent scalar and reversed tuple definitions coexist.
assert!(
table
- .new_btree_global_index_build_builder()
+ .new_global_index_build_builder()
.with_index_column("name")
.execute()
.await
@@ -2819,7 +2819,7 @@ async fn
test_composite_btree_build_query_coverage_and_drop() {
);
assert!(
table
- .new_btree_global_index_build_builder()
+ .new_global_index_build_builder()
.with_index_columns(&["id", "name"])
.execute()
.await
@@ -2872,7 +2872,7 @@ async fn test_composite_btree_column_validation() {
let table = test_table(table_options("10"));
for columns in ["", "name,", "name,name", "name,missing"] {
assert!(table
- .new_btree_global_index_build_builder()
+ .new_global_index_build_builder()
.with_index_column(columns)
.execute()
.await
@@ -2887,7 +2887,7 @@ async fn test_composite_btree_column_validation() {
for index_type in ["bitmap", "multivalue", "fm"] {
assert!(matches!(
table
- .new_btree_global_index_build_builder()
+ .new_global_index_build_builder()
.with_index_columns(&["name", "id"])
.with_index_type(index_type)
.execute()
@@ -2920,7 +2920,7 @@ async fn
test_composite_btree_all_null_tuple_and_residual_filter() {
.await
.unwrap();
table
- .new_btree_global_index_build_builder()
+ .new_global_index_build_builder()
.with_index_columns(&["name", "id"])
.execute()
.await
@@ -3059,7 +3059,7 @@ async fn
test_composite_btree_literal_precision_preserves_scan_results() {
}
for columns in [["d", "id"], ["ts", "id"]] {
table
- .new_btree_global_index_build_builder()
+ .new_global_index_build_builder()
.with_index_columns(&columns)
.execute()
.await
@@ -3099,7 +3099,7 @@ async fn
test_composite_btree_literal_precision_preserves_scan_results() {
for index_type in ["btree", "bitmap"] {
for column in ["d", "ts"] {
table
- .new_btree_global_index_build_builder()
+ .new_global_index_build_builder()
.with_index_column(column)
.with_index_type(index_type)
.execute()
@@ -3162,14 +3162,14 @@ async fn
test_composite_btree_intersects_independent_indexes() {
.await
.unwrap();
table
- .new_btree_global_index_build_builder()
+ .new_global_index_build_builder()
.with_index_type("bitmap")
.with_index_column("flag")
.execute()
.await
.unwrap();
table
- .new_btree_global_index_build_builder()
+ .new_global_index_build_builder()
.with_index_columns(&["name", "id"])
.execute()
.await
@@ -3204,7 +3204,7 @@ async fn
test_composite_btree_intersects_independent_indexes() {
.await
.unwrap();
table
- .new_btree_global_index_build_builder()
+ .new_global_index_build_builder()
.with_index_columns(&["flag", "id"])
.execute()
.await
@@ -3250,12 +3250,12 @@ async fn
test_composite_btree_combines_different_index_coverage() {
.await
.unwrap();
table
- .new_btree_global_index_build_builder()
+ .new_global_index_build_builder()
.with_index_columns(&["name", "id"])
.execute()
.await
.unwrap();
- let mut independent = table.new_btree_global_index_build_builder();
+ let mut independent = table.new_global_index_build_builder();
if independent_type == "bitmap" {
independent
.with_index_type("bitmap")
@@ -3404,7 +3404,7 @@ async fn
test_composite_btree_preserves_exact_column_names() {
.unwrap();
assert_eq!(
table
- .new_btree_global_index_build_builder()
+ .new_global_index_build_builder()
.with_index_columns(&["a,b", " c "])
.execute()
.await
@@ -3413,7 +3413,7 @@ async fn
test_composite_btree_preserves_exact_column_names() {
);
assert_eq!(
table
- .new_btree_global_index_build_builder()
+ .new_global_index_build_builder()
.with_index_column("a,b")
.execute()
.await
@@ -3422,7 +3422,7 @@ async fn
test_composite_btree_preserves_exact_column_names() {
);
assert_eq!(
table
- .new_btree_global_index_build_builder()
+ .new_global_index_build_builder()
.with_index_column(" c ")
.execute()
.await
@@ -3472,7 +3472,7 @@ async fn
test_composite_btree_preserves_exact_column_names() {
1
);
assert!(table
- .new_btree_global_index_build_builder()
+ .new_global_index_build_builder()
.with_index_columns(&[])
.execute()
.await
diff --git a/crates/paimon/src/table/vector_search_test_utils.rs
b/crates/paimon/src/table/vector_search_test_utils.rs
index 65c41c5e..257a1897 100644
--- a/crates/paimon/src/table/vector_search_test_utils.rs
+++ b/crates/paimon/src/table/vector_search_test_utils.rs
@@ -187,14 +187,15 @@ pub(super) async fn de_vector_table() -> Table {
.unwrap();
let built = table
- .new_vindex_index_build_builder(IVF_FLAT_IDENTIFIER)
+ .new_global_index_build_builder()
+ .with_index_type(IVF_FLAT_IDENTIFIER)
.with_index_column("embedding")
.execute()
.await
.unwrap();
assert!(built > 0, "DE fixture must build a global vector index");
let built = table
- .new_sorted_global_index_build_builder()
+ .new_global_index_build_builder()
.with_index_column("id")
.with_index_type("btree")
.execute()
diff --git a/crates/paimon/src/table/vindex_index_build_builder.rs
b/crates/paimon/src/table/vindex_index_build_builder.rs
index e63f3658..ca049b19 100644
--- a/crates/paimon/src/table/vindex_index_build_builder.rs
+++ b/crates/paimon/src/table/vindex_index_build_builder.rs
@@ -18,26 +18,25 @@
mod extraction;
mod pipeline;
mod planning;
-mod timing;
+pub(super) mod timing;
mod validation;
mod writer;
use planning::plan_vindex_shards;
-use timing::vector_index_build_timing_enabled;
use validation::{checked_i32, find_index_field, validate_table_options,
validate_vector_field};
-use crate::spec::CoreOptions;
-use crate::table::{CommitMessage, RowRange, SnapshotManager, Table,
TableCommit};
+use crate::spec::{CoreOptions, Predicate};
+use crate::table::{CommitMessage, RowRange, Table, TableCommit};
use crate::vindex::{is_vindex_index_type, VindexVectorIndexOptions};
use crate::{Error, Result};
use std::collections::HashMap;
-use std::time::Instant;
-pub struct VindexIndexBuildBuilder<'a> {
+pub(super) struct VindexIndexBuildBuilder<'a> {
table: &'a Table,
index_column: Option<String>,
index_type: String,
options: HashMap<String, String>,
+ pub(super) partition_filter: Option<Predicate>,
}
impl<'a> VindexIndexBuildBuilder<'a> {
@@ -47,6 +46,7 @@ impl<'a> VindexIndexBuildBuilder<'a> {
index_column: None,
index_type: index_type.to_string(),
options: HashMap::new(),
+ partition_filter: None,
}
}
@@ -60,7 +60,13 @@ impl<'a> VindexIndexBuildBuilder<'a> {
self
}
- pub async fn execute(&self) -> Result<usize> {
+ pub(super) async fn prepare(
+ &self,
+ ) -> Result<(
+ Option<i64>,
+ Vec<CommitMessage>,
+ Vec<timing::VectorIndexBuildTiming>,
+ )> {
// Building the index scans the table's rows.
CoreOptions::new(self.table.schema().options()).ensure_read_authorized()?;
@@ -81,15 +87,20 @@ impl<'a> VindexIndexBuildBuilder<'a> {
source: None,
})?;
- let core_options = CoreOptions::new(self.table.schema().options());
+ let mut merged_options = self.table.schema().options().clone();
+ merged_options.extend(self.options.clone());
+ let core_options = CoreOptions::new(&merged_options);
validate_table_options(self.table, &core_options)?;
let rows_per_shard = core_options.global_index_row_count_per_shard()?;
let index_field = find_index_field(self.table, index_column)?;
validate_vector_field(index_field)?;
+ // The builder consumes shard sizing; it is not a native writer option.
+ let mut writer_options = self.options.clone();
+ writer_options.remove("global-index.row-count-per-shard");
let vindex_options = VindexVectorIndexOptions::new(
self.table.schema().options(),
- &self.options,
+ &writer_options,
&self.index_type,
index_field,
)?;
@@ -103,21 +114,16 @@ impl<'a> VindexIndexBuildBuilder<'a> {
source: Some(Box::new(e)),
})?;
- let snapshot_manager = SnapshotManager::new(
- self.table.file_io().clone(),
- self.table.location().to_string(),
- );
- let snapshot = snapshot_manager
- .get_latest_snapshot()
- .await?
- .ok_or_else(|| Error::DataInvalid {
- message: "Cannot build vindex index without a
snapshot".to_string(),
- source: None,
- })?;
+ let snapshot_manager = self.table.snapshot_manager();
+ let Some(snapshot) = snapshot_manager.get_latest_snapshot().await?
else {
+ return Ok((None, vec![], vec![]));
+ };
- let manifest_entries = self
- .table
- .new_read_builder()
+ let mut read_builder = self.table.new_read_builder();
+ if let Some(filter) = &self.partition_filter {
+ read_builder.with_filter(filter.clone());
+ }
+ let manifest_entries = read_builder
.new_scan()
.with_scan_all_files()
.plan_manifest_entries(&snapshot)
@@ -141,7 +147,7 @@ impl<'a> VindexIndexBuildBuilder<'a> {
&indexed,
)?;
if shards.is_empty() {
- return Ok(0);
+ return Ok((Some(snapshot.id()), vec![], vec![]));
}
crate::table::global_index_build_common::validate_existing_index_overlap(
@@ -196,18 +202,7 @@ impl<'a> VindexIndexBuildBuilder<'a> {
}
}
- let commit_start =
vector_index_build_timing_enabled().then(Instant::now);
- commit
- .commit_if_latest_snapshot(messages, snapshot.id())
- .await?;
- if let Some(commit_start) = commit_start {
- let commit = commit_start.elapsed();
- for timing in timings {
- timing.log(&self.index_type, commit);
- }
- }
-
- Ok(shard_count)
+ Ok((Some(snapshot.id()), messages, timings))
}
}
diff --git a/crates/paimon/src/table/vindex_index_build_builder/tests.rs
b/crates/paimon/src/table/vindex_index_build_builder/tests.rs
index fb5c1499..97fad54d 100644
--- a/crates/paimon/src/table/vindex_index_build_builder/tests.rs
+++ b/crates/paimon/src/table/vindex_index_build_builder/tests.rs
@@ -594,7 +594,8 @@ async fn vindex_second_build_without_new_data_is_noop() {
assert!(!names_before.is_empty());
let built = table
- .new_vindex_index_build_builder(IVF_FLAT_IDENTIFIER)
+ .new_global_index_build_builder()
+ .with_index_type(IVF_FLAT_IDENTIFIER)
.with_index_column("embedding")
.execute()
.await
@@ -634,7 +635,8 @@ async fn vindex_incremental_build_indexes_only_new_rows() {
)
.await;
let first_built = table
- .new_vindex_index_build_builder(IVF_FLAT_IDENTIFIER)
+ .new_global_index_build_builder()
+ .with_index_type(IVF_FLAT_IDENTIFIER)
.with_index_column("embedding")
.with_options(HashMap::from([(
"ivf-flat.train.sample-ratio".to_string(),
@@ -667,7 +669,8 @@ async fn vindex_incremental_build_indexes_only_new_rows() {
// End-to-end: build #2 must SUCCEED and index the appended rows.
let second_built = table
- .new_vindex_index_build_builder(IVF_FLAT_IDENTIFIER)
+ .new_global_index_build_builder()
+ .with_index_type(IVF_FLAT_IDENTIFIER)
.with_index_column("embedding")
.with_options(HashMap::from([(
"vindex.build.granule.enabled".to_string(),
@@ -747,7 +750,8 @@ async fn
vindex_small_training_sample_preserves_tail_cluster_recall() {
.await;
assert_eq!(
table
- .new_vindex_index_build_builder(crate::vindex::IVF_SQ_IDENTIFIER)
+ .new_global_index_build_builder()
+ .with_index_type(crate::vindex::IVF_SQ_IDENTIFIER)
.with_index_column("embedding")
.execute()
.await
@@ -823,7 +827,8 @@ async fn
vindex_granule_training_sees_file_periodic_cluster() {
assert_eq!(
table
- .new_vindex_index_build_builder(crate::vindex::IVF_SQ_IDENTIFIER)
+ .new_global_index_build_builder()
+ .with_index_type(crate::vindex::IVF_SQ_IDENTIFIER)
.with_index_column("embedding")
.execute()
.await
@@ -967,7 +972,8 @@ async fn
vindex_granule_training_sees_one_oversized_row_group() {
assert_eq!(physical_granules.len(), 1);
assert_eq!(physical_granules[0].row_count, LARGE_ROWS as i64);
- let mut builder =
table.new_vindex_index_build_builder(crate::vindex::IVF_SQ_IDENTIFIER);
+ let mut builder =
+ super::VindexIndexBuildBuilder::new(&table,
crate::vindex::IVF_SQ_IDENTIFIER);
builder
.with_index_column("embedding")
.with_options(HashMap::from([(
@@ -996,7 +1002,20 @@ async fn
vindex_granule_training_sees_one_oversized_row_group() {
assert!(plan.rest.is_empty(), "snapshot {}", snapshot.id());
assert_eq!(plan.first, vec![RowRange::new(0, total_rows as i64 -
1)]);
}
- assert_eq!(builder.execute().await.unwrap(), 1);
+ assert_eq!(
+ table
+ .new_global_index_build_builder()
+ .with_index_column("embedding")
+ .with_index_type(crate::vindex::IVF_SQ_IDENTIFIER)
+ .with_options(HashMap::from([(
+ "vindex.build.granule.enabled".to_string(),
+ granule_enabled.to_string()
+ )]))
+ .execute()
+ .await
+ .unwrap(),
+ 1
+ );
let result = table
.new_vector_search_builder()
.with_vector_column("embedding")
@@ -1033,7 +1052,8 @@ async fn
vindex_build_cleans_written_shards_when_later_shard_fails() {
.await;
let error = table
- .new_vindex_index_build_builder(IVF_FLAT_IDENTIFIER)
+ .new_global_index_build_builder()
+ .with_index_type(IVF_FLAT_IDENTIFIER)
.with_index_column("embedding")
.execute()
.await
@@ -1099,7 +1119,8 @@ async fn
vindex_build_coexists_with_different_index_type_on_same_field() {
// A native-build failure over the tiny synthetic dataset is tolerated;
// only the overlap error is forbidden.
let result = table
- .new_vindex_index_build_builder(IVF_FLAT_IDENTIFIER)
+ .new_global_index_build_builder()
+ .with_index_type(IVF_FLAT_IDENTIFIER)
.with_index_column("embedding")
.execute()
.await;
diff --git a/crates/paimon/src/table/vindex_index_build_builder/timing.rs
b/crates/paimon/src/table/vindex_index_build_builder/timing.rs
index ae43568f..87b3f417 100644
--- a/crates/paimon/src/table/vindex_index_build_builder/timing.rs
+++ b/crates/paimon/src/table/vindex_index_build_builder/timing.rs
@@ -20,14 +20,14 @@ use std::time::Duration;
const VECTOR_INDEX_BUILD_TIMING_ENV: &str =
"PAIMON_LOG_VECTOR_INDEX_BUILD_TIMING";
-pub(super) fn vector_index_build_timing_enabled() -> bool {
+pub(in crate::table) fn vector_index_build_timing_enabled() -> bool {
static ENABLED: OnceLock<bool> = OnceLock::new();
*ENABLED.get_or_init(|| {
std::env::var_os(VECTOR_INDEX_BUILD_TIMING_ENV).is_some_and(|value|
value == "1")
})
}
-pub(super) struct VectorIndexBuildTiming {
+pub(in crate::table) struct VectorIndexBuildTiming {
pub(super) total_without_commit: Duration,
pub(super) source_batch_wait: Duration,
pub(super) oss_read: Duration,
@@ -59,7 +59,7 @@ pub(super) struct VectorIndexBuildTiming {
}
impl VectorIndexBuildTiming {
- pub(super) fn log(self, index_type: &str, commit: Duration) {
+ pub(in crate::table) fn log(self, index_type: &str, commit: Duration) {
let total = self.total_without_commit.saturating_add(commit);
let accounted = self
.source_batch_wait
diff --git a/crates/paimon/src/table/vindex_index_build_builder/writer.rs
b/crates/paimon/src/table/vindex_index_build_builder/writer.rs
index 8c7fc126..bc4c77d2 100644
--- a/crates/paimon/src/table/vindex_index_build_builder/writer.rs
+++ b/crates/paimon/src/table/vindex_index_build_builder/writer.rs
@@ -39,7 +39,6 @@ use std::time::{Duration, Instant};
use tokio::io::AsyncWriteExt;
use tokio_util::io::SyncIoBridge;
-const INDEX_DIR: &str = "index";
const VECTOR_BUFFER_BYTES: usize = 8 * 1024 * 1024;
pub(super) struct BuiltIndexFile {
@@ -426,23 +425,16 @@ impl<'a> VindexIndexBuildBuilder<'a> {
index_meta: Vec<u8>,
row_count: i64,
) -> Result<IndexFileMeta> {
- self.table
- .file_io()
- .mkdirs(&format!(
- "{}/{INDEX_DIR}/",
- self.table.location().trim_end_matches('/')
- ))
- .await?;
let file_name = format!(
"vector-{}-global-index-{}.index",
self.index_type,
uuid::Uuid::new_v4()
);
- let index_path = format!(
- "{}/{INDEX_DIR}/{}",
- self.table.location().trim_end_matches('/'),
- file_name
- );
+ let (index_path, external_path) =
+ crate::table::global_index_build_common::prepare_index_file_path(
+ self.table, &file_name,
+ )
+ .await?;
let write_result = async {
let async_writer = self
.table
@@ -484,13 +476,15 @@ impl<'a> VindexIndexBuildBuilder<'a> {
)?,
row_count,
deletion_vectors_ranges: None,
- external_path: None,
+ external_path,
global_index_meta: Some(GlobalIndexMeta {
row_range_start: shard.row_range_start,
row_range_end: shard.row_range_end,
index_field_id,
extra_field_ids: None,
- source_meta: None,
+ source_meta: Some(
+
crate::spec::DataEvolutionIndexSourceMeta::new(shard.snapshot_id)?.serialize(),
+ ),
index_meta: Some(index_meta),
}),
})
diff --git a/crates/paimon/src/table/write_builder.rs
b/crates/paimon/src/table/write_builder.rs
index bbde338f..1211996e 100644
--- a/crates/paimon/src/table/write_builder.rs
+++ b/crates/paimon/src/table/write_builder.rs
@@ -536,7 +536,7 @@ mod tests {
.unwrap();
let index_err = table
- .new_btree_global_index_build_builder()
+ .new_global_index_build_builder()
.with_index_column("value")
.execute()
.await
diff --git a/crates/paimon/tests/scan_planning_parity_test.rs
b/crates/paimon/tests/scan_planning_parity_test.rs
index f08134db..c5fb07d7 100644
--- a/crates/paimon/tests/scan_planning_parity_test.rs
+++ b/crates/paimon/tests/scan_planning_parity_test.rs
@@ -190,7 +190,7 @@ async fn
global_index_ranges_intersect_positions_after_candidate_assignment() {
append_ids(&table, 3, 6).await;
assert!(
table
- .new_btree_global_index_build_builder()
+ .new_global_index_build_builder()
.with_index_column("id")
.with_index_type(index_type)
.execute()