Skip to main content

Manage Global Indexes

Build and inspect indexes separately from ingesting table data. Use this guide to choose freshness behavior, coordinate index visibility, and remove index files. The SQL examples use a Paimon catalog with db as the current database. CALL examples apply to Spark and Flink; ALTER TABLE examples below use Spark SQL. In Flink, replace SET TBLPROPERTIES (...) with SET (...). Index-specific build and query examples are listed in Global Index.

Build an Index

Create a global index on an existing table. The examples assume a populated db.my_table; partition-scoped examples additionally require a partition column named dt. See Global Index for table setup.

Create global indexes for all partitions or only selected partitions:

CALL sys.create_global_index(
table => 'db.my_table',
index_column => 'name',
index_type => 'btree'
);

CALL sys.create_global_index(
table => 'db.my_table',
index_column => 'name',
index_type => 'btree',
partitions => 'dt=2026-06-18;dt=2026-06-19'
);

PyPaimon global index build currently supports single-column BTree indexes, single-column Bitmap indexes, single-column paimon-vindex vector indexes, and single-column full-text indexes on tables with row tracking enabled.

Re-running a build is incremental: it fills missing coverage. Spark and Flink can also refresh changed ranges under the IGNORE update policy; see Update Indexed Columns for the PyPaimon difference and the required source metadata. Changing a build option alone does not rebuild already covered rows. To replace existing tokenizer or vector-index settings, drop the relevant index and build it again, using the same partition scope when replacing only selected partitions.

Inspect Index Coverage

Global indexes are stored in index files and recorded in table metadata. To inspect index files and their row-id coverage, query the table_indexes system table:

SELECT index_type, index_field_name, row_range_start, row_range_end
FROM `my_table$table_indexes`
WHERE index_field_name IS NOT NULL;

You can also query file_key_ranges to inspect data file row-id ranges and diagnose coverage:

SELECT file_path, first_row_id, record_count
FROM `my_table$file_key_ranges`;

Wait for Index Visibility

For Spark, Flink, and Java batch or bounded-stream writers, the visibility callback waits after the data snapshot is committed for existing indexes to cover newly appended ranges. It delays completion of the writer's commit call; other readers can already see the committed snapshot while it waits.

Build the initial indexes before enabling the callback, and run subsequent index builds in a separate job or session. Running the index build only after the same blocked write returns would make the write time out. The callback waits for coverage; it does not create indexes:

ALTER TABLE my_table SET TBLPROPERTIES (
'visibility-callback.enabled' = 'true',
'visibility-callback.timeout' = '30 min'
);

The callback tracks index definitions already present in each affected partition. It does not wait for a partition that has no existing index, including a newly created partition. On timeout, the committed data remains in the table; inspect the committed snapshot before retrying an append.

PyPaimon writers do not implement this built-in visibility callback. Setting the table property through the Python catalog configures JVM writers of that table, but does not make a Python commit wait. For a Python pipeline, commit the data, run table.create_global_index(...), and inspect coverage before treating that batch as ready for indexed queries.

Coverage and Freshness

Global index files cover row-id ranges. If more rows are appended after an index is built, those new rows are not automatically covered by the existing index files. Build the global index again to create index files for newly uncovered data. The scalar, vector, and full-text families default to fast, which searches indexed row ranges. Choose a search mode deliberately when recently appended data must be included. Updates to already indexed rows also require the update policy.

ModeHow coverage is checkedUse when
fastSearch indexed coverage only.The index is current, or partial coverage is acceptable.
fullCheck row-ID coverage against snapshot nextRowId; scan raw data for detected gaps where supported.Newly appended ranges need to be included.
detailCompare active data-file row-ID ranges with index coverage, then scan detected gaps where supported.Coverage should be checked against current files, including partition filtering.

An index covers the original row range; appended rows need another index build or a search mode that includes uncovered data.

To include appended data for query types that support raw-data search, set:

ALTER TABLE my_table SET TBLPROPERTIES ('vector-index.search-mode' = 'full');

With full search, supported global-index queries compare the allocated row-ID space [0, nextRowId) with index coverage. With detail, they instead inspect the row-ID ranges of current data files. Neither mode compares indexed values with the latest column values. An update that preserves row IDs can therefore leave stale index entries even when coverage is complete. Handle those updates through the index update policy and an index build.

Use scalar-index.search-mode, vector-index.search-mode, or full-text-index.search-mode for one index family. The legacy global-index.search-mode has no default and is used as a fallback when the corresponding family-specific option is not explicitly configured.

