ML-based ranking in the plugin manager

(MP-6452) Fix failing project structure and packaging tests

(MP-6452) Make sure that machine learning score is calculated correctly for each plugin

Previously, there was a cache with mutable list of features. There were two issues with it. First, because of mutability, it could store mlScore for a different query sometimes. Second, there were no guarantees on consequent call of ranking and features logging before the next ranking event happens, so the feature cache could be inconsistent in certain scenarios.

(MP-6452) Introduce search index in plugin manager events that corresponds to actual query order

I also change the definition of plugin manager session. The session is restarted only in two cases: when a user clicks on the Plugins section in settings or when the search is restarted after a plugin installation.

(MP-6452) Add additional features: is result ordered by ML, is user internal, experiment group and version

(MP-6452) Fix missing date of latest plugin update, add days since latest updates and textual features

(MP-6452) Apply suggestions from the code review

(MP-6452) Integrate the ranking plugin with the search and the logging group

In feature extractors, during the first run, I calculate the features for a model to predict plugin relevance and on the second run the features to report to the MP collector (with predicted score, for example). I also implement features cache to calculate the most of the features only once

(MP-6452) Implement the baseline plugin manager session id definition and log it to FUS and MP recorders

I attach the start of a session to enableSearch method of PluginManagerConfigurable that gets triggered on "Plugins" setting group selection in the menu

(MP-6452) Add plugin for plugins ranking in the Plugin Manager

ML in SE: add embedding search integration test subsystem tag


Merge-request: IJ-MR-130364
Merged-by: Evgeny Abramov <Evgeny.Abramov@jetbrains.com>

GitOrigin-RevId: 9136d316aec2ede74bec07798dd8db16e7849f54
This commit is contained in:
Evgeny Abramov
2024-06-04 14:58:32 +00:00
committed by intellij-monorepo-bot
parent d458731be8
commit cc938f1533
30 changed files with 563 additions and 95 deletions

1
.idea/modules.xml generated
View File

@@ -576,6 +576,7 @@
<module fileurl="file://$PROJECT_DIR$/plugins/markdown/spellchecker/intellij.markdown.spellchecker.iml" filepath="$PROJECT_DIR$/plugins/markdown/spellchecker/intellij.markdown.spellchecker.iml" />
<module fileurl="file://$PROJECT_DIR$/plugins/markdown/test/intellij.markdown.tests.iml" filepath="$PROJECT_DIR$/plugins/markdown/test/intellij.markdown.tests.iml" />
<module fileurl="file://$PROJECT_DIR$/plugins/markdown/xml/intellij.markdown.xml.iml" filepath="$PROJECT_DIR$/plugins/markdown/xml/intellij.markdown.xml.iml" />
<module fileurl="file://$PROJECT_DIR$/plugins/marketplace-ml/intellij.marketplaceMl.iml" filepath="$PROJECT_DIR$/plugins/marketplace-ml/intellij.marketplaceMl.iml" />
<module fileurl="file://$PROJECT_DIR$/plugins/maven/intellij.maven.iml" filepath="$PROJECT_DIR$/plugins/maven/intellij.maven.iml" />
<module fileurl="file://$PROJECT_DIR$/plugins/maven/artifact-resolver/common/intellij.maven.artifactResolver.common.iml" filepath="$PROJECT_DIR$/plugins/maven/artifact-resolver/common/intellij.maven.artifactResolver.common.iml" />
<module fileurl="file://$PROJECT_DIR$/plugins/maven/artifact-resolver-m31/intellij.maven.artifactResolver.m31.iml" filepath="$PROJECT_DIR$/plugins/maven/artifact-resolver-m31/intellij.maven.artifactResolver.m31.iml" />

View File

@@ -178,6 +178,7 @@
<orderEntry type="module" module-name="intellij.searchEverywhereMl.semantics.java" scope="RUNTIME" />
<orderEntry type="module" module-name="intellij.searchEverywhereMl.semantics.kotlin" scope="RUNTIME" />
<orderEntry type="module" module-name="intellij.searchEverywhereMl.semantics.python" scope="RUNTIME" />
<orderEntry type="module" module-name="intellij.marketplaceMl" scope="RUNTIME" />
<orderEntry type="module" module-name="intellij.toml" scope="RUNTIME" />
<orderEntry type="module" module-name="intellij.platform.tracing.ide" scope="RUNTIME" />
<orderEntry type="module" module-name="intellij.notebooks.visualization" scope="RUNTIME" />

View File

@@ -84,6 +84,7 @@ val IDEA_BUNDLED_PLUGINS: PersistentList<String> = DEFAULT_BUNDLED_PLUGINS + per
"intellij.grazie",
"intellij.featuresTrainer",
"intellij.searchEverywhereMl",
"intellij.marketplaceMl",
"intellij.platform.tracing.ide",
"intellij.toml",
KotlinPluginBuilder.MAIN_KOTLIN_PLUGIN_MODULE,

View File

@@ -8172,20 +8172,22 @@ f:com.intellij.ide.plugins.marketplace.statistics.features.PluginManagerMarketpl
f:com.intellij.ide.plugins.marketplace.statistics.features.PluginManagerSearchResultFeatureProvider
- sf:INSTANCE:com.intellij.ide.plugins.marketplace.statistics.features.PluginManagerSearchResultFeatureProvider
- f:getFeaturesDefinition():com.intellij.internal.statistic.eventLog.events.EventField[]
- f:getSearchStateFeatures(java.lang.String,com.intellij.ide.plugins.IdeaPluginDescriptor):java.util.List
- f:getSearchStateFeatures(java.lang.String,com.intellij.ide.plugins.IdeaPluginDescriptor,java.util.Map):java.util.List
- bs:getSearchStateFeatures$default(com.intellij.ide.plugins.marketplace.statistics.features.PluginManagerSearchResultFeatureProvider,java.lang.String,com.intellij.ide.plugins.IdeaPluginDescriptor,java.util.Map,I,java.lang.Object):java.util.List
f:com.intellij.ide.plugins.marketplace.statistics.features.PluginManagerSearchResultMarketplaceFeatureProvider
- sf:INSTANCE:com.intellij.ide.plugins.marketplace.statistics.features.PluginManagerSearchResultMarketplaceFeatureProvider
- f:getFeaturesDefinition():com.intellij.internal.statistic.eventLog.events.EventField[]
- f:getSearchStateFeatures(com.intellij.ide.plugins.PluginNode):com.intellij.internal.statistic.eventLog.events.ObjectEventData
- f:getSearchStateFeatures(com.intellij.ide.plugins.PluginNode):java.util.List
f:com.intellij.ide.plugins.marketplace.statistics.features.PluginManagerSearchResultsFeatureProvider
- sf:INSTANCE:com.intellij.ide.plugins.marketplace.statistics.features.PluginManagerSearchResultsFeatureProvider
- f:getCommonFeatures(java.lang.String,java.util.List):java.util.ArrayList
- f:getFeaturesDefinition():com.intellij.internal.statistic.eventLog.events.EventField[]
- f:getSearchStateFeatures(java.lang.String,java.util.List):java.util.ArrayList
- f:getSearchStateFeatures(java.lang.String,java.util.List,java.util.Map):java.util.List
f:com.intellij.ide.plugins.marketplace.statistics.features.PluginManagerUserQueryFeatureProvider
- sf:INSTANCE:com.intellij.ide.plugins.marketplace.statistics.features.PluginManagerUserQueryFeatureProvider
- f:getFeaturesDefinition():com.intellij.internal.statistic.eventLog.events.EventField[]
- f:getSearchStateFeatures(java.lang.String):java.util.List
f:com.intellij.ide.plugins.marketplace.statistics.validators.MarketplaceTagsListValidator
f:com.intellij.ide.plugins.marketplace.statistics.validators.MarketplaceTagValidator
- com.intellij.internal.statistic.eventLog.validator.rules.FUSRule
- com.intellij.internal.statistic.eventLog.validator.rules.PerformanceCareRule
- com.intellij.internal.statistic.eventLog.validator.rules.impl.UtilValidationRule

