Repository: mlflow/mlflow

Issue:
[FR] Add the ability to search runs based on 'user_id'
### Willingness to contribute

No. I cannot contribute this feature at this time.

### Proposal Summary

Enable run search to perform searches based on the `user_id` that submitted the run. 

### Motivation

> #### What is the use case for this feature?
To return runs that have been initiated by a particular user while using run search functionality.

> #### Why is this use case valuable to support for MLflow users in general?
Quality of life improvement for navigating within the UI

> #### Why is this use case valuable to support for your project(s) or organization?

> #### Why is it currently difficult to achieve this use case?
It is not supported right now.


### Details

_No response_

### What component(s) does this bug affect?

- [ ] `area/artifacts`: Artifact stores and artifact logging
- [ ] `area/build`: Build and test infrastructure for MLflow
- [ ] `area/docs`: MLflow documentation pages
- [ ] `area/examples`: Example code
- [ ] `area/model-registry`: Model Registry service, APIs, and the fluent client calls for Model Registry
- [ ] `area/models`: MLmodel format, model serialization/deserialization, flavors
- [ ] `area/pipelines`: Pipelines, Pipeline APIs, Pipeline configs, Pipeline Templates
- [ ] `area/projects`: MLproject format, project running backends
- [ ] `area/scoring`: MLflow Model server, model deployment tools, Spark UDFs
- [ ] `area/server-infra`: MLflow Tracking server backend
- [X] `area/tracking`: Tracking Service, tracking client APIs, autologging

### What interface(s) does this bug affect?

- [ ] `area/uiux`: Front-end, user experience, plotting, JavaScript, JavaScript dev server
- [ ] `area/docker`: Docker use across MLflow's components, such as MLflow Projects and MLflow Models
- [ ] `area/sqlalchemy`: Use of SQLAlchemy in the Tracking Service or Model Registry
- [ ] `area/windows`: Windows support

### What language(s) does this bug affect?

- [ ] `language/r`: R APIs and clients
- [ ] `language/java`: Java APIs and clients
- [ ] `language/new`: Proposals for new client languages

### What integration(s) does this bug affect?

- [ ] `integrations/azure`: Azure and Azure ML integrations
- [ ] `integrations/sagemaker`: SageMaker integrations
- [ ] `integrations/databricks`: Databricks integrations


Discussion:
<!-- assign-maintainer -->
@BenWilson2 @dbczumar @harupy @WeichenXu123 Please assign a maintainer and start triaging this issue.

Resolving patch:
diff --git a/mlflow/entities/run_info.py b/mlflow/entities/run_info.py
--- a/mlflow/entities/run_info.py
+++ b/mlflow/entities/run_info.py
@@ -109,7 +109,7 @@ def run_name(self):
     def _set_run_name(self, new_name):
         self._run_name = new_name
 
-    @property
+    @searchable_attribute
     def user_id(self):
         """String ID of the user who initiated this run."""
         return self._user_id


Test patch:
diff --git a/tests/entities/test_run_info.py b/tests/entities/test_run_info.py
--- a/tests/entities/test_run_info.py
+++ b/tests/entities/test_run_info.py
@@ -147,6 +147,6 @@ def test_creation_and_hydration(self):
 
     def test_searchable_attributes(self):
         self.assertSequenceEqual(
-            {"status", "artifact_uri", "start_time", "end_time"},
+            {"status", "artifact_uri", "start_time", "user_id", "end_time"},
             set(RunInfo.get_searchable_attributes()),
         )
diff --git a/tests/store/tracking/test_sqlalchemy_store.py b/tests/store/tracking/test_sqlalchemy_store.py
--- a/tests/store/tracking/test_sqlalchemy_store.py
+++ b/tests/store/tracking/test_sqlalchemy_store.py
@@ -2450,7 +2450,7 @@ def test_get_attribute_name():
     # we want this to break if a searchable or orderable attribute has been added
     # and not referred to in this test
     # searchable attributes are also orderable
-    assert len(entities.RunInfo.get_orderable_attributes()) == 4
+    assert len(entities.RunInfo.get_orderable_attributes()) == 5
 
 
 def test_get_orderby_clauses():
diff --git a/tests/utils/test_search_utils.py b/tests/utils/test_search_utils.py
--- a/tests/utils/test_search_utils.py
+++ b/tests/utils/test_search_utils.py
@@ -322,6 +322,38 @@ def test_filter_runs_by_start_time():
     assert SearchUtils.filter(runs, "attribute.start_time = 2") == runs[2:]
 
 
+def test_filter_runs_by_user_id():
+    runs = [
+        Run(
+            run_info=RunInfo(
+                run_uuid="a",
+                run_id="a",
+                experiment_id=0,
+                user_id="user-id",
+                status=RunStatus.to_string(RunStatus.FINISHED),
+                start_time=1,
+                end_time=1,
+                lifecycle_stage=LifecycleStage.ACTIVE,
+            ),
+            run_data=RunData(),
+        ),
+        Run(
+            run_info=RunInfo(
+                run_uuid="b",
+                run_id="b",
+                experiment_id=0,
+                user_id="user-id2",
+                status=RunStatus.to_string(RunStatus.FINISHED),
+                start_time=1,
+                end_time=1,
+                lifecycle_stage=LifecycleStage.ACTIVE,
+            ),
+            run_data=RunData(),
+        ),
+    ]
+    assert SearchUtils.filter(runs, "attribute.user_id = 'user-id2'")[0] == runs[1]
+
+
 def test_filter_runs_by_end_time():
     runs = [
         Run(