Browse Source

ESQL: Added Median and MedianAbsoluteDeviation aggregations tests and kibana docs (#111231)

Iván Cea Fontenla 1 năm trước cách đây
mục cha
commit
826d49448b
25 tập tin đã thay đổi với 495 bổ sung68 xóa
  1. 4 4
      docs/reference/esql/functions/aggregation-functions.asciidoc
  2. 7 0
      docs/reference/esql/functions/appendix/median.asciidoc
  3. 7 0
      docs/reference/esql/functions/appendix/median_absolute_deviation.asciidoc
  4. 7 0
      docs/reference/esql/functions/description/median.asciidoc
  5. 7 0
      docs/reference/esql/functions/description/median_absolute_deviation.asciidoc
  6. 22 0
      docs/reference/esql/functions/examples/median.asciidoc
  7. 22 0
      docs/reference/esql/functions/examples/median_absolute_deviation.asciidoc
  8. 49 0
      docs/reference/esql/functions/kibana/definition/median.json
  9. 49 0
      docs/reference/esql/functions/kibana/definition/median_absolute_deviation.json
  10. 12 0
      docs/reference/esql/functions/kibana/docs/median.md
  11. 14 0
      docs/reference/esql/functions/kibana/docs/median_absolute_deviation.md
  12. 16 0
      docs/reference/esql/functions/layout/median.asciidoc
  13. 16 0
      docs/reference/esql/functions/layout/median_absolute_deviation.asciidoc
  14. 0 52
      docs/reference/esql/functions/median.asciidoc
  15. 6 0
      docs/reference/esql/functions/parameters/median.asciidoc
  16. 6 0
      docs/reference/esql/functions/parameters/median_absolute_deviation.asciidoc
  17. 1 0
      docs/reference/esql/functions/signature/median.svg
  18. 1 0
      docs/reference/esql/functions/signature/median_absolute_deviation.svg
  19. 11 0
      docs/reference/esql/functions/types/median.asciidoc
  20. 11 0
      docs/reference/esql/functions/types/median_absolute_deviation.asciidoc
  21. 6 6
      x-pack/plugin/esql/qa/testFixtures/src/main/resources/meta.csv-spec
  22. 21 3
      x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/aggregate/Median.java
  23. 28 3
      x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/aggregate/MedianAbsoluteDeviation.java
  24. 69 0
      x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/aggregate/MedianAbsoluteDeviationTests.java
  25. 103 0
      x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/aggregate/MedianTests.java

+ 4 - 4
docs/reference/esql/functions/aggregation-functions.asciidoc

@@ -12,8 +12,8 @@ The <<esql-stats-by>> command supports these aggregate functions:
 * <<esql-agg-count>>
 * <<esql-agg-count-distinct>>
 * <<esql-max>>
-* <<esql-agg-median>>
-* <<esql-agg-median-absolute-deviation>>
+* <<esql-median>>
+* <<esql-median_absolute_deviation>>
 * <<esql-min>>
 * <<esql-percentile>>
 * experimental:[] <<esql-st_centroid_agg>>
@@ -25,10 +25,10 @@ The <<esql-stats-by>> command supports these aggregate functions:
 
 include::count.asciidoc[]
 include::count-distinct.asciidoc[]
-include::median.asciidoc[]
-include::median-absolute-deviation.asciidoc[]
 include::layout/avg.asciidoc[]
 include::layout/max.asciidoc[]
+include::layout/median.asciidoc[]
+include::layout/median_absolute_deviation.asciidoc[]
 include::layout/min.asciidoc[]
 include::layout/percentile.asciidoc[]
 include::layout/st_centroid_agg.asciidoc[]

+ 7 - 0
docs/reference/esql/functions/appendix/median.asciidoc

@@ -0,0 +1,7 @@
+// This is generated by ESQL's AbstractFunctionTestCase. Do no edit it. See ../README.md for how to regenerate it.
+
+[WARNING]
+====
+`MEDIAN` is also {wikipedia}/Nondeterministic_algorithm[non-deterministic].
+This means you can get slightly different results using the same data.
+====

+ 7 - 0
docs/reference/esql/functions/appendix/median_absolute_deviation.asciidoc

@@ -0,0 +1,7 @@
+// This is generated by ESQL's AbstractFunctionTestCase. Do no edit it. See ../README.md for how to regenerate it.
+
+[WARNING]
+====
+`MEDIAN_ABSOLUTE_DEVIATION` is also {wikipedia}/Nondeterministic_algorithm[non-deterministic].
+This means you can get slightly different results using the same data.
+====

+ 7 - 0
docs/reference/esql/functions/description/median.asciidoc

@@ -0,0 +1,7 @@
+// This is generated by ESQL's AbstractFunctionTestCase. Do no edit it. See ../README.md for how to regenerate it.
+
+*Description*
+
+The value that is greater than half of all values and less than half of all values, also known as the 50% <<esql-percentile>>.
+
+NOTE: Like <<esql-percentile>>, `MEDIAN` is <<esql-percentile-approximate,usually approximate>>.

+ 7 - 0
docs/reference/esql/functions/description/median_absolute_deviation.asciidoc

@@ -0,0 +1,7 @@
+// This is generated by ESQL's AbstractFunctionTestCase. Do no edit it. See ../README.md for how to regenerate it.
+
+*Description*
+
+Returns the median absolute deviation, a measure of variability. It is a robust statistic, meaning that it is useful for describing data that may have outliers, or may not be normally distributed. For such data it can be more descriptive than standard deviation.  It is calculated as the median of each data point's deviation from the median of the entire sample. That is, for a random variable `X`, the median absolute deviation is `median(|median(X) - X|)`.
+
+NOTE: Like <<esql-percentile>>, `MEDIAN_ABSOLUTE_DEVIATION` is <<esql-percentile-approximate,usually approximate>>.

+ 22 - 0
docs/reference/esql/functions/examples/median.asciidoc

@@ -0,0 +1,22 @@
+// This is generated by ESQL's AbstractFunctionTestCase. Do no edit it. See ../README.md for how to regenerate it.
+
+*Examples*
+
+[source.merge.styled,esql]
+----
+include::{esql-specs}/stats_percentile.csv-spec[tag=median]
+----
+[%header.monospaced.styled,format=dsv,separator=|]
+|===
+include::{esql-specs}/stats_percentile.csv-spec[tag=median-result]
+|===
+The expression can use inline functions. For example, to calculate the median of the maximum values of a multivalued column, first use `MV_MAX` to get the maximum value per row, and use the result with the `MEDIAN` function
+[source.merge.styled,esql]
+----
+include::{esql-specs}/stats_percentile.csv-spec[tag=docsStatsMedianNestedExpression]
+----
+[%header.monospaced.styled,format=dsv,separator=|]
+|===
+include::{esql-specs}/stats_percentile.csv-spec[tag=docsStatsMedianNestedExpression-result]
+|===
+

+ 22 - 0
docs/reference/esql/functions/examples/median_absolute_deviation.asciidoc

@@ -0,0 +1,22 @@
+// This is generated by ESQL's AbstractFunctionTestCase. Do no edit it. See ../README.md for how to regenerate it.
+
+*Examples*
+
+[source.merge.styled,esql]
+----
+include::{esql-specs}/stats_percentile.csv-spec[tag=median-absolute-deviation]
+----
+[%header.monospaced.styled,format=dsv,separator=|]
+|===
+include::{esql-specs}/stats_percentile.csv-spec[tag=median-absolute-deviation-result]
+|===
+The expression can use inline functions. For example, to calculate the the median absolute deviation of the maximum values of a multivalued column, first use `MV_MAX` to get the maximum value per row, and use the result with the `MEDIAN_ABSOLUTE_DEVIATION` function
+[source.merge.styled,esql]
+----
+include::{esql-specs}/stats_percentile.csv-spec[tag=docsStatsMADNestedExpression]
+----
+[%header.monospaced.styled,format=dsv,separator=|]
+|===
+include::{esql-specs}/stats_percentile.csv-spec[tag=docsStatsMADNestedExpression-result]
+|===
+

+ 49 - 0
docs/reference/esql/functions/kibana/definition/median.json

@@ -0,0 +1,49 @@
+{
+  "comment" : "This is generated by ESQL's AbstractFunctionTestCase. Do no edit it. See ../README.md for how to regenerate it.",
+  "type" : "agg",
+  "name" : "median",
+  "description" : "The value that is greater than half of all values and less than half of all values, also known as the 50% <<esql-percentile>>.",
+  "note" : "Like <<esql-percentile>>, `MEDIAN` is <<esql-percentile-approximate,usually approximate>>.",
+  "signatures" : [
+    {
+      "params" : [
+        {
+          "name" : "number",
+          "type" : "double",
+          "optional" : false,
+          "description" : ""
+        }
+      ],
+      "variadic" : false,
+      "returnType" : "double"
+    },
+    {
+      "params" : [
+        {
+          "name" : "number",
+          "type" : "integer",
+          "optional" : false,
+          "description" : ""
+        }
+      ],
+      "variadic" : false,
+      "returnType" : "double"
+    },
+    {
+      "params" : [
+        {
+          "name" : "number",
+          "type" : "long",
+          "optional" : false,
+          "description" : ""
+        }
+      ],
+      "variadic" : false,
+      "returnType" : "double"
+    }
+  ],
+  "examples" : [
+    "FROM employees\n| STATS MEDIAN(salary), PERCENTILE(salary, 50)",
+    "FROM employees\n| STATS median_max_salary_change = MEDIAN(MV_MAX(salary_change))"
+  ]
+}

+ 49 - 0
docs/reference/esql/functions/kibana/definition/median_absolute_deviation.json

@@ -0,0 +1,49 @@
+{
+  "comment" : "This is generated by ESQL's AbstractFunctionTestCase. Do no edit it. See ../README.md for how to regenerate it.",
+  "type" : "agg",
+  "name" : "median_absolute_deviation",
+  "description" : "Returns the median absolute deviation, a measure of variability. It is a robust statistic, meaning that it is useful for describing data that may have outliers, or may not be normally distributed. For such data it can be more descriptive than standard deviation.\n\nIt is calculated as the median of each data point's deviation from the median of the entire sample. That is, for a random variable `X`, the median absolute deviation is `median(|median(X) - X|)`.",
+  "note" : "Like <<esql-percentile>>, `MEDIAN_ABSOLUTE_DEVIATION` is <<esql-percentile-approximate,usually approximate>>.",
+  "signatures" : [
+    {
+      "params" : [
+        {
+          "name" : "number",
+          "type" : "double",
+          "optional" : false,
+          "description" : ""
+        }
+      ],
+      "variadic" : false,
+      "returnType" : "double"
+    },
+    {
+      "params" : [
+        {
+          "name" : "number",
+          "type" : "integer",
+          "optional" : false,
+          "description" : ""
+        }
+      ],
+      "variadic" : false,
+      "returnType" : "double"
+    },
+    {
+      "params" : [
+        {
+          "name" : "number",
+          "type" : "long",
+          "optional" : false,
+          "description" : ""
+        }
+      ],
+      "variadic" : false,
+      "returnType" : "double"
+    }
+  ],
+  "examples" : [
+    "FROM employees\n| STATS MEDIAN(salary), MEDIAN_ABSOLUTE_DEVIATION(salary)",
+    "FROM employees\n| STATS m_a_d_max_salary_change = MEDIAN_ABSOLUTE_DEVIATION(MV_MAX(salary_change))"
+  ]
+}

+ 12 - 0
docs/reference/esql/functions/kibana/docs/median.md

@@ -0,0 +1,12 @@
+<!--
+This is generated by ESQL's AbstractFunctionTestCase. Do no edit it. See ../README.md for how to regenerate it.
+-->
+
+### MEDIAN
+The value that is greater than half of all values and less than half of all values, also known as the 50% <<esql-percentile>>.
+
+```
+FROM employees
+| STATS MEDIAN(salary), PERCENTILE(salary, 50)
+```
+Note: Like <<esql-percentile>>, `MEDIAN` is <<esql-percentile-approximate,usually approximate>>.

+ 14 - 0
docs/reference/esql/functions/kibana/docs/median_absolute_deviation.md

@@ -0,0 +1,14 @@
+<!--
+This is generated by ESQL's AbstractFunctionTestCase. Do no edit it. See ../README.md for how to regenerate it.
+-->
+
+### MEDIAN_ABSOLUTE_DEVIATION
+Returns the median absolute deviation, a measure of variability. It is a robust statistic, meaning that it is useful for describing data that may have outliers, or may not be normally distributed. For such data it can be more descriptive than standard deviation.
+
+It is calculated as the median of each data point's deviation from the median of the entire sample. That is, for a random variable `X`, the median absolute deviation is `median(|median(X) - X|)`.
+
+```
+FROM employees
+| STATS MEDIAN(salary), MEDIAN_ABSOLUTE_DEVIATION(salary)
+```
+Note: Like <<esql-percentile>>, `MEDIAN_ABSOLUTE_DEVIATION` is <<esql-percentile-approximate,usually approximate>>.

+ 16 - 0
docs/reference/esql/functions/layout/median.asciidoc

@@ -0,0 +1,16 @@
+// This is generated by ESQL's AbstractFunctionTestCase. Do no edit it. See ../README.md for how to regenerate it.
+
+[discrete]
+[[esql-median]]
+=== `MEDIAN`
+
+*Syntax*
+
+[.text-center]
+image::esql/functions/signature/median.svg[Embedded,opts=inline]
+
+include::../parameters/median.asciidoc[]
+include::../description/median.asciidoc[]
+include::../types/median.asciidoc[]
+include::../examples/median.asciidoc[]
+include::../appendix/median.asciidoc[]

+ 16 - 0
docs/reference/esql/functions/layout/median_absolute_deviation.asciidoc

@@ -0,0 +1,16 @@
+// This is generated by ESQL's AbstractFunctionTestCase. Do no edit it. See ../README.md for how to regenerate it.
+
+[discrete]
+[[esql-median_absolute_deviation]]
+=== `MEDIAN_ABSOLUTE_DEVIATION`
+
+*Syntax*
+
+[.text-center]
+image::esql/functions/signature/median_absolute_deviation.svg[Embedded,opts=inline]
+
+include::../parameters/median_absolute_deviation.asciidoc[]
+include::../description/median_absolute_deviation.asciidoc[]
+include::../types/median_absolute_deviation.asciidoc[]
+include::../examples/median_absolute_deviation.asciidoc[]
+include::../appendix/median_absolute_deviation.asciidoc[]

+ 0 - 52
docs/reference/esql/functions/median.asciidoc

@@ -1,52 +0,0 @@
-[discrete]
-[[esql-agg-median]]
-=== `MEDIAN`
-
-*Syntax*
-
-[source,esql]
-----
-MEDIAN(expression)
-----
-
-*Parameters*
-
-`expression`::
-Expression from which to return the median value.
-
-*Description*
-
-Returns the value that is greater than half of all values and less than half of
-all values, also known as the 50% <<esql-percentile>>.
-
-NOTE: Like <<esql-percentile>>, `MEDIAN` is <<esql-percentile-approximate,usually approximate>>.
-
-[WARNING]
-====
-`MEDIAN` is also {wikipedia}/Nondeterministic_algorithm[non-deterministic].
-This means you can get slightly different results using the same data.
-====
-
-*Example*
-
-[source.merge.styled,esql]
-----
-include::{esql-specs}/stats_percentile.csv-spec[tag=median]
-----
-[%header.monospaced.styled,format=dsv,separator=|]
-|===
-include::{esql-specs}/stats_percentile.csv-spec[tag=median-result]
-|===
-
-The expression can use inline functions. For example, to calculate the median of
-the maximum values of a multivalued column, first use `MV_MAX` to get the
-maximum value per row, and use the result with the `MEDIAN` function:
-
-[source.merge.styled,esql]
-----
-include::{esql-specs}/stats_percentile.csv-spec[tag=docsStatsMedianNestedExpression]
-----
-[%header.monospaced.styled,format=dsv,separator=|]
-|===
-include::{esql-specs}/stats_percentile.csv-spec[tag=docsStatsMedianNestedExpression-result]
-|===

+ 6 - 0
docs/reference/esql/functions/parameters/median.asciidoc

@@ -0,0 +1,6 @@
+// This is generated by ESQL's AbstractFunctionTestCase. Do no edit it. See ../README.md for how to regenerate it.
+
+*Parameters*
+
+`number`::
+

+ 6 - 0
docs/reference/esql/functions/parameters/median_absolute_deviation.asciidoc

@@ -0,0 +1,6 @@
+// This is generated by ESQL's AbstractFunctionTestCase. Do no edit it. See ../README.md for how to regenerate it.
+
+*Parameters*
+
+`number`::
+

+ 1 - 0
docs/reference/esql/functions/signature/median.svg

@@ -0,0 +1 @@
+<svg version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg" width="288" height="46" viewbox="0 0 288 46"><defs><style type="text/css">#guide .c{fill:none;stroke:#222222;}#guide .k{fill:#000000;font-family:Roboto Mono,Sans-serif;font-size:20px;}#guide .s{fill:#e4f4ff;stroke:#222222;}#guide .syn{fill:#8D8D8D;font-family:Roboto Mono,Sans-serif;font-size:20px;}</style></defs><path class="c" d="M0 31h5m92 0h10m32 0h10m92 0h10m32 0h5"/><rect class="s" x="5" y="5" width="92" height="36"/><text class="k" x="15" y="31">MEDIAN</text><rect class="s" x="107" y="5" width="32" height="36" rx="7"/><text class="syn" x="117" y="31">(</text><rect class="s" x="149" y="5" width="92" height="36" rx="7"/><text class="k" x="159" y="31">number</text><rect class="s" x="251" y="5" width="32" height="36" rx="7"/><text class="syn" x="261" y="31">)</text></svg>

+ 1 - 0
docs/reference/esql/functions/signature/median_absolute_deviation.svg

@@ -0,0 +1 @@
+<svg version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg" width="516" height="46" viewbox="0 0 516 46"><defs><style type="text/css">#guide .c{fill:none;stroke:#222222;}#guide .k{fill:#000000;font-family:Roboto Mono,Sans-serif;font-size:20px;}#guide .s{fill:#e4f4ff;stroke:#222222;}#guide .syn{fill:#8D8D8D;font-family:Roboto Mono,Sans-serif;font-size:20px;}</style></defs><path class="c" d="M0 31h5m320 0h10m32 0h10m92 0h10m32 0h5"/><rect class="s" x="5" y="5" width="320" height="36"/><text class="k" x="15" y="31">MEDIAN_ABSOLUTE_DEVIATION</text><rect class="s" x="335" y="5" width="32" height="36" rx="7"/><text class="syn" x="345" y="31">(</text><rect class="s" x="377" y="5" width="92" height="36" rx="7"/><text class="k" x="387" y="31">number</text><rect class="s" x="479" y="5" width="32" height="36" rx="7"/><text class="syn" x="489" y="31">)</text></svg>

+ 11 - 0
docs/reference/esql/functions/types/median.asciidoc

@@ -0,0 +1,11 @@
+// This is generated by ESQL's AbstractFunctionTestCase. Do no edit it. See ../README.md for how to regenerate it.
+
+*Supported types*
+
+[%header.monospaced.styled,format=dsv,separator=|]
+|===
+number | result
+double | double
+integer | double
+long | double
+|===

+ 11 - 0
docs/reference/esql/functions/types/median_absolute_deviation.asciidoc

@@ -0,0 +1,11 @@
+// This is generated by ESQL's AbstractFunctionTestCase. Do no edit it. See ../README.md for how to regenerate it.
+
+*Supported types*
+
+[%header.monospaced.styled,format=dsv,separator=|]
+|===
+number | result
+double | double
+integer | double
+long | double
+|===

+ 6 - 6
x-pack/plugin/esql/qa/testFixtures/src/main/resources/meta.csv-spec

@@ -40,8 +40,8 @@ double e()
 "double log10(number:double|integer|long|unsigned_long)"
 "keyword|text ltrim(string:keyword|text)"
 "boolean|double|integer|long|date|ip max(field:boolean|double|integer|long|date|ip)"
-"double|integer|long median(number:double|integer|long)"
-"double|integer|long median_absolute_deviation(number:double|integer|long)"
+"double median(number:double|integer|long)"
+"double median_absolute_deviation(number:double|integer|long)"
 "boolean|double|integer|long|date|ip min(field:boolean|double|integer|long|date|ip)"
 "boolean|cartesian_point|cartesian_shape|date|double|geo_point|geo_shape|integer|ip|keyword|long|text|version mv_append(field1:boolean|cartesian_point|cartesian_shape|date|double|geo_point|geo_shape|integer|ip|keyword|long|text|version, field2:boolean|cartesian_point|cartesian_shape|date|double|geo_point|geo_shape|integer|ip|keyword|long|text|version)"
 "double mv_avg(number:double|integer|long|unsigned_long)"
@@ -283,8 +283,8 @@ log           |Returns the logarithm of a value to a base. The input can be any
 log10         |Returns the logarithm of a value to base 10. The input can be any numeric value, the return value is always a double.  Logs of 0 and negative numbers return `null` as well as a warning.
 ltrim         |Removes leading whitespaces from a string.
 max           |The maximum value of a field.
-median        |The value that is greater than half of all values and less than half of all values.
-median_absolut|The median absolute deviation, a measure of variability.
+median        |The value that is greater than half of all values and less than half of all values, also known as the 50% <<esql-percentile>>.
+median_absolut|"Returns the median absolute deviation, a measure of variability. It is a robust statistic, meaning that it is useful for describing data that may have outliers, or may not be normally distributed. For such data it can be more descriptive than standard deviation.  It is calculated as the median of each data point's deviation from the median of the entire sample. That is, for a random variable `X`, the median absolute deviation is `median(|median(X) - X|)`."
 min           |The minimum value of a field.
 mv_append     |Concatenates values of two multi-value fields.
 mv_avg        |Converts a multivalued field into a single valued field containing the average of all of the values.
@@ -406,8 +406,8 @@ log           |double
 log10         |double                                                                                                                      |false                       |false           |false
 ltrim         |"keyword|text"                                                                                                              |false                       |false           |false
 max           |"boolean|double|integer|long|date|ip"                                                                                       |false                       |false           |true
-median        |"double|integer|long"                                                                                                       |false                       |false           |true
-median_absolut|"double|integer|long"                                                                                                       |false                       |false           |true
+median        |double                                                                                                                      |false                       |false           |true
+median_absolut|double                                                                                                                      |false                       |false           |true
 min           |"boolean|double|integer|long|date|ip"                                                                                       |false                       |false           |true
 mv_append     |"boolean|cartesian_point|cartesian_shape|date|double|geo_point|geo_shape|integer|ip|keyword|long|text|version"              |[false, false]              |false 		     |false
 mv_avg        |double                                                                                                                      |false                       |false           |false

+ 21 - 3
x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/aggregate/Median.java

@@ -16,6 +16,7 @@ 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.SurrogateExpression;
+import org.elasticsearch.xpack.esql.expression.function.Example;
 import org.elasticsearch.xpack.esql.expression.function.FunctionInfo;
 import org.elasticsearch.xpack.esql.expression.function.Param;
 import org.elasticsearch.xpack.esql.expression.function.scalar.convert.ToDouble;
@@ -32,9 +33,26 @@ public class Median extends AggregateFunction implements SurrogateExpression {
 
     // TODO: Add the compression parameter
     @FunctionInfo(
-        returnType = { "double", "integer", "long" },
-        description = "The value that is greater than half of all values and less than half of all values.",
-        isAggregation = true
+        returnType = "double",
+        description = "The value that is greater than half of all values and less than half of all values, "
+            + "also known as the 50% <<esql-percentile>>.",
+        note = "Like <<esql-percentile>>, `MEDIAN` is <<esql-percentile-approximate,usually approximate>>.",
+        appendix = """
+            [WARNING]
+            ====
+            `MEDIAN` is also {wikipedia}/Nondeterministic_algorithm[non-deterministic].
+            This means you can get slightly different results using the same data.
+            ====""",
+        isAggregation = true,
+        examples = {
+            @Example(file = "stats_percentile", tag = "median"),
+            @Example(
+                description = "The expression can use inline functions. For example, to calculate the median of "
+                    + "the maximum values of a multivalued column, first use `MV_MAX` to get the "
+                    + "maximum value per row, and use the result with the `MEDIAN` function",
+                file = "stats_percentile",
+                tag = "docsStatsMedianNestedExpression"
+            ), }
     )
     public Median(Source source, @Param(name = "number", type = { "double", "integer", "long" }) Expression field) {
         super(source, field);

+ 28 - 3
x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/aggregate/MedianAbsoluteDeviation.java

@@ -16,6 +16,7 @@ import org.elasticsearch.compute.aggregation.MedianAbsoluteDeviationLongAggregat
 import org.elasticsearch.xpack.esql.core.expression.Expression;
 import org.elasticsearch.xpack.esql.core.tree.NodeInfo;
 import org.elasticsearch.xpack.esql.core.tree.Source;
+import org.elasticsearch.xpack.esql.expression.function.Example;
 import org.elasticsearch.xpack.esql.expression.function.FunctionInfo;
 import org.elasticsearch.xpack.esql.expression.function.Param;
 
@@ -31,9 +32,33 @@ public class MedianAbsoluteDeviation extends NumericAggregate {
 
     // TODO: Add parameter
     @FunctionInfo(
-        returnType = { "double", "integer", "long" },
-        description = "The median absolute deviation, a measure of variability.",
-        isAggregation = true
+        returnType = "double",
+        description = "Returns the median absolute deviation, a measure of variability. It is a robust "
+            + "statistic, meaning that it is useful for describing data that may have outliers, "
+            + "or may not be normally distributed. For such data it can be more descriptive "
+            + "than standard deviation."
+            + "\n\n"
+            + "It is calculated as the median of each data point's deviation from the median of "
+            + "the entire sample. That is, for a random variable `X`, the median absolute "
+            + "deviation is `median(|median(X) - X|)`.",
+        note = "Like <<esql-percentile>>, `MEDIAN_ABSOLUTE_DEVIATION` is <<esql-percentile-approximate,usually approximate>>.",
+        appendix = """
+            [WARNING]
+            ====
+            `MEDIAN_ABSOLUTE_DEVIATION` is also {wikipedia}/Nondeterministic_algorithm[non-deterministic].
+            This means you can get slightly different results using the same data.
+            ====""",
+        isAggregation = true,
+        examples = {
+            @Example(file = "stats_percentile", tag = "median-absolute-deviation"),
+            @Example(
+                description = "The expression can use inline functions. For example, to calculate the the "
+                    + "median absolute deviation of the maximum values of a multivalued column, first "
+                    + "use `MV_MAX` to get the maximum value per row, and use the result with the "
+                    + "`MEDIAN_ABSOLUTE_DEVIATION` function",
+                file = "stats_percentile",
+                tag = "docsStatsMADNestedExpression"
+            ), }
     )
     public MedianAbsoluteDeviation(Source source, @Param(name = "number", type = { "double", "integer", "long" }) Expression field) {
         super(source, field);

+ 69 - 0
x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/aggregate/MedianAbsoluteDeviationTests.java

@@ -0,0 +1,69 @@
+/*
+ * 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.search.aggregations.metrics.InternalMedianAbsoluteDeviation;
+import org.elasticsearch.search.aggregations.metrics.TDigestState;
+import org.elasticsearch.xpack.esql.core.expression.Expression;
+import org.elasticsearch.xpack.esql.core.tree.Source;
+import org.elasticsearch.xpack.esql.core.type.DataType;
+import org.elasticsearch.xpack.esql.expression.function.AbstractAggregationTestCase;
+import org.elasticsearch.xpack.esql.expression.function.MultiRowTestCaseSupplier;
+import org.elasticsearch.xpack.esql.expression.function.TestCaseSupplier;
+
+import java.util.List;
+import java.util.function.Supplier;
+import java.util.stream.Stream;
+
+import static org.hamcrest.Matchers.equalTo;
+
+public class MedianAbsoluteDeviationTests extends AbstractAggregationTestCase {
+    public MedianAbsoluteDeviationTests(@Name("TestCase") Supplier<TestCaseSupplier.TestCase> testCaseSupplier) {
+        this.testCase = testCaseSupplier.get();
+    }
+
+    @ParametersFactory
+    public static Iterable<Object[]> parameters() {
+        var suppliers = Stream.of(
+            MultiRowTestCaseSupplier.intCases(1, 1000, Integer.MIN_VALUE, Integer.MAX_VALUE, true),
+            MultiRowTestCaseSupplier.longCases(1, 1000, Long.MIN_VALUE, Long.MAX_VALUE, true),
+            MultiRowTestCaseSupplier.doubleCases(1, 1000, -Double.MAX_VALUE, Double.MAX_VALUE, true)
+        ).flatMap(List::stream).map(MedianAbsoluteDeviationTests::makeSupplier).toList();
+
+        return parameterSuppliersFromTypedDataWithDefaultChecks(suppliers);
+    }
+
+    @Override
+    protected Expression build(Source source, List<Expression> args) {
+        return new MedianAbsoluteDeviation(source, args.get(0));
+    }
+
+    private static TestCaseSupplier makeSupplier(TestCaseSupplier.TypedDataSupplier fieldSupplier) {
+        return new TestCaseSupplier(List.of(fieldSupplier.type()), () -> {
+            var fieldTypedData = fieldSupplier.get();
+
+            var digest = TDigestState.create(1000);
+
+            for (var value : fieldTypedData.multiRowData()) {
+                digest.add(((Number) value).doubleValue());
+            }
+
+            var expected = digest.size() == 0 ? null : InternalMedianAbsoluteDeviation.computeMedianAbsoluteDeviation(digest);
+
+            return new TestCaseSupplier.TestCase(
+                List.of(fieldTypedData),
+                "MedianAbsoluteDeviation[number=Attribute[channel=0]]",
+                DataType.DOUBLE,
+                equalTo(expected)
+            );
+        });
+    }
+}

+ 103 - 0
x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/aggregate/MedianTests.java

@@ -0,0 +1,103 @@
+/*
+ * 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.search.aggregations.metrics.TDigestState;
+import org.elasticsearch.xpack.esql.core.expression.Expression;
+import org.elasticsearch.xpack.esql.core.tree.Source;
+import org.elasticsearch.xpack.esql.core.type.DataType;
+import org.elasticsearch.xpack.esql.expression.function.AbstractAggregationTestCase;
+import org.elasticsearch.xpack.esql.expression.function.MultiRowTestCaseSupplier;
+import org.elasticsearch.xpack.esql.expression.function.TestCaseSupplier;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Supplier;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import static org.hamcrest.Matchers.equalTo;
+
+public class MedianTests extends AbstractAggregationTestCase {
+    public MedianTests(@Name("TestCase") Supplier<TestCaseSupplier.TestCase> testCaseSupplier) {
+        this.testCase = testCaseSupplier.get();
+    }
+
+    @ParametersFactory
+    public static Iterable<Object[]> parameters() {
+        var suppliers = Stream.of(
+            MultiRowTestCaseSupplier.intCases(1, 1000, Integer.MIN_VALUE, Integer.MAX_VALUE, true),
+            MultiRowTestCaseSupplier.longCases(1, 1000, Long.MIN_VALUE, Long.MAX_VALUE, true),
+            MultiRowTestCaseSupplier.doubleCases(1, 1000, -Double.MAX_VALUE, Double.MAX_VALUE, true)
+        ).flatMap(List::stream).map(MedianTests::makeSupplier).collect(Collectors.toCollection(ArrayList::new));
+
+        suppliers.addAll(
+            List.of(
+                // Folding
+                new TestCaseSupplier(
+                    List.of(DataType.INTEGER),
+                    () -> new TestCaseSupplier.TestCase(
+                        List.of(TestCaseSupplier.TypedData.multiRow(List.of(200), DataType.INTEGER, "number")),
+                        "Median[field=Attribute[channel=0]]",
+                        DataType.DOUBLE,
+                        equalTo(200.)
+                    )
+                ),
+                new TestCaseSupplier(
+                    List.of(DataType.LONG),
+                    () -> new TestCaseSupplier.TestCase(
+                        List.of(TestCaseSupplier.TypedData.multiRow(List.of(200L), DataType.LONG, "number")),
+                        "Median[field=Attribute[channel=0]]",
+                        DataType.DOUBLE,
+                        equalTo(200.)
+                    )
+                ),
+                new TestCaseSupplier(
+                    List.of(DataType.DOUBLE),
+                    () -> new TestCaseSupplier.TestCase(
+                        List.of(TestCaseSupplier.TypedData.multiRow(List.of(200.), DataType.DOUBLE, "number")),
+                        "Median[field=Attribute[channel=0]]",
+                        DataType.DOUBLE,
+                        equalTo(200.)
+                    )
+                )
+            )
+        );
+
+        return parameterSuppliersFromTypedDataWithDefaultChecks(suppliers);
+    }
+
+    @Override
+    protected Expression build(Source source, List<Expression> args) {
+        return new Median(source, args.get(0));
+    }
+
+    private static TestCaseSupplier makeSupplier(TestCaseSupplier.TypedDataSupplier fieldSupplier) {
+        return new TestCaseSupplier(List.of(fieldSupplier.type()), () -> {
+            var fieldTypedData = fieldSupplier.get();
+
+            var digest = TDigestState.create(1000);
+
+            for (var value : fieldTypedData.multiRowData()) {
+                digest.add(((Number) value).doubleValue());
+            }
+
+            var expected = digest.size() == 0 ? null : digest.quantile(0.5);
+
+            return new TestCaseSupplier.TestCase(
+                List.of(fieldTypedData),
+                "Median[number=Attribute[channel=0]]",
+                DataType.DOUBLE,
+                equalTo(expected)
+            );
+        });
+    }
+}