This is an automated email from the ASF dual-hosted git repository.

JingsongLi pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/paimon.git


The following commit(s) were added to refs/heads/master by this push:
     new 4276e0cdd7 [feature] Support per-field vector index options (#8239)
4276e0cdd7 is described below

commit 4276e0cdd7ecd9bf997a1f92e54c00266ee29e23
Author: jerry <[email protected]>
AuthorDate: Tue Jun 16 08:59:12 2026 +0800

    [feature] Support per-field vector index options (#8239)
    
    Support per-field vector index options with
    `fields.<field-name>.<option>`.
    
    This allows tables with multiple vector columns using the same index
    type to configure different dimensions or index parameters for each
    column. Field-level options do not include the index-type prefix, and
    they override index-type-level defaults such as `<index-type>.<option>`
    for the matching stored table column name.
---
 docs/docs/multimodal-table/global-index.mdx        |  32 ++++++
 .../vector/index/VectorGlobalIndexerFactory.java   |  25 ++++-
 .../paimon/vector/index/VectorGlobalIndexTest.java |   5 +-
 .../index/VectorGlobalIndexerFactoryTest.java      | 111 ++++++++++++++++++++-
 4 files changed, 165 insertions(+), 8 deletions(-)

diff --git a/docs/docs/multimodal-table/global-index.mdx 
b/docs/docs/multimodal-table/global-index.mdx
index 747ccbaa9a..a7698f124a 100644
--- a/docs/docs/multimodal-table/global-index.mdx
+++ b/docs/docs/multimodal-table/global-index.mdx
@@ -127,6 +127,38 @@ Supported vector index options:
 | `<index-type>.hnsw.ef-construction` | `150` | HNSW construction search width 
for `ivf-hnsw-flat` and `ivf-hnsw-sq`. |
 | `<index-type>.hnsw.max-level` | `7` | Maximum HNSW level for `ivf-hnsw-flat` 
and `ivf-hnsw-sq`. |
 
+**Per-Field Options**
+
+The options above can also be set at the table level (in `TBLPROPERTIES`), 
where they are shared
+by every vector column of the same index type. When a table has multiple 
vector columns, you can
+scope an option to a single column with `fields.<field-name>.<option>`. The 
field-level
+form takes precedence over the column-agnostic `<index-type>.<option>` for 
that column. Use the
+stored table column name exactly as `<field-name>`. Field-level vector options 
do not include the
+index-type prefix; for example, use `fields.image_embedding.nlist` to override 
the shared
+`ivf-pq.nlist` option for `image_embedding`:
+
+```sql
+CREATE TABLE my_table (
+    id INT,
+    title_embedding ARRAY<FLOAT>,
+    image_embedding ARRAY<FLOAT>
+) TBLPROPERTIES (
+    'bucket' = '-1',
+    'row-tracking.enabled' = 'true',
+    'data-evolution.enabled' = 'true',
+    'global-index.enabled' = 'true',
+    -- per-column dimensions
+    'fields.title_embedding.dimension' = '768',
+    'fields.image_embedding.dimension' = '512',
+    -- shared by every ivf-pq column, overridden only for 'image_embedding'
+    'ivf-pq.nlist' = '256',
+    'fields.image_embedding.nlist' = '512'
+);
+```
+
+With the properties above, `title_embedding` is indexed with `nlist=256` while 
`image_embedding`
+uses `nlist=512`.
+
 **Vector Search**
 
 Search-time options are passed with each vector search request:
diff --git 
a/paimon-vector/paimon-vector-index/src/main/java/org/apache/paimon/vector/index/VectorGlobalIndexerFactory.java
 
b/paimon-vector/paimon-vector-index/src/main/java/org/apache/paimon/vector/index/VectorGlobalIndexerFactory.java
index 354c967b56..9367723114 100644
--- 
a/paimon-vector/paimon-vector-index/src/main/java/org/apache/paimon/vector/index/VectorGlobalIndexerFactory.java
+++ 
b/paimon-vector/paimon-vector-index/src/main/java/org/apache/paimon/vector/index/VectorGlobalIndexerFactory.java
@@ -37,14 +37,20 @@ public abstract class VectorGlobalIndexerFactory implements 
GlobalIndexerFactory
     public GlobalIndexer create(DataField field, Options options) {
         String identifier = identifier();
         return new VectorGlobalIndexer(
-                field.type(), nativeOptions(field.type(), options, 
identifier), identifier);
+                field.type(),
+                nativeOptions(field.type(), options, identifier, field.name()),
+                identifier);
     }
 
     static Map<String, String> nativeOptions(
-            DataType fieldType, Options tableOptions, String identifier) {
+            DataType fieldType, Options tableOptions, String identifier, 
String fieldName) {
         Map<String, String> nativeOptions = new LinkedHashMap<>();
         String optionPrefix = identifier + ".";
-        for (Map.Entry<String, String> entry : 
tableOptions.toMap().entrySet()) {
+        String fieldPrefix = "fields." + fieldName + ".";
+        Map<String, String> tableOptionsMap = tableOptions.toMap();
+
+        // First collect index-type level options, e.g. <index-type>.xxx.
+        for (Map.Entry<String, String> entry : tableOptionsMap.entrySet()) {
             String optionKey = entry.getKey();
             if (optionKey.startsWith(optionPrefix)) {
                 String nativeKey = 
nativeOptionKey(optionKey.substring(optionPrefix.length()));
@@ -53,6 +59,19 @@ public abstract class VectorGlobalIndexerFactory implements 
GlobalIndexerFactory
                 }
             }
         }
+
+        // Then collect field level options, e.g. fields.<field-name>.xxx, 
which take precedence
+        // over the index-type level options for this field.
+        for (Map.Entry<String, String> entry : tableOptionsMap.entrySet()) {
+            String optionKey = entry.getKey();
+            if (optionKey.startsWith(fieldPrefix)) {
+                String nativeKey = 
nativeOptionKey(optionKey.substring(fieldPrefix.length()));
+                if (nativeKey != null) {
+                    nativeOptions.put(nativeKey, entry.getValue());
+                }
+            }
+        }
+
         nativeOptions.put("index.type", identifier.replace('-', '_'));
         nativeOptions.put(
                 "dimension", String.valueOf(dimension(fieldType, 
nativeOptions, identifier)));
diff --git 
a/paimon-vector/paimon-vector-index/src/test/java/org/apache/paimon/vector/index/VectorGlobalIndexTest.java
 
b/paimon-vector/paimon-vector-index/src/test/java/org/apache/paimon/vector/index/VectorGlobalIndexTest.java
index 73f8a4b28f..b10c843ef2 100644
--- 
a/paimon-vector/paimon-vector-index/src/test/java/org/apache/paimon/vector/index/VectorGlobalIndexTest.java
+++ 
b/paimon-vector/paimon-vector-index/src/test/java/org/apache/paimon/vector/index/VectorGlobalIndexTest.java
@@ -347,7 +347,7 @@ public class VectorGlobalIndexTest {
                 new VectorGlobalIndexer(
                         vectorType,
                         VectorGlobalIndexerFactory.nativeOptions(
-                                vectorType, options, IVF_PQ_IDENTIFIER),
+                                vectorType, options, IVF_PQ_IDENTIFIER, 
fieldName),
                         IVF_PQ_IDENTIFIER);
 
         GlobalIndexFileWriter fileWriter = createFileWriter(indexPath);
@@ -373,7 +373,8 @@ public class VectorGlobalIndexTest {
         return new VectorGlobalIndexWriter(
                 fileWriter,
                 fieldType,
-                VectorGlobalIndexerFactory.nativeOptions(fieldType, options, 
IVF_PQ_IDENTIFIER),
+                VectorGlobalIndexerFactory.nativeOptions(
+                        fieldType, options, IVF_PQ_IDENTIFIER, fieldName),
                 IVF_PQ_IDENTIFIER);
     }
 
diff --git 
a/paimon-vector/paimon-vector-index/src/test/java/org/apache/paimon/vector/index/VectorGlobalIndexerFactoryTest.java
 
b/paimon-vector/paimon-vector-index/src/test/java/org/apache/paimon/vector/index/VectorGlobalIndexerFactoryTest.java
index 1caf082f2c..b0f33d7706 100644
--- 
a/paimon-vector/paimon-vector-index/src/test/java/org/apache/paimon/vector/index/VectorGlobalIndexerFactoryTest.java
+++ 
b/paimon-vector/paimon-vector-index/src/test/java/org/apache/paimon/vector/index/VectorGlobalIndexerFactoryTest.java
@@ -70,7 +70,8 @@ public class VectorGlobalIndexerFactoryTest {
                 VectorGlobalIndexerFactory.nativeOptions(
                         new ArrayType(new FloatType()),
                         options,
-                        IvfFlatVectorGlobalIndexerFactory.IDENTIFIER);
+                        IvfFlatVectorGlobalIndexerFactory.IDENTIFIER,
+                        "vec");
 
         assertThat(nativeOptions)
                 .containsEntry("index.type", "ivf_flat")
@@ -92,7 +93,8 @@ public class VectorGlobalIndexerFactoryTest {
                 VectorGlobalIndexerFactory.nativeOptions(
                         new VectorType(8, new FloatType()),
                         options,
-                        IvfFlatVectorGlobalIndexerFactory.IDENTIFIER);
+                        IvfFlatVectorGlobalIndexerFactory.IDENTIFIER,
+                        "vec");
 
         assertThat(nativeOptions).containsEntry("dimension", "8");
     }
@@ -107,9 +109,112 @@ public class VectorGlobalIndexerFactoryTest {
                                 VectorGlobalIndexerFactory.nativeOptions(
                                         new ArrayType(new FloatType()),
                                         options,
-                                        
IvfFlatVectorGlobalIndexerFactory.IDENTIFIER))
+                                        
IvfFlatVectorGlobalIndexerFactory.IDENTIFIER,
+                                        "vec"))
                 .isInstanceOf(IllegalArgumentException.class)
                 .hasMessageContaining("ivf-flat.dimension")
                 .hasMessageContaining("positive integer");
     }
