Skip to content

Commit 37f1585

Browse files
authored
DATAES-734 - Add Sort implementation that allows geo distance sorts.
Original PR: spring-projects#382
1 parent bf13ed9 commit 37f1585

File tree

8 files changed

+325
-12
lines changed

8 files changed

+325
-12
lines changed

src/main/asciidoc/reference/elasticsearch-misc.adoc

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,3 +80,17 @@ while (stream.hasNext()) {
8080
}
8181
----
8282
====
83+
84+
[[elasticsearch.misc.sorts]]
85+
== Sort options
86+
87+
In addition to the default sort options described <<repositories.paging-and-sorting>> Spring Data Elasticsearch has a `GeoDistanceOrder` class which can be used to have the result of a search operation ordered by geographical distance.
88+
89+
If the class to be retrieved has a `GeoPoint` property named _location_, the following `Sort` would sort the results by distance to the given point:
90+
91+
[source,java]
92+
----
93+
Sort.by(new GeoDistanceOrder("location", new GeoPoint(48.137154, 11.5761247)))
94+
----
95+
96+

src/main/asciidoc/reference/elasticsearch-new.adoc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
* Cleanup of the API in the `*Operations` interfaces, grouping and renaming methods so that they match the Elasticsearch API, deprecating the old methods, aligning with other Spring Data modules.
1313
* Introduction of `SearchHit<T>` class to represent a found document together with the relevant result metadata for this document (i.e. _sortValues_).
1414
* Introduction of the `SearchHits<T>` class to represent a whole search result together with the metadata for the complete search result (i.e. _max_score_).
15+
* Introduction of the `GeoDistanceOrder` class to be able to create sorting by geographical distance
1516

1617
[[new-features.3-2-0]]
1718
== New in Spring Data Elasticsearch 3.2

src/main/java/org/springframework/data/elasticsearch/core/RequestFactory.java

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@
4141
import org.elasticsearch.client.Requests;
4242
import org.elasticsearch.client.indices.CreateIndexRequest;
4343
import org.elasticsearch.client.indices.PutMappingRequest;
44+
import org.elasticsearch.common.geo.GeoDistance;
45+
import org.elasticsearch.common.unit.DistanceUnit;
4446
import org.elasticsearch.common.unit.TimeValue;
4547
import org.elasticsearch.common.xcontent.XContentBuilder;
4648
import org.elasticsearch.common.xcontent.XContentType;
@@ -56,9 +58,11 @@
5658
import org.elasticsearch.search.fetch.subphase.FetchSourceContext;
5759
import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder;
5860
import org.elasticsearch.search.sort.FieldSortBuilder;
61+
import org.elasticsearch.search.sort.GeoDistanceSortBuilder;
5962
import org.elasticsearch.search.sort.ScoreSortBuilder;
6063
import org.elasticsearch.search.sort.SortBuilder;
6164
import org.elasticsearch.search.sort.SortBuilders;
65+
import org.elasticsearch.search.sort.SortMode;
6266
import org.elasticsearch.search.sort.SortOrder;
6367
import org.springframework.data.domain.Sort;
6468
import org.springframework.data.elasticsearch.ElasticsearchException;
@@ -771,17 +775,29 @@ private SortBuilder<?> getSortBuilder(Sort.Order order, @Nullable ElasticsearchP
771775
: null;
772776
String fieldName = property != null ? property.getFieldName() : order.getProperty();
773777

774-
FieldSortBuilder sort = SortBuilders //
775-
.fieldSort(fieldName) //
776-
.order(sortOrder);
778+
if (order instanceof GeoDistanceOrder) {
779+
GeoDistanceOrder geoDistanceOrder = (GeoDistanceOrder) order;
777780

778-
if (order.getNullHandling() == Sort.NullHandling.NULLS_FIRST) {
779-
sort.missing("_first");
780-
} else if (order.getNullHandling() == Sort.NullHandling.NULLS_LAST) {
781-
sort.missing("_last");
782-
}
781+
GeoDistanceSortBuilder sort = SortBuilders.geoDistanceSort(fieldName, geoDistanceOrder.getGeoPoint().getLat(),
782+
geoDistanceOrder.getGeoPoint().getLon());
783783

784-
return sort;
784+
sort.geoDistance(GeoDistance.fromString(geoDistanceOrder.getDistanceType().name()));
785+
sort.ignoreUnmapped(geoDistanceOrder.getIgnoreUnmapped());
786+
sort.sortMode(SortMode.fromString(geoDistanceOrder.getMode().name()));
787+
sort.unit(DistanceUnit.fromString(geoDistanceOrder.getUnit()));
788+
return sort;
789+
} else {
790+
FieldSortBuilder sort = SortBuilders //
791+
.fieldSort(fieldName) //
792+
.order(sortOrder);
793+
794+
if (order.getNullHandling() == Sort.NullHandling.NULLS_FIRST) {
795+
sort.missing("_first");
796+
} else if (order.getNullHandling() == Sort.NullHandling.NULLS_LAST) {
797+
sort.missing("_last");
798+
}
799+
return sort;
800+
}
785801
}
786802
}
787803

