Alibaba Cloud Elasticsearch accelerates Paimon multi-modal data lake search
In this tutorial, you mount an Apache Paimon table in Alibaba Cloud Elasticsearch and perform vector search, scalar filtering, and field source retrieval on data lake tables based on a Paimon global index. By the end, you will have a searchable Elasticsearch index backed by Paimon data lake storage, accessible through standard Elasticsearch Query DSL and KNN queries.
How it works
The raw data in the Paimon table remains in Object Storage Service (OSS) or other lakehouse storage. After Elasticsearch reads the Paimon table snapshot and global index metadata, it mounts the data lake table index as a queryable Elasticsearch index. You can use Elasticsearch Query DSL, KNN queries, filter conditions, and field return capabilities to access data in the data lake without synchronizing all the raw data to an online Elasticsearch index.
The implementation works as follows:
Build the vector columns in the Paimon table into vector indexes queryable by Elasticsearch.
Write scalar columns such as business IDs, tags, URI paths, timestamps, and categories into the global index for filtering and retrieval.
Mount the Paimon table as an Elasticsearch index by using
/_paimon/mount.Use Elasticsearch KNN queries for vector recall combined with filter conditions such as
term,terms, andrange.After hitting TopK results, read fields from the Paimon table on demand to reduce redundant storage of raw data in the online index.
Use Spark SQL
vector_searchon the Paimon side to verify recall results for the same query vector.
A typical pipeline is as follows:
Business data / images / text / Embedding
-> Write to Paimon table
-> Build Paimon global index
-> Elasticsearch mounts the Paimon table
-> Query using ES Query DSL / KNN
-> Return scalar columns or source fields on demand after hitsScenarios
This feature is applicable to the following scenarios:
Multi-modal data such as images, text, and video frames are stored in Paimon/OSS, and you want to provide online similarity search services through Elasticsearch.
Your business requires both vector search and structured filtering, such as "similar images + category filtering" and "similar documents + tag filtering."
You do not want to synchronize all fields from the data lake table to Elasticsearch and only want to keep the search index and necessary return fields.
You need to use Elasticsearch query DSL, sorting, filtering, aggregation, Kibana, or RAG application ecosystem as the search entry for data in the data lake.
Prerequisites
The following table describes the overall requirements for using this feature. Step 1 and Step 2 in this tutorial demonstrate how to create a Paimon table from scratch and build an es-index global index by using the example tablepaimon_vector_demo. If you already have a Paimon table and an es-index global index that meet the requirements, you can start from Step 3.
| Type | Requirement |
| Elasticsearch instance | Version 9.4 or later. The image has built-in capabilities for Paimon mount, source, and global index reading. For OpenStore and mount verification details, see the notes below this table. |
| Paimon table | Table data has been written to OSS or lakehouse storage managed by Data Lake Formation (DLF), and the table contains vector columns that can be used for search. |
| Global index | An es-index type global index has been built for the Paimon table. |
| Permissions | The credentials used by the Elasticsearch instance or in the mount request must have permissions to read the Paimon table path, manifest, data files, and index files. |
| Spark environment | If you need to build indexes or use Spark SQL for verification, you need a Spark environment that can run the Paimon Spark extension and the corresponding Paimon/ES integration JAR. |
If the Elasticsearch image contains the OpenStore plug-in, configure the following static setting on all nodes and restart the cluster. Dynamic settings cannot be used as a substitute. If the plug-in is not included, no configuration is required.
yaml
cluster.apack.openstore.ruleout.enable: false
After you execute the mount operation, verify the physical index settings to confirm that index.store.type=paimon before performing query verification.
Limits
If you need to backfill fields through
_sourceor Paimon source, you must correctly enablerow-tracking.enabled=trueanddata-evolution.enabled=truein the table creation and write pipeline. Otherwise, data files may lackfirstRowId, which prevents the source stage from establishing the mapping fromrowIdto data files.Accompanying scalar columns that need to participate in filtering or be written to the global index must be declared together with the vector column in
index_columnin Spark SQL. Only placing a field inreturn_fieldsor_sourcedoes not guarantee that it can be used for filter queries such astermandrange.If the vector field uses
dot_productas the similarity metric, both the vectors written to the table and the query vectors typically need to be L2 normalized first to ensure that scores and rankings are comparable.The dimension limits, recall parameters, and performance characteristics of vector index algorithms such as HNSW and DiskBBQ are subject to the current Elasticsearch instance version.
If large-scale HNSW indexes use the
heapmount mode, you need to evaluate JVM heap, shard count, and vector graph size in advance. For feature verification, use themmaporhybridmode first.The query vector of Spark SQL
vector_searchmust be written as Float/Double array literals parseable by Spark, for examplearray(1.234E-2D, -3.456E-2D), to avoid being parsed as DECIMAL or expressions.
Step 1: Prepare the Paimon table
The following example uses an OSS filesystem catalog. For DLF catalog scenarios, replace the catalog and table paths with your actual configurations.
Configure the Spark catalog
Add the following configuration to your Spark session:
# Use the Paimon Spark extension
spark.sql.extensions=org.apache.paimon.spark.extensions.PaimonSparkSessionExtensions
# Configure the OSS filesystem catalog
spark.sql.catalog.oss=org.apache.paimon.spark.SparkCatalog
spark.sql.catalog.oss.metastore=filesystem
spark.sql.catalog.oss.warehouse=oss://<bucket>/<warehouse>
spark.sql.catalog.oss.fs.oss.endpoint=oss-<region>-internal.aliyuncs.com
spark.sql.catalog.oss.fs.oss.accessKeyId=${OSS_AK_ID}
spark.sql.catalog.oss.fs.oss.accessKeySecret=${OSS_AK_SECRET}
# Hadoop OSS read/write configuration
spark.hadoop.fs.oss.endpoint=oss-<region>-internal.aliyuncs.com
spark.hadoop.fs.oss.accessKeyId=${OSS_AK_ID}
spark.hadoop.fs.oss.accessKeySecret=${OSS_AK_SECRET}
# Prevent Paimon v1 function from preempting external function resolution
spark.paimon.v1Function.enabled=falseCreate the example table
The following example table contains:
id: Business primary key.label: Tag column for scalar filtering.content_uri: Object path that can be returned to the business.emb_norm: Normalized vector column.
Run the following SQL statement to create the table:
CREATE TABLE oss.default.paimon_vector_demo (
id BIGINT,
label BIGINT,
content_uri STRING,
emb ARRAY<DOUBLE>,
emb_norm ARRAY<FLOAT>
) TBLPROPERTIES (
'write-mode' = 'append-only',
'bucket' = '-1',
'file.format' = 'parquet',
'row-tracking.enabled' = 'true',
'data-evolution.enabled' = 'true',
'global-index.es-index.fields.emb_norm.algorithm' = 'hnsw',
'global-index.es-index.fields.emb_norm.dimension' = '768',
'global-index.es-index.fields.emb_norm.metric' = 'dot_product'
);If you use multi-modal data such as images and text, generate embeddings in the offline pipeline first and then write them to the Paimon table. When using dot_product, write the normalized vector column, for example emb_norm.
Write data to the table
Write source data to the example table and perform L2 normalization on the vector column. The followingsource_table is your own source data table (containing columns such as id, label, content_uri, and emb). Replace it with your actual source:
INSERT INTO oss.default.paimon_vector_demo
SELECT
id,
label,
content_uri,
emb,
vector_l2_normalize(emb) AS emb_norm
FROM source_table;vector_l2_normalize depends on the corresponding version of the Paimon Spark extension. If you remove the vector_l2_normalize SQL function as agreed, you must ensure the upstream generates normalized ARRAY<FLOAT>.
The following pure SQL approach can be used as an alternative:
INSERT INTO oss.default.paimon_vector_demo
SELECT
id,
label,
content_uri,
emb,
transform(
emb,
x -> CAST(x / sqrt(aggregate(emb, CAST(0 AS DOUBLE), (acc, v) -> acc + v * v)) AS FLOAT)
) AS emb_norm
FROM source_table;Verify the written data
After writing, check the row count, dimensions, and vector normalization results:
SELECT count(*) AS total_count
FROM oss.default.paimon_vector_demo;
SELECT
id,
size(emb_norm) AS dim,
aggregate(emb_norm, CAST(0 AS FLOAT), (acc, v) -> acc + v * v) AS sumsq
FROM oss.default.paimon_vector_demo
LIMIT 10;When normalization is correct, dim should equal the dimension set during table creation (for example, 768), and sumsq should be close to 1.0.
Step 2: Build the Paimon global index
Call the Paimon create_global_index stored procedure in EMR Serverless Spark SQL to build the es-index (in the example,oss.sys.create_global_index, where oss is the catalog name configured in Step 1).
Newer versions of EMR Serverless Spark will include built-in support for the latest Paimon with this capability. Existing older versions do not update the built-in Paimon. To use this capability on older versions, you must manually package compatible Paimon, paimon-eslib, and ESLib/Lucene dependencies, and exclude and replace the built-in Paimon through job configuration. You can use the official emr-3.5 compatible JAR, or package it yourself with the latest community version.
Currently, the index type of products automatically built by managed DLF is inconsistent with the es-index required by the Elasticsearch paimon-store, and cannot directly replace this step. The DLF catalog can still be used to manage Paimon table metadata.
Configure the Spark job and run the build command:
# Older versions of EMR Serverless Spark: exclude and replace the built-in Paimon
spark.emr.serverless.excludedModules paimon
spark.emr.serverless.user.defined.jars oss://<bucket>/jars/<paimon-es-index-bundle>.jar
spark.sql.extensions org.apache.paimon.spark.extensions.PaimonSparkSessionExtensions-- Use Spark SQL to build the es-index
CALL oss.sys.create_global_index(
table => 'default.paimon_vector_demo',
index_column => 'emb_norm,id,label,content_uri',
index_type => 'es-index',
options => 'global-index.row-count-per-shard=100000,global-index.es-index.fields.emb_norm.m=30,global-index.es-index.fields.emb_norm.ef_construction=360'
);The Spark configuration and SQL parameter descriptions are as follows.
| Parameter | Description |
spark.emr.serverless.excludedModules | For EMR Serverless Spark 5.3.1 and earlier versions, set this to paimon to exclude the built-in Paimon module and avoid class conflicts between old and new versions. |
spark.emr.serverless.user.defined.jars | The OSS address of custom JAR files. Must include Paimon compatible with the Spark version, paimon-eslib, and ESLib/Lucene dependencies. You can use the official emr-3.5 compatible JAR, or package it yourself with the latest community version. |
spark.sql.extensions | Must be configured as org.apache.paimon.spark.extensions.PaimonSparkSessionExtensions. |
table | The target Paimon table name. When calling the oss.sys procedure, specify default.paimon_vector_demo. |
index_column | The columns to be written to the same es-index. The primary vector column emb_norm must be placed first, followed by accompanying scalar columns for filtering or return. |
index_type | Fixed as es-index, which generates index files recognizable by the current Elasticsearch paimon-store. |
options | Pass in shard row count and HNSW m, ef_construction, and other build parameters. Call parameters take priority over table properties. |
| Rebuilding | The first call builds existing data. Subsequent calls incrementally build uncovered new data. To force a full rebuild, first call drop_global_index and then call create_global_index again. |
Verify the global index
After the build is complete, check the global index files through the Paimon system table:
SELECT
index_type,
index_field_name,
row_count,
file_name
FROM oss.default.`paimon_vector_demo$table_indexes`
WHERE index_type = 'es-index';The available columns of the paimon_vector_demo$table_indexes system table are file_name, file_size, bucket, index_type, and row_count. Use file_name to view index files, not file_path.
Step 3: Mount the Paimon table as an Elasticsearch index
Call the /_paimon/mount API to mount the Paimon table as an Elasticsearch index.
POST /_paimon/mount
{
"auth_type": "direct",
"table_path": "oss://<bucket>/<warehouse>/default.db/paimon_vector_demo",
"oss_endpoint": "oss-<region>-internal.aliyuncs.com",
"oss_bucket": "<bucket>",
"oss_access_key_id": "${OSS_AK_ID}",
"oss_access_key_secret": "${OSS_AK_SECRET}",
"index_name": "paimon_vector_demo_index",
"vector_field_name": "emb_norm",
"storage_mode": "mmap",
"source_enabled": true,
"return_fields": [
"id",
"label",
"content_uri"
],
"timeout": "600"
}If the call is successful, a response similar to the following is returned:
```json { "acknowledged": true, "alias": "paimon_vector_demo_index", "index": "paimon_vector_demo_index_2" }
<!-- @id="mountrespnote1" -->The mount operation creates an actual index (whose name is<!-- @id="mountrn01" --> `index_name` with a numeric suffix, for example<!-- @id="mountrn02" --> `paimon_vector_demo_index_2`), and points the alias<!-- @id="mountrn03" --> `index_name` to that index. Use the alias<!-- @id="mountrn04" --> `paimon_vector_demo_index` for subsequent queries. In Step 4,<!-- @id="mountrn05" --> `_cat/indices` displays the actual index name with the numeric suffix.
The parameter descriptions are as follows. Among these, `table_path`, `oss_endpoint`, `oss_bucket`, `oss_access_key_id`, and `oss_access_key_secret` are required parameters. The API returns a `Required fields` error if any of them is missing.
| Parameter | Required | Description |
| --- | --- | --- |
| `auth_type` | No | Authentication method. The example uses `direct`, which passes OSS access credentials in the request. For production environments, use more secure managed credentials or RAM role methods. |
| `table_path` | Yes | The Paimon table path. |
| `oss_endpoint` | Yes | The OSS endpoint. Use an internal endpoint in the same region as the Elasticsearch instance. |
| `oss_bucket` | Yes | The OSS bucket where the table is located. |
| `oss_access_key_id` | Yes | The AccessKey ID for accessing OSS. |
| `oss_access_key_secret` | Yes | The AccessKey secret for accessing OSS. |
| `index_name` | No | The name of the Elasticsearch index generated after mounting. |
| `vector_field_name` | No | The name of the field used for vector search. |
| `storage_mode` | No | The index read mode. Valid values: `mmap` (relies on OS page cache, suitable for verification and low memory usage), `hybrid` (balances memory and disk access, suitable for medium to large-scale indexes), and `heap` (best performance but consumes more JVM heap). Available values are subject to the current version. For a detailed comparison, see [How to choose between mmap, hybrid, and heap?](#how-to-choose-between-mmap-hybrid-and-heap) in the FAQ section. |
| `source_enabled` | No | Whether to enable Paimon source retrieval. |
| `return_fields` | No | The fields to be returned from the Paimon table. |
| `timeout` | No | The mount timeout period, in seconds. |
(Recommended) Use `mmap` or `hybrid` for initial verification. If you need to use the `heap` mode for performance benchmarking, evaluate the index scale and JVM heap first.
## Step 4: Verify the mount result
After the mount is complete, check the index status, document count, mapping, and settings:
```bash
GET /_cat/indices/paimon_vector_demo_index?v
GET /paimon_vector_demo_index/_count
GET /paimon_vector_demo_index/_mapping
GET /paimon_vector_demo_index/_settings?flat_settings=trueFocus on the following items:
The index status is
greenor as expected._countis consistent with the Paimon table row count.The vector field mapping has the correct dimension and similarity configurations, for example
dims=768andsimilarity=dot_product.Scalar fields
id,label, andcontent_uriappear in the mapping.Settings such as Paimon table path, snapshot ID, and storage mode are as expected.
Step 5: Query by using Elasticsearch
Pure vector query
Run the following query to perform a pure KNN vector search:
POST /paimon_vector_demo_index/_search
{
"size": 10,
"_source": [
"id",
"label",
"content_uri"
],
"knn": {
"field": "emb_norm",
"query_vector": [/* 768-dimensional normalized query vector */],
"k": 10,
"num_candidates": 100
}
}The scalar columns of the mounted index (id,label,content_uri) are returned through_source source retrieval (corresponding toreturn_fields during mount). Use_source to specify the fields to return. These columns are not Lucene stored fields or doc_values. Using"_source": false withfields will not retrieve values (hits will only contain_index and_score). If you want to return the vector column as well, set_source totrue.
Vector + scalar filtering
Run the following query to combine KNN search with scalar filtering:
POST /paimon_vector_demo_index/_search
{
"size": 10,
"_source": [
"id",
"label",
"content_uri"
],
"knn": {
"field": "emb_norm",
"query_vector": [/* 768-dimensional normalized query vector */],
"k": 10,
"num_candidates": 100,
"filter": {
"term": {
"label": 1
}
}
}
}Use terms or range filtering
Replace the knn.filter in the previous request with the following content.
Terms filter example:
{
"terms": {
"label": [1, 2, 3]
}
}Range filter example:
{
"range": {
"id": {
"lt": 100000
}
}
}Return Paimon table fields
If source_enabled is already enabled, you can return fields from the Paimon table:
POST /paimon_vector_demo_index/_search
{
"size": 10,
"_source": true,
"knn": {
"field": "emb_norm",
"query_vector": [/* 768-dimensional normalized query vector */],
"k": 10,
"num_candidates": 100
}
}If _source is returning empty, check the following:
Whether
row-tracking.enabled=trueanddata-evolution.enabled=trueare enabled during table creation.Whether the write path correctly assigns
firstRowIdto data files.Whether
return_fieldsincludes the fields to be returned.Whether the Elasticsearch instance has loaded the Paimon source capability.
Step 6: Verify query results by using Spark SQL
You can also use the same query vector in Spark SQL to verify the Paimon global index query results.
Run the following Spark SQL query:
USE oss.default;
SELECT
id,
label,
content_uri,
__paimon_search_score AS score
FROM vector_search(
'oss.default.paimon_vector_demo',
'emb_norm',
array(/* 768 double literals, e.g. 1.234567E-2D */),
10,
map('hnsw.num_candidates', '100')
)
ORDER BY score DESC
LIMIT 10;Vector + scalar filtering example:
SELECT
id,
label,
content_uri,
__paimon_search_score AS score
FROM vector_search(
'oss.default.paimon_vector_demo',
'emb_norm',
array(/* same normalized query vector */),
10,
map('hnsw.num_candidates', '100')
)
WHERE label = 1
ORDER BY score DESC
LIMIT 10;Use EXPLAIN FORMATTED to check whether the vector index is used:
EXPLAIN FORMATTED
SELECT
id,
label,
__paimon_search_score AS score
FROM vector_search(
'oss.default.paimon_vector_demo',
'emb_norm',
array(/* same normalized query vector */),
10,
map('hnsw.num_candidates', '100')
)
WHERE label = 1
ORDER BY score DESC;The expected plan should show vector search, global index scan, or VectorSearch related information.
FAQ
Why can KNN queries return results, but scalar filtering cannot find anything?
Check whether the filter field has been added to index_column in Spark SQL. For example, to filter by label, you must include index_column => 'emb_norm,id,label,content_uri' during build time. If you only configure label in return_fields, the field can be used as a source return field but may not be available as an index filter field.
Why is _source returning empty?
A common cause is that the Paimon data files lack the mapping information from rowId to files. Verify the following:
row-tracking.enabled=trueis enabled during table creation.data-evolution.enabled=trueis enabled during table creation.The write pipeline uses a path that supports row tracking.
Old tables or old data files may need to be rewritten or rebuilt.
Should the query vector use the original vector or the normalized vector?
If the index field is the normalized vector column emb_norm and the similarity metric is dot_product, the query vector should also be generated by the same model and normalized. Otherwise, scores may not be comparable, rankings may not meet expectations, or the query may be rejected by vector dimension or value validation.
How to choose between mmap, hybrid, and heap?
mmap — Suitable for feature verification and low memory usage scenarios. Relies on the operating system page cache.
hybrid — A balance between memory and disk access. Suitable for general verification of medium to large-scale indexes.
heap — Suitable for performance benchmarking but consumes more JVM heap. Before using it, evaluate memory requirements based on vector count, vector dimensions, shard count, and replica count.