Repository navigation
fix(global-index): apply global top-k to full-text search across shards - #431
Open
SteNicholas wants to merge 1 commit into
Open
SteNicholas wants to merge 1 commit into
SteNicholas wants to merge 1 commit into
Conversation
UnionGlobalIndexReader::VisitFullTextSearch ORed shard results, so a search with limit k could return up to shards x k rows, and it failed when two shards returned the same row id. It now merges shard results as Java ScoredGlobalIndexResult#merge and #topK do: a duplicate row id keeps its first score, and only the k highest-scoring rows are kept. Scores follow Java Double#compare ordering, and ties among merged rows are broken by ascending row id. Which rows tied at a shard's cutoff the shard returns is still up to its engine, so the API comment and user guide promise row id order only among rows the shards return. OffsetGlobalIndexReader shifted include row ids by the shard offset without clipping them, so row ids before a shard became negative local ids and row ids past it were kept. It now takes the end of its row range and clips include row ids to that range before shifting them. As in Java DataEvolutionFullTextScan#chooseIndexRanges, GlobalIndexScan ranks a row covered by overlapping index ranges only in the first of them, so the top k rows of each shard include all of its rows that score above the global k-th score. Each reader is given the last row that earlier ranges rank, which does not overflow when a range ends at INT64_MAX. A shard with no row left to rank is not searched, but it still rejects a non-positive limit.
SteNicholas
force-pushed
the
PAIMON-402
branch
from
October 13, 2026 12:54
dbb7a6a to
19685f3
Compare
lxy-9602
reviewed
Oct 16, 2026
| /// @param wrapped The inner reader to delegate queries to. Its results are expected to | ||
| /// contain local row ids. | ||
| /// @param offset The offset to add to each row id in the results. | ||
| OffsetGlobalIndexReader(std::shared_ptr<GlobalIndexReader>&& wrapped, int64_t offset); |
Member
There was a problem hiding this comment.
The comment for OffsetGlobalIndexReader seems to fully describe full-text search.
I think its purpose may be more than just FTS?
| /// @param to The last global row id (inclusive) covered by the wrapped reader. | ||
| OffsetGlobalIndexReader(std::shared_ptr<GlobalIndexReader>&& wrapped, int64_t offset, | ||
| int64_t to); | ||
|
|
Member
There was a problem hiding this comment.
It looks like the three-parameter version isn’t used in production code—can we remove it?
Member
There was a problem hiding this comment.
I checked Java’s chooseIndexRanges—it seems more complex than the C++ side, and it passes includeRowIds rather than inside OffsetGlobalIndexReader. I wonder what’s the rationale behind this design?
This branch has not been deployed
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 #402
UnionGlobalIndexReader::VisitFullTextSearchORed shard results, so a full-text search withlimitk over multiple index shards could return up to shards × k rows, and it failed when twoshards returned the same row id.
OffsetGlobalIndexReadershifted globalinclude_row_idsby theshard offset without clipping them, so row ids before a shard became negative local ids, row ids
past it were kept, and a shard with no included row was still searched.
This PR follows Java
DataEvolutionFullTextRead, which asks each split forlimitrows, combinesthem with
ScoredGlobalIndexResult#mergeand applies#topK(limit)last.UnionGlobalIndexReader::VisitFullTextSearch: merges shard results, where a duplicate row idkeeps its first score, and keeps the
limithighest-scoring rows. Scores are compared as JavaDouble#comparedoes (viaFieldsComparator::CompareFloatingPoint), and ties among merged rowsare broken by ascending row id. A null search or a non-positive
limitis rejected before anyshard is searched.
OffsetGlobalIndexReader: takes the last row id of its shard and clipsinclude_row_idstothe shard with
FullTextSearch::OffsetRange(offset, to), as JavaOffsetGlobalIndexReader(wrapped, offset, to)does. A shard whose include set becomes empty isnot searched.
DataEvolutionFullTextScan#chooseIndexRanges,GlobalIndexScanImplranks a row covered by overlapping ranges of one index type only in thefirst of them in
(from, to)order. EachOffsetGlobalIndexReaderis given the last row rankedby earlier ranges (
skip_to) and ranks only the rows after it, so the toplimitrows of eachshard include all of its rows that score above the global
limit-th score. Passing the lastskipped row instead of the first row to rank does not overflow when a range ends at
INT64_MAX.A fully covered shard is not searched but still rejects a non-positive
limit.As in Java, each shard's index engine decides which of its rows tied at its cutoff score it
returns, so ascending row id order is guaranteed only among the rows the shards return.
Tests
UT:
src/paimon/common/global_index/union_global_index_reader_test.cpp:TestVisitFullTextSearchUnion,TestVisitFullTextSearchKeepsTopKAcrossReaders(top-k and tiesby row id),
TestVisitFullTextSearchDuplicateRowIdKeepsFirstScore,TestVisitFullTextSearchOrdersSpecialScoresAsJava(NaN, infinity and signed zeros),TestVisitFullTextSearchAllNullptr,TestVisitFullTextSearchErrors.src/paimon/common/global_index/offset_global_index_reader_test.cpp:TestVisitFullTextSearchConvertsIncludeRowIds(clipping to the shard, and an empty include setthat is not searched),
TestVisitFullTextSearchRejectsNonPositiveLimit,TestVisitFullTextSearchSkipsRows,TestVisitFullTextSearchEndingAtLargestRowId. The othertests pass the shard end.
IT,
test/inte/global_index_test.cpp:TestWriteCommitScanReadLuceneIndexWithPartitionandTestWriteCommitScanReadFullTextIndexWithPartition: global top-k for everylimitacross twoshards, with and without
include_row_ids.TestLuceneSearchOverlappingIndexRanges: index ranges[0, 2],[1, 1]and[1, 3], where thebest match is covered only by the tail of
[1, 3], also withinclude_row_idsand aRowRangeIndexthat prunes[0, 2].API and Format
No signature change in
include/. The documented behavior changes:GlobalIndexReader::VisitFullTextSearch(include/paimon/global_index/global_index_reader.h)returns at most
limitrows across all index shards.FullTextSearch::limit(include/paimon/predicate/full_text_search.h) is the maximum number ofrows returned; each index shard is asked for that many rows before the merge.
No storage format change.
Documentation
Yes.
docs/source/user_guide/global_index.rstreplaces the note that searches across shards returnthe union of shard candidates with the global top-k behavior, tie handling, overlapping index
ranges and
include_row_idsclipping.Generative AI tooling
Generated-by: Claude Code 2.1.296 (Claude Opus 5.5)
🤖 Generated with Claude Code