SteNicholas opened a new pull request, #461:
URL: https://github.com/apache/paimon-cpp/pull/461

   <!-- PR titles must follow Conventional Commits: <type>(<optional-scope>): 
<description> -->
   
   ### Purpose
   
   <!-- Linking this pull request to the issue -->
   Linked issue: close #404
   
   <!-- What is the purpose of the change -->
   Paimon C++ only exposed the reader primitive for full-text search: a caller 
had to call
   `GlobalIndexScan::CreateReaders`, run `VisitFullTextSearch` on the readers 
and pass the result to
   `ScanContextBuilder::SetGlobalIndexResult`, choosing index types and shard 
ranges along the way,
   with no serializable plan for running scan and read in different processes. 
This adds the
   table-level API, aligned with Java `FullTextSearchBuilder`, 
`DataEvolutionFullTextScan` and
   `DataEvolutionFullTextRead`.
   
   - **`FullTextSearchBuilder`**: `SetQuery`, `SetLimit`, `SetPartitionFilter`,
     `AddOption`/`SetOptions`, 
`WithMemoryPool`/`WithExecutor`/`WithFileSystem`, `NewFullTextScan()`,
     `NewFullTextRead()` and `ExecuteLocal()`. As in Java, the query column 
must exist in the latest
     schema and a read requires a positive limit. A column with a primary-key 
full-text index
     (`pk-full-text.index.columns`) is rejected with `NotImplemented` for now; 
Java searches it with
     `PrimaryKeyFullTextScan` and `PrimaryKeyFullTextRead`.
   - **`DataEvolutionFullTextScan`**: resolves the snapshot as Java 
`TimeTravelUtil#tryTravelOrLatest`
     does, whatever `scan.mode` is: from `scan.snapshot-id`, `scan.tag-name`, 
`scan.timestamp` or
     `scan.timestamp-millis`, or else the latest one. It keeps the index files 
that pass the partition
     filter, index the column as their indexed field or as an extra field, and 
