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

dongjoon-hyun pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/spark-connect-swift.git


The following commit(s) were added to refs/heads/main by this push:
     new e41476a  [SPARK-57542] Support `nearestByJoin` for `DataFrame`
e41476a is described below

commit e41476ae2184e1b6764290cc756600b53f475a41
Author: Dongjoon Hyun <[email protected]>
AuthorDate: Thu Jun 18 14:51:52 2026 -0700

    [SPARK-57542] Support `nearestByJoin` for `DataFrame`
    
    ### What changes were proposed in this pull request?
    
    This PR adds the public `DataFrame.nearestByJoin` API .
    
    ### Why are the changes needed?
    
    `NearestByJoin` is a new relation in Apache Spark `4.2.0`. This keeps the 
Swift
    client at API parity with PySpark/Spark SQL.
    
    ### Does this PR introduce _any_ user-facing change?
    
    No behavior change because this is a new method.
    
    ### How was this patch tested?
    
    Pass the CIs with a newly added test case.
    
    ### Was this patch authored or co-authored using generative AI tooling?
    
    Generated-by: Claude Opus 4.8
    
    Closes #424 from dongjoon-hyun/SPARK-57542.
    
    Authored-by: Dongjoon Hyun <[email protected]>
    Signed-off-by: Dongjoon Hyun <[email protected]>
---
 .../SparkConnect/DataFrame+Transformations.swift   | 34 ++++++++++++++++++++
 Sources/SparkConnect/SparkConnectClient.swift      | 16 ++++++++++
 Sources/SparkConnect/TypeAliases.swift             |  1 +
 Tests/SparkConnectTests/DataFrameTests.swift       | 36 ++++++++++++++++++++++
 4 files changed, 87 insertions(+)

diff --git a/Sources/SparkConnect/DataFrame+Transformations.swift 
b/Sources/SparkConnect/DataFrame+Transformations.swift
index 2f21d29..e4511eb 100644
--- a/Sources/SparkConnect/DataFrame+Transformations.swift
+++ b/Sources/SparkConnect/DataFrame+Transformations.swift
@@ -474,6 +474,40 @@ extension DataFrame {
     return DataFrame(spark: self.spark, plan: plan)
   }
 
+  /// Nearest-by top-K ranking join with another ``DataFrame``.
+  ///
+  /// For each row on the left (query) side, returns up to `numResults` rows 
from `right`
+  /// (the base side), ranked by `rankingExpression`.
+  ///
+  /// - Parameters:
+  ///   - right: The base ``DataFrame`` to search for matches.
+  ///   - rankingExpression: A scalar expression string used to rank candidate 
rows.
+  ///   - numResults: Maximum number of matches per left row. Must be between 
1 and 100000.
+  ///   - mode: Search algorithm contract. One of `approx`, `exact`.
+  ///   - direction: Ranking direction. One of `distance`, `similarity`.
+  ///   - joinType: One of `inner` (default), `leftouter`.
+  /// - Returns: A ``DataFrame``.
+  public func nearestByJoin(
+    _ right: DataFrame,
+    rankingExpression: String,
+    numResults: Int32,
+    mode: String,
+    direction: String,
+    joinType: String = "inner"
+  ) async -> DataFrame {
+    let rightPlan = await (right.getPlan() as! Plan).root
+    let plan = SparkConnectClient.getNearestByJoin(
+      self.plan.root,
+      rightPlan,
+      rankingExpression: rankingExpression,
+      numResults: numResults,
+      mode: mode,
+      direction: direction,
+      joinType: joinType
+    )
+    return DataFrame(spark: self.spark, plan: plan)
+  }
+
   // MARK: - Set Operations
 
   /// Returns a new `DataFrame` containing rows in this `DataFrame` but not in 
another `DataFrame`.
diff --git a/Sources/SparkConnect/SparkConnectClient.swift 
b/Sources/SparkConnect/SparkConnectClient.swift
index d424a63..a68a465 100644
--- a/Sources/SparkConnect/SparkConnectClient.swift
+++ b/Sources/SparkConnect/SparkConnectClient.swift
@@ -1035,6 +1035,22 @@ public actor SparkConnectClient {
     return createPlan { $0.lateralJoin = lateralJoin }
   }
 
