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

zhengruifeng pushed a commit to branch branch-4.x
in repository https://gitbox.apache.org/repos/asf/spark.git


The following commit(s) were added to refs/heads/branch-4.x by this push:
     new aa4df79f810c [SPARK-58606][ML] Move association-rule collection and 
preparation in FPGrowthModel transform from driver to executors
aa4df79f810c is described below

commit aa4df79f810c025a2cc1da9ccf37c586c7f441fc
Author: Ruifeng Zheng <[email protected]>
AuthorDate: Thu Aug 6 15:22:54 2026 +0800

    [SPARK-58606][ML] Move association-rule collection and preparation in 
FPGrowthModel transform from driver to executors
    
    ### What changes were proposed in this pull request?
    
    This PR replaces the manually collected association-rule array and 
user-defined function in `FPGrowthModel.transform` with DataFrame operations. 
It joins input transactions with matching rules, aggregates consequents for 
each input row, and uses `array_except` to exclude items already present in the 
transaction.
    
    The join has no broadcast hint, so Catalyst may use a broadcast nested-loop 
join only when the plan statistics consider a side small enough.
    
    ### Why are the changes needed?
    
    Collecting every association rule before a transform creates avoidable 
driver-memory pressure, particularly for Spark Connect server workloads with 
many rules.
    
    ### Does this PR introduce _any_ user-facing change?
    
    No.
    
    ### How was this patch tested?
    
    - `build/sbt -java-home /usr/lib/jvm/java-17-openjdk-amd64 mllib/compile`
    - The focused `FPGrowthSuite` has not been run locally yet.
    
    ### Was this patch authored or co-authored using generative AI tooling?
    
    Generated-by: Codex (GPT-5)
    
    Closes #57806 from zhengruifeng/ml_fpgrowth_transform_join.
    
    Authored-by: Ruifeng Zheng <[email protected]>
    Signed-off-by: Ruifeng Zheng <[email protected]>
    (cherry picked from commit 21a1ae1ef937633ee3135a0963922bb0df39ddf9)
    Signed-off-by: Ruifeng Zheng <[email protected]>
---
 .../scala/org/apache/spark/ml/fpm/FPGrowth.scala   | 34 +++++++++-------------
 1 file changed, 14 insertions(+), 20 deletions(-)

diff --git a/mllib/src/main/scala/org/apache/spark/ml/fpm/FPGrowth.scala 
b/mllib/src/main/scala/org/apache/spark/ml/fpm/FPGrowth.scala
index 0a5213228a8b..d92a8f4c27a5 100644
--- a/mllib/src/main/scala/org/apache/spark/ml/fpm/FPGrowth.scala
+++ b/mllib/src/main/scala/org/apache/spark/ml/fpm/FPGrowth.scala
@@ -273,35 +273,29 @@ class FPGrowthModel private[ml] (
    * from all the applicable rules as prediction. The prediction column has 
the same data type as
    * the input column(Array[T]) and will not contain existing items in the 
input column. The null
    * values in the itemsCol columns are treated as empty sets.
-   * WARNING: internally it collects association rules to the driver and uses 
broadcast for
-   * efficiency. This may bring pressure to driver memory for large set of 
association rules.
+   * Internally, transform aggregates association rules into a single-row 
DataFrame and joins it
+   * with the input dataset.
    */
   @Since("2.2.0")
   override def transform(dataset: Dataset[_]): DataFrame = {
     transformSchema(dataset.schema, logging = true)
-    genericTransform(dataset)
-  }
-
-  private def genericTransform(dataset: Dataset[_]): DataFrame = {
-    val rules: Array[(Seq[Any], Seq[Any])] = 
associationRules.select("antecedent", "consequent")
-      .rdd.map(r => (r.getSeq(0), r.getSeq(1)))
-      .collect().asInstanceOf[Array[(Seq[Any], Seq[Any])]]
-    val brRules = dataset.sparkSession.sparkContext.broadcast(rules)
-
     val dt = dataset.schema($(itemsCol)).dataType
-    // For each rule, examine the input items and summarize the consequents
-    val predictUDF = SparkUserDefinedFunction((items: Seq[Any]) => {
+    // For each rule, examine the input items and summarize the consequents.
+    val predictFunc = (items: Seq[Any], rules: Seq[Row]) => {
       if (items != null) {
         val itemset = items.toSet
-        brRules.value.filter(_._1.forall(itemset.contains))
-          .flatMap(_._2.filter(!itemset.contains(_))).distinct
+        rules.filter(_.getSeq[Any](0).forall(itemset.contains))
+          .flatMap(_.getSeq[Any](1).filter(!itemset.contains(_))).distinct
       } else {
         Seq.empty
-      }},
-      dt,
-      Nil
-    )
-    dataset.withColumn($(predictionCol), predictUDF(col($(itemsCol))))
+      }
+    }
+    val predictUDF = SparkUserDefinedFunction(predictFunc, dt, Nil)
+    dataset.join(
+      associationRules.select("antecedent", "consequent")
+        .agg(collect_set(struct("antecedent", "consequent")).as("rules")))
+      .withColumn($(predictionCol), predictUDF(col($(itemsCol)), col("rules")))
+      .drop("rules")
   }
 
   @Since("2.2.0")


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

Reply via email to