Skip to main content

FM Index

The FM index is an exact byte-oriented substring index for CHAR, VARCHAR, and STRING columns. It supports CONTAINS needles of any byte length without a configured gram size. Null values do not match; empty needles follow the normal Paimon predicate semantics.

The writer divides source rows into independent partitions and appends them to one checksummed container file. Each partition stores a compressed wavelet matrix, sampled suffix-array values, row boundaries, and null rows. It does not duplicate the source values. Reads demand-load bounded blocks instead of downloading the complete container. If locating matches would cost too much, the index declines the predicate so the normal data path can scan the source values and preserve exact results.

Create a Global FM Index

Create the index on a Data Evolution table with row tracking enabled:

CALL sys.create_global_index(
table => 'db.documents',
index_column => 'content',
index_type => 'fmindex',
options => 'fm-index.partition-row-count=100000'
);

Drop it with the same index type:

CALL sys.drop_global_index(
table => 'db.documents',
index_column => 'content',
index_type => 'fmindex'
);

See Global Index for row-tracking requirements, lifecycle, and partial-coverage semantics.

Query

After a connector converts a literal substring expression to Paimon's CONTAINS predicate, the normal batch scan uses the FM index automatically. For example, Spark pushes down:

SELECT id, content
FROM documents
WHERE content LIKE '%needle%';

Results are exact inside every indexed row range. A table-wide global-index query can still omit matches in row ranges that have never been indexed; rebuild the index for newly appended ranges or use the visibility callback described on the Global Index page.

For a primary-key table, configure pk-fm.index.columns instead. Primary-key FM indexes follow data compaction and scan uncovered files through the ordinary data path, so partial coverage does not make CONTAINS results incomplete. See Primary-Key Indexes.

Options

Global options can be passed to create_global_index. For a primary-key index, place the same keys in fields.<column>.pk-fm.index.options; the fm-index. prefix may be omitted inside that JSON object.

OptionDefaultDescription
fm-index.partition-size16 mbMaximum encoded text buffered by one independently readable partition.
fm-index.partition-row-count100000Maximum source rows in one partition.
fm-index.sa-sample-rate32Suffix-array sample rate. A smaller power of two speeds locate at the cost of a larger index.
fm-index.compressionlz4Compression codec for independently checksummed FM blocks.
fm-index.compression-level1Compression level for codecs which support levels.
fm-index.read-cache-size64 mbMaximum decoded rank and sample block cache per indexer.
fm-index.demand-page-size512 kbTarget contiguous range size when demand-loading blocks.
fm-index.locate-cost-ratio0.001Maximum estimated suffix-array locate work relative to source text bytes before declining index evaluation.

fm-index.partition-size and fm-index.partition-row-count bound build memory and the unit of independent reads. Smaller partitions reduce peak construction memory but increase the number of partitions searched per query. All partitions produced by one writer are stored in one physical index file and represented by one index manifest entry. A lower fm-index.sa-sample-rate accelerates locating matched rows but stores more suffix-array samples.

Limitations

  • The index is single-column and accepts only character string types.
  • Matching is byte-oriented and case-sensitive; it does not apply a tokenizer, collation, or Unicode normalization.
  • Global FM indexes inherit the partial-coverage behavior of table-wide global indexes.
  • Primary-key FM indexes are built from eligible compact output, not directly from Level-0 appends.