whose indexer supports
     full-text search. As Java `chooseIndexRanges` does (apache/paimon#8547), 
it gives every row to
     one shard across overlapping and multi-field indexes, a dedicated index 
first, then by index
     identity, start and end, and plans one split per chosen shard with its 
physical range and its
     assigned search row ranges. A `full-text` or `lucene-fts` index whose 
backend is not built fails
     the plan instead of silently dropping its rows.
   - **`DataEvolutionFullTextRead`**: reads each split with an indexer created 
from its own index
     files, including their extra fields, through `OffsetGlobalIndexReader`, 
ranking only its search
     row ranges and live rows, and asks each split for `limit` candidates only 
(apache/paimon#9955).
     Splits are searched in parallel on the given executor, or on one with 
`global-index.thread-num`
     threads. The results are merged, keeping the first score of a duplicate 
row id, and cut to the
     global top-k by score descending, then row id ascending. Live rows are 
pinned to the snapshot the
     splits were planned from (apache/paimon#9031), which is loaded from a tag 
once it has expired.
   - **`FullTextSearchSplit` / `IndexFullTextSearchSplit`**: carry the snapshot 
id, column, physical
     range, search row ranges and index files, with a versioned encoding 
specific to Paimon C++ (Java
     uses Java serialization). Deserialization checks every count, range and 
binary row layout before
     decoding it, and the read rejects splits from different snapshots, of 
another column, or whose
     index files no longer index the field id of the column, such as after it 
was replaced.
   - **`GlobalIndexer::SupportsFullTextSearch()`**: `false` by default and 
`true` for `full-text` and
     `lucene-fts`, as Java `GlobalIndexerFactory#supportsFullTextSearch`.
   - The scored top-k merge (`ScoredGlobalIndexResultUtils::MergeTopK`), the 
row id range helpers
     (`GlobalIndexUtils::RowIdsOf` / `RowIdsWithin`) and executor creation
     (`GlobalIndexScanImpl::GetOrCreateExecutor`) are shared with the existing 
union reader and scan.
   
   Row filters (`withFilter`, #405) are not part of this change, and rows that 
no index shard covers
   are not searched.
   
   ### Tests
   
   <!-- List UT and IT cases to verify this change -->
   UT:
   
   - `src/paimon/core/table/source/data_evolution_full_text_scan_test.cpp`:
     `TestIndexIdentityAndIndexedFields`, `TestDisjointShards` (including a 
shard ending at
     `INT64_MAX`), `TestOverlappingShardsOfOneIndex`, 
`TestDedicatedIndexBeforeExtraField`,
     `TestIdentityOrderBreaksTies` and `TestServesFullTextSearch`.
   - `src/paimon/core/table/source/data_evolution_full_text_read_test.cpp`, 
with a fake full-text
     indexer that records each search: `TestMergesShardResultsIntoGlobalTopK`,
     `TestRanksOnlySearchRowRanges`, `TestIncludeRowIds`, 
`TestReadsEachSplitWithItsOwnIndexFields`,
     `TestRejectsInvalidSplits`, `TestRejectsSplitOfReplacedColumn`, 
`TestBuilderValidatesSettings`,
     `TestBuilderRejectsPrimaryKeyFullTextIndex` and `TestResolvesPlanSnapshot`.
   - `src/paimon/core/table/source/full_text_search_split_test.cpp`: 
`TestSerializeRoundTrip`,
     `TestDeserializeRejectsInvalidBytes`, 
`TestDeserializeRejectsCorruptFileLayout`,
     `TestDeserializeRejectsCorruptEntries` and `TestCreateValidatesArguments`.
   - `src/paimon/common/global_index/global_index_utils_test.cpp`: 
`TestRowIdsOfAndWithin`.
   - `SupportsFullTextSearch()` is checked in `global_indexer_factory_test.cpp` 
(bitmap),
     `full_text_global_index_test.cpp` and `lucene_global_index_test.cpp`.
   
   IT (`test/inte/global_index_test.cpp`):
   
   - `TestFullTextTableFullTextSearch` and `TestLuceneTableFullTextSearch`: two 
shards, the table
     search equals the reader search for every limit, serialized splits read 
back, the snapshot
     selected by `scan.snapshot-id` (also under `scan.mode` = `latest-full`), 
`scan.tag-name` and
     `scan.timestamp-millis`, a plan of an earlier snapshot, reading 
`_INDEX_SCORE`, and a bitmap
     index on the column is ignored.
   - `TestFullTextSearchOverlappingIndexRanges` and 
`TestLuceneSearchOverlappingIndexRanges`: the
     planned shards and search ranges, and the table search equals the reader 
search.
   - `TestFullTextSearchSkipsRowsDeletedByDeletionVectors` and
     `TestLuceneSearchSkipsRowsDeletedByDeletionVectors`: only live rows are 
ranked, a plan made before
     the deletion still ranks its rows, the read falls back to a tag once the 
snapshot is deleted,
     including when an unrelated tag is deleted while the tags are listed, and 
fails when the snapshot
     is missing or unreadable.
   - `TestWriteCommitScanReadFullTextIndexWithPartition` and
     `TestWriteCommitScanReadLuceneIndexWithPartition`: partition filters.
   
   ### API and Format
   
   <!-- Does this change affect API in include dir or storage format or 
protocol -->
   New public API in `include/paimon/table/source/`: 
`full_text_search_builder.h`, `full_text_scan.h`,
   `full_text_read.h` and `full_text_search_split.h`, all marked 
`PAIMON_EXPORT`. `GlobalIndexer`
   gains the virtual `SupportsFullTextSearch()`, which defaults to `false`. 
Full-text search splits
   have a new serialized form (magic `PFTSPLT1`, version 1), specific to Paimon 
C++. No storage format
   or protocol change.
   
   ### Documentation
   
   <!-- Does this change introduce a new feature -->
   Yes. `docs/source/user_guide/global_index.rst` adds a "Table-Level Full-Text 
Search" section,
   covering local search, pinning the data scan to the plan snapshot and 
distributed search with
   serialized splits, and `docs/source/api/global_index.rst` lists the new 
classes.
   
   ### Generative AI tooling
   
   <!--
   If generative AI tooling has been used in the process of authoring this 
patch, please include the
   phrase: 'Generated-by: ' followed by the name of the tool and its version.
   If no, write 'No'.
   Please refer to the [ASF Generative Tooling 
Guidance](https://www.apache.org/legal/generative-tooling.html) for details.
   -->
   Generated-by: Claude Code 2.1.309 (Claude Opus 5.5)
   
   🤖 Generated with [Claude Code](https://claude.com/claude-code)
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to