To temporarily disable global-index scan acceleration while keeping the index files, set:

ALTER TABLE my_table SET TBLPROPERTIES ('global-index.enabled' = 'false');

Set it back to true to use global indexes during scans again.

Update Indexed Columns

Coverage and freshness are different checks: an index can cover a row ID while still holding that row's old value. For example, changing an indexed name from a200 to a250 keeps the row ID but makes the old index entry stale. Switching to detail does not repair or detect that value change.

Choose global-index.column-update-action before updating an indexed column:

PolicyEffect of the updateWhat to do next
THROW_ERROR (default)Rejects the indexed-column update.Choose a supported update policy or explicitly drop the index before updating.
DROP_PARTITION_INDEXDrops affected partition indexes as part of the update.Build indexes again for the affected scope.
IGNOREKeeps existing index files with their old values.Refresh or replace those indexes before relying on indexed results for the changed data.

With IGNORE, Spark and Flink incremental builds use source metadata stored in index files to find and rebuild changed row ranges. Older index files without that metadata are not refreshed automatically; drop and rebuild them. PyPaimon incremental builds currently fill uncovered ranges only. For updates to already covered rows through a Python pipeline, explicitly drop and rebuild the affected index instead of relying on a repeated create_global_index call alone.

Keep the same partition scope when dropping and replacing an index. Until the replacement is built, query results depend on the remaining coverage, search mode, and query API.

Read Scored Results

Vector, full-text, and hybrid search select a set of top-K matching row IDs and associate scores with them. A subsequent table scan reads the selected records in its scan order. The top-K selection does not guarantee that returned rows are displayed in descending score order.

In Spark SQL, request the score and sort explicitly. For example, after building the Full-Text Index:

SELECT id, content, __paimon_search_score
FROM full_text_search('my_table', 'content', '{"match":{"query":"paimon"}}', 10)
ORDER BY __paimon_search_score DESC, id;

For Java or Python, obtain each score from the search result by Paimon row ID. The following snippets assume result is the scored result returned by a search builder; the IDs below are Paimon row IDs, not values of the business id column.

ScoredGlobalIndexResult scored = (ScoredGlobalIndexResult) result;
ScoreGetter scores = scored.scoreGetter();
List<Long> rowIds = new ArrayList<>();
for (long rowId : scored.results()) {
rowIds.add(rowId);
}
rowIds.sort(Comparator.<Long>comparingDouble(rowId -> scores.score(rowId))
.reversed()
.thenComparingLong(rowId -> rowId));
for (long rowId : rowIds) {
System.out.println(rowId + ": " + scores.score(rowId));
}

Associate materialized rows and scores by row ID. Do not zip a table scan's rows with a separately sorted score list: the two orders can differ.

Drop an Index

Drop index files:

CALL sys.drop_global_index(
table => 'db.my_table',
index_column => 'name',
index_type => 'btree'
);

Shared Options

These table options affect global index build and read behavior:

OptionDefaultDescription
global-index.enabledtrueWhether scans can use global indexes.
global-index.search-modeNot setLegacy fallback search mode for global-index queries. Family-specific options take precedence.
scalar-index.search-modefastSearch mode for BTree, Bitmap, Multivalue, and FM queries.
vector-index.search-modefastSearch mode for vector queries.
full-text-index.search-modefastSearch mode for full-text queries.
global-index.external-pathNot setRoot directory for global index files. If not set, files are stored under the table index directory.
global-index.column-update-actionTHROW_ERRORUpdate policy: THROW_ERROR, DROP_PARTITION_INDEX, or IGNORE. See Update Indexed Columns for refresh requirements and engine differences.
sorted-index.records-per-range10000000Expected number of records per sorted global index file for BTree, Bitmap, and Multivalue builds.
sorted-index.build.max-parallelism4096Maximum Flink or Spark parallelism for building sorted global indexes.
global-index.row-count-per-shard100000Target row count per shard for non-sorted global index builds such as vector and full-text indexes.
global-index.build.max-shard32Preferred maximum shard count for global index builds.
global-index.build.max-parallelism4096Maximum Flink or Spark parallelism for building non-sorted global indexes.
global-index.thread-num32Maximum number of concurrent threads for global index I/O.
visibility-callback.enabledfalseWhether JVM batch or bounded-stream writers wait after commit for existing partition indexes to cover newly appended ranges. Does not hide the committed snapshot.
visibility-callback.timeout30 minMaximum wait time for visibility callback.