Skip to content

Commit 453034b

Browse files
committed
Merge branch '8.19' of https://github.com/elastic/elasticsearch into esql-inference-feature-manual-backport
2 parents 2851d08 + 595251d commit 453034b

File tree

3 files changed

+108
-6
lines changed

3 files changed

+108
-6
lines changed

docs/changelog/116043.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
pr: 116043
2+
summary: Support partial sort fields in TopN pushdown
3+
area: ES|QL
4+
type: enhancement
5+
issues:
6+
- 114515

x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/rules/physical/local/PushTopNToSource.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -189,8 +189,7 @@ && canPushDownOrders(topNExec.order(), lucenePushdownPredicates)) {
189189
break;
190190
}
191191
}
192-
// TODO: We can push down partial sorts where `pushableSorts.size() < orders.size()`, but that should involve benchmarks
193-
if (pushableSorts.size() > 0 && pushableSorts.size() == orders.size()) {
192+
if (pushableSorts.isEmpty() == false) {
194193
return new PushableCompoundExec(evalExec, queryExec, pushableSorts);
195194
}
196195
}

x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/PhysicalPlanOptimizerTests.java

Lines changed: 101 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6036,8 +6036,101 @@ public void testPushTopNDistanceWithCompoundFilterToSource() {
60366036
}
60376037

