Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Boxplot Aggregation #51948

Merged
merged 3 commits into from
Feb 7, 2020
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
204 changes: 204 additions & 0 deletions docs/reference/aggregations/metrics/boxplot-aggregation.asciidoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
[role="xpack"]
[testenv="basic"]
[[search-aggregations-metrics-boxplot-aggregation]]
=== Boxplot Aggregation

A `boxplot` metrics aggregation that computes boxplot of numeric values extracted from the aggregated documents.
These values can be extracted either from specific numeric fields in the documents, or be generated by a provided script.

The `boxplot` aggregation returns essential information for making a https://en.wikipedia.org/wiki/Box_plot[box plot]: minimum, maximum
median, first quartile (25th percentile) and third quartile (75th percentile) values.

==== Syntax

A `boxplot` aggregation looks like this in isolation:

[source,js]
--------------------------------------------------
{
"boxplot": {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder, should we snake_case this to box_plot? I personally don't really like snake casing but it would be more consistent (date_histogram, significant_terms, etc).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wikipedia states both spellings and boxplot was shorter, but I don't have strong feelings about it.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In that case, boxplot it is. I like shorter too

"buckets_path": "my_cardinality_agg"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo re: "buckets_path" instead of "field"?

}
}
--------------------------------------------------
// NOTCONSOLE

Let's look at a boxplot representing load time:

[source,console]
--------------------------------------------------
GET latency/_search
{
"size": 0,
"aggs" : {
"load_time_boxplot" : {
"boxplot" : {
"field" : "load_time" <1>
}
}
}
}
--------------------------------------------------
// TEST[setup:latency]
<1> The field `load_time` must be a numeric field

The response will look like this:

[source,console-result]
--------------------------------------------------
{
...

"aggregations": {
"load_time_boxplot": {
"min": 0.0,
"max": 990.0,
"q1": 165.0,
"q2": 445.0,
"q3": 725.0
}
}
}
--------------------------------------------------
// TESTRESPONSE[s/\.\.\./"took": $body.took,"timed_out": false,"_shards": $body._shards,"hits": $body.hits,/]

As you can see, the aggregation will return a calculated value for each percentile
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like copy pasta leftovers :)

in the default range. If we assume response times are in milliseconds, it is
immediately obvious that the webpage normally loads in 10-725ms, but occasionally
spikes to 945-985ms.

==== Script

The boxplot metric supports scripting. For example, if our load times
are in milliseconds but we want percentiles calculated in seconds, we could use
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"percentiles" typo

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm, I am not sure how to call them here. Technically they are percentiles :) Values?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Whoops, yeah this is probably fine. Was thinking it was a copy/paste leftover.

a script to convert them on-the-fly:

[source,console]
--------------------------------------------------
GET latency/_search
{
"size": 0,
"aggs" : {
"load_time_boxplot" : {
"boxplot" : {
"script" : {
"lang": "painless",
"source": "doc['load_time'].value / params.timeUnit", <1>
"params" : {
"timeUnit" : 1000 <2>
}
}
}
}
}
}
--------------------------------------------------
// TEST[setup:latency]

<1> The `field` parameter is replaced with a `script` parameter, which uses the
script to generate values which percentiles are calculated on
<2> Scripting supports parameterized input just like any other script

This will interpret the `script` parameter as an `inline` script with the `painless` script language and no script parameters. To use a
stored script use the following syntax:

[source,console]
--------------------------------------------------
GET latency/_search
{
"size": 0,
"aggs" : {
"load_time_boxplot" : {
"boxplot" : {
"script" : {
"id": "my_script",
"params": {
"field": "load_time"
}
}
}
}
}
}
--------------------------------------------------
// TEST[setup:latency,stored_example_script]

[[search-aggregations-metrics-boxplot-aggregation-approximation]]
==== Boxplot values are (usually) approximate

