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 4137611284 [spark] skip table lookup for meta-columns-only vector 
search since row ids and scores are already available from the index result 
(#8485)
4137611284 is described below

commit 4137611284a339ed70f625d132f2f62738da3d80
Author: Stefanietry <[email protected]>
AuthorDate: Tue Jul 7 15:20:49 2026 +0800

    [spark] skip table lookup for meta-columns-only vector search since row ids 
and scores are already available from the index result (#8485)
---
 .../apache/paimon/spark/PaimonScanBuilder.scala    | 16 +++++
 .../org/apache/paimon/spark/PaimonBaseScan.scala   | 54 ++++++++++-------
 .../paimon/spark/PaimonRecordReaderIterator.scala  |  6 +-
 .../apache/paimon/spark/PaimonScanBuilder.scala    | 18 +++++-
 .../plans/logical/PaimonTableValuedFunctions.scala |  2 +-
 .../paimon/spark/execution/PaimonStrategy.scala    | 31 ++++++++++
 .../spark/read/VectorSearchResultUtils.scala       | 70 ++++++++++++++++++++++
 .../paimon/spark/schema/PaimonMetadataColumn.scala |  3 +
 .../apache/paimon/spark/SparkMultimodalITCase.java | 41 +++++++++++++
 9 files changed, 216 insertions(+), 25 deletions(-)

diff --git 
a/paimon-spark/paimon-spark-3.2/src/main/scala/org/apache/paimon/spark/PaimonScanBuilder.scala
 
b/paimon-spark/paimon-spark-3.2/src/main/scala/org/apache/paimon/spark/PaimonScanBuilder.scala
index 5253cc9894..e451a433cd 100644
--- 
a/paimon-spark/paimon-spark-3.2/src/main/scala/org/apache/paimon/spark/PaimonScanBuilder.scala
+++ 
b/paimon-spark/paimon-spark-3.2/src/main/scala/org/apache/paimon/spark/PaimonScanBuilder.scala
@@ -18,6 +18,7 @@
 
 package org.apache.paimon.spark
 
+import org.apache.paimon.spark.read.{PaimonLocalScan, VectorSearchResultUtils}
 import org.apache.paimon.table.InnerTable
 
 import org.apache.spark.sql.connector.read.Scan
@@ -34,6 +35,21 @@ class PaimonScanBuilder(val table: InnerTable) extends 
PaimonBaseScanBuilder {
         (ftst.origin(), None, None, Option(ftst.fullTextSearch()))
       case _ => (table, pushedVectorSearch, None, pushedFullTextSearch)
     }
+    if (
+      vectorSearch.isDefined &&
+      
VectorSearchResultUtils.isVectorSearchMetaOnly(requiredSchema.fieldNames.toSeq)
+    ) {
+      val result = PaimonBaseScan.evalVectorSearch(
+        actualTable,
+        vectorSearch.get,
+        pushedPartitionFilters,
+        pushedDataFilters)
+      return PaimonLocalScan(
+        VectorSearchResultUtils.toRows(result, requiredSchema),
+        requiredSchema,
+        actualTable,
+        pushedPartitionFilters)
+    }
     PaimonScan(
       actualTable,
       requiredSchema,
diff --git 
a/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/PaimonBaseScan.scala
 
b/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/PaimonBaseScan.scala
index c3dd135b9d..9249507467 100644
--- 
a/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/PaimonBaseScan.scala
+++ 
b/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/PaimonBaseScan.scala
@@ -21,7 +21,7 @@ package org.apache.paimon.spark
 import org.apache.paimon.CoreOptions
 import org.apache.paimon.globalindex.GlobalIndexResult
 import org.apache.paimon.partition.PartitionPredicate
-import org.apache.paimon.predicate.PredicateBuilder
+import org.apache.paimon.predicate.{Predicate, PredicateBuilder, VectorSearch}
 import org.apache.paimon.spark.metric.SparkMetricRegistry
 import org.apache.paimon.spark.read.{BaseScan, BatchReadTagCleanupListener, 
PaimonSupportsRuntimeFiltering, SparkHybridSearchBuilderImpl, 
SparkVectorSearchBuilderImpl}
 import org.apache.paimon.spark.sources.PaimonMicroBatchStream
@@ -83,25 +83,11 @@ abstract class PaimonBaseScan(table: InnerTable)
   }
 
   private def evalVectorSearch(): GlobalIndexResult = {
-    val vectorSearch = pushedVectorSearch.get
-    val vectorSearchBuilder =
-      if (CoreOptions.fromMap(table.options).vectorSearchDistributeEnabled()) {
-        new SparkVectorSearchBuilderImpl(table)
-      } else {
-        table.newVectorSearchBuilder()
-      }
-    val vectorBuilder = vectorSearchBuilder
-      .withVector(vectorSearch.vector())
-      .withVectorColumn(vectorSearch.fieldName())
-      .withLimit(vectorSearch.limit())
-      .withOptions(vectorSearch.options())
-    if (pushedPartitionFilters.nonEmpty) {
-      
vectorBuilder.withPartitionFilter(PartitionPredicate.and(pushedPartitionFilters.asJava))
-    }
-    if (pushedDataFilters.nonEmpty) {
-      vectorBuilder.withFilter(PredicateBuilder.and(pushedDataFilters.asJava))
-    }
-    vectorBuilder.newVectorRead().read(vectorBuilder.newVectorScan().scan())
+    PaimonBaseScan.evalVectorSearch(
+      table,
+      pushedVectorSearch.get,
+      pushedPartitionFilters,
+      pushedDataFilters)
   }
 
   private def evalHybridSearch(): GlobalIndexResult = {
@@ -182,3 +168,31 @@ abstract class PaimonBaseScan(table: InnerTable)
     }
   }
 }