60386038
/**
6039-
* This test shows that with an additional EVAL used in the filter, we can no longer push down the SORT distance.
6040-
* TODO: This could be optimized in future work. Consider moving much of EnableSpatialDistancePushdown into logical planning.
6039+
* Tests that multiple sorts, including distance and a field, are pushed down to the source.
6040+
* <code>
6041+
* ProjectExec[[abbrev{f}#25, name{f}#26, location{f}#29, country{f}#30, city{f}#31, scalerank{f}#27, scale{r}#7]]
6042+
* \_TopNExec[[
6043+
* Order[distance{r}#4,ASC,LAST],
6044+
* Order[scalerank{f}#27,ASC,LAST],
6045+
* Order[scale{r}#7,DESC,FIRST],
6046+
* Order[loc{r}#10,DESC,FIRST]
6047+
* ],5[INTEGER],0]
6048+
* \_ExchangeExec[[abbrev{f}#25, name{f}#26, location{f}#29, country{f}#30, city{f}#31, scalerank{f}#27, scale{r}#7,
6049+
* distance{r}#4, loc{r}#10],false]
6050+
* \_ProjectExec[[abbrev{f}#25, name{f}#26, location{f}#29, country{f}#30, city{f}#31, scalerank{f}#27, scale{r}#7,
6051+
* distance{r}#4, loc{r}#10]]
6052+
* \_FieldExtractExec[abbrev{f}#25, name{f}#26, country{f}#30, city{f}#31][]
6053+
* \_EvalExec[[
6054+
* STDISTANCE(location{f}#29,[1 1 0 0 0 e1 7a 14 ae 47 21 29 40 a0 1a 2f dd 24 d6 4b 40][GEO_POINT]) AS distance,
6055+
* 10[INTEGER] - scalerank{f}#27 AS scale, TOSTRING(location{f}#29) AS loc
6056+
* ]]
6057+
* \_FieldExtractExec[location{f}#29, scalerank{f}#27][]
6058+
* \_EsQueryExec[airports], indexMode[standard], query[{
6059+
* "bool":{
6060+
* "filter":[
6061+
* {"esql_single_value":{"field":"scalerank","next":{...},"source":"scalerank &lt; 6@3:31"}},
6062+
* {"bool":{
6063+
* "must":[
6064+
* {"geo_shape":{"location":{"relation":"INTERSECTS","shape":{...}}}},
6065+
* {"geo_shape":{"location":{"relation":"DISJOINT","shape":{...}}}}
6066+
* ],"boost":1.0}}],"boost":1.0}}][_doc{f}#44], limit[5], sort[[
6067+
* GeoDistanceSort[field=location{f}#29, direction=ASC, lat=55.673, lon=12.565],
6068+
* FieldSort[field=scalerank{f}#27, direction=ASC, nulls=LAST]
6069+
* ]] estimatedRowSize[303]
6070+
* </code>
6071+
*/
6072+
public void testPushTopNDistanceAndPushableFieldWithCompoundFilterToSource() {
6073+
var optimized = optimizedPlan(physicalPlan("""
6074+
FROM airports
6075+
| EVAL distance = ST_DISTANCE(location, TO_GEOPOINT("POINT(12.565 55.673)")), scale = 10 - scalerank, loc = location::string
6076+
| WHERE distance < 500000 AND scalerank < 6 AND distance > 10000
6077+
| SORT distance ASC, scalerank ASC, scale DESC, loc DESC
6078+
| LIMIT 5
6079+
| KEEP abbrev, name, location, country, city, scalerank, scale
6080+
""", airports));
6081+
6082+
var project = as(optimized, ProjectExec.class);
6083+
var topN = as(project.child(), TopNExec.class);
6084+
assertThat(topN.order().size(), is(4));
6085+
var exchange = asRemoteExchange(topN.child());
6086+
6087+
project = as(exchange.child(), ProjectExec.class);
6088+
assertThat(
6089+
names(project.projections()),
6090+
contains("abbrev", "name", "location", "country", "city", "scalerank", "scale", "distance", "loc")
6091+
);
6092+
var extract = as(project.child(), FieldExtractExec.class);
6093+
assertThat(names(extract.attributesToExtract()), contains("abbrev", "name", "country", "city"));
6094+
var evalExec = as(extract.child(), EvalExec.class);
6095+
var alias = as(evalExec.fields().get(0), Alias.class);
6096+
assertThat(alias.name(), is("distance"));
6097+
var stDistance = as(alias.child(), StDistance.class);
6098+
assertThat(stDistance.left().toString(), startsWith("location"));
6099+
extract = as(evalExec.child(), FieldExtractExec.class);
6100+
assertThat(names(extract.attributesToExtract()), contains("location", "scalerank"));
6101+
var source = source(extract.child());
6102+
6103+
// Assert that the TopN(distance) is pushed down as geo-sort(location)
6104+
assertThat(source.limit(), is(topN.limit()));
6105+
Set<String> orderSet = orderAsSet(topN.order().subList(0, 2));
6106+
Set<String> sortsSet = sortsAsSet(source.sorts(), Map.of("location", "distance"));
6107+
assertThat(orderSet, is(sortsSet));
6108+
6109+
// Fine-grained checks on the pushed down sort
6110+
assertThat(source.limit(), is(l(5)));
6111+
assertThat(source.sorts().size(), is(2));
6112+
EsQueryExec.Sort sort = source.sorts().get(0);
6113+
assertThat(sort.direction(), is(Order.OrderDirection.ASC));
6114+
assertThat(name(sort.field()), is("location"));
6115+
assertThat(sort.sortBuilder(), isA(GeoDistanceSortBuilder.class));
6116+
sort = source.sorts().get(1);
6117+
assertThat(sort.direction(), is(Order.OrderDirection.ASC));
6118+
assertThat(name(sort.field()), is("scalerank"));
6119+
assertThat(sort.sortBuilder(), isA(FieldSortBuilder.class));
6120+
6121+
// Fine-grained checks on the pushed down query
6122+
var bool = as(source.query(), BoolQueryBuilder.class);
6123+
var rangeQueryBuilders = bool.filter().stream().filter(p -> p instanceof SingleValueQuery.Builder).toList();
6124+
assertThat("Expected one range query builder", rangeQueryBuilders.size(), equalTo(1));
6125+
assertThat(((SingleValueQuery.Builder) rangeQueryBuilders.get(0)).field(), equalTo("scalerank"));
6126+
var filterBool = bool.filter().stream().filter(p -> p instanceof BoolQueryBuilder).toList();
6127+
var fb = as(filterBool.get(0), BoolQueryBuilder.class);
6128+
var shapeQueryBuilders = fb.must().stream().filter(p -> p instanceof SpatialRelatesQuery.ShapeQueryBuilder).toList();
6129+
assertShapeQueryRange(shapeQueryBuilders, 10000.0, 500000.0);
6130+
}
6131+
6132+
/**
6133+
* This test shows that if the filter contains a predicate on the same field that is sorted, we cannot push down the sort.
60416134
* <code>
60426135
* ProjectExec[[abbrev{f}#23, name{f}#24, location{f}#27, country{f}#28, city{f}#29, scalerank{f}#25 AS scale]]
60436136
* \_TopNExec[[Order[distance{r}#4,ASC,LAST], Order[scalerank{f}#25,ASC,LAST]],5[INTEGER],0]
@@ -6073,6 +6166,7 @@ public void testPushTopNDistanceAndNonPushableEvalWithCompoundFilterToSource() {
60736166

60746167
var project = as(optimized, ProjectExec.class);
60756168
var topN = as(project.child(), TopNExec.class);
6169+
assertThat(topN.order().size(), is(2));
60766170
var exchange = asRemoteExchange(topN.child());
60776171

60786172
project = as(exchange.child(), ProjectExec.class);
@@ -6111,7 +6205,7 @@ public void testPushTopNDistanceAndNonPushableEvalWithCompoundFilterToSource() {
61116205
}
61126206

61136207
/**
6114-
* This test further shows that with a non-aliasing function, with the same name, less gets pushed down.
6208+
* This test shows that if the filter contains a predicate on the same field that is sorted, we cannot push down the sort.
61156209
* <code>
61166210
* ProjectExec[[abbrev{f}#23, name{f}#24, location{f}#27, country{f}#28, city{f}#29, scale{r}#10]]
61176211
* \_TopNExec[[Order[distance{r}#4,ASC,LAST], Order[scale{r}#10,ASC,LAST]],5[INTEGER],0]
@@ -6148,6 +6242,7 @@ public void testPushTopNDistanceAndNonPushableEvalsWithCompoundFilterToSource()
61486242
""", airports));
61496243
var project = as(optimized, ProjectExec.class);
61506244
var topN = as(project.child(), TopNExec.class);
6245+
assertThat(topN.order().size(), is(2));
61516246
var exchange = asRemoteExchange(topN.child());
61526247

61536248
project = as(exchange.child(), ProjectExec.class);
@@ -6185,7 +6280,8 @@ public void testPushTopNDistanceAndNonPushableEvalsWithCompoundFilterToSource()
61856280
}
61866281

61876282
/**
6188-
* This test shows that with if the top level AND'd predicate contains a non-pushable component, we should not push anything.
6283+
* This test shows that with if the top level predicate contains a non-pushable component (eg. disjunction),
6284+
* we should not push down the filter.
61896285
* <code>
61906286
* ProjectExec[[abbrev{f}#8612, name{f}#8613, location{f}#8616, country{f}#8617, city{f}#8618, scalerank{f}#8614 AS scale]]
61916287
* \_TopNExec[[Order[distance{r}#8596,ASC,LAST], Order[scalerank{f}#8614,ASC,LAST]],5[INTEGER],0]
@@ -6223,6 +6319,7 @@ public void testPushTopNDistanceWithCompoundFilterToSourceAndDisjunctiveNonPusha
62236319

62246320
var project = as(optimized, ProjectExec.class);
62256321
var topN = as(project.child(), TopNExec.class);
6322+
assertThat(topN.order().size(), is(2));
62266323
var exchange = asRemoteExchange(topN.child());
62276324

62286325
project = as(exchange.child(), ProjectExec.class);

0 commit comments

Comments
 (0)