The algorithm used by the `boxplot` metric is called TDigest (introduced by
Ted Dunning in
https://github.com/tdunning/t-digest/blob/master/docs/t-digest-paper/histo.pdf[Computing Accurate Quantiles using T-Digests]).

[WARNING]
====
Boxplot as other percentile aggregations are also
https://en.wikipedia.org/wiki/Nondeterministic_algorithm[non-deterministic].
This means you can get slightly different results using the same data.
====

[[search-aggregations-metrics-boxplot-aggregation-compression]]
==== Compression

Approximate algorithms must balance memory utilization with estimation accuracy.
This balance can be controlled using a `compression` parameter:

[source,console]
--------------------------------------------------
GET latency/_search
{
"size": 0,
"aggs" : {
"load_time_boxplot" : {
"boxplot" : {
"field" : "load_time",
"compression" : 200 <1>
}
}
}
}
--------------------------------------------------
// TEST[setup:latency]

<1> Compression controls memory usage and approximation error

The TDigest algorithm uses a number of "nodes" to approximate percentiles -- the
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm assuming this is the same as the Percentiles page? Maybe we should link to it instead? Or maybe there's a fancy way to embed a snippet from a different page? @nik9000 do you happen to know?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

more nodes available, the higher the accuracy (and large memory footprint) proportional
to the volume of data. The `compression` parameter limits the maximum number of
nodes to `20 * compression`.

Therefore, by increasing the compression value, you can increase the accuracy of
your percentiles at the cost of more memory. Larger compression values also
make the algorithm slower since the underlying tree data structure grows in size,
resulting in more expensive operations. The default compression value is
`100`.

A "node" uses roughly 32 bytes of memory, so under worst-case scenarios (large amount
of data which arrives sorted and in-order) the default settings will produce a
TDigest roughly 64KB in size. In practice data tends to be more random and
the TDigest will use less memory.

==== Missing value

The `missing` parameter defines how documents that are missing a value should be treated.
By default they will be ignored but it is also possible to treat them as if they
had a value.

[source,console]
--------------------------------------------------
GET latency/_search
{
"size": 0,
"aggs" : {
"grade_boxplot" : {
"boxplot" : {
"field" : "grade",
"missing": 10 <1>
}
}
}
}
--------------------------------------------------
// TEST[setup:latency]

<1> Documents without a value in the `grade` field will fall into the same bucket as documents that have the value `10`.
Original file line number Diff line number Diff line change
Expand Up @@ -378,7 +378,7 @@ public void testGetProperty() throws IOException {
iw.addDocument(singleton(new NumericDocValuesField("number", 7)));
iw.addDocument(singleton(new NumericDocValuesField("number", 1)));
}, (Consumer<InternalGlobal>) global -> {
assertEquals(1.0, global.getDocCount(), 2);
assertEquals(2, global.getDocCount());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

😅

assertTrue(AggregationInspectionHelper.hasValue(global));
assertNotNull(global.getAggregations().asMap().get("min"));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,15 @@

import org.elasticsearch.action.ActionRequest;
import org.elasticsearch.action.ActionResponse;
import org.elasticsearch.common.xcontent.ContextParser;
import org.elasticsearch.index.mapper.Mapper;
import org.elasticsearch.license.XPackLicenseState;
import org.elasticsearch.plugins.ActionPlugin;
import org.elasticsearch.plugins.MapperPlugin;
import org.elasticsearch.plugins.Plugin;
import org.elasticsearch.plugins.SearchPlugin;
import org.elasticsearch.search.aggregations.AggregationBuilder;
import org.elasticsearch.xpack.analytics.boxplot.InternalBoxplot;
import org.elasticsearch.xpack.analytics.mapper.HistogramFieldMapper;
import org.elasticsearch.xpack.core.XPackPlugin;
import org.elasticsearch.xpack.core.action.XPackInfoFeatureAction;
Expand All @@ -21,6 +24,7 @@
import org.elasticsearch.xpack.analytics.action.AnalyticsInfoTransportAction;
import org.elasticsearch.xpack.analytics.action.AnalyticsUsageTransportAction;
import org.elasticsearch.xpack.analytics.action.TransportAnalyticsStatsAction;
import org.elasticsearch.xpack.analytics.boxplot.BoxplotAggregationBuilder;
import org.elasticsearch.xpack.analytics.cumulativecardinality.CumulativeCardinalityPipelineAggregationBuilder;
import org.elasticsearch.xpack.analytics.cumulativecardinality.CumulativeCardinalityPipelineAggregator;
import org.elasticsearch.xpack.analytics.stringstats.InternalStringStats;
Expand Down Expand Up @@ -56,11 +60,16 @@ public List<PipelineAggregationSpec> getPipelineAggregations() {

@Override
public List<AggregationSpec> getAggregations() {
return singletonList(
return Arrays.asList(
new AggregationSpec(
StringStatsAggregationBuilder.NAME,
StringStatsAggregationBuilder::new,
StringStatsAggregationBuilder::parse).addResultReader(InternalStringStats::new)
StringStatsAggregationBuilder::parse).addResultReader(InternalStringStats::new),
new AggregationSpec(
BoxplotAggregationBuilder.NAME,
BoxplotAggregationBuilder::new,
(ContextParser<String, AggregationBuilder>) (p, c) -> BoxplotAggregationBuilder.parse(c, p))
.addResultReader(InternalBoxplot::new)
);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

package org.elasticsearch.xpack.analytics.boxplot;

import org.elasticsearch.search.aggregations.metrics.NumericMetricsAggregation;

public interface Boxplot extends NumericMetricsAggregation.MultiValue {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need these any more?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I'm not sure tbh. We might want to keep them around as a bridge to the HLRC? E.g. InternalMin and ParsedMin both implement Min, and it might help us make sure they don't diverge?

Or maybe useless bloat now that the TC is gone and we can get rid of them... but probably better to investigate that in a separate PR and apply it to all aggs if we decide we can remove them?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The HLRC won't have Boxplot on the classpath because it doesn't have the analytics module. With top_metrics I just pointed folks to ParsedTopMetrics. I'm fine either way. It is just that when I made my new agg I figured I could save myself a little work an not make it.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Argh, you're right. Keep forgetting this is in xpack. 👍 probably not necessary given the current situation


/**
* @return The minimum value of all aggregated values.
*/
double getMin();

/**
* @return The maximum value of all aggregated values.
*/
double getMax();

/**
* @return The first quartile of all aggregated values.
*/
double getQ1();

/**
* @return The second quartile of all aggregated values.
*/
double getQ2();

/**
* @return The third quartile of all aggregated values.
*/
double getQ3();

/**
* @return The minimum value of all aggregated values as a String.
*/
String getMinAsString();

/**
* @return The maximum value of all aggregated values as a String.
*/
String getMaxAsString();

/**
* @return The first quartile of all aggregated values as a String.
*/
String getQ1AsString();

/**
* @return The second quartile of all aggregated values as a String.
*/
String getQ2AsString();

/**
* @return The third quartile of all aggregated values as a String.
*/
String getQ3AsString();

}
Loading