+
+object PaimonBaseScan {
+
+  private[spark] def evalVectorSearch(
+      table: InnerTable,
+      vectorSearch: VectorSearch,
+      pushedPartitionFilters: Seq[PartitionPredicate],
+      pushedDataFilters: Seq[Predicate]): GlobalIndexResult = {
+    val vectorSearchBuilder =
+      if (CoreOptions.fromMap(table.options).vectorSearchDistributeEnabled()) {
+        new SparkVectorSearchBuilderImpl(table)
+      } else {
+        table.newVectorSearchBuilder()
+      }
+    val vectorBuilder = vectorSearchBuilder
+      .withVector(vectorSearch.vector())
+      .withVectorColumn(vectorSearch.fieldName())
+      .withLimit(vectorSearch.limit())
+      .withOptions(vectorSearch.options())
+    if (pushedPartitionFilters.nonEmpty) {
+      
vectorBuilder.withPartitionFilter(PartitionPredicate.and(pushedPartitionFilters.asJava))
+    }
+    if (pushedDataFilters.nonEmpty) {
+      vectorBuilder.withFilter(PredicateBuilder.and(pushedDataFilters.asJava))
+    }
+    vectorBuilder.newVectorRead().read(vectorBuilder.newVectorScan().scan())
+  }
+}
diff --git 
a/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/PaimonRecordReaderIterator.scala
 
b/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/PaimonRecordReaderIterator.scala
index 706715f7ed..64b0b5166c 100644
--- 
a/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/PaimonRecordReaderIterator.scala
+++ 
b/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/PaimonRecordReaderIterator.scala
@@ -23,7 +23,7 @@ import org.apache.paimon.fs.Path
 import org.apache.paimon.globalindex.IndexedSplitRecordReader
 import org.apache.paimon.reader.{FileRecordIterator, RecordReader, 
ScoreRecordIterator}
 import org.apache.paimon.spark.schema.PaimonMetadataColumn
-import 
org.apache.paimon.spark.schema.PaimonMetadataColumn.{PARTITION_AND_BUCKET_META_COLUMNS,
 PATH_AND_INDEX_META_COLUMNS, ROW_ID_COLUMN, SEARCH_SCORE_COLUMN}
