-
Notifications
You must be signed in to change notification settings - Fork 24.9k
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
Disable optimization if we aren't sure its faster #74260
Disable optimization if we aren't sure its faster #74260
Conversation
This disables the filter-by-filter aggregation optimization used by `terms`, `range`, `date_histogram`, and `date_range` aggregations unless we're *sure* that its faster than the "native" implementation. Mostly this is when the top level query is empty or we can merge it into the filter generated by the agg rewrite process. Now that we have hard and fast rules we can drop the cost estimation framework without too much fear. So we remove it in this change. It stomps a bunch of complexity. Sadly, without the cost estimation stuff we have to add a separate mechanism for blocking the optimization against runtime fields for which it'd be kind of garbage. For that I added another rule preventing the filter-by-filter aggregation from running against the queries made by runtime fields. Its not fool-proof, but we have control over what queries we pass as a filter so its not wide open. I spent a lot of time working on an alternative to this that preserved that fancy filter-by-filter collection mechanism and was much more kind to the query cache. It detected cases where going full filter-by-filter was bad and grouped those filters together to collect in one pass with a funny ORing collector. It *worked*. And, if we were super concerned with the performance of the `filters` aggregation it'd be the way to go. But it was very complex and it was actually slower than using the native aggregation for things like `terms` and `date_histogram`. It was glorious. But it was wrong for us. Too complex and optimized the wrong things. So here we are. Hopefully this is a fairly simple solution to a sneaky problem.
Go jenkins go! Run all those tests. |
Pinging @elastic/es-analytics-geo (Team:Analytics) |
@@ -623,30 +622,23 @@ public void testFilterByFilter() throws InterruptedException, IOException { | |||
assertThat(breakdown.get(COLLECT), equalTo(0L)); | |||
assertThat(breakdown.get(BUILD_AGGREGATION).longValue(), greaterThan(0L)); | |||
assertThat(breakdown.get(REDUCE), equalTo(0L)); | |||
Map<String, Object> debug = histoAggResult.getDebugInfo(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There are a lot of test changes in this PR. The tests started failing because we no longer calculate the cost of the agg so I took the opportunity to convert them to debugTestCase
and MapMatcher
.
@@ -53,7 +53,7 @@ public KeywordScriptFieldType(String name) { | |||
this(name, StringFieldScript.PARSE_FROM_SOURCE, null, Collections.emptyMap(), (builder, params) -> builder); | |||
} | |||
|
|||
KeywordScriptFieldType( | |||
public KeywordScriptFieldType( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd have to go through a ton of mocking without this and I'm explicitly trying to test runtime fields so I think its probably worth it to just make it public. There is a private builder factory thing but it goes through a lot of parsing infrastructure we don't have in the tests.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This may be a dumb question, but can you not just use the public constructor above? Particularly as it looks like your use of this is just doing a source lookup, which is what the name-only constructor gives you.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, it's a good question. I could use it if I built the _source but all the rest of those tests operate on doc values so I don't really have the infrastructure in them to build the source. Maybe that's simpler than I though and I should just do it. I'll have a look.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I gave this a shot and it didn't work. I think those public ctors don't currently make useful fields. That's worth fixing, I think, but probably not in this PR.
@@ -30,7 +30,7 @@ | |||
public AdaptingAggregator( | |||
Aggregator parent, | |||
AggregatorFactories subAggregators, | |||
CheckedFunction<AggregatorFactories, Aggregator, IOException> delegate | |||
CheckedFunction<AggregatorFactories, ? extends Aggregator, IOException> delegate |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This delegate thing is required so we can "pin" the parent references in the sub aggregators. I wish we didn't have to do that stuff, but here we are.
@@ -43,14 +42,6 @@ long count(LeafReaderContext ctx, FiltersAggregator.Counter counter, Bits live) | |||
return super.count(ctx, counter, live); | |||
} | |||
|
|||
@Override | |||
long estimateCountCost(LeafReaderContext ctx, CheckedSupplier<Boolean, IOException> canUseMetadata) throws IOException { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No more estimate*Cost methods because we use heuristics on query type.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The problem with estimate*Cost methods was that they couldn't detect when a query was slow to prepare because you had to prepare to get the cost. And, now that we disable the optimization if the top level is non-match-all there isn't really any complex heuristics to do. We have a lot more control over which queries come in on the filters, at least in the "optimize" route.
* {@link Compatible} but doesn't support when there is a parent aggregator | ||
* or any child aggregators. | ||
*/ | ||
public class FilterByFilterAggregator extends FiltersAggregator { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I yanked this out to the top level because it was complex. And because it has a builder.....
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I appreciate that.
* stop counting once we've passed {@code maxEstimatedCost} if we aren't profiling. | ||
*/ | ||
@SuppressWarnings("resource") // We're not in change of anything Closeable | ||
public long estimateCost(long maxCost) throws IOException { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This method is entirely gone.
* {@link Compatible} but doesn't support when there is a parent aggregator | ||
* or any child aggregators. | ||
*/ | ||
public static class FilterByFilter extends FiltersAggregator { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Most of this class is just moved into its own file. The cost estimation is entirely gone.
* Use {@link #bulkScorer(LeafReaderContext, Runnable)} to build the scorer | ||
* when needed. | ||
*/ | ||
private BulkScorer[] bulkScorers; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No need to cache scorers because we use them once. We still want to cache the weight because we use it for every segment.
public boolean isInefficientUnion() { | ||
return true; | ||
} | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm mostly building the boolean query here out of spite - we don't use it but it is the correct query to use if we didn't disable the optimization.
@@ -291,23 +290,8 @@ public static Aggregator build( | |||
cardinality, | |||
metadata | |||
); | |||
Map<String, Object> filtersDebug = null; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No more need to capture the filters debug - if we built the filters agg we kept it. If we didn't we don't have anything to keep. I suppose we could add a debug callback to the build process, but we already capture the queries it tries to build and those are all we use to decide whether or not to use filter by filter.
Let me see about resolving those test conflicts.... |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM. It's magic, but it's well documented magic with a lot of tests, and that's really all I can ask for.
* {@link Compatible} but doesn't support when there is a parent aggregator | ||
* or any child aggregators. | ||
*/ | ||
public class FilterByFilterAggregator extends FiltersAggregator { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I appreciate that.
.../main/java/org/elasticsearch/search/aggregations/bucket/filter/FilterByFilterAggregator.java
Outdated
Show resolved
Hide resolved
* "native" aggregation implementation rather than go filter by filter. | ||
*/ | ||
public boolean isInefficientUnion() { | ||
return false; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
so the default is that the union is good, and we override specific cases to return false for known bad unions?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The default is that filters are not "inefficient unions". In the case where we're fairly sure it is an inefficient union we override to true and bail as fast as we can.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The trick here is that when we rewrite we're fairly sure we're making efficient queries. Well, we check for runtime fields queries, but otherwise, we expect rewriting to make fairly efficient queries.
When you are using the filters
aggregator you very well could provide a terrible query. But it's going to be the same speed to run it filter by filter as it would be to run it in the compatible way. There are likely some cases where it'd have been faster to rewrite it. Or, better yet, to use the "disjunction collector" idea that I showed you to run all of the filters in parallel with the top level query. But this change should conservatively fall back to the old performance. Which wasn't great, but at least it wasn't terrible.
Co-authored-by: Mark Tozzi <[email protected]>
if (query instanceof AbstractScriptFieldQuery) { | ||
/* | ||
* We know that runtime fields aren't not fast to query at all | ||
* but we expect all other sorts of queries are at least as |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: aren't not
-> aren't
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM. Indeed this approach is easy to follow.
Thanks folks! I'll fix up the spelling and merge when jenkins likes it. |
…c#74260) This disables the filter-by-filter aggregation optimization used by `terms`, `range`, `date_histogram`, and `date_range` aggregations unless we're *sure* that its faster than the "native" implementation. Mostly this is when the top level query is empty or we can merge it into the filter generated by the agg rewrite process. Now that we have hard and fast rules we can drop the cost estimation framework without too much fear. So we remove it in this change. It stomps a bunch of complexity. Sadly, without the cost estimation stuff we have to add a separate mechanism for blocking the optimization against runtime fields for which it'd be kind of garbage. For that I added another rule preventing the filter-by-filter aggregation from running against the queries made by runtime fields. Its not fool-proof, but we have control over what queries we pass as a filter so its not wide open. I spent a lot of time working on an alternative to this that preserved that fancy filter-by-filter collection mechanism and was much more kind to the query cache. It detected cases where going full filter-by-filter was bad and grouped those filters together to collect in one pass with a funny ORing collector. It *worked*. And, if we were super concerned with the performance of the `filters` aggregation it'd be the way to go. But it was very complex and it was actually slower than using the native aggregation for things like `terms` and `date_histogram`. It was glorious. But it was wrong for us. Too complex and optimized the wrong things. So here we are. Hopefully this is a fairly simple solution to a sneaky problem.
#74563) This disables the filter-by-filter aggregation optimization used by `terms`, `range`, `date_histogram`, and `date_range` aggregations unless we're *sure* that its faster than the "native" implementation. Mostly this is when the top level query is empty or we can merge it into the filter generated by the agg rewrite process. Now that we have hard and fast rules we can drop the cost estimation framework without too much fear. So we remove it in this change. It stomps a bunch of complexity. Sadly, without the cost estimation stuff we have to add a separate mechanism for blocking the optimization against runtime fields for which it'd be kind of garbage. For that I added another rule preventing the filter-by-filter aggregation from running against the queries made by runtime fields. Its not fool-proof, but we have control over what queries we pass as a filter so its not wide open. I spent a lot of time working on an alternative to this that preserved that fancy filter-by-filter collection mechanism and was much more kind to the query cache. It detected cases where going full filter-by-filter was bad and grouped those filters together to collect in one pass with a funny ORing collector. It *worked*. And, if we were super concerned with the performance of the `filters` aggregation it'd be the way to go. But it was very complex and it was actually slower than using the native aggregation for things like `terms` and `date_histogram`. It was glorious. But it was wrong for us. Too complex and optimized the wrong things. So here we are. Hopefully this is a fairly simple solution to a sneaky problem.
In elastic#74260 we disabled the "filter by filter" aggregations optimizations when we couldn't be 100% sure that they were both faster and *safe* for the cluster. Before that, mostly in the 7.13 line, we would aggressively enable the optimization whenever we could and there were lots of cases where the well meaning optimization was way way slower than the original implementation. There were also cases where it'd fill up memory and knock over the cluster! Disaster. With elastic#74260 we're safe. The optimization still kicks in when its faster - like when the top level query is empty - but otherwise it get's out of the way and lets all the normal aggs run.
In #74260 we disabled the "filter by filter" aggregations optimizations when we couldn't be 100% sure that they were both faster and *safe* for the cluster. Before that, mostly in the 7.13 line, we would aggressively enable the optimization whenever we could and there were lots of cases where the well meaning optimization was way way slower than the original implementation. There were also cases where it'd fill up memory and knock over the cluster! Disaster. With #74260 we're safe. The optimization still kicks in when its faster - like when the top level query is empty - but otherwise it get's out of the way and lets all the normal aggs run.
In elastic#74260 we disabled the "filter by filter" aggregations optimizations when we couldn't be 100% sure that they were both faster and *safe* for the cluster. Before that, mostly in the 7.13 line, we would aggressively enable the optimization whenever we could and there were lots of cases where the well meaning optimization was way way slower than the original implementation. There were also cases where it'd fill up memory and knock over the cluster! Disaster. With elastic#74260 we're safe. The optimization still kicks in when its faster - like when the top level query is empty - but otherwise it get's out of the way and lets all the normal aggs run.
…5859) In #74260 we disabled the "filter by filter" aggregations optimizations when we couldn't be 100% sure that they were both faster and *safe* for the cluster. Before that, mostly in the 7.13 line, we would aggressively enable the optimization whenever we could and there were lots of cases where the well meaning optimization was way way slower than the original implementation. There were also cases where it'd fill up memory and knock over the cluster! Disaster. With #74260 we're safe. The optimization still kicks in when its faster - like when the top level query is empty - but otherwise it get's out of the way and lets all the normal aggs run.
This disables the filter-by-filter aggregation optimization used by
terms
,range
,date_histogram
, anddate_range
aggregations unlesswe're sure that its faster than the "native" implementation. Mostly this
is when the top level query is empty or we can merge it into the filter
generated by the agg rewrite process.
Now that we have hard and fast rules we can drop the cost estimation
framework without too much fear. So we remove it in this change. It
stomps a bunch of complexity. Sadly, without the cost estimation stuff
we have to add a separate mechanism for blocking the optimization
against runtime fields for which it'd be kind of garbage. For that I
added another rule preventing the filter-by-filter aggregation from
running against the queries made by runtime fields. Its not fool-proof,
but we have control over what queries we pass as a filter so its not
wide open.
I spent a lot of time working on an alternative to this that preserved
that fancy filter-by-filter collection mechanism and was much more kind
to the query cache. It detected cases where going full filter-by-filter
was bad and grouped those filters together to collect in one pass with a
funny ORing collector. It worked. And, if we were super concerned with
the performance of the
filters
aggregation it'd be the way to go. Butit was very complex and it was actually slower than using the native
aggregation for things like
terms
anddate_histogram
. It wasglorious. But it was wrong for us. Too complex and optimized the wrong
things.
So here we are. Hopefully this is a fairly simple solution to a sneaky
problem.
Closes #74480