Skip to content

ESQL: Introduce per agg filter#113735

Merged
costin merged 25 commits intoelastic:mainfrom
costin:esql/per-agg-filtering
Oct 15, 2024
Merged

ESQL: Introduce per agg filter#113735
costin merged 25 commits intoelastic:mainfrom
costin:esql/per-agg-filtering

Conversation

@costin
Copy link
Copy Markdown
Member

@costin costin commented Sep 28, 2024

Add support for aggregation scoped filters that work dynamically on the
data in each group.

| STATS
    success = COUNT(*) WHERE 200 <= code AND code < 300,
   redirect = COUNT(*) WHERE 300 <= code AND code < 400,
 client_err = COUNT(*) WHERE 400 <= code AND code < 500,
 server_err = COUNT(*) WHERE 500 <= code AND code < 600,
total_count = COUNT(*)

Implementation wise, the base AggregateFunction has been extended to
allow a filter to be passed on. This is required to incorporate the
filter as part of the aggregate equality/identify which would fail with
the filter as an external component.

As part of the process, the serialization for the existing aggregations
had to be fixed so AggregateFunction implementations so that it
delegates to their parent first.

Comment on lines +122 to +129
: (qualifiedName ASSIGN)? booleanExpression
;

fromCommand
: FROM indexPattern (COMMA indexPattern)* metadata?
;

indexPattern
: clusterString COLON indexString
| indexString
: (clusterString COLON)? indexString
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Small improvements to the grammar that don't change the parsing.

Comment on lines +305 to +313
// unwrap filtered expression
if (e instanceof FilteredExpression fe) {
e = fe.delegate();
// TODO add verification for filter clause
}
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Still TBD

Comment on lines +75 to +84
this(
Source.readFrom((PlanStreamInput) in),
in.readNamedWriteable(Expression.class),
in.getTransportVersion().onOrAfter(TransportVersions.ESQL_PER_AGGREGATE_FILTER)
? in.readNamedWriteable(Expression.class)
: Literal.TRUE,
in.getTransportVersion().onOrAfter(TransportVersions.ESQL_PER_AGGREGATE_FILTER)
? in.readNamedWriteableCollectionAsList(Expression.class)
: emptyList()
);
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Uniform serialization across all aggregate functions - we should had that since the beginning to avoid having each subclass add its own repetitive serialization.
/cc @nik9000

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think it's easier to make the filter wrap the aggregation rather than inside each one. I think also that serializing a list in case aggs use it is probably not a great choice either. It's fine, but feels like something we'd do when we need it.

Comment on lines +82 to +92
return NodeInfo.create(this, Avg::new, field(), filter());
}

@Override
public Avg replaceChildren(List<Expression> newChildren) {
return new Avg(source(), newChildren.get(0));
return new Avg(source(), newChildren.get(0), newChildren.get(1));
}

