Skip to content

[Feature] Support table-level full-text search for data-evolution tables #404

Closed

Description

@SteNicholas

Search before asking

  • I searched in the issues and found nothing similar.

Motivation

Sub-issue of #399 (step 4: table-level full-text search).

Paimon C++ only exposes the reader primitive. To run a full-text search, a caller has to:

  1. call GlobalIndexScan::CreateReaders;
  2. call VisitFullTextSearch on the readers;
  3. pass the result to ScanContextBuilder::SetGlobalIndexResult.

Along the way the caller also has to choose index types and shard ranges, and there is no serializable plan for running scan and read in different processes. Java offers a table-level API instead:

GlobalIndexResult result =
        table.newFullTextSearchBuilder()
                .withQuery("content", "{\"match\":{\"query\":\"paimon lake\"}}")
                .withLimit(10)
                .executeLocal();
TableScan.Plan plan = table.newReadBuilder().newScan().withGlobalIndexResult(result).plan();

The Java components live in paimon-core org.apache.paimon.table.source, plus one method in paimon-common:

Java class / method What it provides
FullTextSearchBuilder / FullTextSearchBuilderImpl withPartitionFilter, withFilter (see #405), withLimit, withQuery(fieldName, query), newFullTextScan(), newFullTextRead(), executeLocal(). A package-private withSnapshot is used by hybrid search. The query column must exist, and newFullTextRead requires limit > 0.
FullTextScan#scan() Returns Plan { splits(), snapshot() }; implemented by DataEvolutionFullTextScan.
FullTextRead#read(plan | splits) Returns a GlobalIndexResult; implemented by DataEvolutionFullTextRead.
FullTextSearchSplit, IndexFullTextSearchSplit The split carries the column, the physical rowRangeStart/rowRangeEnd, the assigned searchRowRanges, the full-text index files and the scalar index files, with versioned serialization.
GlobalIndexerFactory#supportsFullTextSearch() (paimon-common) Defaults to false and is true for full-text (apache/paimon#8308). The scan uses it to decide which index files can serve full-text search.

Solution

  1. API. Add C++ equivalents of the builder, scan, read and splits, following the existing builder conventions (ScanContextBuilder / ReadContextBuilder). Splits must be serializable so scan and read can run in different processes. The read result plugs into ScanContextBuilder::SetGlobalIndexResult, and scores stay readable through _INDEX_SCORE.
  2. Capability. Add SupportsFullTextSearch() to GlobalIndexerFactory or GlobalIndexer, returning true for full-text. Decide whether lucene-fts returns true as well.
  3. Scan, following DataEvolutionFullTextScan:
    • Pin the snapshot: the latest one, or the time-travel target.
    • Keep index manifest entries that have GlobalIndexMeta and pass the partition filter.
    • Treat a file as a full-text input when the column is its index_field_id or one of its extra_field_ids, and its factory supports full-text search.
    • Select ranges across overlapping and multi-field indexes ([global-index] Fix full-text range selection and scalar-filter coverage for multi-field indexes paimon#8547):
      1. Group candidates by column → physical range → index identity, where the identity is type|index_field_id|extra_field_ids.
      2. Sweep the elementary intervals and give each one to the best active candidate. A dedicated index comes first, then order by identity, from and to.
      3. Merge adjacent intervals.
      4. Emit one IndexFullTextSearchSplit per selection. It keeps the physical range, which is needed for the row-id offset, and the assigned searchRowRanges.
  4. Read, following DataEvolutionFullTextRead:
  5. Tests. Cover:
    • multiple shards
    • overlapping index ranges
    • a text column that is an extra field of another index
    • partition filters
    • snapshot pinning
    • deletion-vector tables
    • a split serialization round trip

Anything else?

Are you willing to submit a PR?

  • I'm willing to submit a PR!

Activity

  1. self-assigned this
    on Oct 29, 2026
  2. SteNicholas commented on Nov 2, 2026

    @SteNicholas
    MemberAuthor

    Follow-up after #461: since it was merged, the tsan-x86_64 job of Build and Test runs past its 120-minute timeout (run 41000500442). paimon-global-index-test grew from about 1400s to about 3500s on tsan-x86_64, and from about 1000s to about 3000s on tsan-aarch64.

    Root cause: every LuceneGlobalIndexReader and LuceneGlobalIndexWriter constructed its own cppjieba::Jieba, reloading about 11.6MB of Jieba dictionaries each time. A table-level full-text search creates one reader per shard, so every query paid this cost once per shard, and the table-level full-text search tests added in #461 run many such queries. The cost is not limited to tests: every table-level search on a lucene-fts index pays it in production too.

    Fix: #486 loads each dictionary directory once per process and shares a const segmenter with all Lucene index readers and writers. With it, paimon-global-index-test takes about 156s on tsan-x86_64 and about 103s on tsan-aarch64, and the tsan-x86_64 job passes in about an hour.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions