@@ -5853,8 +5853,101 @@ public void testPushTopNDistanceWithCompoundFilterToSource() {
5853
5853
}
5854
5854
5855
5855
/**
5856
- * This test shows that with an additional EVAL used in the filter, we can no longer push down the SORT distance.
5857
- * TODO: This could be optimized in future work. Consider moving much of EnableSpatialDistancePushdown into logical planning.
5856
+ * Tests that multiple sorts, including distance and a field, are pushed down to the source.
5857
+ * <code>
5858
+ * ProjectExec[[abbrev{f}#25, name{f}#26, location{f}#29, country{f}#30, city{f}#31, scalerank{f}#27, scale{r}#7]]
5859
+ * \_TopNExec[[
5860
+ * Order[distance{r}#4,ASC,LAST],
5861
+ * Order[scalerank{f}#27,ASC,LAST],
5862
+ * Order[scale{r}#7,DESC,FIRST],
5863
+ * Order[loc{r}#10,DESC,FIRST]
5864
+ * ],5[INTEGER],0]
5865
+ * \_ExchangeExec[[abbrev{f}#25, name{f}#26, location{f}#29, country{f}#30, city{f}#31, scalerank{f}#27, scale{r}#7,
5866
+ * distance{r}#4, loc{r}#10],false]
5867
+ * \_ProjectExec[[abbrev{f}#25, name{f}#26, location{f}#29, country{f}#30, city{f}#31, scalerank{f}#27, scale{r}#7,
5868
+ * distance{r}#4, loc{r}#10]]
5869
+ * \_FieldExtractExec[abbrev{f}#25, name{f}#26, country{f}#30, city{f}#31][]
5870
+ * \_EvalExec[[
5871
+ * 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,
5872
+ * 10[INTEGER] - scalerank{f}#27 AS scale, TOSTRING(location{f}#29) AS loc
5873
+ * ]]
5874
+ * \_FieldExtractExec[location{f}#29, scalerank{f}#27][]
5875
+ * \_EsQueryExec[airports], indexMode[standard], query[{
5876
+ * "bool":{
5877
+ * "filter":[
5878
+ * {"esql_single_value":{"field":"scalerank","next":{...},"source":"scalerank < 6@3:31"}},
5879
+ * {"bool":{
5880
+ * "must":[
5881
+ * {"geo_shape":{"location":{"relation":"INTERSECTS","shape":{...}}}},
5882
+ * {"geo_shape":{"location":{"relation":"DISJOINT","shape":{...}}}}
5883
+ * ],"boost":1.0}}],"boost":1.0}}][_doc{f}#44], limit[5], sort[[
5884
+ * GeoDistanceSort[field=location{f}#29, direction=ASC, lat=55.673, lon=12.565],
5885
+ * FieldSort[field=scalerank{f}#27, direction=ASC, nulls=LAST]
5886
+ * ]] estimatedRowSize[303]
5887
+ * </code>
5888
+ */
5889
+ public void testPushTopNDistanceAndPushableFieldWithCompoundFilterToSource () {
5890
+ var optimized = optimizedPlan (physicalPlan ("""
5891
+ FROM airports
5892
+ | EVAL distance = ST_DISTANCE(location, TO_GEOPOINT("POINT(12.565 55.673)")), scale = 10 - scalerank, loc = location::string
5893
+ | WHERE distance < 500000 AND scalerank < 6 AND distance > 10000
5894
+ | SORT distance ASC, scalerank ASC, scale DESC, loc DESC
5895
+ | LIMIT 5
5896
+ | KEEP abbrev, name, location, country, city, scalerank, scale
5897
+ """ , airports ));
5898
+
5899
+ var project = as (optimized , ProjectExec .class );
5900
+ var topN = as (project .child (), TopNExec .class );
5901
+ assertThat (topN .order ().size (), is (4 ));
5902
+ var exchange = asRemoteExchange (topN .child ());
5903
+
5904
+ project = as (exchange .child (), ProjectExec .class );
5905
+ assertThat (
5906
+ names (project .projections ()),
5907
+ contains ("abbrev" , "name" , "location" , "country" , "city" , "scalerank" , "scale" , "distance" , "loc" )
5908
+ );
5909
+ var extract = as (project .child (), FieldExtractExec .class );
5910
+ assertThat (names (extract .attributesToExtract ()), contains ("abbrev" , "name" , "country" , "city" ));
5911
+ var evalExec = as (extract .child (), EvalExec .class );
5912
+ var alias = as (evalExec .fields ().get (0 ), Alias .class );
5913
+ assertThat (alias .name (), is ("distance" ));
5914
+ var stDistance = as (alias .child (), StDistance .class );
5915
+ assertThat (stDistance .left ().toString (), startsWith ("location" ));
5916
+ extract = as (evalExec .child (), FieldExtractExec .class );
5917
+ assertThat (names (extract .attributesToExtract ()), contains ("location" , "scalerank" ));
5918
+ var source = source (extract .child ());
5919
+
5920
+ // Assert that the TopN(distance) is pushed down as geo-sort(location)
5921
+ assertThat (source .limit (), is (topN .limit ()));
5922
+ Set <String > orderSet = orderAsSet (topN .order ().subList (0 , 2 ));
5923
+ Set <String > sortsSet = sortsAsSet (source .sorts (), Map .of ("location" , "distance" ));
5924
+ assertThat (orderSet , is (sortsSet ));
5925
+
5926
+ // Fine-grained checks on the pushed down sort
5927
+ assertThat (source .limit (), is (l (5 )));
5928
+ assertThat (source .sorts ().size (), is (2 ));
5929
+ EsQueryExec .Sort sort = source .sorts ().get (0 );
5930
+ assertThat (sort .direction (), is (Order .OrderDirection .ASC ));
5931
+ assertThat (name (sort .field ()), is ("location" ));
5932
+ assertThat (sort .sortBuilder (), isA (GeoDistanceSortBuilder .class ));
5933
+ sort = source .sorts ().get (1 );
5934
+ assertThat (sort .direction (), is (Order .OrderDirection .ASC ));
5935
+ assertThat (name (sort .field ()), is ("scalerank" ));
5936
+ assertThat (sort .sortBuilder (), isA (FieldSortBuilder .class ));
5937
+
5938
+ // Fine-grained checks on the pushed down query
5939
+ var bool = as (source .query (), BoolQueryBuilder .class );
5940
+ var rangeQueryBuilders = bool .filter ().stream ().filter (p -> p instanceof SingleValueQuery .Builder ).toList ();
5941
+ assertThat ("Expected one range query builder" , rangeQueryBuilders .size (), equalTo (1 ));
5942
+ assertThat (((SingleValueQuery .Builder ) rangeQueryBuilders .get (0 )).field (), equalTo ("scalerank" ));
5943
+ var filterBool = bool .filter ().stream ().filter (p -> p instanceof BoolQueryBuilder ).toList ();
5944
+ var fb = as (filterBool .get (0 ), BoolQueryBuilder .class );
5945
+ var shapeQueryBuilders = fb .must ().stream ().filter (p -> p instanceof SpatialRelatesQuery .ShapeQueryBuilder ).toList ();
5946
+ assertShapeQueryRange (shapeQueryBuilders , 10000.0 , 500000.0 );
5947
+ }
5948
+
5949
+ /**
5950
+ * This test shows that if the filter contains a predicate on the same field that is sorted, we cannot push down the sort.
5858
5951
* <code>
5859
5952
* ProjectExec[[abbrev{f}#23, name{f}#24, location{f}#27, country{f}#28, city{f}#29, scalerank{f}#25 AS scale]]
5860
5953
* \_TopNExec[[Order[distance{r}#4,ASC,LAST], Order[scalerank{f}#25,ASC,LAST]],5[INTEGER],0]
@@ -5890,6 +5983,7 @@ public void testPushTopNDistanceAndNonPushableEvalWithCompoundFilterToSource() {
5890
5983
5891
5984
var project = as (optimized , ProjectExec .class );
5892
5985
var topN = as (project .child (), TopNExec .class );
5986
+ assertThat (topN .order ().size (), is (2 ));
5893
5987
var exchange = asRemoteExchange (topN .child ());
5894
5988
5895
5989
project = as (exchange .child (), ProjectExec .class );
@@ -5928,7 +6022,7 @@ public void testPushTopNDistanceAndNonPushableEvalWithCompoundFilterToSource() {
5928
6022
}
5929
6023
5930
6024
/**
5931
- * This test further shows that with a non-aliasing function, with the same name, less gets pushed down.
6025
+ * This test shows that if the filter contains a predicate on the same field that is sorted, we cannot push down the sort .
5932
6026
* <code>
5933
6027
* ProjectExec[[abbrev{f}#23, name{f}#24, location{f}#27, country{f}#28, city{f}#29, scale{r}#10]]
5934
6028
* \_TopNExec[[Order[distance{r}#4,ASC,LAST], Order[scale{r}#10,ASC,LAST]],5[INTEGER],0]
@@ -5965,6 +6059,7 @@ public void testPushTopNDistanceAndNonPushableEvalsWithCompoundFilterToSource()
5965
6059
""" , airports ));
5966
6060
var project = as (optimized , ProjectExec .class );
5967
6061
var topN = as (project .child (), TopNExec .class );
6062
+ assertThat (topN .order ().size (), is (2 ));
5968
6063
var exchange = asRemoteExchange (topN .child ());
5969
6064
5970
6065
project = as (exchange .child (), ProjectExec .class );
@@ -6002,7 +6097,8 @@ public void testPushTopNDistanceAndNonPushableEvalsWithCompoundFilterToSource()
6002
6097
}
6003
6098
6004
6099
/**
6005
- * This test shows that with if the top level AND'd predicate contains a non-pushable component, we should not push anything.
6100
+ * This test shows that with if the top level predicate contains a non-pushable component (eg. disjunction),
6101
+ * we should not push down the filter.
6006
6102
* <code>
6007
6103
* ProjectExec[[abbrev{f}#8612, name{f}#8613, location{f}#8616, country{f}#8617, city{f}#8618, scalerank{f}#8614 AS scale]]
6008
6104
* \_TopNExec[[Order[distance{r}#8596,ASC,LAST], Order[scalerank{f}#8614,ASC,LAST]],5[INTEGER],0]
@@ -6040,6 +6136,7 @@ public void testPushTopNDistanceWithCompoundFilterToSourceAndDisjunctiveNonPusha
6040
6136
6041
6137
var project = as (optimized , ProjectExec .class );
6042
6138
var topN = as (project .child (), TopNExec .class );
6139
+ assertThat (topN .order ().size (), is (2 ));
6043
6140
var exchange = asRemoteExchange (topN .child ());
6044
6141
6045
6142
project = as (exchange .child (), ProjectExec .class );
0 commit comments