@Override
public Avg withFilter(Expression filter) {
return new Avg(source(), field(), filter);
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

The side effect is all aggs had to be modified to take into account the parent filter.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Could we wrap them with something like we do at runtime? It feels like that might work.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Not sure what you mean. My initial approach was to wrap the agg and filter in separate class (FilteredAggregate with two children - aggregate and filter/expression ) however this messed up the optimizer since the rules picked the aggregate functions but had no idea there was a filter associated with it.
This is problematic for scenario where the same agg is defined with different filter:
STATS c = count(). c_f = count() WHERE a > 1
since the where filter is disjoined from the count, the rules replaced the second count with the first one but that is incorrect since they have different execution paths.
Furthermore at compute time the filter and actual agg gets fused anyway so the two are a pair anyway.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

They do get fused anyway, but it's just a lot more convenient if every agg doesn't have to worry about being filtered. If filter is a member here we have to think about it all over the place. It's enough that the fused filter at compute time has to add all the tests for it, I'd love to avoid that bubbling all the way out. OTOH, if that's how it has to be, I'm not deep enough to object.

Copy link
Copy Markdown
Member

@fang-xing-esql fang-xing-esql Oct 3, 2024

Choose a reason for hiding this comment

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

I wonder if this means each aggregation evaluates its own filter? If so, from performance perspective, one of the advantages of separating filter from aggregation is that, if the same filter applies to different aggregations, it makes it easier to recognize that it is the same filter and can be evaluated once for all aggregations that share it, in stead of evaluating each filter within each aggregation separately. However I couldn’t figure out how the current infrastructure supports it yet. At first look at this, it reminds me of the (common)table expressions and union all subqueries in SQL, but we don’t have this infrastructure in ES|QL yet, and it could be a much broader scope.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This approach doesn't prevent optimizing the filter evaluation.
Each aggregation ends up having its own filter because count(*) is different than count(*) where a > 10. Initially they were decoupled however the rules considered them the same since the filter was sitting outside the aggregation.
When dealing with the same filter, we can assemble a different pipeline so the mask or masked block is reused across multiple aggregations.

Comment on lines +69 to +71
for (int i = 0, s = aggs.size(); i < s; i++) {
NamedExpression agg = aggs.get(i);

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This needs to be rolled back.

Copy link
Copy Markdown
Member Author

@costin costin left a comment

Choose a reason for hiding this comment

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

Draft PR to check the CI.
Was based on an old commit to avoid some of the volatility in main.
Will rebase and polish after a few CI rounds.

@costin costin force-pushed the esql/per-agg-filtering branch from 60af98d to d2db8db Compare October 1, 2024 01:36
@costin costin force-pushed the esql/per-agg-filtering branch from d2db8db to 8a201ec Compare October 1, 2024 01:55
@costin costin marked this pull request as ready for review October 1, 2024 01:57
@elasticsearchmachine elasticsearchmachine added the Team:Analytics Meta label for analytical engine team (ESQL/Aggs/Geo) label Oct 1, 2024
@elasticsearchmachine
Copy link
Copy Markdown
Collaborator

Pinging @elastic/es-analytical-engine (Team:Analytics)

@elasticsearchmachine
Copy link
Copy Markdown
Collaborator

Pinging @elastic/kibana-esql (ES|QL-ui)

Add support for aggregation scoped filters that work dynamically on the
 data in each group.

| STATS
    success = COUNT(*) WHERE 200 <= code AND code < 300,
   redirect = COUNT(*) WHERE 300 <= code AND code < 400,
 client_err = COUNT(*) WHERE 400 <= code AND code < 500,
 server_err = COUNT(*) WHERE 500 <= code AND code < 600,
 total_count = COUNT(*)

Implementation wise, the base AggregateFunction has been extended to
 allow a filter to be passed on. This is required to incorporate the
 filter as part of the aggregate equality/identify which would fail with
 the filter as an external component.

As part of the process, the serialization for the existing aggregations
 had to be fixed so AggregateFunction implementations so that it
 delegates to their parent first.
@costin costin force-pushed the esql/per-agg-filtering branch from 8a201ec to 839dbbf Compare October 1, 2024 02:31
Copy link
Copy Markdown
Member Author

@costin costin left a comment

Choose a reason for hiding this comment

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

Addressed TBDs, add verifier code thus promoted from draft and rebased on latest main.

@costin costin added the >feature label Oct 1, 2024
@elasticsearchmachine
Copy link
Copy Markdown
Collaborator

Hi @costin, I've created a changelog YAML for you. Note that since this PR is labelled release highlight, you need to update the changelog YAML to fill out the extended information sections.

@elasticsearchmachine
Copy link
Copy Markdown
Collaborator

💔 Backport failed

The backport operation could not be completed due to the following error:

An unexpected error occurred when attempting to backport this PR.

You can use sqren/backport to manually backport by running backport --upstream elastic/elasticsearch --pr 113735

davidkyle pushed a commit that referenced this pull request Oct 15, 2024
Add support for aggregation scoped filters that work dynamically on the
 data in each group.

| STATS
    success = COUNT(*) WHERE 200 <= code AND code < 300,
   redirect = COUNT(*) WHERE 300 <= code AND code < 400,
 client_err = COUNT(*) WHERE 400 <= code AND code < 500,
 server_err = COUNT(*) WHERE 500 <= code AND code < 600,
 total_count = COUNT(*)

Implementation wise, the base AggregateFunction has been extended to
 allow a filter to be passed on. This is required to incorporate the
 filter as part of the aggregate equality/identify which would fail with
 the filter as an external component.

As part of the process, the serialization for the existing aggregations
 had to be fixed so AggregateFunction implementations so that it
 delegates to their parent first.
costin added a commit to costin/elasticsearch that referenced this pull request Oct 15, 2024
Add support for aggregation scoped filters that work dynamically on the
 data in each group.

| STATS
    success = COUNT(*) WHERE 200 <= code AND code < 300,
   redirect = COUNT(*) WHERE 300 <= code AND code < 400,
 client_err = COUNT(*) WHERE 400 <= code AND code < 500,
 server_err = COUNT(*) WHERE 500 <= code AND code < 600,
 total_count = COUNT(*)

Implementation wise, the base AggregateFunction has been extended to
 allow a filter to be passed on. This is required to incorporate the
 filter as part of the aggregate equality/identify which would fail with
 the filter as an external component.

As part of the process, the serialization for the existing aggregations
 had to be fixed so AggregateFunction implementations so that it
 delegates to their parent first.

(cherry picked from commit d102659)
costin added a commit to costin/elasticsearch that referenced this pull request Oct 15, 2024
Remove dev flag left in grammar for agg filtering
Related to elastic#113735
elasticsearchmachine pushed a commit that referenced this pull request Oct 15, 2024
* ESQL: Introduce per agg filter (#113735)

Add support for aggregation scoped filters that work dynamically on the
 data in each group.

| STATS
    success = COUNT(*) WHERE 200 <= code AND code < 300,
   redirect = COUNT(*) WHERE 300 <= code AND code < 400,
 client_err = COUNT(*) WHERE 400 <= code AND code < 500,
 server_err = COUNT(*) WHERE 500 <= code AND code < 600,
 total_count = COUNT(*)

Implementation wise, the base AggregateFunction has been extended to
 allow a filter to be passed on. This is required to incorporate the
 filter as part of the aggregate equality/identify which would fail with
 the filter as an external component.

As part of the process, the serialization for the existing aggregations
 had to be fixed so AggregateFunction implementations so that it
 delegates to their parent first.

(cherry picked from commit d102659)

* Update docs/changelog/114842.yaml

* Delete docs/changelog/114842.yaml
costin added a commit that referenced this pull request Oct 15, 2024
Remove dev flag left in grammar for agg filtering
Related to #113735
costin added a commit to costin/elasticsearch that referenced this pull request Oct 15, 2024
Remove dev flag left in grammar for agg filtering
Related to elastic#113735

(cherry picked from commit 5e59ab5)
elasticsearchmachine pushed a commit that referenced this pull request Oct 15, 2024
Remove dev flag left in grammar for agg filtering
Related to #113735

(cherry picked from commit 5e59ab5)
@bpintea
Copy link
Copy Markdown
Contributor

bpintea commented Oct 24, 2024

From above:

we can prevent queries where a filter is specified through the verifier.

@costin, I guess this hasn't been implemented right? I've opened #115522, but do we want a fix before addressing that?

georgewallace pushed a commit to georgewallace/elasticsearch that referenced this pull request Oct 25, 2024
Add support for aggregation scoped filters that work dynamically on the
 data in each group.

| STATS
    success = COUNT(*) WHERE 200 <= code AND code < 300,
   redirect = COUNT(*) WHERE 300 <= code AND code < 400,
 client_err = COUNT(*) WHERE 400 <= code AND code < 500,
 server_err = COUNT(*) WHERE 500 <= code AND code < 600,
 total_count = COUNT(*)

Implementation wise, the base AggregateFunction has been extended to
 allow a filter to be passed on. This is required to incorporate the
 filter as part of the aggregate equality/identify which would fail with
 the filter as an external component.

As part of the process, the serialization for the existing aggregations
 had to be fixed so AggregateFunction implementations so that it
 delegates to their parent first.
georgewallace pushed a commit to georgewallace/elasticsearch that referenced this pull request Oct 25, 2024
Remove dev flag left in grammar for agg filtering
Related to elastic#113735
craigtaverner added a commit that referenced this pull request Feb 12, 2026
When using a WHERE clause within the STATS of a spatial aggregation, and there are more than one aggregation function, the results are incorrect, and reflect the values of the first aggregating function. With some investigation by Cursor it turns out this is a side effect of the fact that the replaceChildren function was not capturing the filter (and not the window or field-extract preference either). This was probably due to the first implementation of the filter in #113735 back in October 2024, which appears to have not updated this method in the SpatialCentroid class, although it did update that method in other aggregations. Then SpatialExtents, inspired by SpatialCentroid code, inherited this flaw. Since it is rare to perform queries like this, and our test suite did not include any, we did not notice this until working on a more advanced feature, extracting a combination of both centroid and bounds from shape doc-values, which requires both centroid and shape aggregations in the same query.
craigtaverner added a commit to craigtaverner/elasticsearch that referenced this pull request Feb 12, 2026
When using a WHERE clause within the STATS of a spatial aggregation, and there are more than one aggregation function, the results are incorrect, and reflect the values of the first aggregating function. With some investigation by Cursor it turns out this is a side effect of the fact that the replaceChildren function was not capturing the filter (and not the window or field-extract preference either). This was probably due to the first implementation of the filter in elastic#113735 back in October 2024, which appears to have not updated this method in the SpatialCentroid class, although it did update that method in other aggregations. Then SpatialExtents, inspired by SpatialCentroid code, inherited this flaw. Since it is rare to perform queries like this, and our test suite did not include any, we did not notice this until working on a more advanced feature, extracting a combination of both centroid and bounds from shape doc-values, which requires both centroid and shape aggregations in the same query.
elasticsearchmachine pushed a commit that referenced this pull request Feb 13, 2026
)

When using a WHERE clause within the STATS of a spatial aggregation, and there are more than one aggregation function, the results are incorrect, and reflect the values of the first aggregating function. With some investigation by Cursor it turns out this is a side effect of the fact that the replaceChildren function was not capturing the filter (and not the window or field-extract preference either). This was probably due to the first implementation of the filter in #113735 back in October 2024, which appears to have not updated this method in the SpatialCentroid class, although it did update that method in other aggregations. Then SpatialExtents, inspired by SpatialCentroid code, inherited this flaw. Since it is rare to perform queries like this, and our test suite did not include any, we did not notice this until working on a more advanced feature, extracting a combination of both centroid and bounds from shape doc-values, which requires both centroid and shape aggregations in the same query.
craigtaverner added a commit to craigtaverner/elasticsearch that referenced this pull request Feb 13, 2026
elastic#142385)

When using a WHERE clause within the STATS of a spatial aggregation, and there are more than one aggregation function, the results are incorrect, and reflect the values of the first aggregating function. With some investigation by Cursor it turns out this is a side effect of the fact that the replaceChildren function was not capturing the filter (and not the window or field-extract preference either). This was probably due to the first implementation of the filter in elastic#113735 back in October 2024, which appears to have not updated this method in the SpatialCentroid class, although it did update that method in other aggregations. Then SpatialExtents, inspired by SpatialCentroid code, inherited this flaw. Since it is rare to perform queries like this, and our test suite did not include any, we did not notice this until working on a more advanced feature, extracting a combination of both centroid and bounds from shape doc-values, which requires both centroid and shape aggregations in the same query.
elasticsearchmachine pushed a commit that referenced this pull request Feb 13, 2026
) (#142487)

When using a WHERE clause within the STATS of a spatial aggregation, and there are more than one aggregation function, the results are incorrect, and reflect the values of the first aggregating function. With some investigation by Cursor it turns out this is a side effect of the fact that the replaceChildren function was not capturing the filter (and not the window or field-extract preference either). This was probably due to the first implementation of the filter in #113735 back in October 2024, which appears to have not updated this method in the SpatialCentroid class, although it did update that method in other aggregations. Then SpatialExtents, inspired by SpatialCentroid code, inherited this flaw. Since it is rare to perform queries like this, and our test suite did not include any, we did not notice this until working on a more advanced feature, extracting a combination of both centroid and bounds from shape doc-values, which requires both centroid and shape aggregations in the same query.
craigtaverner added a commit to craigtaverner/elasticsearch that referenced this pull request Feb 16, 2026
elastic#142385) (elastic#142487)

When using a WHERE clause within the STATS of a spatial aggregation, and there are more than one aggregation function, the results are incorrect, and reflect the values of the first aggregating function. With some investigation by Cursor it turns out this is a side effect of the fact that the replaceChildren function was not capturing the filter (and not the window or field-extract preference either). This was probably due to the first implementation of the filter in elastic#113735 back in October 2024, which appears to have not updated this method in the SpatialCentroid class, although it did update that method in other aggregations. Then SpatialExtents, inspired by SpatialCentroid code, inherited this flaw. Since it is rare to perform queries like this, and our test suite did not include any, we did not notice this until working on a more advanced feature, extracting a combination of both centroid and bounds from shape doc-values, which requires both centroid and shape aggregations in the same query.
elasticsearchmachine pushed a commit that referenced this pull request Feb 16, 2026
) (#142487) (#142533)

When using a WHERE clause within the STATS of a spatial aggregation, and there are more than one aggregation function, the results are incorrect, and reflect the values of the first aggregating function. With some investigation by Cursor it turns out this is a side effect of the fact that the replaceChildren function was not capturing the filter (and not the window or field-extract preference either). This was probably due to the first implementation of the filter in #113735 back in October 2024, which appears to have not updated this method in the SpatialCentroid class, although it did update that method in other aggregations. Then SpatialExtents, inspired by SpatialCentroid code, inherited this flaw. Since it is rare to perform queries like this, and our test suite did not include any, we did not notice this until working on a more advanced feature, extracting a combination of both centroid and bounds from shape doc-values, which requires both centroid and shape aggregations in the same query.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

:Analytics/ES|QL AKA ESQL auto-backport Automatically create backport pull requests when merged ES|QL-ui Impacts ES|QL UI >feature release highlight Team:Analytics Meta label for analytical engine team (ESQL/Aggs/Geo) v8.16.0 v9.0.0

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants