Repository navigation
fix(global-index): exclude deleted rows from full-text ranking - #448
Merged
Merged
Conversation
Deletion vectors were applied only when rows were read, so a full-text search on a table with deletion vectors still ranked deleted rows. They could take every top-k slot, and the read then dropped them, returning fewer than limit rows even when enough live rows matched. As Java GlobalIndexLiveRowFilter does, GlobalIndexScan now computes the rows live in the snapshot it reads the index from: the union of the row id ranges of the data files in the scanned partitions, minus the rows deleted by their deletion vectors. Every range is added before any deletion vector is subtracted, so a deleted row is not added back by another file covering it. DataFileMeta::NonNullRowIdRange computes each range as Java nonNullRowIdRange does, and rejects one that would pass the largest int64 value. As in Java, DataEvolutionSplitGenerator takes its ranges from it too, so every data-evolution scan, including the one live rows are read from, rejects such a range instead of overflowing. A deletion vector that cannot be read is reported with its data file, snapshot and table. Live rows are read only for tables with deletion vectors and row tracking, once per scan on its first full-text search that ranks rows. A search without a positive limit, or whose include row ids miss the rows every shard ranks, neither reads them nor fails on them. As Java DataEvolutionFullTextRead#includeRowIds does, each index shard then ranks only the live rows of its search range, intersected with the include row ids if given, before the shard results are merged into the global top-k. BM25 statistics still come from the whole index, and a shard without deleted rows gets no extra filter. Live rows are loaded before the search fans out to the shards, so loading them never waits for the executor from one of its own threads. Predicate and vector searches are delegated unchanged.
SteNicholas
force-pushed
the
PAIMON-403
branch
from
October 21, 2026 15:37
d095a20 to
73e5b6a
Compare
Collaborator
|
Thanks for fixing the full-text case. As noted in the description, vector search on data-evolution tables with deletion vectors enabled still has the same top-k issue: deleted rows can occupy result slots and then be dropped during the data read. Do you have plans to address this in a follow-up? If not, we'll schedule support on our side. |
2 tasks done
Member
Author
|
Thanks for the review! Yes, I plan to cover vector search in a follow-up and opened #454 to track it. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Purpose
Linked issue: close #403
Deletion vectors were applied only when rows were read, so a full-text search on a data-evolution
table with
deletion-vectors.enabledstill ranked deleted rows. They could take every top-k slot,and the read then dropped them: a search with
limitk returned fewer than k rows, or none, evenwhen enough live rows matched.
The fix aligns with Java
GlobalIndexLiveRowFilterandDataEvolutionFullTextRead#includeRowIds:the rows live in the snapshot the index is read from are computed once and intersected into the
rows each index shard ranks, while BM25 statistics still come from the whole index.
GlobalIndexLiveRowFilter::LiveRows: as JavaGlobalIndexLiveRowFilter#liveRowsdoes, readsthe data files of the scanned partitions in the pinned snapshot with
ScanMode::ALL, unionstheir row id ranges, then subtracts the rows deleted by their deletion vectors. Every range is
added before any deletion vector is subtracted, so a deleted row is not added back by another
file covering the same rows, and only one deletion vector is held at a time. It returns nullopt,
so every row counts as live, when deletion vectors or row tracking are disabled. A deletion
vector that cannot be read is reported with its data file, snapshot and table.
DataFileMeta::NonNullRowIdRange: computes a file's row id range as JavaDataFileMeta#nonNullRowIdRangedoes, and rejects a null or negative first row id, a file withno rows, or a range that would pass
INT64_MAX, instead of overflowing. As in Java,DataEvolutionSplitGeneratortakes its ranges from it too, which covers the split planning thatlive rows are read through.
LiveRowGlobalIndexReader: wraps each index shard. A full-text search ranks only the liverows of the shard, intersected with the include row ids if given, and is passed on unchanged
when the whole shard is live. Predicate and vector searches are delegated unchanged.
LiveRowGlobalIndexReader::CreateUnion: loads live rows on the visiting thread before thesearch fans out to the shards, so a load that reads manifests on the shared executor never waits
for it from one of its own threads. A search without a positive
limit, or whose include rowids miss the rows every shard ranks, neither reads live rows nor fails on them.
GlobalIndexScanImpl: for tables with deletion vectors, wraps the shard readers of everyindex and loads live rows at most once per scan, on its first full-text search that ranks rows,
pinned to the snapshot the scan reads the index from. A failed load is retried on the next
search.
FileStoreScan::WithPartitionFilter: restricts the live-row scan to the partitions theglobal index scan reads.
Vector search still ranks deleted rows, as before; Java filters it too, and the same live-row
helper can be reused for it and for the table-level read in a follow-up.
Tests
UT:
src/paimon/core/global_index/live_row_global_index_reader_test.cpp:TestSearchUnchangedWhenEveryRowIsLive,TestSearchOnlyLiveRowsOfShard(including a shardending at
INT64_MAX),TestIncludeRowIdsClippedToShard,TestCreateUnionLoadsLiveRowsBeforeSearchingShards(live rows loaded on the visiting thread witha one-thread executor),
TestCreateUnionRanksLiveRowsOfSearchRanges(overlapping shards),TestCacheLiveRows(cached, retried after a failure, and nullopt when every row is live),TestLiveRowsErrorOnlyFailsSearchesThatRankRows(a failed load fails a search that ranks rows,but not one without a positive
limitor whose include row ids miss every searched shard) andTestOtherVisitsDoNotReadLiveRows.src/paimon/core/io/data_file_meta_test.cpp:TestNonNullRowIdRange(a range ending atINT64_MAX, one past it, no rows, a negative and a null first row id).src/paimon/core/table/source/split_generator_test.cpp:TestDataEvolutionRowIdRangeAtInt64Max.IT:
test/inte/data_evolution_table_test.cpp:DataEvolutionDvTableTest.TestGlobalIndexLiveRows(live rows with and without a partition filter, in the snapshot before the deletion, and the
error when a deletion vector cannot be read).
test/inte/global_index_test.cpp:TestLuceneSearchSkipsRowsDeletedByDeletionVectorsandTestFullTextSearchSkipsRowsDeletedByDeletionVectorsshareCheckSearchSkipsDeletedRows: acrosstwo shards, live rows keep the scores they had before the deletion, every
limitreturns thetop-k live rows, include row ids are intersected with the live rows, a scan created before the
deletion still ranks every row, and reading the matched rows returns all of them.
API and Format
No change in
include/. For tables with deletion vectors and row tracking, full-text searchesthrough readers created by
GlobalIndexScanrank only live rows. Data-evolution scans now reject adata file whose row id range is invalid instead of overflowing. No storage format change.
Documentation
No new feature.
docs/source/user_guide/global_index.rstdescribes how deleted rows are excludedfrom full-text ranking and shows passing one snapshot id to both the index scan and the data scan.
Generative AI tooling
Generated-by: Claude Code 2.1.303 (Claude Opus 5.5)
🤖 Generated with Claude Code