Repository: mlflow/mlflow

Issue:
[DOC-FIX] Add start_time search examples to SearchRuns documentation
### Willingness to contribute

No. I cannot contribute a documentation fix at this time.

### URL(s) with the issue

https://mlflow.org/docs/latest/search-runs.html

### Description of proposal (what needs changing)

Add an example that demonstrates how to search for runs by start time to https://mlflow.org/docs/latest/search-runs.html. Right now, it's not obvious how this can be done.

Example filter string:

```
attributes.start_time > 1663889475000
```

Where `1663889475000` is milliseconds since the unix epoch.


Discussion:

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
@@ -115,7 +115,7 @@ def start_time(self):
         """Start time of the run, in number of milliseconds since the UNIX epoch."""
         return self._start_time
 
-    @orderable_attribute
+    @searchable_attribute
     def end_time(self):
         """End time of the run, in number of milliseconds since the UNIX epoch."""
         return self._end_time
diff --git a/mlflow/utils/search_utils.py b/mlflow/utils/search_utils.py
--- a/mlflow/utils/search_utils.py
+++ b/mlflow/utils/search_utils.py
@@ -43,7 +43,7 @@ class SearchUtils:
     VALID_TAG_COMPARATORS = {"!=", "=", LIKE_OPERATOR, ILIKE_OPERATOR}
     VALID_STRING_ATTRIBUTE_COMPARATORS = {"!=", "=", LIKE_OPERATOR, ILIKE_OPERATOR}
     VALID_NUMERIC_ATTRIBUTE_COMPARATORS = VALID_METRIC_COMPARATORS
-    NUMERIC_ATTRIBUTES = {"start_time"}
+    NUMERIC_ATTRIBUTES = {"start_time", "end_time"}
     CASE_INSENSITIVE_STRING_COMPARISON_OPERATORS = {LIKE_OPERATOR, ILIKE_OPERATOR}
     VALID_REGISTERED_MODEL_SEARCH_COMPARATORS = CASE_INSENSITIVE_STRING_COMPARISON_OPERATORS.union(
         {"="}


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
@@ -142,5 +142,6 @@ def test_creation_and_hydration(self):
 
     def test_searchable_attributes(self):
         self.assertSequenceEqual(
-            {"status", "artifact_uri", "start_time"}, set(RunInfo.get_searchable_attributes())
+            {"status", "artifact_uri", "start_time", "end_time"},
+            set(RunInfo.get_searchable_attributes()),
         )
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
@@ -128,7 +128,6 @@ def test_correct_quote_trimming(filter_string, parsed_filter):
         ("`dummy.A > 0.1", "Invalid clause(s) in filter string"),
         ("dummy`.A > 0.1", "Invalid clause(s) in filter string"),
         ("attribute.start != 1", "Invalid attribute key"),
-        ("attribute.end_time != 1", "Invalid attribute key"),
         ("attribute.run_id != 1", "Invalid attribute key"),
         ("attribute.run_uuid != 1", "Invalid attribute key"),
         ("attribute.experiment_id != 1", "Invalid attribute key"),
@@ -323,6 +322,28 @@ def test_filter_runs_by_start_time():
     assert SearchUtils.filter(runs, "attribute.start_time = 2") == runs[2:]
 
 
+def test_filter_runs_by_end_time():
+    runs = [
+        Run(
+            run_info=RunInfo(
+                run_uuid=run_id,
+                run_id=run_id,
+                experiment_id=0,
+                user_id="user-id",
+                status=RunStatus.to_string(RunStatus.FINISHED),
+                start_time=idx,
+                end_time=idx,
+                lifecycle_stage=LifecycleStage.ACTIVE,
+            ),
+            run_data=RunData(),
+        )
+        for idx, run_id in enumerate(["a", "b", "c"])
+    ]
+    assert SearchUtils.filter(runs, "attribute.end_time >= 0") == runs
+    assert SearchUtils.filter(runs, "attribute.end_time > 1") == runs[2:]
+    assert SearchUtils.filter(runs, "attribute.end_time = 2") == runs[2:]
+
+
 @pytest.mark.parametrize(
     ("order_bys", "matching_runs"),
     [