You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@spark.apache.org by ru...@apache.org on 2026/08/10 04:10:27 UTC

(spark) branch master updated: [SPARK-58634][ML] Avoid caching VectorIndexer transformation state

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

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


The following commit(s) were added to refs/heads/master by this push:
     new a42f5aa001c4 [SPARK-58634][ML] Avoid caching VectorIndexer transformation state
a42f5aa001c4 is described below

commit a42f5aa001c465963724c53cb89c9883c5600ad3
Author: Ruifeng Zheng <ru...@apache.org>
AuthorDate: Mon Aug 10 12:10:15 2026 +0800

    [SPARK-58634][ML] Avoid caching VectorIndexer transformation state
    
    ### What changes were proposed in this pull request?
    
    This PR constructs VectorIndexerModel's per-vector transformation function for each `transform` call instead of caching it on the model. The function captures only the state needed for that transformation.
    
    It also adds a regression test that changes `handleInvalid` after a transform has been executed.
    
    ### Why are the changes needed?
    
    The cached function retained transformation-only state on the model and continued using the initial `handleInvalid` value after the parameter changed. Constructing it at transform time releases that state and honors the current parameter value.
    
    ### Does this PR introduce _any_ user-facing change?
    
    Yes. Updating `handleInvalid` on an existing VectorIndexerModel now takes effect after an earlier transformation.
    
    ### How was this patch tested?
    
    Added a regression case to `VectorIndexerSuite` and ran:
    
    ```
    build/sbt -Dsbt.supershell=false 'mllib/testOnly org.apache.spark.ml.feature.VectorIndexerSuite'
    ```
    
    ### Was this patch authored or co-authored using generative AI tooling?
    
    Generated-by: Codex (GPT-5)
    
    Closes #57839 from zhengruifeng/SPARK-58634-vector-indexer-transform-state.
    
    Authored-by: Ruifeng Zheng <ru...@apache.org>
    Signed-off-by: Ruifeng Zheng <ru...@foxmail.com>
---
 .../main/scala/org/apache/spark/ml/feature/VectorIndexer.scala    | 8 +++-----
 .../scala/org/apache/spark/ml/feature/VectorIndexerSuite.scala    | 7 +++++++
 2 files changed, 10 insertions(+), 5 deletions(-)

diff --git a/mllib/src/main/scala/org/apache/spark/ml/feature/VectorIndexer.scala b/mllib/src/main/scala/org/apache/spark/ml/feature/VectorIndexer.scala
index d1810d8092ed..682746421994 100644
--- a/mllib/src/main/scala/org/apache/spark/ml/feature/VectorIndexer.scala
+++ b/mllib/src/main/scala/org/apache/spark/ml/feature/VectorIndexer.scala
@@ -365,17 +365,15 @@ class VectorIndexerModel private[ml] (
     attrs
   }
 
-  // TODO: Check more carefully about whether this whole class will be included in a closure.
-
   /** Per-vector transform function */
-  private lazy val transformFunc: Vector => Vector = {
+  private def getTransformFunc: Vector => Vector = {
     val sortedCatFeatureIndices = categoryMaps.keys.toArray.sorted
     val localVectorMap = categoryMaps
     val localNumFeatures = numFeatures
     val localHandleInvalid = getHandleInvalid
     val f: Vector => Vector = { (v: Vector) =>
       assert(v.size == localNumFeatures, "VectorIndexerModel expected vector of length" +
-        s" $numFeatures but found length ${v.size}")
+        s" $localNumFeatures but found length ${v.size}")
       v match {
         case dv: DenseVector =>
           var hasInvalid = false
@@ -448,7 +446,7 @@ class VectorIndexerModel private[ml] (
   override def transform(dataset: Dataset[_]): DataFrame = {
     transformSchema(dataset.schema, logging = true)
     val newField = prepOutputField(dataset.schema)
-    val transformUDF = udf { vector: Vector => transformFunc(vector) }
+    val transformUDF = udf(getTransformFunc)
     val newCol = transformUDF(dataset($(inputCol)))
     val ds = dataset.withColumn($(outputCol), newCol, newField.metadata)
     if (getHandleInvalid == VectorIndexer.SKIP_INVALID) {
diff --git a/mllib/src/test/scala/org/apache/spark/ml/feature/VectorIndexerSuite.scala b/mllib/src/test/scala/org/apache/spark/ml/feature/VectorIndexerSuite.scala
index 1529b4c661ce..664a1937b381 100644
--- a/mllib/src/test/scala/org/apache/spark/ml/feature/VectorIndexerSuite.scala
+++ b/mllib/src/test/scala/org/apache/spark/ml/feature/VectorIndexerSuite.scala
@@ -271,6 +271,13 @@ class VectorIndexerSuite extends MLTest with DefaultReadWriteTest with Logging {
       testTransformerByGlobalCheckFunc[FeatureData](points, model1, "indexed") { rows =>
         assert(rows.map(_(0)) == expected)
       }
+      model1.set(model1.handleInvalid, "keep")
+      testTransformerByGlobalCheckFunc[FeatureData](pointsTestInvalid, model1, "indexed") { rows =>
+        assert(rows.map(_(0)) == expected ++ Array(
+          Vectors.dense(2.0, 2.0, 0.0),
+          Vectors dense(0.0, 4.0, 2.0),
+          Vectors.dense(1.0, 3.0, 3.0)))
+      }
       val vectorIndexer2 = getIndexer.setMaxCategories(4).setHandleInvalid("keep")
       val model2 = vectorIndexer2.fit(points)
       testTransformerByGlobalCheckFunc[FeatureData](pointsTestInvalid, model2, "indexed") { rows =>


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@spark.apache.org
For additional commands, e-mail: commits-help@spark.apache.org