+import 
org.apache.paimon.spark.schema.PaimonMetadataColumn.{PARTITION_AND_BUCKET_META_COLUMNS,
 PATH_AND_INDEX_META_COLUMNS, VECTOR_SEARCH_META_COLUMN_NAMES}
 import org.apache.paimon.table.source.{DataSplit, Split}
 import org.apache.paimon.utils.{CloseableIterator, Preconditions}
 
@@ -50,11 +50,11 @@ case class PaimonRecordReaderIterator(
   private val needPathAndIndexMetadata =
     metadataColumns.exists(c => PATH_AND_INDEX_META_COLUMNS.contains(c.name))
   private val needVectorSearchMetadata = 
reader.isInstanceOf[IndexedSplitRecordReader] &&
-    metadataColumns.exists(c => c.name == ROW_ID_COLUMN || c.name == 
SEARCH_SCORE_COLUMN)
+    metadataColumns.exists(c => 
VECTOR_SEARCH_META_COLUMN_NAMES.contains(c.name))
 
   Preconditions.checkArgument(
     !needVectorSearchMetadata ||
-      metadataColumns.forall(c => c.name == ROW_ID_COLUMN || c.name == 
SEARCH_SCORE_COLUMN))
+      metadataColumns.forall(c => 
VECTOR_SEARCH_META_COLUMN_NAMES.contains(c.name)))
 
   private val metadataRow: GenericRow =
     GenericRow.of(Array.fill(metadataColumns.size)(null.asInstanceOf[AnyRef]): 
_*)
diff --git 
a/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/PaimonScanBuilder.scala
 
b/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/PaimonScanBuilder.scala
index 23f0924f1f..6d7f894bea 100644
--- 
a/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/PaimonScanBuilder.scala
+++ 
b/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/PaimonScanBuilder.scala
@@ -22,7 +22,7 @@ import org.apache.paimon.partition.PartitionPredicate
 import org.apache.paimon.predicate._
 import org.apache.paimon.predicate.SortValue.{NullOrdering, SortDirection}
 import 
org.apache.paimon.spark.aggregate.AggregatePushDownUtils.tryPushdownAggregation
-import org.apache.paimon.spark.read.{PaimonLocalScan, 
PaimonSupportsPushDownVariantExtractions}
+import org.apache.paimon.spark.read.{PaimonLocalScan, 
PaimonSupportsPushDownVariantExtractions, VectorSearchResultUtils}
 import org.apache.paimon.table.{FileStoreTable, InnerTable}
 
 import org.apache.spark.sql.connector.expressions
@@ -139,6 +139,22 @@ class PaimonScanBuilder(val table: InnerTable)
           case _ => (table, pushedVectorSearch, None, pushedFullTextSearch)
         }
 
