JingsongLi commented on code in PR #8252:
URL: https://github.com/apache/paimon/pull/8252#discussion_r3457409791


##########
paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/execution/PaimonStrategy.scala:
##########
@@ -215,3 +238,262 @@ case class PaimonStrategy(spark: SparkSession)
     SparkShimLoader.shim.classicApi.recacheByPlan(spark, v2Relation)
   }
 }
+
+case class LateralVectorSearchExec(
+    innerTable: InnerTable,
+    columnName: String,
+    queryVectorExpr: Expression,
+    limit: Int,
+    options: Map[String, String],
+    vectorSearchOutput: Seq[Attribute],
+    projectList: Seq[NamedExpression],
+    projectOutput: Seq[Attribute],
+    child: SparkPlan)
+  extends SparkPlan {
+
+  override def children: Seq[SparkPlan] = Seq(child)
+
+  override def output: Seq[Attribute] = child.output ++ projectOutput
+
+  @transient override lazy val producedAttributes: AttributeSet = 
AttributeSet(vectorSearchOutput)
+
+  @transient
+  override lazy val references: AttributeSet = {
+    AttributeSet.fromAttributeSets(expressions.map(_.references)) -- 
producedAttributes
+  }
+
+  override protected def withNewChildrenInternal(newChildren: 
IndexedSeq[SparkPlan]): SparkPlan = {
+    copy(child = newChildren.head)
+  }
+
+  override protected def doExecute(): RDD[InternalRow] = {
+    child.execute().mapPartitions {
+      outerRows =>
+        val strippedQueryExpr = queryVectorExpr.transform {
+          case OuterReference(namedExpression) => namedExpression.toAttribute
+        }
+        val queryVectorProjection = 
UnsafeProjection.create(Seq(strippedQueryExpr), child.output)
+        val rightProjection = UnsafeProjection.create(projectList, 
vectorSearchOutput)
+        val joinedRow = new JoinedRow
+        lazy val searchContext = createSearchContext(rightProjection)
+        val batchSize = searchContext.batchSize
+
+        outerRows.map(_.copy()).grouped(batchSize).flatMap {
+          outerRowBatch =>
+            val searchBatch = ArrayBuffer[LateralVectorSearchQuery]()
+            outerRowBatch.foreach {
+              outerRow =>
+                toFloatArray(queryVectorProjection(outerRow).get(0, 
strippedQueryExpr.dataType))
+                  .foreach(
+                    queryVector => searchBatch += 
LateralVectorSearchQuery(outerRow, queryVector))
+            }
+
+            if (searchBatch.isEmpty) {
+              Iterator.empty
+            } else {
+              search(searchBatch.toVector, searchContext).map {
+                case (outerRow, rightRow) =>
+                  joinedRow(outerRow, rightRow)
+                  joinedRow.copy()
+              }
+            }
+        }
+    }
+  }
+
+  private def createSearchContext(rightProjection: UnsafeProjection): 
LateralVectorSearchContext = {
+    val rowType = innerTable.rowType()
+    val readFieldNames = vectorSearchOutput
+      .filterNot(_.name == PaimonMetadataColumn.SEARCH_SCORE_COLUMN)
+      .map(_.name)
+    val readFieldNamesWithRowId =
+      if (readFieldNames.contains(SpecialFields.ROW_ID.name())) {
+        readFieldNames
+      } else {
+        readFieldNames :+ SpecialFields.ROW_ID.name()
+      }
+    val rowTypeWithRowId = SpecialFields.rowTypeWithRowId(rowType)
+    val readRowType = rowType.project(readFieldNames.asJava)
+    val readRowTypeWithRowId = SpecialFields.rowTypeWithRowId(readRowType)
+    val readBuilder = innerTable
+      .newReadBuilder()
+      .withReadType(rowTypeWithRowId.project(readFieldNamesWithRowId.asJava))
+    val scoreMetadataColumns =
+      if (vectorSearchOutput.exists(_.name == 
PaimonMetadataColumn.SEARCH_SCORE_COLUMN)) {
+        Seq(PaimonMetadataColumn.SEARCH_SCORE)
+      } else {
+        Seq.empty
+      }
+    val resultRowType =
+      if (scoreMetadataColumns.isEmpty) {
+        readRowTypeWithRowId
+      } else {
+        new RowType(
+          (readRowTypeWithRowId.getFields.asScala ++ scoreMetadataColumns.map(
+            _.toPaimonDataField)).asJava)
+      }
+    val sparkRow = SparkInternalRow.create(resultRowType)
+    val vectorSearchBuilder = innerTable

Review Comment:
   Normal Spark `vector_search` scans apply pushed partition/data filters 
before top-k (`PaimonBaseScan.evalVectorSearch` passes 
`pushedPartitionFilters`/`pushedDataFilters` into the builder). This lateral 
executor builds the `BatchVectorSearchBuilder` here without carrying predicates 
from the search side; `PushDownLateralVectorSearchFilter` only pushes 
predicates that reference the left child, so predicates on `r.dt` or other 
searched-table columns stay above `LateralVectorSearch`. A query like `... JOIN 
LATERAL (...) r WHERE r.dt = '20260420'` will pick topK over all partitions and 
then filter the joined rows, which can return fewer or wrong rows compared with 
non-lateral `vector_search(...) WHERE dt = ...`. Please preserve search-side 
filters and apply them via `withPartitionFilter`/`withFilter` before 
`newVectorScan()`/`readBatch()`, or reject such predicates explicitly.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to