IJPL-235909 ML in SE: Convert feature names on prediction / Fix regression

Following the renaming of features to follow FUS recommendations,
the feature mapper used by our ML models no longer worked as intended,
given that most feature names have changed.

This commit introduces a temporary conversion layer that,
until new models are trained, converts the feature names
back to the old names.


(cherry picked from commit 1bf999feb62e1bbb90ca567b5fe60f672fc335e4)

IJ-CR-192893

GitOrigin-RevId: 188f66fb074f262a999d8f12fde9fd3c09a6cbd3
This commit is contained in:
Adam Malek
2026-02-25 11:42:12 +00:00
committed by intellij-monorepo-bot
parent c49593a948
commit a963b2cab7
3 changed files with 80 additions and 1 deletions
@@ -0,0 +1,41 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.intellij.searchEverywhereMl.ranking.core.model
// This conversion is required while bundled models still expect legacy camelCase names.
// It can be removed after models are re-trained with snake_case feature names.
private val LEGACY_FEATURE_NAME_ALIASES = mapOf(
"action_plugin_type" to "pluginType",
"is_in_library" to "isFromLibrary",
"max_usage_se" to "maxUsageSE",
"min_usage_se" to "minUsageSE",
)
internal fun withLegacyFeatureNameAliases(features: Map<String, Any?>): Map<String, Any?> {
val featuresWithLegacyAliases = HashMap<String, Any?>(features.size * 2)
for ((featureName, value) in features) {
featuresWithLegacyAliases[featureName] = value
featuresWithLegacyAliases.putIfAbsent(toLegacyFeatureName(featureName), value)
}
return featuresWithLegacyAliases
}
internal fun toLegacyFeatureName(featureName: String): String {
return LEGACY_FEATURE_NAME_ALIASES[featureName] ?: featureName.snakeCaseToCamelCase()
}
private fun String.snakeCaseToCamelCase(): String {
if (indexOf('_') < 0) return this
val result = StringBuilder(length)
var uppercaseNext = false
for (char in this) {
if (char == '_') {
uppercaseNext = true
continue
}
result.append(if (uppercaseNext) char.uppercaseChar() else char)
uppercaseNext = false
}
return result.toString()
}
@@ -12,7 +12,7 @@ import com.intellij.searchEverywhereMl.ranking.core.features.SearchEverywhereFil
internal abstract class SearchEverywhereRankingModel(protected val model: DecisionFunction) {
abstract fun predict(features: Map<String, Any?>): Double
protected fun rawModelPredict(features: Map<String, Any?>): Double {
return model.predict(buildArray(model.featuresOrder, features))
return model.predict(buildArray(model.featuresOrder, withLegacyFeatureNameAliases(features)))
}
private fun buildArray(featuresOrder: Array<FeatureMapper>, features: Map<String, Any?>): DoubleArray {
@@ -0,0 +1,38 @@
package com.intellij.searchEverywhereMl.ranking.core.model
import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
@RunWith(JUnit4::class)
internal class SearchEverywhereFeatureNameConverterTest {
@Test
fun `snake case name is converted to camelCase`() {
assertEquals("queryLength", toLegacyFeatureName("query_length"))
}
@Test
fun `non trivial legacy names are taken from lookup table`() {
assertEquals("pluginType", toLegacyFeatureName("action_plugin_type"))
assertEquals("isFromLibrary", toLegacyFeatureName("is_in_library"))
assertEquals("maxUsageSE", toLegacyFeatureName("max_usage_se"))
assertEquals("minUsageSE", toLegacyFeatureName("min_usage_se"))
}
@Test
fun `aliases are added and existing keys are preserved`() {
val features = mapOf(
"query_length" to 7,
"queryLength" to 3,
"is_in_library" to true,
)
val featuresWithAliases = withLegacyFeatureNameAliases(features)
assertEquals(7, featuresWithAliases["query_length"])
assertEquals(3, featuresWithAliases["queryLength"])
assertEquals(true, featuresWithAliases["is_in_library"])
assertEquals(true, featuresWithAliases["isFromLibrary"])
}
}