src/main/java/org/springframework/data/elasticsearch/core/geo/GeoPoint.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
*
2323
* @author Franck Marchand
2424
* @author Mohsin Husen
25+
* @author Peter-Josef Meisch
2526
*/
2627
public class GeoPoint {
2728

@@ -58,6 +59,14 @@ public static GeoPoint fromPoint(Point point) {
5859
public static Point toPoint(GeoPoint point) {
5960
return new Point(point.getLat(), point.getLon());
6061
}
62+
63+
@Override
64+
public String toString() {
65+
return "GeoPoint{" +
66+
"lat=" + lat +
67+
", lon=" + lon +
68+
'}';
69+
}
6170
}
6271

6372

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
/*
2+
* Copyright 2020 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.elasticsearch.core.query;
17+
18+
import org.springframework.data.domain.Sort;
19+
import org.springframework.data.elasticsearch.core.geo.GeoPoint;
20+
21+
/**
22+
* {@link org.springframework.data.domain.Sort.Order} derived class to be able to define a _geo_distance order for a
23+
* search.
24+
*
25+
* @author Peter-Josef Meisch
26+
* @since 4.0
27+
*/
28+
public class GeoDistanceOrder extends Sort.Order {
29+
30+
private static final DistanceType DEFAULT_DISTANCE_TYPE = DistanceType.arc;
31+
private static final Mode DEFAULT_MODE = Mode.min;
32+
private static final String DEFAULT_UNIT = "m";
33+
private static final Boolean DEFAULT_IGNORE_UNMAPPED = false;
34+
35+
private final GeoPoint geoPoint;
36+
private final DistanceType distanceType;
37+
private final Mode mode;
38+
private final String unit;
39+
private final Boolean ignoreUnmapped;
40+
41+
public GeoDistanceOrder(String property, GeoPoint geoPoint) {
42+
this(property, geoPoint, Sort.Direction.ASC, DEFAULT_DISTANCE_TYPE, DEFAULT_MODE, DEFAULT_UNIT,
43+
DEFAULT_IGNORE_UNMAPPED);
44+
}
45+
46+
private GeoDistanceOrder(String property, GeoPoint geoPoint, Sort.Direction direction, DistanceType distanceType,
47+
Mode mode, String unit, Boolean ignoreUnmapped) {
48+
super(direction, property);
49+
this.geoPoint = geoPoint;
50+
this.distanceType = distanceType;
51+
this.mode = mode;
52+
this.unit = unit;
53+
this.ignoreUnmapped = ignoreUnmapped;
54+
}
55+
56+
public GeoPoint getGeoPoint() {
57+
return geoPoint;
58+
}
59+
60+
public DistanceType getDistanceType() {
61+
return distanceType;
62+
}
63+
64+
public Mode getMode() {
65+
return mode;
66+
}
67+
68+
public String getUnit() {
69+
return unit;
70+
}
71+
72+
public Boolean getIgnoreUnmapped() {
73+
return ignoreUnmapped;
74+
}
75+
76+
@Override
77+
public GeoDistanceOrder withProperty(String property) {
78+
return new GeoDistanceOrder(property, getGeoPoint(), getDirection(), getDistanceType(), getMode(), getUnit(),
79+
getIgnoreUnmapped());
80+
}
81+
82+
@Override
83+
public GeoDistanceOrder with(Sort.Direction direction) {
84+
return new GeoDistanceOrder(getProperty(), getGeoPoint(), direction, getDistanceType(), getMode(), getUnit(),
85+
getIgnoreUnmapped());
86+
}
87+
88+
@Override
89+
public GeoDistanceOrder with(Sort.NullHandling nullHandling) {
90+
throw new UnsupportedOperationException("null handling is not supported for _geo_distance sorts");
91+
}
92+
93+
public GeoDistanceOrder with(DistanceType distanceType) {
94+
return new GeoDistanceOrder(getProperty(), getGeoPoint(), getDirection(), distanceType, getMode(), getUnit(),
95+
getIgnoreUnmapped());
96+
}
97+
98+
public GeoDistanceOrder with(Mode mode) {
99+
return new GeoDistanceOrder(getProperty(), getGeoPoint(), getDirection(), getDistanceType(), mode, getUnit(),
100+
getIgnoreUnmapped());
101+
}
102+
103+
public GeoDistanceOrder withUnit(String unit) {
104+
return new GeoDistanceOrder(getProperty(), getGeoPoint(), getDirection(), getDistanceType(), getMode(), unit,
105+
getIgnoreUnmapped());
106+
}
107+
108+
public GeoDistanceOrder withIgnoreUnmapped(Boolean ignoreUnmapped) {
109+
return new GeoDistanceOrder(getProperty(), getGeoPoint(), getDirection(), getDistanceType(), getMode(), getUnit(),
110+
ignoreUnmapped);
111+
}
112+
113+
@Override
114+
public String toString() {
115+
return "GeoDistanceOrder{" + "geoPoint=" + geoPoint + ", distanceType=" + distanceType + ", mode=" + mode
116+
+ ", unit='" + unit + '\'' + ", ignoreUnmapped=" + ignoreUnmapped + "} " + super.toString();
117+
}
118+
119+
public enum DistanceType {
120+
arc, plane
121+
}
122+
123+
public enum Mode {
124+
min, max, median, avg
125+
}
126+
}

src/test/java/org/springframework/data/elasticsearch/core/CriteriaQueryMappingTests.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
/**
3737
* Tests for the mapping of {@link CriteriaQuery} by a
3838
* {@link org.springframework.data.elasticsearch.core.convert.MappingElasticsearchConverter}. In the same package as
39-
* {@link CriteriaQueryProcessor} as this is needed to get the String represenation to assert.
39+
* {@link CriteriaQueryProcessor} as this is needed to get the String representation to assert.
4040
*
4141
* @author Peter-Josef Meisch
4242
*/
@@ -59,8 +59,8 @@ void setUp() {
5959
void shouldMapNamesAndConvertValuesInCriteriaQuery() throws JSONException {
6060

6161
// use POJO properties and types in the query building
62-
CriteriaQuery criteriaQuery = new CriteriaQuery(
63-
new Criteria("birthDate").between(LocalDate.of(1989, 11, 9), LocalDate.of(1990, 11, 9)).or("birthDate").is(LocalDate.of(2019, 12, 28)));
62+
CriteriaQuery criteriaQuery = new CriteriaQuery(new Criteria("birthDate")
63+
.between(LocalDate.of(1989, 11, 9), LocalDate.of(1990, 11, 9)).or("birthDate").is(LocalDate.of(2019, 12, 28)));
6464

6565
// mapped field name and converted parameter
6666
String expected = '{' + //
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
/*
2+
* Copyright 2020 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.elasticsearch.core;
17+
18+
import static org.skyscreamer.jsonassert.JSONAssert.*;
19+
20+
import java.util.Collections;
21+
22+
import org.json.JSONException;
23+
import org.junit.jupiter.api.BeforeAll;
24+
import org.junit.jupiter.api.Test;
25+
import org.springframework.core.convert.support.GenericConversionService;
26+
import org.springframework.data.annotation.Id;
27+
import org.springframework.data.domain.Sort;
28+
import org.springframework.data.elasticsearch.annotations.Field;
29+
import org.springframework.data.elasticsearch.core.convert.MappingElasticsearchConverter;
30+
import org.springframework.data.elasticsearch.core.geo.GeoPoint;
31+
import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates;
32+
import org.springframework.data.elasticsearch.core.mapping.SimpleElasticsearchMappingContext;
33+
import org.springframework.data.elasticsearch.core.query.Criteria;
34+
import org.springframework.data.elasticsearch.core.query.CriteriaQuery;
35+
import org.springframework.data.elasticsearch.core.query.GeoDistanceOrder;
36+
37+
/**
38+
* @author Peter-Josef Meisch
39+
*/
40+
class RequestFactoryTest {
41+
42+
private static RequestFactory requestFactory;
43+
private static MappingElasticsearchConverter converter;
44+
45+
@BeforeAll
46+
47+
static void setUpAll() {
48+
SimpleElasticsearchMappingContext mappingContext = new SimpleElasticsearchMappingContext();
49+
mappingContext.setInitialEntitySet(Collections.singleton(Person.class));
50+
mappingContext.afterPropertiesSet();
51+
52+
converter = new MappingElasticsearchConverter(mappingContext, new GenericConversionService());
53+
converter.afterPropertiesSet();
54+
55+
requestFactory = new RequestFactory((converter));
56+
}
57+
58+
@Test // FPI-734
59+
void shouldBuildSearchWithGeoSortSort() throws JSONException {
60+
CriteriaQuery query = new CriteriaQuery(new Criteria("lastName").is("Smith"));
61+
Sort sort = Sort.by(new GeoDistanceOrder("location", new GeoPoint(49.0, 8.4)));
62+
query.addSort(sort);
63+
64+
converter.updateQuery(query, Person.class);
65+
66+
String expected = '{' + //
67+
" \"query\": {" + //
68+
" \"bool\": {" + //
69+
" \"must\": [" + //
70+
" {" + //
71+
" \"query_string\": {" + //
72+
" \"query\": \"Smith\"," + //
73+
" \"fields\": [" + //
74+
" \"first-name^1.0\"" + //
75+
" ]" + //
76+
" }" + //
77+
" }" + //
78+
" ]" + //
79+
" }" + //
80+
" }," + //
81+
" \"sort\": [" + //
82+
" {" + //
83+
" \"_geo_distance\": {" + //
84+
" \"current-location\": [" + //
85+
" {" + //
86+
" \"lat\": 49.0," + //
87+
" \"lon\": 8.4" + //
88+
" }" + //
89+
" ]," + //
90+
" \"unit\": \"m\"," + //
91+
" \"distance_type\": \"arc\"," + //
92+
" \"order\": \"asc\"," + //
93+
" \"mode\": \"min\"," + //
94+
" \"ignore_unmapped\": false" + //
95+
" }" + //
96+
" }" + //
97+
" ]" + //
98+
'}';
99+
100+
String searchRequest = requestFactory.searchRequest(query, Person.class, IndexCoordinates.of("persons")).source()
101+
.toString();
102+
103+
assertEquals(expected, searchRequest, false);
104+
}
105+
106+
static class Person {
107+
@Id String id;
108+
@Field(name = "last-name") String lastName;
109+
@Field(name = "current-location") GeoPoint location;
110+
}
111+
}

0 commit comments

Comments
 (0)