Full-text search global index for Apache Paimon, backed by the native paimon-full-text-index engine.
This module provides full-text search for both Data Evolution (append) tables through the Global
Index framework and compaction-visible primary-key tables through file-aligned index archives. It
contains only the Paimon integration layer. Native full-text access, JNI, FFI, index archive
handling, and query parsing are provided by the separate paimon-full-text-index dependency.
┌─────────────────────────────────────────────────────┐
│ Paimon Engine │
│ (FullTextSearchBuilder / FullTextScan / FullTextRead)│
└──────────────────────┬──────────────────────────────┘
│ GlobalIndexer SPI
┌──────────────────────▼──────────────────────────────┐
│ paimon-full-text │
│ GlobalIndexer SPI / Paimon stream adapters │
└──────────────────────┬──────────────────────────────┘
│ Java API
┌──────────────────────▼──────────────────────────────┐
│ paimon-full-text-index │
│ JNI / FFI / native full-text index writer and reader │
└─────────────────────────────────────────────────────┘
Native full-text index uses a fixed two-field schema:
| Field | Native Type | Description |
|---|---|---|
row_id |
u64 (stored, indexed) | Paimon's global row ID, used to map search results back to table rows |
text |
TEXT (tokenized, indexed) | The text content from the indexed column |
The writer produces a single archive file that bundles all native full-text segment files into one sequential stream. This format is designed to be stored on any Paimon-supported file system (HDFS, S3, OSS, etc.) and read back without extracting to local disk.
All integers are big-endian.
┌─────────────────────────────────────────────────┐
│ File Count (4 bytes, int32) │
├─────────────────────────────────────────────────┤
│ File Entry 1 │
│ ┌─────────────────────────────────────────────┐│
│ │ Name Length (4 bytes, int32) ││
│ │ Name (N bytes, UTF-8) ││
│ │ Data Length (8 bytes, int64) ││
│ │ Data (M bytes, raw) ││
│ └─────────────────────────────────────────────┘│
├─────────────────────────────────────────────────┤
│ File Entry 2 │
│ ┌─────────────────────────────────────────────┐│
│ │ Name Length (4 bytes, int32) ││
│ │ Name (N bytes, UTF-8) ││
│ │ Data Length (8 bytes, int64) ││
│ │ Data (M bytes, raw) ││
│ └─────────────────────────────────────────────┘│
├─────────────────────────────────────────────────┤
│ ... │
└─────────────────────────────────────────────────┘
| Field | Size | Type | Description |
|---|---|---|---|
| File Count | 4 bytes | int32 | Number of files in the archive |
| Name Length | 4 bytes | int32 | Byte length of the file name |
| Name | N bytes | UTF-8 | native segment file name (e.g. meta.json, *.term, *.pos, *.store) |
| Data Length | 8 bytes | int64 | Byte length of the file data |
| Data | M bytes | raw | Raw file content |
NativeFullTextGlobalIndexWriterreceives text values viawrite(Object), one per row.- Each non-null text is passed to
paimon-full-text-index'sFullTextIndexWriter, whererowIdis a 0-based sequential counter. - On
finish(),paimon-full-text-indexcommits the native full-text index and packs it into the archive format above. - The archive is written as a single file to Paimon's global index file system.
- Temporary native resources are owned and cleaned up by
paimon-full-text-index.
NativeFullTextGlobalIndexReaderopens the archive file as aSeekableInputStream.- The stream is passed to
paimon-full-text-index'sFullTextIndexReaderthrough a positional read adapter. - The native reader reads file data on demand through
preadcallbacks backed by Paimon's file IO. No temp files are created in Paimon. - Search queries are converted to the native single-column query JSON and executed with BM25 scoring, returning
(rowId, score)pairs.
Primary-key full-text indexing uses the fixed full-text SPI automatically. Configure the text
column directly on the table; no implementation selector is needed.
CREATE TABLE articles (
id BIGINT,
content STRING,
PRIMARY KEY (id) NOT ENFORCED
) WITH (
'bucket' = '16',
'deletion-vectors.enabled' = 'true',
'pk-full-text.index.columns' = 'content',
'fields.content.pk-full-text.index.options' = '{"tokenizer":"jieba"}'
);Paimon creates one native archive for the complete set of eligible COMPACT data files in each
Level-1-or-higher data level. Data compaction replaces the affected level archive atomically. One
archive can cover multiple ordered source files; its row IDs concatenate their physical row
positions.
Primary-key full-text search currently supports only full-text-index.search-mode=fast. Level-0 and
other uncovered files are not searched; their rows become searchable after compaction publishes
an eligible data file and persistent archive. Search applies each source file's deletion vector,
preserves native relevance scores, and selects a global Top-K. Only Hybrid search rewrites route
scores through its configured rrf, weighted_score, or mrr ranker.
CALL sys.full_text_search(
`table` => 'default.articles',
`column` => 'content',
query => '{"match":{"query":"paimon lake"}}',
top_k => 10,
projection => 'id,content,__paimon_search_score'
);See Primary-Key Indexes for requirements, Spark and Java examples, Hybrid search, and current limitations.
CALL sys.create_global_index(
table => 'db.my_table',
index_column => 'content',
index_type => 'full-text'
);By default, the native full-text index uses its built-in tokenizer. For Chinese or other languages where users often
search by short character fragments, build the index with the ngram tokenizer:
CALL sys.create_global_index(
table => 'db.my_table',
index_column => 'content',
index_type => 'full-text',
options => 'full-text.tokenizer=ngram,full-text.ngram.min-gram=2,full-text.ngram.max-gram=2'
);For Chinese word segmentation, build the index with the jieba tokenizer:
CALL sys.create_global_index(
table => 'db.my_table',
index_column => 'content',
index_type => 'full-text',
options => 'full-text.tokenizer=jieba'
);Available tokenizer options:
| Option | Default | Description |
|---|---|---|
full-text.tokenizer |
default |
Tokenizer used by the full-text index. Supported values: default, simple, whitespace, raw, ngram, jieba. |
full-text.ngram.min-gram |
3 |
Minimum gram length for the ngram tokenizer. |
full-text.ngram.max-gram |
3 |
Maximum gram length for the ngram tokenizer. |
full-text.ngram.prefix-only |
false |
Whether the ngram tokenizer only emits prefix ngrams. |
full-text.jieba.search-mode |
true |
Whether the jieba tokenizer uses search mode. |
full-text.jieba.ordinal-position |
true |
Whether the jieba tokenizer uses ordinal positions. |
full-text.lower-case |
true |
Whether configurable tokenizers lowercase emitted tokens. |
full-text.max-token-length |
40 |
Maximum token length kept by configurable tokenizers. |
full-text.ascii-folding |
true |
Whether to normalize non-ASCII Latin characters to ASCII. |
full-text.stem |
true |
Whether to apply stemming to emitted tokens. |
full-text.language |
english |
Language used by stemming and built-in stop word filters. |
full-text.remove-stop-words |
true |
Whether to remove built-in stop words for the configured language. |
full-text.stop-words |
empty | Semicolon-separated custom stop words to remove. Requires full-text.remove-stop-words=true. |
full-text.with-position |
true |
Whether to store term positions for phrase queries. |
Tokenizer settings are persisted in each global index file's metadata. Readers use that metadata
when reopening an index, so changing table/procedure options later does not make existing index
files use a different analyzer.
Custom analysis is provided by composing the supported tokenizer and filter options above; Paimon
does not load arbitrary Rust tokenizer plugins from configuration.
PyPaimon can query jieba indexes when the Python jieba package is installed.
SELECT * FROM full_text_search(
'my_table',
'content',
'{"match":{"query":"search query"}}',
10
);Table table = catalog.getTable(identifier);
GlobalIndexResult result = table.newFullTextSearchBuilder()
.withQuery("content", "{\"match\":{\"query\":\"search query\"}}")
.withLimit(10)
.executeLocal();
ReadBuilder readBuilder = table.newReadBuilder();
TableScan.Plan plan = readBuilder.newScan()
.withGlobalIndexResult(result).plan();
try (RecordReader<InternalRow> reader = readBuilder.newRead().createReader(plan)) {
reader.forEachRemaining(row -> System.out.println(row));
}The index type full-text is registered via Java SPI:
META-INF/services/org.apache.paimon.globalindex.GlobalIndexerFactory
→ org.apache.paimon.fulltext.index.NativeFullTextGlobalIndexerFactory