+        if (
+          vectorSearch.isDefined &&
+          
VectorSearchResultUtils.isVectorSearchMetaOnly(requiredSchema.fieldNames.toSeq)
+        ) {
+          val result = PaimonBaseScan.evalVectorSearch(
+            actualTable,
+            vectorSearch.get,
+            pushedPartitionFilters,
+            pushedDataFilters)
+          return PaimonLocalScan(
+            VectorSearchResultUtils.toRows(result, requiredSchema),
+            requiredSchema,
+            actualTable,
+            pushedPartitionFilters)
+        }
+
         PaimonScan(
           actualTable,
           requiredSchema,
diff --git 
a/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/catalyst/plans/logical/PaimonTableValuedFunctions.scala
 
b/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/catalyst/plans/logical/PaimonTableValuedFunctions.scala
index d3d9194150..851acf933e 100644
--- 
a/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/catalyst/plans/logical/PaimonTableValuedFunctions.scala
+++ 
b/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/catalyst/plans/logical/PaimonTableValuedFunctions.scala
@@ -846,7 +846,7 @@ case class DynamicVectorSearchRelation(
 
   private lazy val outputWithMetaFields: Seq[Attribute] =
     relationOutput ++
-      Seq(PaimonMetadataColumn.ROW_ID.toAttribute, 
PaimonMetadataColumn.SEARCH_SCORE.toAttribute)
+      PaimonMetadataColumn.VECTOR_SEARCH_META_COLUMNS.map(_.toAttribute)
 
   override def output: Seq[Attribute] = outputWithMetaFields
 }
diff --git 
a/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/execution/PaimonStrategy.scala
 
b/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/execution/PaimonStrategy.scala
index 43e2e11264..b3c23e03d6 100644
--- 
a/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/execution/PaimonStrategy.scala
+++ 
b/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/execution/PaimonStrategy.scala
@@ -28,6 +28,7 @@ import org.apache.paimon.spark.catalog.{SparkBaseCatalog, 
SupportView}
 import org.apache.paimon.spark.catalyst.analysis.ResolvedPaimonView
 import 
org.apache.paimon.spark.catalyst.plans.logical.{CopyIntoLocationCommand, 
CopyIntoLocationSource, CopyIntoTableCommand, CreateOrReplaceTagCommand, 
CreatePaimonView, DeleteTagCommand, DropPaimonView, LateralVectorSearch, 
PaimonCallCommand, PaimonDropPartitions, PaimonTableValuedFunctions, 
RenameTagCommand, ResolvedIdentifier, ShowPaimonViews, ShowTagsCommand, 
TruncatePaimonTableWithFilter}
 import org.apache.paimon.spark.data.SparkInternalRow
+import org.apache.paimon.spark.read.VectorSearchResultUtils
 import org.apache.paimon.spark.schema.PaimonMetadataColumn
 import org.apache.paimon.table.{InnerTable, SpecialFields, Table}
 import org.apache.paimon.table.source.{BatchVectorSearchBuilder, 
InnerTableScan, ReadBuilder, VectorScan}
@@ -360,6 +361,8 @@ case class LateralVectorSearchExec(
       scoreMetadataColumns,
       sparkRow,
       rowIdOrdinal = resultRowType.getFieldIndex(SpecialFields.ROW_ID.name()),
+      metaColumnsOnly =
+        
VectorSearchResultUtils.isVectorSearchMetaOnly(vectorSearchOutput.map(_.name)),
       projectionInputOrdinals = vectorSearchOutput.map {
         attr =>
           if (attr.name == PaimonMetadataColumn.SEARCH_SCORE_COLUMN) {
@@ -431,6 +434,9 @@ case class LateralVectorSearchExec(
       s"Batch vector search returned ${globalIndexResults.size} results for 
${queries.size} " +
         "query vectors. The result count must match the query count."
     )
+    if (context.metaColumnsOnly) {
+      return searchMetaColumns(queries, globalIndexResults, context)
+    }
     val rowIdToMatches = createRowIdToMatches(queries, globalIndexResults)
     val batchGlobalIndexResult = 
createBatchGlobalIndexResult(globalIndexResults)
     val scan = context.readBuilder
@@ -466,6 +472,30 @@ case class LateralVectorSearchExec(
     }
   }
 
+  private def searchMetaColumns(
+      queries: Seq[LateralVectorSearchQuery],
+      globalIndexResults: Seq[GlobalIndexResult],
+      context: LateralVectorSearchContext): Iterator[(InternalRow, 
InternalRow)] = {
+    queries.zip(globalIndexResults).iterator.flatMap {
+      case (query, result) =>
+        val scoreGetter = result match {
+          case scored: ScoredGlobalIndexResult => Some(scored.scoreGetter())
+          case _ => None
+        }
+        result.results().iterator().asScala.map {
+          rowId =>
+            val values = vectorSearchOutput
+              .map(attr => VectorSearchResultUtils.valueOf(attr.name, rowId, 
scoreGetter))
+              .toArray
+            val projectedRow = context.rightProjection(
+              new JoinedRow(
+                query.outerRow,
+                new GenericInternalRow(values.asInstanceOf[Array[Any]])))
+            (query.outerRow, projectedRow)
+        }
+    }
+  }
+
   private def projectRightRow(
       rightRow: InternalRow,
       searchMatch: LateralVectorSearchMatch,
@@ -574,6 +604,7 @@ case class LateralVectorSearchExec(
       scoreMetadataColumns: Seq[PaimonMetadataColumn],
       sparkRow: SparkInternalRow,
       rowIdOrdinal: Int,
+      metaColumnsOnly: Boolean,
       projectionInputOrdinals: Seq[Int],
       rightProjection: UnsafeProjection,
       batchSize: Int,
diff --git 
a/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/read/VectorSearchResultUtils.scala
 
b/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/read/VectorSearchResultUtils.scala
new file mode 100644
index 0000000000..e224d0b154
--- /dev/null
+++ 
b/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/read/VectorSearchResultUtils.scala
@@ -0,0 +1,70 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.paimon.spark.read
+
+import org.apache.paimon.globalindex.{GlobalIndexResult, 
ScoredGlobalIndexResult}
+import 
org.apache.paimon.spark.catalyst.plans.logical.DynamicVectorSearchRelation
+import org.apache.paimon.spark.schema.PaimonMetadataColumn
+
+import org.apache.spark.sql.catalyst.InternalRow
+import org.apache.spark.sql.catalyst.expressions.GenericInternalRow
+import org.apache.spark.sql.types.StructType
+
+import scala.collection.JavaConverters._
+
+/** Utilities for returning vector search results without reading table rows. 
*/
+object VectorSearchResultUtils {
+
+  def isVectorSearchMetaOnly(fieldNames: Seq[String]): Boolean = {
+    fieldNames.nonEmpty && fieldNames.forall(
+      PaimonMetadataColumn.VECTOR_SEARCH_META_COLUMN_NAMES.contains)
+  }
+
+  def toRows(result: GlobalIndexResult, requiredSchema: StructType): 
Array[InternalRow] = {
+    val fieldNames = requiredSchema.fieldNames
+    val scoreGetter = result match {
+      case scored: ScoredGlobalIndexResult => Some(scored.scoreGetter())
+      case _ => None
+    }
+    result
+      .results()
+      .iterator()
+      .asScala
+      .map {
+        rowId =>
+          new GenericInternalRow(
+            fieldNames.map(valueOf(_, rowId, 
scoreGetter)).asInstanceOf[Array[Any]])
+      }
+      .toArray
+  }
+
+  def valueOf(
+      fieldName: String,
+      rowId: Long,
+      scoreGetter: Option[org.apache.paimon.globalindex.ScoreGetter]): Any = {
+    fieldName match {
+      case PaimonMetadataColumn.ROW_ID_COLUMN => rowId
+      case PaimonMetadataColumn.SEARCH_SCORE_COLUMN =>
+        scoreGetter.map(_.score(rowId)).getOrElse(Float.NaN)
+      case _ =>
+        throw new IllegalArgumentException(
+          s"Field $fieldName cannot be returned directly from vector search 
result.")
+    }
+  }
+}
diff --git 
a/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/schema/PaimonMetadataColumn.scala
 
b/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/schema/PaimonMetadataColumn.scala
index d61dfb8c59..41c6cdaaaf 100644
--- 
a/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/schema/PaimonMetadataColumn.scala
+++ 
b/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/schema/PaimonMetadataColumn.scala
@@ -61,6 +61,7 @@ object PaimonMetadataColumn {
   val PATH_AND_INDEX_META_COLUMNS: Seq[String] = Seq(FILE_PATH_COLUMN, 
ROW_INDEX_COLUMN)
   val PARTITION_AND_BUCKET_META_COLUMNS: Seq[String] = Seq(PARTITION_COLUMN, 
BUCKET_COLUMN)
   val ROW_TRACKING_META_COLUMNS: Seq[String] = Seq(ROW_ID_COLUMN, 
SEQUENCE_NUMBER_COLUMN)
+  val VECTOR_SEARCH_META_COLUMN_NAMES: Seq[String] = Seq(ROW_ID_COLUMN, 
SEARCH_SCORE_COLUMN)
 
   val SUPPORTED_METADATA_COLUMNS: Seq[String] = Seq(
     ROW_INDEX_COLUMN,
@@ -92,6 +93,8 @@ object PaimonMetadataColumn {
   val SEARCH_SCORE: PaimonMetadataColumn =
     PaimonMetadataColumn(Integer.MAX_VALUE - 106, SEARCH_SCORE_COLUMN, 
FloatType)
 
+  val VECTOR_SEARCH_META_COLUMNS: Seq[PaimonMetadataColumn] = Seq(ROW_ID, 
SEARCH_SCORE)
+
   def dvMetaCols: Seq[PaimonMetadataColumn] = Seq(FILE_PATH, ROW_INDEX)
 
   def get(metadataColumn: String, partitionType: StructType): 
PaimonMetadataColumn = {
diff --git 
a/paimon-spark/paimon-spark-ut/src/test/java/org/apache/paimon/spark/SparkMultimodalITCase.java
 
b/paimon-spark/paimon-spark-ut/src/test/java/org/apache/paimon/spark/SparkMultimodalITCase.java
index 340122a12a..8fc7d603b1 100644
--- 
a/paimon-spark/paimon-spark-ut/src/test/java/org/apache/paimon/spark/SparkMultimodalITCase.java
+++ 
b/paimon-spark/paimon-spark-ut/src/test/java/org/apache/paimon/spark/SparkMultimodalITCase.java
@@ -195,6 +195,21 @@ public class SparkMultimodalITCase {
                                                         
.equals(row.getLong(3))))
                 .isTrue();
 
+        // **vector search with metadata columns only */
+        String vectorSearchWithMetadataColumnsOnlySql =
+                "select _row_id AS _row_id, __paimon_search_score "
+                        + "from vector_search('my_db1.vector_test', 'embs', 
array(1.0f, 2.0f, 3.0f, 4.0f), 5) "
+                        + "where date = '20260420'";
+        df = spark.sql(vectorSearchWithMetadataColumnsOnlySql);
+        assertThat(df.columns()).hasSize(2);
+        assertThat(df.columns()).contains("_row_id", "__paimon_search_score");
+        rows = df.collectAsList();
+        assertThat(rows).hasSize(5);
+        assertThat(rows.stream().allMatch(row -> !row.isNullAt(0) && 
!row.isNullAt(1))).isTrue();
+        assertThat(rows.stream().allMatch(row -> 
baseRowIds.containsValue(row.getLong(0))))
+                .isTrue();
+        assertThat(rows.stream().map(row -> 
row.getLong(0)).collect(Collectors.toSet())).hasSize(5);
+
         // **vector search with score */
         String vectorSearchSql =
                 "select gid, sid,  embs, __paimon_search_score "
@@ -256,6 +271,32 @@ public class SparkMultimodalITCase {
                 .containsEntry(6L, 5L)
                 .containsEntry(7L, 5L)
                 .containsEntry(8L, 5L);
+
+        // ** lateral vector search with metadata columns only in subquery */
+        rows =
+                spark.sql(
+                                "SELECT q.gid AS query_gid, "
+                                        + "r._row_id AS result_row_id, "
+                                        + "r.__paimon_search_score AS 
result_score "
+                                        + "FROM my_db1.vector_test AS q, "
+                                        + "LATERAL (SELECT _row_id, 
__paimon_search_score "
+                                        + "FROM 
vector_search('my_db1.vector_test', 'embs', q.embs, 5)) AS r "
+                                        + "WHERE q.`date` = '20260420';")
+                        .collectAsList();
+        assertThat(rows).hasSize(40);
+        assertThat(rows.stream().allMatch(row -> !row.isNullAt(0) && 
!row.isNullAt(1))).isTrue();
+        assertThat(rows.stream().allMatch(row -> 
baseRowIds.containsValue(row.getLong(1))))
+                .isTrue();
+        Map<Long, java.util.Set<Long>> rowIdsPerQueryGid =
+                rows.stream()
+                        .collect(
+                                Collectors.groupingBy(
+                                        row -> row.getLong(0),
+                                        Collectors.mapping(
+                                                row -> row.getLong(1), 
Collectors.toSet())));
+        assertThat(rowIdsPerQueryGid).hasSize(8);
+        assertThat(rowIdsPerQueryGid.values().stream().allMatch(rowIds -> 
rowIds.size() == 5))
+                .isTrue();
         spark.close();
 
         spark = builder.getOrCreate();

Reply via email to