View File

@@ -34,7 +34,6 @@ com/intellij/internal/statistic/eventLog/events/EventId1
com/intellij/internal/statistic/eventLog/events/EventId2
com/intellij/internal/statistic/eventLog/events/EventId3
com/intellij/internal/statistic/eventLog/events/IntListEventField
com/intellij/internal/statistic/eventLog/events/ObjectEventData
com/intellij/internal/statistic/eventLog/events/ObjectEventField
com/intellij/internal/statistic/eventLog/events/PrimitiveEventField
com/intellij/internal/statistic/eventLog/events/StringEventField

View File

@@ -12,7 +12,9 @@ import com.intellij.ide.plugins.certificates.PluginCertificateManager;
import com.intellij.ide.plugins.enums.PluginsGroupType;
import com.intellij.ide.plugins.enums.SortBy;
import com.intellij.ide.plugins.marketplace.MarketplaceRequests;
import com.intellij.ide.plugins.marketplace.ranking.MarketplaceLocalRanker;
import com.intellij.ide.plugins.marketplace.statistics.PluginManagerUsageCollector;
import com.intellij.ide.plugins.marketplace.statistics.features.PluginManagerSearchResultFeatureProvider;
import com.intellij.ide.plugins.newui.*;
import com.intellij.ide.util.PropertiesComponent;
import com.intellij.openapi.actionSystem.*;
@@ -225,6 +227,8 @@ public final class PluginManagerConfigurable
createMarketplaceTab();
createInstalledTab();
PluginManagerUsageCollector.sessionStarted();
myCardPanel = new MultiPanel() {
@Override
protected JComponent create(Integer key) {
@@ -772,9 +776,12 @@ public final class PluginManagerConfigurable
new SearchResultPanel(marketplaceController, panel, true, 0, 0) {
@Override
protected void handleQuery(@NotNull String query, @NotNull PluginsGroup result) {
int searchIndex = PluginManagerUsageCollector.updateAndGetSearchIndex();
try {
SearchQueryParser.Marketplace parser = new SearchQueryParser.Marketplace(query);
Map<IdeaPluginDescriptor, Double> pluginToScore = null;
if (parser.internal) {
PluginsViewCustomizer.PluginsGroupDescriptor groupDescriptor =
getPluginsViewCustomizer().getInternalPluginsGroupDescriptor();
@@ -841,6 +848,11 @@ public final class PluginManagerConfigurable
ContainerUtil.removeDuplicates(result.descriptors);
final var localRanker = MarketplaceLocalRanker.getInstanceIfEnabled();
if (localRanker != null) {
pluginToScore = localRanker.rankPlugins(parser, result.descriptors);
}
if (!result.descriptors.isEmpty()) {
String title = IdeBundle.message("plugin.manager.action.label.sort.by.1");
@@ -868,7 +880,8 @@ public final class PluginManagerConfigurable
}
}
PluginManagerUsageCollector.performMarketplaceSearch(ProjectUtil.getActiveProject(), parser, result.descriptors);
PluginManagerUsageCollector.performMarketplaceSearch(
ProjectUtil.getActiveProject(), parser, result.descriptors, searchIndex, pluginToScore);
}
catch (IOException e) {
LOG.info(e);
@@ -1164,6 +1177,7 @@ public final class PluginManagerConfigurable
@Override
protected void handleQuery(@NotNull String query, @NotNull PluginsGroup result) {
int searchIndex = PluginManagerUsageCollector.updateAndGetSearchIndex();
myPluginModel.setInvalidFixCallback(null);
SearchQueryParser.Installed parser = new SearchQueryParser.Installed(query);
@@ -1224,7 +1238,8 @@ public final class PluginManagerConfigurable
}
result.descriptors.addAll(descriptors);
PluginManagerUsageCollector.performInstalledTabSearch(ProjectUtil.getActiveProject(), parser, result.descriptors);
PluginManagerUsageCollector.performInstalledTabSearch(
ProjectUtil.getActiveProject(), parser, result.descriptors, searchIndex, null);
if (!result.descriptors.isEmpty()) {
if (parser.invalid) {
@@ -1988,6 +2003,7 @@ public final class PluginManagerConfigurable
return () -> {
};
}
if (StringUtil.isEmpty(option) && (myTabHeaderComponent.getSelectionTab() == MARKETPLACE_TAB || myInstalledSearchPanel.isEmpty())) {
return null;
}

View File

@@ -83,7 +83,7 @@ internal class MarketplaceSearchPluginData(
var isPaid: Boolean = false,
val rating: Double = 0.0,
val name: String = "",
private val cdate: Long? = null,
val cdate: Long? = null,
val organization: String = "",
@get:JsonProperty("updateId")
val externalUpdateId: String? = null,

View File

@@ -0,0 +1,34 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.ide.plugins.marketplace.ranking
import com.intellij.ide.plugins.IdeaPluginDescriptor
import com.intellij.ide.plugins.newui.SearchQueryParser
import com.intellij.openapi.extensions.ExtensionPointName
import org.jetbrains.annotations.ApiStatus
@ApiStatus.Internal
interface MarketplaceLocalRanker {
companion object {
val EP_NAME: ExtensionPointName<MarketplaceLocalRanker> = ExtensionPointName.create("com.intellij.marketplaceLocalRanker")
@JvmStatic
fun getInstanceIfEnabled(): MarketplaceLocalRanker? {
return EP_NAME.extensionList.firstOrNull()?.takeIf { it.isEnabled() }
}
}
/**
* Indicates whether machine learning in Marketplace is enabled.
* This method can return false if ML-ranking is disabled and no experiments are allowed
*/
fun isEnabled(): Boolean
/**
* Ranks the plugins inplace within a single lookup in the Marketplace tab of Plugin Manager.
* Returns the plugin relevance scores assigned by the ranking model.
*/
fun rankPlugins(queryParser: SearchQueryParser.Marketplace, plugins: MutableList<IdeaPluginDescriptor>): Map<IdeaPluginDescriptor, Double>
val experimentGroup: Int
val experimentVersion: Int
}

View File

@@ -12,6 +12,8 @@ import com.intellij.ide.plugins.newui.SearchQueryParser
import com.intellij.openapi.extensions.PluginId
import com.intellij.openapi.project.Project
import org.jetbrains.annotations.ApiStatus
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicInteger
/*
Collects plugin manager usage statistics:
@@ -26,33 +28,63 @@ internal object PluginManagerUsageCollector {
private val fusCollector = PluginManagerFUSCollector()
private val mpCollector = PluginManagerMPCollector()
// Plugin manager search session identifier which is unique within one IDE session
private val sessionId = AtomicInteger(-1)
// Search index within one plugin manager search session. The order corresponds to the order of query updates
private val searchIndex = AtomicInteger(0)
private val installedPluginInSession = AtomicBoolean(false)
@JvmStatic
fun sessionStarted(): Int {
searchIndex.set(0)
installedPluginInSession.set(false)
return sessionId.getAndIncrement()
}
/**
* Event that happens on search restart intention, before receiving the list with plugins.
* Unlike with [performMarketplaceSearch] and [performInstalledTabSearch],
* the order of these events corresponds to the order of query updates.
*/
@JvmStatic
fun updateAndGetSearchIndex(): Int {
// If we perform search after installing a plugin, we consider this as a new search session:
if (installedPluginInSession.compareAndSet(true, false)) sessionStarted()
return searchIndex.getAndIncrement()
}
@JvmStatic
fun performMarketplaceSearch(
project: Project?,
query: SearchQueryParser.Marketplace,
results: List<IdeaPluginDescriptor>
) = mpCollector.performMarketplaceSearch(project, query, results)
results: List<IdeaPluginDescriptor>,
searchIndex: Int,
pluginToScore: Map<IdeaPluginDescriptor, Double>? = null
) = mpCollector.performMarketplaceSearch(project, query, results, searchIndex, sessionId.get(), pluginToScore)
@JvmStatic
fun performInstalledTabSearch(
project: Project?,
query: SearchQueryParser.Installed,
results: List<IdeaPluginDescriptor>
) = mpCollector.performInstalledTabSearch(project, query, results)
results: List<IdeaPluginDescriptor>,
searchIndex: Int,
pluginToScore: Map<IdeaPluginDescriptor, Double>? = null
) = mpCollector.performInstalledTabSearch(project, query, results, searchIndex, sessionId.get(), pluginToScore)
@JvmStatic
fun searchReset() = mpCollector.searchReset()
fun searchReset() = mpCollector.searchReset(sessionId.get())
@JvmStatic
fun pluginCardOpened(descriptor: IdeaPluginDescriptor, group: PluginsGroup?) {
fusCollector.pluginCardOpened(descriptor, group)
mpCollector.pluginCardOpened(descriptor, group)
fusCollector.pluginCardOpened(descriptor, group, sessionId.get())
mpCollector.pluginCardOpened(descriptor, group, sessionId.get())
}
@JvmStatic
fun thirdPartyAcceptanceCheck(result: DialogAcceptanceResultEnum) {
fusCollector.thirdPartyAcceptanceCheck(result)
mpCollector.thirdPartyAcceptanceCheck(result)
fusCollector.thirdPartyAcceptanceCheck(result, sessionId.get())
mpCollector.thirdPartyAcceptanceCheck(result, sessionId.get())
}
@JvmStatic
@@ -61,14 +93,14 @@ internal object PluginManagerUsageCollector {
enable: Boolean,
project: Project? = null,
) {
fusCollector.pluginsStateChanged(descriptors, enable, project)
mpCollector.pluginsStateChanged(descriptors, enable, project)
fusCollector.pluginsStateChanged(descriptors, enable, project, sessionId.get())
mpCollector.pluginsStateChanged(descriptors, enable, project, sessionId.get())
}
@JvmStatic
fun pluginRemoved(pluginId: PluginId) {
fusCollector.pluginRemoved(pluginId)
mpCollector.pluginRemoved(pluginId)
fusCollector.pluginRemoved(pluginId, sessionId.get())
mpCollector.pluginRemoved(pluginId, sessionId.get())
}
@JvmStatic
@@ -77,23 +109,24 @@ internal object PluginManagerUsageCollector {
source: InstallationSourceEnum,
previousVersion: String? = null
) {
fusCollector.pluginInstallationStarted(descriptor, source, previousVersion)
mpCollector.pluginInstallationStarted(descriptor, source, previousVersion)
fusCollector.pluginInstallationStarted(descriptor, source, sessionId.get(), previousVersion)
mpCollector.pluginInstallationStarted(descriptor, source, sessionId.get(), previousVersion)
}
@JvmStatic
fun pluginInstallationFinished(descriptor: IdeaPluginDescriptor) {
fusCollector.pluginInstallationFinished(descriptor)
mpCollector.pluginInstallationFinished(descriptor)
installedPluginInSession.set(true)
fusCollector.pluginInstallationFinished(descriptor, sessionId.get())
mpCollector.pluginInstallationFinished(descriptor, sessionId.get())
}
fun signatureCheckResult(descriptor: IdeaPluginDescriptor, result: SignatureVerificationResult) {
fusCollector.signatureCheckResult(descriptor, result)
mpCollector.signatureCheckResult(descriptor, result)
fusCollector.signatureCheckResult(descriptor, result, sessionId.get())
mpCollector.signatureCheckResult(descriptor, result, sessionId.get())
}
fun signatureWarningShown(descriptor: IdeaPluginDescriptor, result: DialogAcceptanceResultEnum) {
fusCollector.signatureWarningShown(descriptor, result)
mpCollector.signatureWarningShown(descriptor, result)
fusCollector.signatureWarningShown(descriptor, result, sessionId.get())
mpCollector.signatureWarningShown(descriptor, result, sessionId.get())
}
}

View File

@@ -12,6 +12,7 @@ import com.intellij.ide.plugins.newui.PluginsGroup
import com.intellij.internal.statistic.eventLog.EventLogGroup
import com.intellij.internal.statistic.eventLog.events.BaseEventId
import com.intellij.internal.statistic.eventLog.events.EventFields
import com.intellij.internal.statistic.eventLog.events.IntEventField
import com.intellij.internal.statistic.service.fus.collectors.CounterUsagesCollector
import com.intellij.internal.statistic.utils.getPluginInfoByDescriptor
import com.intellij.internal.statistic.utils.getPluginInfoById
@@ -21,84 +22,97 @@ import com.intellij.openapi.project.Project
import org.jetbrains.annotations.ApiStatus
internal const val PM_FUS_GROUP_ID = "plugin.manager"
internal const val PM_FUS_GROUP_VERSION = 8
internal const val PM_FUS_GROUP_VERSION = 9
private val EVENT_GROUP = EventLogGroup(PM_FUS_GROUP_ID, PM_FUS_GROUP_VERSION)
@ApiStatus.Internal
open class PluginManagerFUSCollector : CounterUsagesCollector() {
override fun getGroup() = EVENT_GROUP
@Suppress("PropertyName")
protected val PLUGIN_MANAGER_SESSION_ID = IntEventField("sessionId")
@Suppress("PropertyName")
protected val PLUGIN_MANAGER_SEARCH_INDEX = IntEventField("searchIndex")
private val PLUGINS_GROUP_TYPE = EventFields.Enum<PluginsGroupType>("group")
private val ENABLE_DISABLE_ACTION = EventFields.Enum<PluginEnabledState>("enabled_state")
private val ACCEPTANCE_RESULT = EventFields.Enum<DialogAcceptanceResultEnum>("acceptance_result")
private val PLUGIN_SOURCE = EventFields.Enum<InstallationSourceEnum>("source")
private val PREVIOUS_VERSION = PluginVersionEventField("previous_version")
private val SIGNATURE_CHECK_RESULT = EventFields.Enum<SignatureVerificationResult>("signature_check_result")
private val PLUGIN_LIST_INDEX = EventFields.Int("index")
private val PLUGIN_CARD_OPENED = group.registerEvent(
"plugin.search.card.opened", EventFields.PluginInfo, PLUGINS_GROUP_TYPE, EventFields.Int("index")
private val PLUGIN_CARD_OPENED = group.registerVarargEvent(
"plugin.search.card.opened", EventFields.PluginInfo, PLUGINS_GROUP_TYPE,
PLUGIN_LIST_INDEX, PLUGIN_MANAGER_SESSION_ID
)
private val THIRD_PARTY_ACCEPTANCE_CHECK = group.registerEvent("plugin.install.third.party.check",
ACCEPTANCE_RESULT)
ACCEPTANCE_RESULT, PLUGIN_MANAGER_SESSION_ID)
private val PLUGIN_SIGNATURE_WARNING = group.registerEvent(
"plugin.signature.warning.shown", EventFields.PluginInfo, ACCEPTANCE_RESULT
"plugin.signature.warning.shown", EventFields.PluginInfo, ACCEPTANCE_RESULT, PLUGIN_MANAGER_SESSION_ID
)
private val PLUGIN_SIGNATURE_CHECK_RESULT = group.registerEvent(
"plugin.signature.check.result", EventFields.PluginInfo, SIGNATURE_CHECK_RESULT
"plugin.signature.check.result", EventFields.PluginInfo, SIGNATURE_CHECK_RESULT, PLUGIN_MANAGER_SESSION_ID
)
private val PLUGIN_STATE_CHANGED = group.registerEvent(
"plugin.state.changed", EventFields.PluginInfo, ENABLE_DISABLE_ACTION
"plugin.state.changed", EventFields.PluginInfo, ENABLE_DISABLE_ACTION, PLUGIN_MANAGER_SESSION_ID
)
private val PLUGIN_INSTALLATION_STARTED = group.registerEvent(
"plugin.installation.started", PLUGIN_SOURCE, EventFields.PluginInfo, PREVIOUS_VERSION
private val PLUGIN_INSTALLATION_STARTED = group.registerVarargEvent(
"plugin.installation.started", PLUGIN_SOURCE, EventFields.PluginInfo, PREVIOUS_VERSION, PLUGIN_MANAGER_SESSION_ID
)
private val PLUGIN_INSTALLATION_FINISHED = group.registerEvent("plugin.installation.finished", EventFields.PluginInfo)
private val PLUGIN_REMOVED = group.registerEvent("plugin.was.removed", EventFields.PluginInfo)
private val PLUGIN_INSTALLATION_FINISHED = group.registerEvent("plugin.installation.finished", EventFields.PluginInfo, PLUGIN_MANAGER_SESSION_ID)
private val PLUGIN_REMOVED = group.registerEvent("plugin.was.removed", EventFields.PluginInfo, PLUGIN_MANAGER_SESSION_ID)
fun pluginCardOpened(descriptor: IdeaPluginDescriptor, group: PluginsGroup?): Unit? = group?.let {
PLUGIN_CARD_OPENED.log(getPluginInfoByDescriptor(descriptor), it.type, it.getPluginIndex(descriptor.pluginId))
fun pluginCardOpened(descriptor: IdeaPluginDescriptor, group: PluginsGroup?, sessionId: Int): Unit? = group?.let {
PLUGIN_CARD_OPENED.log(
EventFields.PluginInfo.with(getPluginInfoByDescriptor(descriptor)),
PLUGINS_GROUP_TYPE.with(it.type),
EventFields.Int("index").with(it.getPluginIndex(descriptor.pluginId)),
PLUGIN_MANAGER_SESSION_ID.with(sessionId)
)
}
fun thirdPartyAcceptanceCheck(result: DialogAcceptanceResultEnum) {
THIRD_PARTY_ACCEPTANCE_CHECK.getIfInitializedOrNull()?.log(result)
fun thirdPartyAcceptanceCheck(result: DialogAcceptanceResultEnum, sessionId: Int) {
THIRD_PARTY_ACCEPTANCE_CHECK.getIfInitializedOrNull()?.log(result, sessionId)
}
fun pluginsStateChanged(
descriptors: Collection<IdeaPluginDescriptor>,
enable: Boolean,
project: Project? = null,
sessionId: Int
) {
PLUGIN_STATE_CHANGED.getIfInitializedOrNull()?.let { event ->
descriptors.forEach { descriptor ->
event.log(
project,
getPluginInfoByDescriptor(descriptor),
PluginEnabledState.getState(enable),
)
event.log(project, getPluginInfoByDescriptor(descriptor), PluginEnabledState.getState(enable), sessionId)
}
}
}
fun pluginRemoved(pluginId: PluginId): Unit? = PLUGIN_REMOVED.getIfInitializedOrNull()?.log(getPluginInfoById(pluginId))
fun pluginRemoved(pluginId: PluginId, sessionId: Int): Unit? = PLUGIN_REMOVED.getIfInitializedOrNull()
?.log(getPluginInfoById(pluginId), sessionId)
fun pluginInstallationStarted(
descriptor: IdeaPluginDescriptor,
source: InstallationSourceEnum,
previousVersion: String? = null
sessionId: Int,
previousVersion: String? = null,
) {
val pluginInfo = getPluginInfoByDescriptor(descriptor)
PLUGIN_INSTALLATION_STARTED.getIfInitializedOrNull()?.log(source, pluginInfo, pluginInfo to previousVersion)
PLUGIN_INSTALLATION_STARTED.getIfInitializedOrNull()?.log(
PLUGIN_SOURCE.with(source), EventFields.PluginInfo.with(pluginInfo),
PREVIOUS_VERSION.with(pluginInfo to previousVersion), PLUGIN_MANAGER_SESSION_ID.with(sessionId))
}
fun pluginInstallationFinished(descriptor: IdeaPluginDescriptor): Unit? = getPluginInfoByDescriptor(descriptor).let {
PLUGIN_INSTALLATION_FINISHED.getIfInitializedOrNull()?.log(it)
fun pluginInstallationFinished(descriptor: IdeaPluginDescriptor, sessionId: Int): Unit? = getPluginInfoByDescriptor(descriptor).let {
PLUGIN_INSTALLATION_FINISHED.getIfInitializedOrNull()?.log(it, sessionId)
}
fun signatureCheckResult(descriptor: IdeaPluginDescriptor, result: SignatureVerificationResult): Unit? =
PLUGIN_SIGNATURE_CHECK_RESULT.getIfInitializedOrNull()?.log(getPluginInfoByDescriptor(descriptor), result)
fun signatureCheckResult(descriptor: IdeaPluginDescriptor, result: SignatureVerificationResult, sessionId: Int): Unit? =
PLUGIN_SIGNATURE_CHECK_RESULT.getIfInitializedOrNull()?.log(getPluginInfoByDescriptor(descriptor), result, sessionId)
fun signatureWarningShown(descriptor: IdeaPluginDescriptor, result: DialogAcceptanceResultEnum): Unit? =
PLUGIN_SIGNATURE_WARNING.getIfInitializedOrNull()?.log(getPluginInfoByDescriptor(descriptor), result)
fun signatureWarningShown(descriptor: IdeaPluginDescriptor, result: DialogAcceptanceResultEnum, sessionId: Int): Unit? =
PLUGIN_SIGNATURE_WARNING.getIfInitializedOrNull()?.log(getPluginInfoByDescriptor(descriptor), result, sessionId)
// We don't want to log actions when app did not initialize yet (e.g. migration process)
protected fun <T : BaseEventId> T.getIfInitializedOrNull(): T? = if (ApplicationManager.getApplication() == null) null else this

View File

@@ -16,7 +16,7 @@ import org.jetbrains.annotations.ApiStatus
private const val PM_MP_GROUP_ID = "mp.$PM_FUS_GROUP_ID"
private const val PM_MP_GROUP_VERSION = 1
private const val PM_MP_GROUP_VERSION = 2
private val EVENT_GROUP = EventLogGroup(
PM_MP_GROUP_ID,
// this is needed to be able to change `PM_MP_GROUP_ID` child group without a requirement to update `PM_FUS_GROUP_ID` parent group version.
@@ -43,14 +43,18 @@ class PluginManagerMPCollector : PluginManagerFUSCollector() {
)
private val MARKETPLACE_TAB_SEARCH_PERFORMED = group.registerVarargEvent(
"marketplace.tab.search", USER_QUERY_FEATURES_DATA_KEY, MARKETPLACE_SEARCH_FEATURES_DATA_KEY, SEARCH_RESULTS_FEATURES_DATA_KEY
"marketplace.tab.search", USER_QUERY_FEATURES_DATA_KEY, MARKETPLACE_SEARCH_FEATURES_DATA_KEY,
SEARCH_RESULTS_FEATURES_DATA_KEY, PLUGIN_MANAGER_SESSION_ID, PLUGIN_MANAGER_SEARCH_INDEX
)
private val INSTALLED_TAB_SEARCH_PERFORMED = group.registerVarargEvent(
"installed.tab.search", USER_QUERY_FEATURES_DATA_KEY, LOCAL_SEARCH_FEATURES_DATA_KEY, SEARCH_RESULTS_FEATURES_DATA_KEY
"installed.tab.search", USER_QUERY_FEATURES_DATA_KEY, LOCAL_SEARCH_FEATURES_DATA_KEY,
SEARCH_RESULTS_FEATURES_DATA_KEY, PLUGIN_MANAGER_SESSION_ID, PLUGIN_MANAGER_SEARCH_INDEX
)
private val SEARCH_RESET = group.registerEvent("search.reset")
private val SEARCH_RESET = group.registerEvent("search.reset", PLUGIN_MANAGER_SESSION_ID)
fun performMarketplaceSearch(project: Project?, query: SearchQueryParser.Marketplace, results: List<IdeaPluginDescriptor>) {
fun performMarketplaceSearch(project: Project?, query: SearchQueryParser.Marketplace,
results: List<IdeaPluginDescriptor>, searchIndex: Int, sessionId: Int,
pluginToScore: Map<IdeaPluginDescriptor, Double>? = null) {
MARKETPLACE_TAB_SEARCH_PERFORMED.getIfInitializedOrNull()?.log(project) {
add(USER_QUERY_FEATURES_DATA_KEY.with(ObjectEventData(
PluginManagerUserQueryFeatureProvider.getSearchStateFeatures(query.searchQuery)
@@ -59,12 +63,16 @@ class PluginManagerMPCollector : PluginManagerFUSCollector() {
PluginManagerMarketplaceSearchFeatureProvider.getSearchStateFeatures(query)
)))
add(SEARCH_RESULTS_FEATURES_DATA_KEY.with(ObjectEventData(
PluginManagerSearchResultsFeatureProvider.getSearchStateFeatures(query.searchQuery, results)
PluginManagerSearchResultsFeatureProvider.getSearchStateFeatures(query.searchQuery, results, pluginToScore)
)))
add(PLUGIN_MANAGER_SESSION_ID.with(sessionId))
add(PLUGIN_MANAGER_SEARCH_INDEX.with(searchIndex))
}
}
fun performInstalledTabSearch(project: Project?, query: SearchQueryParser.Installed, results: List<IdeaPluginDescriptor>) {
fun performInstalledTabSearch(project: Project?, query: SearchQueryParser.Installed,
results: List<IdeaPluginDescriptor>, searchIndex: Int, sessionId: Int,
pluginToScore: Map<IdeaPluginDescriptor, Double>? = null) {
INSTALLED_TAB_SEARCH_PERFORMED.getIfInitializedOrNull()?.log(project) {
add(USER_QUERY_FEATURES_DATA_KEY.with(ObjectEventData(
PluginManagerUserQueryFeatureProvider.getSearchStateFeatures(query.searchQuery)
@@ -73,12 +81,14 @@ class PluginManagerMPCollector : PluginManagerFUSCollector() {
PluginManagerLocalSearchFeatureProvider.getSearchStateFeatures(query)
)))
add(SEARCH_RESULTS_FEATURES_DATA_KEY.with(ObjectEventData(
PluginManagerSearchResultsFeatureProvider.getSearchStateFeatures(query.searchQuery, results)
PluginManagerSearchResultsFeatureProvider.getSearchStateFeatures(query.searchQuery, results, pluginToScore)
)))
add(PLUGIN_MANAGER_SESSION_ID.with(sessionId))
add(PLUGIN_MANAGER_SEARCH_INDEX.with(searchIndex))
}
}
fun searchReset() {
SEARCH_RESET.log()
fun searchReset(sessionId: Int) {
SEARCH_RESET.log(sessionId)
}
}

View File

@@ -0,0 +1,23 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.ide.plugins.marketplace.statistics.features
import com.intellij.internal.statistic.eventLog.events.EventField
import com.intellij.openapi.extensions.ExtensionPointName
import com.intellij.internal.statistic.eventLog.events.EventPair
import org.jetbrains.annotations.ApiStatus
@ApiStatus.Internal
interface MarketplaceTextualFeaturesProvider {
companion object {
val EP_NAME: ExtensionPointName<MarketplaceTextualFeaturesProvider> = ExtensionPointName.create("com.intellij.marketplaceTextualFeaturesProvider")
@JvmStatic
fun getInstanceIfEnabled(): MarketplaceTextualFeaturesProvider? {
return EP_NAME.extensionList.firstOrNull()
}
}
fun getFeaturesDefinition(): Array<EventField<*>>
fun getTextualFeatures(query: String, match: String): List<EventPair<*>>
}

View File

@@ -2,13 +2,15 @@
package com.intellij.ide.plugins.marketplace.statistics.features
import com.intellij.ide.plugins.enums.SortBy
import com.intellij.ide.plugins.marketplace.statistics.validators.MarketplaceTagsListValidator
import com.intellij.ide.plugins.marketplace.ranking.MarketplaceLocalRanker
import com.intellij.ide.plugins.marketplace.statistics.validators.MarketplaceTagValidator
import com.intellij.ide.plugins.marketplace.statistics.validators.MarketplaceVendorsListValidator
import com.intellij.ide.plugins.marketplace.utils.MarketplaceUrls
import com.intellij.ide.plugins.newui.SearchQueryParser
import com.intellij.internal.statistic.eventLog.events.EventField
import com.intellij.internal.statistic.eventLog.events.EventFields
import com.intellij.internal.statistic.eventLog.events.EventPair
import com.intellij.openapi.application.ApplicationManager
object PluginManagerMarketplaceSearchFeatureProvider {
private val IS_SUGGESTED_DATA_KEY = EventFields.Boolean("isSuggested")
@@ -20,24 +22,39 @@ object PluginManagerMarketplaceSearchFeatureProvider {
"vendorsListFilter", MarketplaceVendorsListValidator::class.java
)
private val TAGS_LIST_FILTER_DATA_KEY = EventFields.StringListValidatedByCustomRule(
"tagsListFilter", MarketplaceTagsListValidator::class.java
"tagsListFilter", MarketplaceTagValidator::class.java
)
private val IS_ORDERED_BY_ML = EventFields.Boolean("isOrderedByML")
private val ML_EXPERIMENT_GROUP = EventFields.Int("experimentGroup")
private val ML_EXPERIMENT_VERSION = EventFields.Int("experimentVersion")
private val IS_USER_INTERNAL = EventFields.Boolean("isUserInternal")
fun getFeaturesDefinition(): Array<EventField<*>> {
return arrayOf(
IS_SUGGESTED_DATA_KEY, IS_STAFF_PICKS_DATA_KEY, CUSTOM_REPOSITORY_COUNT_DATA_KEY, MARKETPLACE_CUSTOM_REPOSITORY_COUNT_DATA_KEY,
SORT_BY_DATA_KEY, VENDORS_LIST_FILTER_DATA_KEY, TAGS_LIST_FILTER_DATA_KEY
SORT_BY_DATA_KEY, VENDORS_LIST_FILTER_DATA_KEY, TAGS_LIST_FILTER_DATA_KEY, IS_ORDERED_BY_ML, ML_EXPERIMENT_GROUP,
ML_EXPERIMENT_VERSION, IS_USER_INTERNAL
)
}
fun getSearchStateFeatures(query: SearchQueryParser.Marketplace): List<EventPair<*>> = buildList {
val localRanker = MarketplaceLocalRanker.getInstanceIfEnabled()
addAll(listOf(
IS_SUGGESTED_DATA_KEY.with(query.suggested),
IS_STAFF_PICKS_DATA_KEY.with(query.staffPicks),
CUSTOM_REPOSITORY_COUNT_DATA_KEY.with(query.repositories.size),
MARKETPLACE_CUSTOM_REPOSITORY_COUNT_DATA_KEY.with(query.repositories.count { it.contains(MarketplaceUrls.getPluginManagerHost()) })
MARKETPLACE_CUSTOM_REPOSITORY_COUNT_DATA_KEY.with(query.repositories.count { it.contains(MarketplaceUrls.getPluginManagerHost()) }),
IS_ORDERED_BY_ML.with(localRanker != null)
))
localRanker?.run {
add(ML_EXPERIMENT_GROUP.with(experimentGroup))
add(ML_EXPERIMENT_VERSION.with(experimentVersion))
}
add(IS_USER_INTERNAL.with(ApplicationManager.getApplication().isInternal))
query.sortBy?.let { add(SORT_BY_DATA_KEY.with(it)) }
query.vendors?.toList()?.let { add(VENDORS_LIST_FILTER_DATA_KEY.with(it)) }
query.tags?.toList()?.let { add(TAGS_LIST_FILTER_DATA_KEY.with(it)) }

View File

@@ -5,28 +5,44 @@ import com.intellij.ide.plugins.IdeaPluginDescriptor
import com.intellij.ide.plugins.PluginNode
import com.intellij.internal.statistic.eventLog.events.*
import com.intellij.internal.statistic.utils.getPluginInfoByDescriptor
import kotlin.math.round
object PluginManagerSearchResultFeatureProvider {
private val NAME_LENGTH_DATA_KEY = EventFields.Int("nameLength")
private val DEVELOPED_BY_JETBRAINS_DATA_KEY = EventFields.Boolean("byJetBrains")
private val MARKETPLACE_INFO_DATA_KEY = ObjectEventField(
"marketplaceInfo", *PluginManagerSearchResultMarketplaceFeatureProvider.getFeaturesDefinition()
)
private val ML_SCORE = EventFields.Double("mlScore")
fun getFeaturesDefinition(): Array<EventField<*>> {
return arrayOf(
NAME_LENGTH_DATA_KEY, DEVELOPED_BY_JETBRAINS_DATA_KEY, EventFields.PluginInfo, MARKETPLACE_INFO_DATA_KEY
NAME_LENGTH_DATA_KEY, DEVELOPED_BY_JETBRAINS_DATA_KEY, EventFields.PluginInfo, ML_SCORE,
*PluginManagerSearchResultMarketplaceFeatureProvider.getFeaturesDefinition(),
*(MarketplaceTextualFeaturesProvider.getInstanceIfEnabled()?.getFeaturesDefinition() ?: emptyArray())
)
}
fun getSearchStateFeatures(userQuery: String?, descriptor: IdeaPluginDescriptor): List<EventPair<*>> = buildList {
val pluginInfo = getPluginInfoByDescriptor(descriptor)
fun getSearchStateFeatures(userQuery: String?, descriptor: IdeaPluginDescriptor,
pluginToScore: Map<IdeaPluginDescriptor, Double>? = null): List<EventPair<*>> {
return buildList {
val pluginInfo = getPluginInfoByDescriptor(descriptor)
add(NAME_LENGTH_DATA_KEY.with(descriptor.name.length))
add(DEVELOPED_BY_JETBRAINS_DATA_KEY.with(pluginInfo.isDevelopedByJetBrains()))
add(EventFields.PluginInfo.with(pluginInfo))
if (pluginInfo.isSafeToReport() && descriptor is PluginNode) {
add(MARKETPLACE_INFO_DATA_KEY.with(PluginManagerSearchResultMarketplaceFeatureProvider.getSearchStateFeatures(descriptor)))
add(NAME_LENGTH_DATA_KEY.with(descriptor.name.length))
add(DEVELOPED_BY_JETBRAINS_DATA_KEY.with(pluginInfo.isDevelopedByJetBrains()))
add(EventFields.PluginInfo.with(pluginInfo))
if (pluginInfo.isSafeToReport() && descriptor is PluginNode) {
addAll(PluginManagerSearchResultMarketplaceFeatureProvider.getSearchStateFeatures(descriptor))
}
if (userQuery != null) {
MarketplaceTextualFeaturesProvider.getInstanceIfEnabled()
?.getTextualFeatures(userQuery, descriptor.name)
?.also { addAll(it) }
}
pluginToScore?.get(descriptor)?.let {
add(ML_SCORE.with(roundDouble(it)))
}
}
}
private fun roundDouble(value: Double) = if (!value.isFinite()) -1.0 else round(value * 100000) / 100000
}

View File

@@ -4,7 +4,7 @@ package com.intellij.ide.plugins.marketplace.statistics.features
import com.intellij.ide.plugins.PluginNode
import com.intellij.internal.statistic.eventLog.events.EventField
import com.intellij.internal.statistic.eventLog.events.EventFields
import com.intellij.internal.statistic.eventLog.events.ObjectEventData
import com.intellij.internal.statistic.eventLog.events.EventPair
object PluginManagerSearchResultMarketplaceFeatureProvider {
@@ -13,6 +13,13 @@ object PluginManagerSearchResultMarketplaceFeatureProvider {
private val MARKETPLACE_DOWNLOADS_DATA_KEY = EventFields.Int("downloads")
private val MARKETPLACE_PLUGIN_ID_DATA_KEY = EventFields.Int("marketplaceId")
private val MARKETPLACE_PLUGIN_CDATE_DATA_KEY = EventFields.Long("date")
private val MARKETPLACE_PLUGIN_DAYS_SINCE_LATEST_UPDATE = EventFields.Long("daysSinceLatestUpdate")
// TODO: add when sent from backend:
// private val MARKETPLACE_PLUGIN_TAGS = EventFields.StringListValidatedByCustomRule<MarketplaceTagValidator>("pluginTags")
// private val MARKETPLACE_PLUGIN_HAS_SCREENSHOTS = EventFields.Boolean("hasScreenshots")
// TODO: add how often plugin gets updated
private const val MILLIS_IN_DAY = 1000 * 60 * 60 * 24
fun getFeaturesDefinition(): Array<EventField<*>> {
return arrayOf(
@@ -20,11 +27,12 @@ object PluginManagerSearchResultMarketplaceFeatureProvider {
MARKETPLACE_PAID_DATA_KEY,
MARKETPLACE_DOWNLOADS_DATA_KEY,
MARKETPLACE_PLUGIN_ID_DATA_KEY,
MARKETPLACE_PLUGIN_CDATE_DATA_KEY
MARKETPLACE_PLUGIN_CDATE_DATA_KEY,
MARKETPLACE_PLUGIN_DAYS_SINCE_LATEST_UPDATE,
)
}
fun getSearchStateFeatures(pluginNode: PluginNode): ObjectEventData = ObjectEventData(buildList {
fun getSearchStateFeatures(pluginNode: PluginNode): List<EventPair<*>> = buildList {
add(MARKETPLACE_PAID_DATA_KEY.with(pluginNode.isPaid))
pluginNode.rating?.toFloatOrNull()?.let {
add(MARKETPLACE_RATING_DATA_KEY.with(it))
@@ -35,6 +43,9 @@ object PluginManagerSearchResultMarketplaceFeatureProvider {
pluginNode.externalPluginId?.toIntOrNull()?.let {
add(MARKETPLACE_PLUGIN_ID_DATA_KEY.with(it))
}
add(MARKETPLACE_PLUGIN_CDATE_DATA_KEY.with(pluginNode.date))
})
if (pluginNode.date != Long.MAX_VALUE) {
add(MARKETPLACE_PLUGIN_CDATE_DATA_KEY.with(pluginNode.date))
add(MARKETPLACE_PLUGIN_DAYS_SINCE_LATEST_UPDATE.with((System.currentTimeMillis() - pluginNode.date) / MILLIS_IN_DAY))
}
}
}

View File

@@ -20,12 +20,18 @@ object PluginManagerSearchResultsFeatureProvider {
)
}
fun getSearchStateFeatures(userQuery: String?, result: List<IdeaPluginDescriptor>) = arrayListOf<EventPair<*>>(
fun getCommonFeatures(userQuery: String?, result: List<IdeaPluginDescriptor>) = arrayListOf<EventPair<*>>(
IS_EMPTY_DATA_KEY.with(result.isEmpty()),
RESULTS_COUNT_DATA_KEY.with(result.size),
RESULTS_COUNT_LIMIT_DATA_KEY.with(RESULTS_REPORT_COUNT),
RESULTS_DATA_KEY.with(result.take(RESULTS_REPORT_COUNT).map {
ObjectEventData(PluginManagerSearchResultFeatureProvider.getSearchStateFeatures(userQuery, it))
})
RESULTS_COUNT_LIMIT_DATA_KEY.with(RESULTS_REPORT_COUNT)
)
fun getSearchStateFeatures(userQuery: String?, result: List<IdeaPluginDescriptor>,
pluginToScore: Map<IdeaPluginDescriptor, Double>?): List<EventPair<*>> {
return getCommonFeatures(userQuery, result).apply {
add(RESULTS_DATA_KEY.with(result.take(RESULTS_REPORT_COUNT).map {
ObjectEventData(PluginManagerSearchResultFeatureProvider.getSearchStateFeatures(userQuery, it, pluginToScore))
}))
}
}
}

View File

@@ -9,7 +9,7 @@ import com.intellij.internal.statistic.eventLog.validator.rules.impl.CustomValid
/*
Validates if the tag name was provided by Marketplace and is therefore safe to report.
*/
class MarketplaceTagsListValidator : CustomValidationRule() {
class MarketplaceTagValidator : CustomValidationRule() {
override fun getRuleId(): String {
return "mp_tags_list"
}

View File

@@ -697,5 +697,9 @@
<extensionPoint name="editorSearchAreaProvider" interface="com.intellij.find.impl.livePreview.EditorSearchAreaProvider" dynamic="true"/>
<extensionPoint qualifiedName="com.intellij.intentionPopupProvider" dynamic="true"
interface="com.intellij.codeInsight.intention.impl.IntentionPopupProvider"/>
<extensionPoint qualifiedName="com.intellij.marketplaceLocalRanker" dynamic="true"
interface="com.intellij.ide.plugins.marketplace.ranking.MarketplaceLocalRanker"/>
<extensionPoint qualifiedName="com.intellij.marketplaceTextualFeaturesProvider" dynamic="true"
interface="com.intellij.ide.plugins.marketplace.statistics.features.MarketplaceTextualFeaturesProvider"/>
</extensionPoints>
</idea-plugin>

View File

@@ -946,7 +946,7 @@
<statistics.counterUsagesCollector
implementationClass="com.intellij.ide.plugins.marketplace.statistics.collectors.PluginManagerFUSCollector"/>
<statistics.validation.customValidationRule
implementation="com.intellij.ide.plugins.marketplace.statistics.validators.MarketplaceTagsListValidator"/>
implementation="com.intellij.ide.plugins.marketplace.statistics.validators.MarketplaceTagValidator"/>
<statistics.validation.customValidationRule
implementation="com.intellij.ide.plugins.marketplace.statistics.validators.MarketplaceVendorsListValidator"/>
<statistics.counterUsagesCollector implementationClass="com.intellij.codeInsight.template.impl.LiveTemplateRunLogger"/>

View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/resources" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" name="kotlin-stdlib" level="project" />
<orderEntry type="module" module-name="intellij.platform.lang.impl" />
<orderEntry type="module" module-name="intellij.platform.testFramework" scope="TEST" />
<orderEntry type="module" module-name="intellij.platform.statistics" />
<orderEntry type="module" module-name="intellij.platform.ide.impl" />
<orderEntry type="library" name="caffeine" level="project" />
<orderEntry type="module" module-name="intellij.platform.ml.impl" />
</component>
</module>

View File

@@ -0,0 +1,34 @@
<idea-plugin package="com.intellij.marketplaceMl">
<id>com.intellij.marketplace.ml</id>
<name>Machine Learning in Marketplace</name>
<vendor>JetBrains</vendor>
<category>Local AI/ML Tools</category>
<description><![CDATA[
<p>The plugin improves the Marketplace search feature by ordering the search results using machine learning,
making more relevant results appear higher up the list.
</p>
<br>
<i>Machine learning ranking is currently in experimental mode</i>
]]></description>
<resource-bundle>messages.marketplaceMlBundle</resource-bundle>
<extensions defaultExtensionNs="com.intellij">
<registryKey
defaultValue="-1"
description="Manual machine learning ranking experiment group in the Plugin Manager"
key="marketplace.ml.ranking.experiment.group"/>
<registryKey
defaultValue="true"
description="Disable machine learning ranking experiment in the Plugin Manager"
key="marketplace.ml.ranking.disable.experiments"/>
<marketplaceLocalRanker implementation="com.intellij.marketplaceMl.MarketplaceLocalRankerImpl"/>
<marketplaceTextualFeaturesProvider implementation="com.intellij.marketplaceMl.features.MarketplaceTextualFeaturesProviderImpl"/>
</extensions>
<extensionPoints>
</extensionPoints>
</idea-plugin>

View File

@@ -0,0 +1,8 @@
package com.intellij.marketplaceMl
import com.intellij.ide.plugins.marketplace.ranking.MarketplaceLocalRanker
val rankingService
get() = MarketplaceLocalRankingService.getInstance()
class MarketplaceLocalRankerImpl : MarketplaceLocalRanker by rankingService

View File

@@ -0,0 +1,63 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.marketplaceMl
import com.github.benmanes.caffeine.cache.Cache
import com.github.benmanes.caffeine.cache.Caffeine
import com.intellij.ide.plugins.IdeaPluginDescriptor
import com.intellij.ide.plugins.marketplace.ranking.MarketplaceLocalRanker
import com.intellij.ide.plugins.marketplace.statistics.features.PluginManagerMarketplaceSearchFeatureProvider
import com.intellij.ide.plugins.marketplace.statistics.features.PluginManagerSearchResultFeatureProvider
import com.intellij.ide.plugins.marketplace.statistics.features.PluginManagerSearchResultsFeatureProvider
import com.intellij.ide.plugins.marketplace.statistics.features.PluginManagerUserQueryFeatureProvider
import com.intellij.ide.plugins.newui.SearchQueryParser
import com.intellij.marketplaceMl.model.MarketplaceRankingModel
import com.intellij.openapi.components.Service
import com.intellij.openapi.components.service
import com.intellij.marketplaceMl.MarketplaceMLExperiment.ExperimentOption
import kotlin.time.Duration.Companion.seconds
import kotlin.time.toJavaDuration
@Service(Service.Level.APP)
class MarketplaceLocalRankingService : MarketplaceLocalRanker {
private val modelCache: Cache<Unit, MarketplaceRankingModel> =
Caffeine.newBuilder().expireAfterAccess(60.seconds.toJavaDuration()).maximumSize(1).build()
val model: MarketplaceRankingModel
get() = modelCache.get(Unit) { MarketplaceRankingModel() }
override fun isEnabled(): Boolean = MarketplaceMLExperiment.getExperiment() == ExperimentOption.USE_ML
override fun rankPlugins(
queryParser: SearchQueryParser.Marketplace,
plugins: MutableList<IdeaPluginDescriptor>
): Map<IdeaPluginDescriptor, Double> {
val pluginToScore = mutableMapOf<IdeaPluginDescriptor, Double>()
val searchQuery = queryParser.searchQuery
val queryFeatures = PluginManagerUserQueryFeatureProvider.getSearchStateFeatures(searchQuery)
val marketplaceFeatures = PluginManagerMarketplaceSearchFeatureProvider.getSearchStateFeatures(queryParser)
val commonResultFeatures = PluginManagerSearchResultsFeatureProvider.getCommonFeatures(searchQuery, plugins)
val allItemFeatures = plugins.map { PluginManagerSearchResultFeatureProvider.getSearchStateFeatures(searchQuery, it) }
for ((index, pluginWithFeatures) in (plugins zip allItemFeatures).withIndex()) {
val (plugin, itemFeatures) = pluginWithFeatures
val allFeatures = queryFeatures + marketplaceFeatures + commonResultFeatures + itemFeatures
val featuresMap = allFeatures.associate { it.field.name to it.data }
// TODO: replace with model prediction once we have a trained model
pluginToScore[plugin] = (plugins.size - index).toDouble() / plugins.size
// pluginToScore[plugin] = model.predictScore(featuresMap)
}
plugins.sortByDescending { pluginToScore[it] }
return pluginToScore
}
override val experimentGroup: Int
get() = MarketplaceMLExperiment.experimentGroup
override val experimentVersion: Int
get() = MarketplaceMLExperiment.VERSION
companion object {
fun getInstance(): MarketplaceLocalRankingService = service()
}
}

View File

@@ -0,0 +1,43 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.marketplaceMl
import com.intellij.internal.statistic.eventLog.EventLogConfiguration
import com.intellij.internal.statistic.eventLog.mp.MP_RECORDER_ID
import com.intellij.internal.statistic.utils.StatisticsUploadAssistant
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.util.registry.Registry
import com.intellij.util.MathUtil
import org.jetbrains.annotations.TestOnly
object MarketplaceMLExperiment {
const val VERSION = 0
const val NUMBER_OF_GROUPS = 4
enum class ExperimentOption { NO_ML, USE_ML }
private val defaultExperimentOption = ExperimentOption.NO_ML
private val marketplaceExperiments = mapOf(
0 to ExperimentOption.USE_ML,
)
val experimentGroup: Int
get() = if (isExperimentalMode) {
val registryExperimentGroup = Registry.intValue("marketplace.ml.ranking.experiment.group", -1, -1, NUMBER_OF_GROUPS - 1)
if (registryExperimentGroup >= 0) registryExperimentGroup else computedGroup
}
else -1
var isExperimentalMode: Boolean = StatisticsUploadAssistant.isSendAllowed() && ApplicationManager.getApplication().isEAP
@TestOnly set
private val computedGroup: Int by lazy {
val mpLogConfiguration = EventLogConfiguration.getInstance().getOrCreate(MP_RECORDER_ID)
// experiment groups get updated on the VERSION property change:
MathUtil.nonNegativeAbs((mpLogConfiguration.deviceId + VERSION).hashCode()) % NUMBER_OF_GROUPS
}
fun getExperiment(): ExperimentOption {
return if (Registry.`is`("marketplace.ml.ranking.disable.experiments")) defaultExperimentOption
else marketplaceExperiments.getOrDefault(experimentGroup, defaultExperimentOption)
}
}

View File

@@ -0,0 +1,59 @@
package com.intellij.marketplaceMl.features
import com.intellij.ide.plugins.marketplace.statistics.features.MarketplaceTextualFeaturesProvider
import com.intellij.internal.statistic.eventLog.events.EventField
import com.intellij.internal.statistic.eventLog.events.EventFields
import com.intellij.internal.statistic.eventLog.events.EventPair
import com.intellij.textMatching.PrefixMatchingType
import com.intellij.textMatching.PrefixMatchingUtil
import kotlin.math.round
class MarketplaceTextualFeaturesProviderImpl : MarketplaceTextualFeaturesProvider {
object Fields {
internal val SAME_START_COUNT = EventFields.Int("prefixSameStartCount")
internal val GREEDY_SCORE = EventFields.Double("prefixGreedyScore")
internal val GREEDY_WITH_CASE_SCORE = EventFields.Double("prefixGreedyWithCaseScore")
internal val MATCHED_WORDS_SCORE = EventFields.Double("prefixMatchedWordsScore")
internal val MATCHED_WORDS_RELATIVE = EventFields.Double("prefixMatchedWordsRelative")
internal val MATCHED_WORDS_WITH_CASE_SCORE = EventFields.Double("prefixMatchedWordsWithCaseScore")
internal val MATCHED_WORDS_WITH_CASE_RELATIVE = EventFields.Double("prefixMatchedWordsWithCaseRelative")
internal val SKIPPED_WORDS = EventFields.Int("prefixSkippedWords")
internal val MATCHING_TYPE = EventFields.Enum<PrefixMatchingType>("prefixMatchingType")
internal val EXACT = EventFields.Boolean("prefixExact")
internal val MATCHED_LAST_WORD = EventFields.Boolean("prefixMatchedLastWord")
}
override fun getFeaturesDefinition(): Array<EventField<*>> {
return Fields.run {
arrayOf(
SAME_START_COUNT, GREEDY_SCORE, GREEDY_WITH_CASE_SCORE, MATCHED_WORDS_SCORE, MATCHED_WORDS_RELATIVE, MATCHED_WORDS_WITH_CASE_SCORE,
MATCHED_WORDS_WITH_CASE_RELATIVE, SKIPPED_WORDS, MATCHING_TYPE, EXACT, MATCHED_LAST_WORD
)
}
}
override fun getTextualFeatures(query: String, match: String): List<EventPair<*>> {
if (query.isEmpty() || match.isEmpty()) return emptyList()
val scores = PrefixMatchingUtil.PrefixMatchingScores.Builder().build(query, match)
return Fields.run {
listOf(
SAME_START_COUNT.with(scores.start),
GREEDY_SCORE.with(roundDouble(scores.greedy)),
GREEDY_WITH_CASE_SCORE.with(roundDouble(scores.greedyWithCase)),
MATCHED_WORDS_SCORE.with(roundDouble(scores.words)),
MATCHED_WORDS_RELATIVE.with(roundDouble(scores.wordsRelative)),
MATCHED_WORDS_WITH_CASE_SCORE.with(roundDouble(scores.wordsWithCase)),
MATCHED_WORDS_WITH_CASE_RELATIVE.with(roundDouble(scores.wordsWithCaseRelative)),
SKIPPED_WORDS.with(scores.skippedWords),
MATCHING_TYPE.with(scores.type),
EXACT.with(scores.exact),
MATCHED_LAST_WORD.with(scores.exactFinal)
)
}
}
private fun roundDouble(value: Double): Double {
if (!value.isFinite()) return -1.0
return round(value * 100000) / 100000
}
}

View File

@@ -0,0 +1,16 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.marketplaceMl.model
import com.intellij.internal.ml.DecisionFunction
import com.intellij.internal.ml.FeatureMapper
import com.intellij.internal.ml.ModelMetadata
abstract class MarketplaceRankingDecisionFunction(private val metadata: ModelMetadata) : DecisionFunction {
override fun getFeaturesOrder(): Array<FeatureMapper> = metadata.featuresOrder
override fun getRequiredFeatures(): List<String> = emptyList()
override fun getUnknownFeatures(features: Collection<String>): List<String> = emptyList()
override fun version(): String? = metadata.version
}

View File

@@ -0,0 +1,19 @@
package com.intellij.marketplaceMl.model
import com.intellij.internal.ml.DecisionFunction
import com.intellij.internal.ml.FeatureMapper
class MarketplaceRankingModel {
private val decisionFunction: DecisionFunction = MarketplaceRankingModelLoader.loadModel()
fun predictScore(featureMap: Map<String, Any?>): Double {
return decisionFunction.predict(buildArray(decisionFunction.featuresOrder, featureMap))
}
private fun buildArray(featuresOrder: Array<FeatureMapper>, featureMap: Map<String, Any?>): DoubleArray {
return DoubleArray(featuresOrder.size) {
val mapper = featuresOrder[it]
mapper.asArrayValue(featureMap[mapper.featureName])
}
}
}

View File

@@ -0,0 +1,18 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.marketplaceMl.model
import com.intellij.internal.ml.DecisionFunction
import com.intellij.internal.ml.FeaturesInfo
import kotlin.random.Random
class MarketplaceRankingModelLoader {
companion object {
fun loadModel(): DecisionFunction {
val emptyMetadata = FeaturesInfo(emptySet(), emptyList(), emptyList(), emptyList(), emptyArray(), null)
return object : MarketplaceRankingDecisionFunction(emptyMetadata) {
override fun predict(features: DoubleArray?): Double = Random.nextDouble()
}
}
}
}

View File

@@ -14,6 +14,7 @@ intellij.markdown
intellij.platform.langInjection
intellij.properties
intellij.searchEverywhereMl
intellij.marketplaceMl
intellij.settingsSync
intellij.sh
intellij.statsCollector