+  static func getNearestByJoin(
+    _ left: Relation, _ right: Relation,
+    rankingExpression: String, numResults: Int32,
+    mode: String, direction: String, joinType: String
+  ) -> Plan {
+    var nearestByJoin = NearestByJoin()
+    nearestByJoin.left = left
+    nearestByJoin.right = right
+    nearestByJoin.rankingExpression = rankingExpression.toExpression
+    nearestByJoin.numResults = numResults
+    nearestByJoin.mode = mode
+    nearestByJoin.direction = direction
+    nearestByJoin.joinType = joinType
+    return createPlan { $0.nearestByJoin = nearestByJoin }
+  }
+
   static func getSetOperation(
     _ left: Relation, _ right: Relation, _ opType: SetOpType, isAll: Bool = 
false,
     byName: Bool = false, allowMissingColumns: Bool = false
diff --git a/Sources/SparkConnect/TypeAliases.swift 
b/Sources/SparkConnect/TypeAliases.swift
index d70d161..315d2e0 100644
--- a/Sources/SparkConnect/TypeAliases.swift
+++ b/Sources/SparkConnect/TypeAliases.swift
@@ -42,6 +42,7 @@ typealias MapType = Spark_Connect_DataType.Map
 typealias MergeAction = Spark_Connect_MergeAction
 typealias MergeIntoTableCommand = Spark_Connect_MergeIntoTableCommand
 typealias NamedTable = Spark_Connect_Read.NamedTable
+typealias NearestByJoin = Spark_Connect_NearestByJoin
 typealias OneOf_Analyze = AnalyzePlanRequest.OneOf_Analyze
 typealias OneOf_CatType = Spark_Connect_Catalog.OneOf_CatType
 typealias OutputType = Spark_Connect_OutputType
diff --git a/Tests/SparkConnectTests/DataFrameTests.swift 
b/Tests/SparkConnectTests/DataFrameTests.swift
index 139c0ed..b54e459 100644
--- a/Tests/SparkConnectTests/DataFrameTests.swift
+++ b/Tests/SparkConnectTests/DataFrameTests.swift
@@ -630,6 +630,42 @@ struct DataFrameTests {
     await spark.stop()
   }
 
+  @Test
+  func nearestByJoin() async throws {
+    let spark = try await SparkSession.builder.getOrCreate()
+    let version = await spark.version
+    if version >= "4.2", !version.contains("preview") {
+      let users = try await spark.sql(
+        "SELECT * FROM VALUES (1, 10.0), (2, 20.0), (3, 30.0) AS T(user_id, 
score)")
+      let products = try await spark.sql(
+        "SELECT * FROM VALUES ('A', 11.0), ('B', 22.0), ('C', 5.0) AS 
S(product, pscore)")
+
+      let distance = await users.nearestByJoin(
+        products,
+        rankingExpression: "abs(score - pscore)",
+        numResults: 2,
+        mode: "approx",
+        direction: "distance"
+      ).select("user_id", "product").orderBy("user_id", "product")
+      #expect(
+        try await distance.collect() == [
+          Row(1, "A"), Row(1, "C"),
+          Row(2, "A"), Row(2, "B"),
+          Row(3, "A"), Row(3, "B"),
+        ])
+
+      let similarity = await users.nearestByJoin(
+        products,
+        rankingExpression: "-abs(score - pscore)",
+        numResults: 1,
+        mode: "approx",
+        direction: "similarity"
+      ).select("user_id", "product").orderBy("user_id")
+      #expect(try await similarity.collect() == [Row(1, "A"), Row(2, "B"), 
Row(3, "B")])
+    }
+    await spark.stop()
+  }
+
   @Test
   func except() async throws {
     let spark = try await SparkSession.builder.getOrCreate()


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to