+
+    @Test
+    public void testFieldLevelOptionsOverrideIndexTypeOptions() {
+        Options options = new Options();
+        options.setString("ivf-flat.dimension", "32");
+        options.setString("ivf-flat.nlist", "128");
+        options.setString("fields.vec.nlist", "256");
+
+        Map<String, String> nativeOptions =
+                VectorGlobalIndexerFactory.nativeOptions(
+                        new ArrayType(new FloatType()),
+                        options,
+                        IvfFlatVectorGlobalIndexerFactory.IDENTIFIER,
+                        "vec");
+
+        assertThat(nativeOptions)
+                .containsEntry("dimension", "32")
+                .containsEntry("nlist", "256")
+                .doesNotContainEntry("nlist", "128");
+    }
+
+    @Test
+    public void testFieldLevelDimensionOverridesIndexTypeDimension() {
+        Options options = new Options();
+        options.setString("ivf-flat.dimension", "32");
+        options.setString("fields.vec.dimension", "64");
+
+        Map<String, String> nativeOptions =
+                VectorGlobalIndexerFactory.nativeOptions(
+                        new ArrayType(new FloatType()),
+                        options,
+                        IvfFlatVectorGlobalIndexerFactory.IDENTIFIER,
+                        "vec");
+
+        assertThat(nativeOptions).containsEntry("dimension", "64");
+    }
+
+    @Test
+    public void testFieldLevelOptionsOnlyApplyToMatchingField() {
+        Options options = new Options();
+        options.setString("ivf-flat.nlist", "128");
+        options.setString("fields.vec.nlist", "256");
+
+        Map<String, String> nativeOptions =
+                VectorGlobalIndexerFactory.nativeOptions(
+                        new ArrayType(new FloatType()),
+                        options,
+                        IvfFlatVectorGlobalIndexerFactory.IDENTIFIER,
+                        "other");
+
+        assertThat(nativeOptions).containsEntry("nlist", "128");
+    }
+
+    @Test
+    public void testFieldLevelOptionsRequireExactFieldName() {
+        Options options = new Options();
+        options.setString("ivf-flat.nlist", "128");
+        options.setString("fields.vec_extra.nlist", "512");
+
+        Map<String, String> nativeOptions =
+                VectorGlobalIndexerFactory.nativeOptions(
+                        new ArrayType(new FloatType()),
+                        options,
+                        IvfFlatVectorGlobalIndexerFactory.IDENTIFIER,
+                        "vec");
+
+        assertThat(nativeOptions).containsEntry("nlist", "128");
+    }
+
+    @Test
+    public void testFieldLevelOptionsWithoutIndexTypeOption() {
+        Options options = new Options();
+        options.setString("fields.vec.distance.metric", "cosine");
+
+        Map<String, String> nativeOptions =
+                VectorGlobalIndexerFactory.nativeOptions(
+                        new ArrayType(new FloatType()),
+                        options,
+                        IvfFlatVectorGlobalIndexerFactory.IDENTIFIER,
+                        "vec");
+
+        assertThat(nativeOptions).containsEntry("metric", "cosine");
+    }
+
+    @Test
+    public void testFieldLevelVectorOptionsCoexistWithCoreFieldOptions() {
+        Options options = new Options();
+        options.setString("ivf-flat.nlist", "128");
+        options.setString("fields.vec.nlist", "256");
+        options.setString("fields.vec.aggregate-function", "sum");
+
+        Map<String, String> nativeOptions =
+                VectorGlobalIndexerFactory.nativeOptions(
+                        new ArrayType(new FloatType()),
+                        options,
+                        IvfFlatVectorGlobalIndexerFactory.IDENTIFIER,
+                        "vec");
+
+        assertThat(nativeOptions)
+                .containsEntry("nlist", "256")
+                .doesNotContainKey("aggregate-function");
+    }
 }

Reply via email to