diff --git a/docs/reference/query-languages/esql/_snippets/functions/description/present_over_time.md b/docs/reference/query-languages/esql/_snippets/functions/description/present_over_time.md
new file mode 100644
index 0000000000000..9a4d5cabcc704
--- /dev/null
+++ b/docs/reference/query-languages/esql/_snippets/functions/description/present_over_time.md
@@ -0,0 +1,11 @@
+% This is generated by ESQL's AbstractFunctionTestCase. Do not edit it. See ../README.md for how to regenerate it.
+
+**Description**
+
+The presence of a field in the output result over time range.
+
+::::{note}
+Available with the [TS](/reference/query-languages/esql/commands/source-commands.md#esql-ts) command in snapshot builds
+::::
+
+
diff --git a/docs/reference/query-languages/esql/_snippets/functions/examples/present.md b/docs/reference/query-languages/esql/_snippets/functions/examples/present.md
index 45c27f180480a..9529afccfedbe 100644
--- a/docs/reference/query-languages/esql/_snippets/functions/examples/present.md
+++ b/docs/reference/query-languages/esql/_snippets/functions/examples/present.md
@@ -27,4 +27,16 @@ FROM employees
| true | 5 |
| true | null |
+To check for the presence and return 1 when it's true and 0 when it's false
+
+```esql
+FROM employees
+| WHERE emp_no == 10020
+| STATS is_present = TO_INTEGER(PRESENT(languages))
+```
+
+| is_present:integer |
+| --- |
+| 0 |
+
diff --git a/docs/reference/query-languages/esql/_snippets/functions/examples/present_over_time.md b/docs/reference/query-languages/esql/_snippets/functions/examples/present_over_time.md
new file mode 100644
index 0000000000000..62062133386c8
--- /dev/null
+++ b/docs/reference/query-languages/esql/_snippets/functions/examples/present_over_time.md
@@ -0,0 +1,18 @@
+% This is generated by ESQL's AbstractFunctionTestCase. Do not edit it. See ../README.md for how to regenerate it.
+
+**Example**
+
+```esql
+TS k8s
+| WHERE cluster == "prod" AND pod == "two"
+| STATS events_received = max(present_over_time(events_received)) BY pod, time_bucket = tbucket(2 minute)
+```
+
+| events_received:boolean | pod:keyword | time_bucket:datetime |
+| --- | --- | --- |
+| true | two | 2024-05-10T00:02:00.000Z |
+| true | two | 2024-05-10T00:08:00.000Z |
+| false | two | 2024-05-10T00:10:00.000Z |
+| false | two | 2024-05-10T00:12:00.000Z |
+
+
diff --git a/docs/reference/query-languages/esql/_snippets/functions/layout/present_over_time.md b/docs/reference/query-languages/esql/_snippets/functions/layout/present_over_time.md
new file mode 100644
index 0000000000000..71b9b8ea596dc
--- /dev/null
+++ b/docs/reference/query-languages/esql/_snippets/functions/layout/present_over_time.md
@@ -0,0 +1,26 @@
+% This is generated by ESQL's AbstractFunctionTestCase. Do not edit it. See ../README.md for how to regenerate it.
+
+## `PRESENT_OVER_TIME` [esql-present_over_time]
+```{applies_to}
+stack: unavailable
+```
+
+**Syntax**
+
+:::{image} ../../../images/functions/present_over_time.svg
+:alt: Embedded
+:class: text-center
+:::
+
+
+:::{include} ../parameters/present_over_time.md
+:::
+
+:::{include} ../description/present_over_time.md
+:::
+
+:::{include} ../types/present_over_time.md
+:::
+
+:::{include} ../examples/present_over_time.md
+:::
diff --git a/docs/reference/query-languages/esql/_snippets/functions/parameters/present_over_time.md b/docs/reference/query-languages/esql/_snippets/functions/parameters/present_over_time.md
new file mode 100644
index 0000000000000..24fedc1dde506
--- /dev/null
+++ b/docs/reference/query-languages/esql/_snippets/functions/parameters/present_over_time.md
@@ -0,0 +1,7 @@
+% This is generated by ESQL's AbstractFunctionTestCase. Do not edit it. See ../README.md for how to regenerate it.
+
+**Parameters**
+
+`field`
+:
+
diff --git a/docs/reference/query-languages/esql/_snippets/functions/types/present_over_time.md b/docs/reference/query-languages/esql/_snippets/functions/types/present_over_time.md
new file mode 100644
index 0000000000000..626f34b097399
--- /dev/null
+++ b/docs/reference/query-languages/esql/_snippets/functions/types/present_over_time.md
@@ -0,0 +1,25 @@
+% This is generated by ESQL's AbstractFunctionTestCase. Do not edit it. See ../README.md for how to regenerate it.
+
+**Supported types**
+
+| field | result |
+| --- | --- |
+| boolean | boolean |
+| cartesian_point | boolean |
+| cartesian_shape | boolean |
+| date | boolean |
+| date_nanos | boolean |
+| double | boolean |
+| geo_point | boolean |
+| geo_shape | boolean |
+| geohash | boolean |
+| geohex | boolean |
+| geotile | boolean |
+| integer | boolean |
+| ip | boolean |
+| keyword | boolean |
+| long | boolean |
+| text | boolean |
+| unsigned_long | boolean |
+| version | boolean |
+
diff --git a/docs/reference/query-languages/esql/images/functions/present_over_time.svg b/docs/reference/query-languages/esql/images/functions/present_over_time.svg
new file mode 100644
index 0000000000000..8dcace898dafd
--- /dev/null
+++ b/docs/reference/query-languages/esql/images/functions/present_over_time.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/docs/reference/query-languages/esql/kibana/definition/functions/present.json b/docs/reference/query-languages/esql/kibana/definition/functions/present.json
index 584843213c5a4..d75d787b4762c 100644
--- a/docs/reference/query-languages/esql/kibana/definition/functions/present.json
+++ b/docs/reference/query-languages/esql/kibana/definition/functions/present.json
@@ -223,7 +223,8 @@
],
"examples" : [
"FROM employees\n| STATS is_present = PRESENT(languages)",
- "FROM employees\n| STATS is_present = PRESENT(salary) BY languages"
+ "FROM employees\n| STATS is_present = PRESENT(salary) BY languages",
+ "FROM employees\n| WHERE emp_no == 10020\n| STATS is_present = TO_INTEGER(PRESENT(languages))"
],
"preview" : false,
"snapshot_only" : false
diff --git a/docs/reference/query-languages/esql/kibana/definition/functions/present_over_time.json b/docs/reference/query-languages/esql/kibana/definition/functions/present_over_time.json
new file mode 100644
index 0000000000000..4bccb8d0a6ebc
--- /dev/null
+++ b/docs/reference/query-languages/esql/kibana/definition/functions/present_over_time.json
@@ -0,0 +1,230 @@
+{
+ "comment" : "This is generated by ESQL's AbstractFunctionTestCase. Do not edit it. See ../README.md for how to regenerate it.",
+ "type" : "time_series_agg",
+ "name" : "present_over_time",
+ "description" : "The presence of a field in the output result over time range.",
+ "note" : "Available with the TS command in snapshot builds",
+ "signatures" : [
+ {
+ "params" : [
+ {
+ "name" : "field",
+ "type" : "boolean",
+ "optional" : false,
+ "description" : ""
+ }
+ ],
+ "variadic" : false,
+ "returnType" : "boolean"
+ },
+ {
+ "params" : [
+ {
+ "name" : "field",
+ "type" : "cartesian_point",
+ "optional" : false,
+ "description" : ""
+ }
+ ],
+ "variadic" : false,
+ "returnType" : "boolean"
+ },
+ {
+ "params" : [
+ {
+ "name" : "field",
+ "type" : "cartesian_shape",
+ "optional" : false,
+ "description" : ""
+ }
+ ],
+ "variadic" : false,
+ "returnType" : "boolean"
+ },
+ {
+ "params" : [
+ {
+ "name" : "field",
+ "type" : "date",
+ "optional" : false,
+ "description" : ""
+ }
+ ],
+ "variadic" : false,
+ "returnType" : "boolean"
+ },
+ {
+ "params" : [
+ {
+ "name" : "field",
+ "type" : "date_nanos",
+ "optional" : false,
+ "description" : ""
+ }
+ ],
+ "variadic" : false,
+ "returnType" : "boolean"
+ },
+ {
+ "params" : [
+ {
+ "name" : "field",
+ "type" : "double",
+ "optional" : false,
+ "description" : ""
+ }
+ ],
+ "variadic" : false,
+ "returnType" : "boolean"
+ },
+ {
+ "params" : [
+ {
+ "name" : "field",
+ "type" : "geo_point",
+ "optional" : false,
+ "description" : ""
+ }
+ ],
+ "variadic" : false,
+ "returnType" : "boolean"
+ },
+ {
+ "params" : [
+ {
+ "name" : "field",
+ "type" : "geo_shape",
+ "optional" : false,
+ "description" : ""
+ }
+ ],
+ "variadic" : false,
+ "returnType" : "boolean"
+ },
+ {
+ "params" : [
+ {
+ "name" : "field",
+ "type" : "geohash",
+ "optional" : false,
+ "description" : ""
+ }
+ ],
+ "variadic" : false,
+ "returnType" : "boolean"
+ },
+ {
+ "params" : [
+ {
+ "name" : "field",
+ "type" : "geohex",
+ "optional" : false,
+ "description" : ""
+ }
+ ],
+ "variadic" : false,
+ "returnType" : "boolean"
+ },
+ {
+ "params" : [
+ {
+ "name" : "field",
+ "type" : "geotile",
+ "optional" : false,
+ "description" : ""
+ }
+ ],
+ "variadic" : false,
+ "returnType" : "boolean"
+ },
+ {
+ "params" : [
+ {
+ "name" : "field",
+ "type" : "integer",
+ "optional" : false,
+ "description" : ""
+ }
+ ],
+ "variadic" : false,
+ "returnType" : "boolean"
+ },
+ {
+ "params" : [
+ {
+ "name" : "field",
+ "type" : "ip",
+ "optional" : false,
+ "description" : ""
+ }
+ ],
+ "variadic" : false,
+ "returnType" : "boolean"
+ },
+ {
+ "params" : [
+ {
+ "name" : "field",
+ "type" : "keyword",
+ "optional" : false,
+ "description" : ""
+ }
+ ],
+ "variadic" : false,
+ "returnType" : "boolean"
+ },
+ {
+ "params" : [
+ {
+ "name" : "field",
+ "type" : "long",
+ "optional" : false,
+ "description" : ""
+ }
+ ],
+ "variadic" : false,
+ "returnType" : "boolean"
+ },
+ {
+ "params" : [
+ {
+ "name" : "field",
+ "type" : "text",
+ "optional" : false,
+ "description" : ""
+ }
+ ],
+ "variadic" : false,
+ "returnType" : "boolean"
+ },
+ {
+ "params" : [
+ {
+ "name" : "field",
+ "type" : "unsigned_long",
+ "optional" : false,
+ "description" : ""
+ }
+ ],
+ "variadic" : false,
+ "returnType" : "boolean"
+ },
+ {
+ "params" : [
+ {
+ "name" : "field",
+ "type" : "version",
+ "optional" : false,
+ "description" : ""
+ }
+ ],
+ "variadic" : false,
+ "returnType" : "boolean"
+ }
+ ],
+ "examples" : [
+ "TS k8s\n| WHERE cluster == \"prod\" AND pod == \"two\"\n| STATS events_received = max(present_over_time(events_received)) BY pod, time_bucket = tbucket(2 minute)"
+ ],
+ "preview" : false,
+ "snapshot_only" : true
+}
diff --git a/docs/reference/query-languages/esql/kibana/docs/functions/present_over_time.md b/docs/reference/query-languages/esql/kibana/docs/functions/present_over_time.md
new file mode 100644
index 0000000000000..319f9187727ba
--- /dev/null
+++ b/docs/reference/query-languages/esql/kibana/docs/functions/present_over_time.md
@@ -0,0 +1,12 @@
+% This is generated by ESQL's AbstractFunctionTestCase. Do not edit it. See ../README.md for how to regenerate it.
+
+### PRESENT OVER TIME
+The presence of a field in the output result over time range.
+
+Note: Available with the [TS](https://www.elastic.co/docs/reference/query-languages/esql/commands/source-commands#esql-ts) command in snapshot builds
+
+```esql
+TS k8s
+| WHERE cluster == "prod" AND pod == "two"
+| STATS events_received = max(present_over_time(events_received)) BY pod, time_bucket = tbucket(2 minute)
+```
diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/k8s-timeseries-present-over-time.csv-spec b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/k8s-timeseries-present-over-time.csv-spec
new file mode 100644
index 0000000000000..8a4142730943c
--- /dev/null
+++ b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/k8s-timeseries-present-over-time.csv-spec
@@ -0,0 +1,323 @@
+present_over_time_events_received
+required_capability: metrics_command
+required_capability: present_over_time
+
+TS k8s
+| WHERE cluster == "prod" AND pod == "two"
+| STATS events_received = max(present_over_time(events_received)) BY pod, time_bucket = tbucket(2 minute)
+| SORT time_bucket
+;
+ignoreOrder:true
+
+events_received:boolean | pod:keyword | time_bucket:datetime
+true | two | 2024-05-10T00:02:00.000Z
+true | two | 2024-05-10T00:08:00.000Z
+false | two | 2024-05-10T00:10:00.000Z
+false | two | 2024-05-10T00:12:00.000Z
+true | two | 2024-05-10T00:14:00.000Z
+true | two | 2024-05-10T00:16:00.000Z
+false | two | 2024-05-10T00:18:00.000Z
+true | two | 2024-05-10T00:20:00.000Z
+true | two | 2024-05-10T00:22:00.000Z
+;
+
+present_over_time_of_long
+required_capability: metrics_command
+required_capability: present_over_time
+TS k8s | STATS is_present = max(present_over_time(network.bytes_in)) BY cluster, time_bucket = tbucket(10minute) | SORT cluster, time_bucket | LIMIT 10;
+
+is_present:boolean | cluster:keyword | time_bucket:datetime
+true | prod | 2024-05-10T00:00:00.000Z
+true | prod | 2024-05-10T00:10:00.000Z
+true | prod | 2024-05-10T00:20:00.000Z
+true | qa | 2024-05-10T00:00:00.000Z
+true | qa | 2024-05-10T00:10:00.000Z
+true | qa | 2024-05-10T00:20:00.000Z
+true | staging | 2024-05-10T00:00:00.000Z
+true | staging | 2024-05-10T00:10:00.000Z
+true | staging | 2024-05-10T00:20:00.000Z
+;
+
+present_over_time_of_boolean
+required_capability: metrics_command
+required_capability: present_over_time
+required_capability: k8s_dataset_additional_fields
+TS k8s | STATS is_present = max(present_over_time(network.eth0.up)) BY cluster, time_bucket = tbucket(10minute) | SORT time_bucket, cluster | LIMIT 10;
+
+is_present:boolean | cluster:keyword | time_bucket:datetime
+true | prod | 2024-05-10T00:00:00.000Z
+true | qa | 2024-05-10T00:00:00.000Z
+true | staging | 2024-05-10T00:00:00.000Z
+true | prod | 2024-05-10T00:10:00.000Z
+true | qa | 2024-05-10T00:10:00.000Z
+true | staging | 2024-05-10T00:10:00.000Z
+true | prod | 2024-05-10T00:20:00.000Z
+true | qa | 2024-05-10T00:20:00.000Z
+true | staging | 2024-05-10T00:20:00.000Z
+;
+
+present_over_time_of_date_nanos
+required_capability: metrics_command
+required_capability: present_over_time
+required_capability: k8s_dataset_additional_fields
+TS k8s | STATS is_present = max(present_over_time(network.eth0.last_up)) BY cluster, time_bucket = tbucket(10minute) | SORT time_bucket, cluster | LIMIT 10;
+
+is_present:boolean | cluster:keyword | time_bucket:datetime
+true | prod | 2024-05-10T00:00:00.000Z
+true | qa | 2024-05-10T00:00:00.000Z
+true | staging | 2024-05-10T00:00:00.000Z
+true | prod | 2024-05-10T00:10:00.000Z
+true | qa | 2024-05-10T00:10:00.000Z
+true | staging | 2024-05-10T00:10:00.000Z
+true | prod | 2024-05-10T00:20:00.000Z
+true | qa | 2024-05-10T00:20:00.000Z
+true | staging | 2024-05-10T00:20:00.000Z
+;
+
+present_over_time_of_date
+required_capability: metrics_command
+required_capability: present_over_time
+required_capability: k8s_dataset_additional_fields
+TS k8s | STATS is_present = max(present_over_time(to_datetime(network.eth0.last_up))) BY cluster, time_bucket = tbucket(10minute) | SORT time_bucket, cluster | LIMIT 10;
+
+is_present:boolean | cluster:keyword | time_bucket:datetime
+true | prod | 2024-05-10T00:00:00.000Z
+true | qa | 2024-05-10T00:00:00.000Z
+true | staging | 2024-05-10T00:00:00.000Z
+true | prod | 2024-05-10T00:10:00.000Z
+true | qa | 2024-05-10T00:10:00.000Z
+true | staging | 2024-05-10T00:10:00.000Z
+true | prod | 2024-05-10T00:20:00.000Z
+true | qa | 2024-05-10T00:20:00.000Z
+true | staging | 2024-05-10T00:20:00.000Z
+;
+
+present_over_time_of_version
+required_capability: metrics_command
+required_capability: present_over_time
+required_capability: k8s_dataset_additional_fields
+TS k8s | STATS is_present = max(present_over_time(network.eth0.firmware_version)) BY cluster, time_bucket = tbucket(10minute) | SORT time_bucket, cluster | LIMIT 10;
+
+is_present:boolean | cluster:keyword | time_bucket:datetime
+true | prod | 2024-05-10T00:00:00.000Z
+true | qa | 2024-05-10T00:00:00.000Z
+true | staging | 2024-05-10T00:00:00.000Z
+true | prod | 2024-05-10T00:10:00.000Z
+true | qa | 2024-05-10T00:10:00.000Z
+true | staging | 2024-05-10T00:10:00.000Z
+true | prod | 2024-05-10T00:20:00.000Z
+true | qa | 2024-05-10T00:20:00.000Z
+true | staging | 2024-05-10T00:20:00.000Z
+;
+
+present_over_time_of_integer
+required_capability: metrics_command
+required_capability: present_over_time
+required_capability: k8s_dataset_additional_fields
+TS k8s | STATS is_present = max(present_over_time(network.eth0.currently_connected_clients)) BY cluster, time_bucket = tbucket(10minute) | SORT time_bucket, cluster | LIMIT 10;
+
+is_present:boolean | cluster:keyword | time_bucket:datetime
+true | prod | 2024-05-10T00:00:00.000Z
+true | qa | 2024-05-10T00:00:00.000Z
+true | staging | 2024-05-10T00:00:00.000Z
+true | prod | 2024-05-10T00:10:00.000Z
+true | qa | 2024-05-10T00:10:00.000Z
+true | staging | 2024-05-10T00:10:00.000Z
+true | prod | 2024-05-10T00:20:00.000Z
+true | qa | 2024-05-10T00:20:00.000Z
+true | staging | 2024-05-10T00:20:00.000Z
+;
+
+present_over_time_of_text
+required_capability: metrics_command
+required_capability: present_over_time
+TS k8s | STATS is_present = max(present_over_time(event_log)) BY cluster, time_bucket = tbucket(10minute) | SORT cluster, time_bucket | LIMIT 10;
+
+is_present:boolean | cluster:keyword | time_bucket:datetime
+true | prod | 2024-05-10T00:00:00.000Z
+true | prod | 2024-05-10T00:10:00.000Z
+true | prod | 2024-05-10T00:20:00.000Z
+true | qa | 2024-05-10T00:00:00.000Z
+true | qa | 2024-05-10T00:10:00.000Z
+true | qa | 2024-05-10T00:20:00.000Z
+true | staging | 2024-05-10T00:00:00.000Z
+true | staging | 2024-05-10T00:10:00.000Z
+true | staging | 2024-05-10T00:20:00.000Z
+;
+
+present_over_time_of_keyword
+required_capability: metrics_command
+required_capability: present_over_time
+required_capability: k8s_dataset_additional_fields
+TS k8s | STATS is_present = max(present_over_time(pod)) BY cluster, time_bucket = tbucket(10minute) | SORT time_bucket, cluster | LIMIT 10;
+
+is_present:boolean | cluster:keyword | time_bucket:datetime
+true | prod | 2024-05-10T00:00:00.000Z
+true | qa | 2024-05-10T00:00:00.000Z
+true | staging | 2024-05-10T00:00:00.000Z
+true | prod | 2024-05-10T00:10:00.000Z
+true | qa | 2024-05-10T00:10:00.000Z
+true | staging | 2024-05-10T00:10:00.000Z
+true | prod | 2024-05-10T00:20:00.000Z
+true | qa | 2024-05-10T00:20:00.000Z
+true | staging | 2024-05-10T00:20:00.000Z
+;
+
+present_over_time_of_aggregate_metric_double
+required_capability: metrics_command
+required_capability: present_over_time
+TS k8s-downsampled | STATS is_present = max(present_over_time(network.eth0.tx)) BY cluster, time_bucket = tbucket(10 minute) | SORT time_bucket, cluster | LIMIT 10;
+
+is_present:boolean | cluster:keyword | time_bucket:datetime
+true | prod | 2024-05-09T23:30:00.000Z
+true | qa | 2024-05-09T23:30:00.000Z
+true | staging | 2024-05-09T23:30:00.000Z
+true | prod | 2024-05-09T23:40:00.000Z
+true | qa | 2024-05-09T23:40:00.000Z
+true | staging | 2024-05-09T23:40:00.000Z
+true | prod | 2024-05-09T23:50:00.000Z
+true | qa | 2024-05-09T23:50:00.000Z
+true | staging | 2024-05-09T23:50:00.000Z
+;
+
+present_over_time_of_geopoint
+required_capability: metrics_command
+required_capability: present_over_time
+required_capability: k8s_datasets_geospatial_fields
+TS k8s | STATS is_present = max(present_over_time(event_city)) BY cluster, time_bucket = tbucket(10minute) | SORT time_bucket, cluster | LIMIT 10;
+
+is_present:boolean | cluster:keyword | time_bucket:datetime
+true | prod | 2024-05-10T00:00:00.000Z
+true | qa | 2024-05-10T00:00:00.000Z
+true | staging | 2024-05-10T00:00:00.000Z
+true | prod | 2024-05-10T00:10:00.000Z
+true | qa | 2024-05-10T00:10:00.000Z
+true | staging | 2024-05-10T00:10:00.000Z
+true | prod | 2024-05-10T00:20:00.000Z
+true | qa | 2024-05-10T00:20:00.000Z
+true | staging | 2024-05-10T00:20:00.000Z
+;
+
+present_over_time_of_geoshape
+required_capability: metrics_command
+required_capability: present_over_time
+required_capability: k8s_datasets_geospatial_fields
+TS k8s | STATS is_present = max(present_over_time(event_city_boundary)) BY cluster, time_bucket = tbucket(10minute) | SORT time_bucket, cluster | LIMIT 10;
+
+is_present:boolean | cluster:keyword | time_bucket:datetime
+true | prod | 2024-05-10T00:00:00.000Z
+true | qa | 2024-05-10T00:00:00.000Z
+true | staging | 2024-05-10T00:00:00.000Z
+true | prod | 2024-05-10T00:10:00.000Z
+true | qa | 2024-05-10T00:10:00.000Z
+true | staging | 2024-05-10T00:10:00.000Z
+true | prod | 2024-05-10T00:20:00.000Z
+true | qa | 2024-05-10T00:20:00.000Z
+true | staging | 2024-05-10T00:20:00.000Z
+;
+
+present_over_time_of_shape
+required_capability: metrics_command
+required_capability: present_over_time
+required_capability: k8s_datasets_geospatial_fields
+TS k8s | STATS is_present = max(present_over_time(event_shape)) BY cluster, time_bucket = tbucket(10minute) | SORT time_bucket, cluster | LIMIT 10;
+
+is_present:boolean | cluster:keyword | time_bucket:datetime
+true | prod | 2024-05-10T00:00:00.000Z
+true | qa | 2024-05-10T00:00:00.000Z
+true | staging | 2024-05-10T00:00:00.000Z
+true | prod | 2024-05-10T00:10:00.000Z
+true | qa | 2024-05-10T00:10:00.000Z
+true | staging | 2024-05-10T00:10:00.000Z
+true | prod | 2024-05-10T00:20:00.000Z
+true | qa | 2024-05-10T00:20:00.000Z
+true | staging | 2024-05-10T00:20:00.000Z
+;
+
+present_over_time_of_point
+required_capability: metrics_command
+required_capability: present_over_time
+required_capability: k8s_datasets_geospatial_fields
+TS k8s | STATS is_present = max(present_over_time(event_location)) BY cluster, time_bucket = tbucket(10minute) | SORT time_bucket, cluster | LIMIT 10;
+
+is_present:boolean | cluster:keyword | time_bucket:datetime
+true | prod | 2024-05-10T00:00:00.000Z
+true | qa | 2024-05-10T00:00:00.000Z
+true | staging | 2024-05-10T00:00:00.000Z
+true | prod | 2024-05-10T00:10:00.000Z
+true | qa | 2024-05-10T00:10:00.000Z
+true | staging | 2024-05-10T00:10:00.000Z
+true | prod | 2024-05-10T00:20:00.000Z
+true | qa | 2024-05-10T00:20:00.000Z
+true | staging | 2024-05-10T00:20:00.000Z
+;
+
+present_over_time_with_filtering
+required_capability: metrics_command
+required_capability: present_over_time
+TS k8s | WHERE pod != "three" | STATS is_present = max(present_over_time(network.bytes_in)) BY cluster, time_bucket = tbucket(10 minute) | SORT time_bucket, cluster | LIMIT 10;
+
+is_present:boolean | cluster:keyword | time_bucket:datetime
+true | prod | 2024-05-10T00:00:00.000Z
+true | qa | 2024-05-10T00:00:00.000Z
+true | staging | 2024-05-10T00:00:00.000Z
+true | prod | 2024-05-10T00:10:00.000Z
+true | qa | 2024-05-10T00:10:00.000Z
+true | staging | 2024-05-10T00:10:00.000Z
+true | prod | 2024-05-10T00:20:00.000Z
+true | qa | 2024-05-10T00:20:00.000Z
+true | staging | 2024-05-10T00:20:00.000Z
+;
+
+present_over_time_older_than_10d
+required_capability: metrics_command
+required_capability: present_over_time
+TS k8s-downsampled | WHERE cluster == "qa" AND @timestamp < now() - 10 day | STATS is_present = max(present_over_time(network.eth0.rx)) BY pod, time_bucket = tbucket(10 minute) | SORT time_bucket, pod | LIMIT 5;
+
+is_present:boolean | pod:keyword | time_bucket:datetime
+true | one | 2024-05-09T23:30:00.000Z
+true | three | 2024-05-09T23:30:00.000Z
+true | two | 2024-05-09T23:30:00.000Z
+true | one | 2024-05-09T23:40:00.000Z
+true | three | 2024-05-09T23:40:00.000Z
+;
+
+eval_on_present_over_time
+required_capability: metrics_command
+required_capability: present_over_time
+TS k8s | STATS is_present = max(present_over_time(network.bytes_in)) BY pod, time_bucket = tbucket(10 minute) | EVAL int = to_integer(is_present) | LIMIT 10 | SORT time_bucket, pod;
+
+is_present:boolean | pod:keyword | time_bucket:datetime | int:integer
+true | one | 2024-05-10T00:00:00.000Z | 1
+true | three | 2024-05-10T00:00:00.000Z | 1
+true | two | 2024-05-10T00:00:00.000Z | 1
+true | one | 2024-05-10T00:10:00.000Z | 1
+true | three | 2024-05-10T00:10:00.000Z | 1
+true | two | 2024-05-10T00:10:00.000Z | 1
+true | one | 2024-05-10T00:20:00.000Z | 1
+true | three | 2024-05-10T00:20:00.000Z | 1
+true | two | 2024-05-10T00:20:00.000Z | 1
+;
+
+present_over_time_events_received_as_integer
+required_capability: metrics_command
+required_capability: present_over_time
+
+TS k8s
+| WHERE cluster == "prod" AND pod == "two"
+| STATS events_received = max(to_integer(present_over_time(events_received))) BY pod, time_bucket = tbucket(2 minute)
+| SORT time_bucket
+;
+ignoreOrder:true
+
+events_received:integer | pod:keyword | time_bucket:datetime
+1 | two | 2024-05-10T00:02:00.000Z
+1 | two | 2024-05-10T00:08:00.000Z
+0 | two | 2024-05-10T00:10:00.000Z
+0 | two | 2024-05-10T00:12:00.000Z
+1 | two | 2024-05-10T00:14:00.000Z
+1 | two | 2024-05-10T00:16:00.000Z
+0 | two | 2024-05-10T00:18:00.000Z
+1 | two | 2024-05-10T00:20:00.000Z
+1 | two | 2024-05-10T00:22:00.000Z
+;
diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/k8s-timeseries.csv-spec b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/k8s-timeseries.csv-spec
index da92b4dbb99c3..08dfe22d3f3c7 100644
--- a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/k8s-timeseries.csv-spec
+++ b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/k8s-timeseries.csv-spec
@@ -446,3 +446,30 @@ cost_per_mb:double | cluster:keyword | time_bucket:datetime
2.071474095190272 | prod | 2024-05-10T00:15:00.000Z
1.59556462585034 | staging | 2024-05-10T00:10:00.000Z
;
+
+present_over_time
+required_capability: metrics_command
+required_capability: present_over_time
+
+// tag::present_over_time[]
+TS k8s
+| WHERE cluster == "prod" AND pod == "two"
+| STATS events_received = max(present_over_time(events_received)) BY pod, time_bucket = tbucket(2 minute)
+// end::present_over_time[]
+| SORT time_bucket
+;
+ignoreOrder:true
+
+// tag::present_over_time-result[]
+events_received:boolean | pod:keyword | time_bucket:datetime
+true | two | 2024-05-10T00:02:00.000Z
+true | two | 2024-05-10T00:08:00.000Z
+false | two | 2024-05-10T00:10:00.000Z
+false | two | 2024-05-10T00:12:00.000Z
+// end::present_over_time-result[]
+true | two | 2024-05-10T00:14:00.000Z
+true | two | 2024-05-10T00:16:00.000Z
+false | two | 2024-05-10T00:18:00.000Z
+true | two | 2024-05-10T00:20:00.000Z
+true | two | 2024-05-10T00:22:00.000Z
+;
diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/present.csv-spec b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/present.csv-spec
index 42b206fdbcb66..e9d3e67b7b10d 100644
--- a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/present.csv-spec
+++ b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/present.csv-spec
@@ -110,3 +110,43 @@ FROM employees
p_true:boolean | p_false:boolean
true | false
;
+
+PRESENT with per-agg filter with toInteger
+required_capability: fn_present
+
+FROM employees
+| STATS p_true = TO_INTEGER(PRESENT(salary)) WHERE gender == "M",
+ p_false = TO_INTEGER(PRESENT(salary)) WHERE gender == "X"
+;
+
+p_true:integer | p_false:integer
+1 | 0
+;
+
+TO_INTEGER(PRESENT) with filter returns 1
+required_capability: fn_present
+
+FROM employees
+| WHERE emp_no IN (10019, 10020)
+| STATS is_present = TO_INTEGER(PRESENT(languages))
+;
+
+is_present:integer
+1
+;
+
+TO_INTEGER(PRESENT) with filter returns 0
+required_capability: fn_present
+
+// tag::present-as-integer[]
+FROM employees
+| WHERE emp_no == 10020
+| STATS is_present = TO_INTEGER(PRESENT(languages))
+// end::present-as-integer[]
+;
+
+// tag::present-as-integer-result[]
+is_present:integer
+0
+// end::present-as-integer-result[]
+;
diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/EsqlCapabilities.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/EsqlCapabilities.java
index 3bee4b70ab912..902ecf37b20f4 100644
--- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/EsqlCapabilities.java
+++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/EsqlCapabilities.java
@@ -1472,7 +1472,12 @@ public enum Cap {
/**
* TO_DENSE_VECTOR function.
*/
- TO_DENSE_VECTOR_FUNCTION(Build.current().isSnapshot());
+ TO_DENSE_VECTOR_FUNCTION(Build.current().isSnapshot()),
+
+ /**
+ * Support present_over_time aggregation that gets evaluated per time-series
+ */
+ PRESENT_OVER_TIME(Build.current().isSnapshot());
private final boolean enabled;
diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/analysis/Analyzer.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/analysis/Analyzer.java
index 55ec36630d509..1f51055094c92 100644
--- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/analysis/Analyzer.java
+++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/analysis/Analyzer.java
@@ -63,6 +63,8 @@
import org.elasticsearch.xpack.esql.expression.function.aggregate.MaxOverTime;
import org.elasticsearch.xpack.esql.expression.function.aggregate.Min;
import org.elasticsearch.xpack.esql.expression.function.aggregate.MinOverTime;
+import org.elasticsearch.xpack.esql.expression.function.aggregate.Present;
+import org.elasticsearch.xpack.esql.expression.function.aggregate.PresentOverTime;
import org.elasticsearch.xpack.esql.expression.function.aggregate.Sum;
import org.elasticsearch.xpack.esql.expression.function.aggregate.SumOverTime;
import org.elasticsearch.xpack.esql.expression.function.aggregate.SummationMode;
@@ -2129,6 +2131,9 @@ private static AggregateMetricDoubleBlockBuilder.Metric getMetric(AggregateFunct
if (aggFunc instanceof Avg || aggFunc instanceof AvgOverTime) {
return AggregateMetricDoubleBlockBuilder.Metric.COUNT;
}
+ if (aggFunc instanceof Present || aggFunc instanceof PresentOverTime) {
+ return AggregateMetricDoubleBlockBuilder.Metric.COUNT;
+ }
return null;
}
}
diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/EsqlFunctionRegistry.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/EsqlFunctionRegistry.java
index 79f610bc9ad98..409b656bf131a 100644
--- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/EsqlFunctionRegistry.java
+++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/EsqlFunctionRegistry.java
@@ -38,6 +38,7 @@
import org.elasticsearch.xpack.esql.expression.function.aggregate.MinOverTime;
import org.elasticsearch.xpack.esql.expression.function.aggregate.Percentile;
import org.elasticsearch.xpack.esql.expression.function.aggregate.Present;
+import org.elasticsearch.xpack.esql.expression.function.aggregate.PresentOverTime;
import org.elasticsearch.xpack.esql.expression.function.aggregate.Rate;
import org.elasticsearch.xpack.esql.expression.function.aggregate.Sample;
import org.elasticsearch.xpack.esql.expression.function.aggregate.SpatialCentroid;
@@ -529,7 +530,8 @@ private static FunctionDefinition[][] snapshotFunctions() {
def(Magnitude.class, Magnitude::new, "v_magnitude"),
def(Hamming.class, Hamming::new, "v_hamming"),
def(UrlEncode.class, UrlEncode::new, "url_encode"),
- def(UrlDecode.class, UrlDecode::new, "url_decode") } };
+ def(UrlDecode.class, UrlDecode::new, "url_decode"),
+ def(PresentOverTime.class, uni(PresentOverTime::new), "present_over_time") } };
}
public EsqlFunctionRegistry snapshotRegistry() {
diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/aggregate/AggregateWritables.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/aggregate/AggregateWritables.java
index fd799c6f47128..5e11d09c044ef 100644
--- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/aggregate/AggregateWritables.java
+++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/aggregate/AggregateWritables.java
@@ -43,7 +43,8 @@ public static List getNamedWriteables() {
CountOverTime.ENTRY,
CountDistinctOverTime.ENTRY,
WeightedAvg.ENTRY,
- Present.ENTRY
+ Present.ENTRY,
+ PresentOverTime.ENTRY
);
}
}
diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/aggregate/Present.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/aggregate/Present.java
index 7f3ef8f47b917..e134bd412d91e 100644
--- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/aggregate/Present.java
+++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/aggregate/Present.java
@@ -48,6 +48,11 @@ public class Present extends AggregateFunction implements ToAggregator {
description = "To check for the presence inside a group use `PRESENT()` and `BY` clauses",
file = "present",
tag = "present-by"
+ ),
+ @Example(
+ description = "To check for the presence and return 1 when it's true and 0 when it's false",
+ file = "present",
+ tag = "present-as-integer"
) }
)
public Present(
diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/aggregate/PresentOverTime.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/aggregate/PresentOverTime.java
new file mode 100644
index 0000000000000..f01b76e78cb8a
--- /dev/null
+++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/aggregate/PresentOverTime.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+package org.elasticsearch.xpack.esql.expression.function.aggregate;
+
+import org.elasticsearch.common.io.stream.NamedWriteableRegistry;
+import org.elasticsearch.common.io.stream.StreamInput;
+import org.elasticsearch.xpack.esql.core.expression.Expression;
+import org.elasticsearch.xpack.esql.core.expression.Literal;
+import org.elasticsearch.xpack.esql.core.tree.NodeInfo;
+import org.elasticsearch.xpack.esql.core.tree.Source;
+import org.elasticsearch.xpack.esql.core.type.DataType;
+import org.elasticsearch.xpack.esql.expression.function.Example;
+import org.elasticsearch.xpack.esql.expression.function.FunctionAppliesTo;
+import org.elasticsearch.xpack.esql.expression.function.FunctionAppliesToLifecycle;
+import org.elasticsearch.xpack.esql.expression.function.FunctionInfo;
+import org.elasticsearch.xpack.esql.expression.function.FunctionType;
+import org.elasticsearch.xpack.esql.expression.function.Param;
+
+import java.io.IOException;
+import java.util.List;
+
+import static java.util.Collections.emptyList;
+
+/**
+ * Similar to {@link Present}, but it is used to check the presence of values over a time series in the given field.
+ */
+public class PresentOverTime extends TimeSeriesAggregateFunction {
+ public static final NamedWriteableRegistry.Entry ENTRY = new NamedWriteableRegistry.Entry(
+ Expression.class,
+ "PresentOverTime",
+ PresentOverTime::new
+ );
+
+ @FunctionInfo(
+ type = FunctionType.TIME_SERIES_AGGREGATE,
+ returnType = { "boolean" },
+ description = "The presence of a field in the output result over time range.",
+ appliesTo = { @FunctionAppliesTo(lifeCycle = FunctionAppliesToLifecycle.UNAVAILABLE) },
+ note = "Available with the [TS](/reference/query-languages/esql/commands/source-commands.md#esql-ts) command in snapshot builds",
+ examples = { @Example(file = "k8s-timeseries", tag = "present_over_time") }
+ )
+ public PresentOverTime(
+ Source source,
+ @Param(
+ name = "field",
+ type = {
+ "aggregate_metric_double",
+ "boolean",
+ "cartesian_point",
+ "cartesian_shape",
+ "date",
+ "date_nanos",
+ "double",
+ "geo_point",
+ "geo_shape",
+ "geohash",
+ "geotile",
+ "geohex",
+ "integer",
+ "ip",
+ "keyword",
+ "long",
+ "text",
+ "unsigned_long",
+ "version" }
+ ) Expression field
+ ) {
+ this(source, field, Literal.TRUE);
+ }
+
+ public PresentOverTime(Source source, Expression field, Expression filter) {
+ super(source, field, filter, emptyList());
+ }
+
+ private PresentOverTime(StreamInput in) throws IOException {
+ super(in);
+ }
+
+ @Override
+ public String getWriteableName() {
+ return ENTRY.name;
+ }
+
+ @Override
+ public PresentOverTime withFilter(Expression filter) {
+ return new PresentOverTime(source(), field(), filter);
+ }
+
+ @Override
+ protected NodeInfo info() {
+ return NodeInfo.create(this, PresentOverTime::new, field(), filter());
+ }
+
+ @Override
+ public PresentOverTime replaceChildren(List newChildren) {
+ return new PresentOverTime(source(), newChildren.get(0), newChildren.get(1));
+ }
+
+ @Override
+ protected TypeResolution resolveType() {
+ return perTimeSeriesAggregation().resolveType();
+ }
+
+ @Override
+ public DataType dataType() {
+ return perTimeSeriesAggregation().dataType();
+ }
+
+ @Override
+ public Present perTimeSeriesAggregation() {
+ return new Present(source(), field(), filter());
+ }
+}
diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/aggregate/PresentOverTimeTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/aggregate/PresentOverTimeTests.java
new file mode 100644
index 0000000000000..cc1084105ab8e
--- /dev/null
+++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/aggregate/PresentOverTimeTests.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+package org.elasticsearch.xpack.esql.expression.function.aggregate;
+
+import com.carrotsearch.randomizedtesting.annotations.Name;
+import com.carrotsearch.randomizedtesting.annotations.ParametersFactory;
+
+import org.elasticsearch.xpack.esql.core.expression.Expression;
+import org.elasticsearch.xpack.esql.core.tree.Source;
+import org.elasticsearch.xpack.esql.expression.function.AbstractFunctionTestCase;
+import org.elasticsearch.xpack.esql.expression.function.TestCaseSupplier;
+
+import java.util.List;
+import java.util.function.Supplier;
+
+public class PresentOverTimeTests extends AbstractFunctionTestCase {
+ public PresentOverTimeTests(@Name("TestCase") Supplier testCaseSupplier) {
+ this.testCase = testCaseSupplier.get();
+ }
+
+ @ParametersFactory
+ public static Iterable