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 04282834bb [core] Fix vector scan matching multi-column filter indexes
(#8284)
04282834bb is described below
commit 04282834bb4962387e476f8f789739b98aa70aa9
Author: QuakeWang <[email protected]>
AuthorDate: Fri Jun 19 11:49:14 2026 +0800
[core] Fix vector scan matching multi-column filter indexes (#8284)
Vector search splits must keep the vector index selection tied to the
vector column as the primary index field, because vector/full-text
readers rebuild the search indexer from `indexFieldId`.
The scalar pre-filter path is different: a multi-column global index can
satisfy a filter field stored in `extraFieldIds`. Previously
`VectorScanImpl` only matched filter fields against `indexFieldId`, so
vector search missed usable scalar indexes when the filtered column was
an extra field.
This PR matches vector filter fields against all indexed fields while
keeping vector/full-text search columns restricted to primary index
fields, and adds regression coverage for that contract.
---
.../apache/paimon/table/source/VectorScanImpl.java | 27 +++++++++++-----
.../table/source/FullTextSearchBuilderTest.java | 36 ++++++++++++++++++++-
.../table/source/VectorSearchBuilderTest.java | 37 +++++++++++++++++++++-
3 files changed, 90 insertions(+), 10 deletions(-)
diff --git
a/paimon-core/src/main/java/org/apache/paimon/table/source/VectorScanImpl.java
b/paimon-core/src/main/java/org/apache/paimon/table/source/VectorScanImpl.java
index b59363a326..74ae2afe8d 100644
---
a/paimon-core/src/main/java/org/apache/paimon/table/source/VectorScanImpl.java
+++
b/paimon-core/src/main/java/org/apache/paimon/table/source/VectorScanImpl.java
@@ -81,16 +81,12 @@ public class VectorScanImpl implements VectorScan {
if (globalIndex == null) {
return false;
}
- int fieldId = globalIndex.indexFieldId();
- if (vectorColumn.id() == fieldId ||
filterFieldIds.contains(fieldId)) {
+ if (isPrimaryColumn(globalIndex, vectorColumn.id())) {
return true;
}
- int[] extras = globalIndex.extraFieldIds();
- if (extras != null) {
- for (int extra : extras) {
- if (filterFieldIds.contains(extra)) {
- return true;
- }
+ for (int filterFieldId : filterFieldIds) {
+ if (containsField(globalIndex, filterFieldId)) {
+ return true;
}
}
return false;
@@ -137,4 +133,19 @@ public class VectorScanImpl implements VectorScan {
private static boolean isPrimaryColumn(GlobalIndexMeta meta, int fieldId) {
return meta.indexFieldId() == fieldId;
}
+
+ private static boolean containsField(GlobalIndexMeta meta, int fieldId) {
+ if (meta.indexFieldId() == fieldId) {
+ return true;
+ }
+ int[] extraFieldIds = meta.extraFieldIds();
+ if (extraFieldIds != null) {
+ for (int extraFieldId : extraFieldIds) {
+ if (extraFieldId == fieldId) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
}
diff --git
a/paimon-core/src/test/java/org/apache/paimon/table/source/FullTextSearchBuilderTest.java
b/paimon-core/src/test/java/org/apache/paimon/table/source/FullTextSearchBuilderTest.java
index 13f84d1b97..00eccd5dc9 100644
---
a/paimon-core/src/test/java/org/apache/paimon/table/source/FullTextSearchBuilderTest.java
+++
b/paimon-core/src/test/java/org/apache/paimon/table/source/FullTextSearchBuilderTest.java
@@ -51,6 +51,7 @@ import org.apache.paimon.utils.Range;
import org.junit.jupiter.api.Test;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collections;
import java.util.List;
@@ -344,6 +345,30 @@ public class FullTextSearchBuilderTest extends
TableTestBase {
assertThat(ids).contains(0, 1, 2);
}
+ @Test
+ public void testFullTextSearchRequiresTextColumnAsPrimaryField() throws
Exception {
+ createTableDefault();
+ FileStoreTable table = getTableDefault();
+
+ String[] documents = {"Apache Paimon", "vector search"};
+ writeDocuments(table, documents);
+ buildAndCommitIndexWithFields(
+ table,
+ documents,
+ Arrays.asList(
+ table.rowType().getField("id"),
table.rowType().getField(TEXT_FIELD_NAME)));
+
+ FullTextSearchBuilder searchBuilder =
+ table.newFullTextSearchBuilder()
+ .withQueryText("Paimon")
+ .withLimit(2)
+ .withTextColumn(TEXT_FIELD_NAME);
+
+ FullTextScan.Plan plan = searchBuilder.newFullTextScan().scan();
+ assertThat(plan.splits()).isEmpty();
+ assertThat(searchBuilder.executeLocal().results().isEmpty()).isTrue();
+ }
+
// ====================== Helper methods ======================
private void writeDocuments(FileStoreTable table, String[] documents)
throws Exception {
@@ -358,6 +383,15 @@ public class FullTextSearchBuilderTest extends
TableTestBase {
}
private void buildAndCommitIndex(FileStoreTable table, String[] documents)
throws Exception {
+ buildAndCommitIndexWithFields(
+ table,
+ documents,
+
Collections.singletonList(table.rowType().getField(TEXT_FIELD_NAME)));
+ }
+
+ private void buildAndCommitIndexWithFields(
+ FileStoreTable table, String[] documents, List<DataField>
indexFields)
+ throws Exception {
Options options = table.coreOptions().toConfiguration();
DataField textField = table.rowType().getField(TEXT_FIELD_NAME);
@@ -380,7 +414,7 @@ public class FullTextSearchBuilderTest extends
TableTestBase {
table.store().pathFactory().globalIndexFileFactory(),
table.coreOptions(),
rowRange,
- textField.id(),
+ indexFields,
TestFullTextGlobalIndexerFactory.IDENTIFIER,
entries);
diff --git
a/paimon-core/src/test/java/org/apache/paimon/table/source/VectorSearchBuilderTest.java
b/paimon-core/src/test/java/org/apache/paimon/table/source/VectorSearchBuilderTest.java
index 9898f93594..522d9fa732 100644
---
a/paimon-core/src/test/java/org/apache/paimon/table/source/VectorSearchBuilderTest.java
+++
b/paimon-core/src/test/java/org/apache/paimon/table/source/VectorSearchBuilderTest.java
@@ -60,6 +60,7 @@ import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collections;
import java.util.List;
@@ -519,6 +520,30 @@ public class VectorSearchBuilderTest extends TableTestBase
{
assertThat(result.results().isEmpty()).isTrue();
}
+ @Test
+ public void testVectorSearchRequiresVectorColumnAsPrimaryField() throws
Exception {
+ createTableDefault();
+ FileStoreTable table = getTableDefault();
+
+ float[][] vectors = {{1.0f, 0.0f}, {0.0f, 1.0f}};
+ writeVectors(table, vectors);
+ buildAndCommitVectorIndexWithFields(
+ table,
+ vectors,
+ new Range(0, 1),
+ Arrays.asList(table.rowType().getField("id"),
table.rowType().getField("vec")));
+
+ VectorSearchBuilder searchBuilder =
+ table.newVectorSearchBuilder()
+ .withVector(new float[] {1.0f, 0.0f})
+ .withLimit(2)
+ .withVectorColumn(VECTOR_FIELD_NAME);
+
+ VectorScan.Plan plan = searchBuilder.newVectorScan().scan();
+ assertThat(plan.splits()).isEmpty();
+ assertThat(searchBuilder.executeLocal().results().isEmpty()).isTrue();
+ }
+
@Test
public void testVectorSearchSplitSerialization() throws Exception {
createTableDefault();
@@ -957,6 +982,16 @@ public class VectorSearchBuilderTest extends TableTestBase
{
private void buildAndCommitVectorIndex(FileStoreTable table, float[][]
vectors, Range rowRange)
throws Exception {
+ buildAndCommitVectorIndexWithFields(
+ table,
+ vectors,
+ rowRange,
+
Collections.singletonList(table.rowType().getField(VECTOR_FIELD_NAME)));
+ }
+
+ private void buildAndCommitVectorIndexWithFields(
+ FileStoreTable table, float[][] vectors, Range rowRange,
List<DataField> indexFields)
+ throws Exception {
Options options = table.coreOptions().toConfiguration();
DataField vectorField = table.rowType().getField(VECTOR_FIELD_NAME);
@@ -978,7 +1013,7 @@ public class VectorSearchBuilderTest extends TableTestBase
{
table.store().pathFactory().globalIndexFileFactory(),
table.coreOptions(),
rowRange,
- vectorField.id(),
+ indexFields,
TestVectorGlobalIndexerFactory.IDENTIFIER,
entries);