DATAES-734 - Add Sort implementation that allows geo distance sorts.

Original PR: #382
This commit is contained in:
Peter-Josef Meisch 2020-01-23 18:03:37 +01:00 committed by GitHub
parent bf13ed919f
commit 37f15853c0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 325 additions and 12 deletions

View File

@ -80,3 +80,17 @@ while (stream.hasNext()) {
}
----
====
[[elasticsearch.misc.sorts]]
== Sort options
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.
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:
[source,java]
----
Sort.by(new GeoDistanceOrder("location", new GeoPoint(48.137154, 11.5761247)))
----

View File

@ -12,6 +12,7 @@
* 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.
* Introduction of `SearchHit<T>` class to represent a found document together with the relevant result metadata for this document (i.e. _sortValues_).
* 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_).
* Introduction of the `GeoDistanceOrder` class to be able to create sorting by geographical distance
[[new-features.3-2-0]]
== New in Spring Data Elasticsearch 3.2

View File

@ -41,6 +41,8 @@ import org.elasticsearch.client.Client;
import org.elasticsearch.client.Requests;
import org.elasticsearch.client.indices.CreateIndexRequest;
import org.elasticsearch.client.indices.PutMappingRequest;
import org.elasticsearch.common.geo.GeoDistance;
import org.elasticsearch.common.unit.DistanceUnit;
import org.elasticsearch.common.unit.TimeValue;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentType;
@ -56,9 +58,11 @@ import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.elasticsearch.search.fetch.subphase.FetchSourceContext;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder;
import org.elasticsearch.search.sort.FieldSortBuilder;
import org.elasticsearch.search.sort.GeoDistanceSortBuilder;
import org.elasticsearch.search.sort.ScoreSortBuilder;
import org.elasticsearch.search.sort.SortBuilder;
import org.elasticsearch.search.sort.SortBuilders;
import org.elasticsearch.search.sort.SortMode;
import org.elasticsearch.search.sort.SortOrder;
import org.springframework.data.domain.Sort;
import org.springframework.data.elasticsearch.ElasticsearchException;
@ -771,17 +775,29 @@ class RequestFactory {
: null;
String fieldName = property != null ? property.getFieldName() : order.getProperty();
FieldSortBuilder sort = SortBuilders //
.fieldSort(fieldName) //
.order(sortOrder);
if (order instanceof GeoDistanceOrder) {
GeoDistanceOrder geoDistanceOrder = (GeoDistanceOrder) order;
if (order.getNullHandling() == Sort.NullHandling.NULLS_FIRST) {
sort.missing("_first");
} else if (order.getNullHandling() == Sort.NullHandling.NULLS_LAST) {
sort.missing("_last");
GeoDistanceSortBuilder sort = SortBuilders.geoDistanceSort(fieldName, geoDistanceOrder.getGeoPoint().getLat(),
geoDistanceOrder.getGeoPoint().getLon());
sort.geoDistance(GeoDistance.fromString(geoDistanceOrder.getDistanceType().name()));
sort.ignoreUnmapped(geoDistanceOrder.getIgnoreUnmapped());
sort.sortMode(SortMode.fromString(geoDistanceOrder.getMode().name()));
sort.unit(DistanceUnit.fromString(geoDistanceOrder.getUnit()));
return sort;
} else {
FieldSortBuilder sort = SortBuilders //
.fieldSort(fieldName) //
.order(sortOrder);
if (order.getNullHandling() == Sort.NullHandling.NULLS_FIRST) {
sort.missing("_first");
} else if (order.getNullHandling() == Sort.NullHandling.NULLS_LAST) {
sort.missing("_last");
}
return sort;
}
return sort;
}
}

View File

@ -22,6 +22,7 @@ import org.springframework.data.geo.Point;
*
* @author Franck Marchand
* @author Mohsin Husen
* @author Peter-Josef Meisch
*/
public class GeoPoint {
@ -58,6 +59,14 @@ public class GeoPoint {
public static Point toPoint(GeoPoint point) {
return new Point(point.getLat(), point.getLon());
}
@Override
public String toString() {
return "GeoPoint{" +
"lat=" + lat +
", lon=" + lon +
'}';
}
}

View File

@ -0,0 +1,126 @@
/*
* Copyright 2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.elasticsearch.core.query;
import org.springframework.data.domain.Sort;
import org.springframework.data.elasticsearch.core.geo.GeoPoint;
/**
* {@link org.springframework.data.domain.Sort.Order} derived class to be able to define a _geo_distance order for a
* search.
*
* @author Peter-Josef Meisch
* @since 4.0
*/
public class GeoDistanceOrder extends Sort.Order {
private static final DistanceType DEFAULT_DISTANCE_TYPE = DistanceType.arc;
private static final Mode DEFAULT_MODE = Mode.min;
private static final String DEFAULT_UNIT = "m";
private static final Boolean DEFAULT_IGNORE_UNMAPPED = false;
private final GeoPoint geoPoint;
private final DistanceType distanceType;
private final Mode mode;
private final String unit;
private final Boolean ignoreUnmapped;
public GeoDistanceOrder(String property, GeoPoint geoPoint) {
this(property, geoPoint, Sort.Direction.ASC, DEFAULT_DISTANCE_TYPE, DEFAULT_MODE, DEFAULT_UNIT,
DEFAULT_IGNORE_UNMAPPED);
}
private GeoDistanceOrder(String property, GeoPoint geoPoint, Sort.Direction direction, DistanceType distanceType,
Mode mode, String unit, Boolean ignoreUnmapped) {
super(direction, property);
this.geoPoint = geoPoint;
this.distanceType = distanceType;
this.mode = mode;
this.unit = unit;
this.ignoreUnmapped = ignoreUnmapped;
}
public GeoPoint getGeoPoint() {
return geoPoint;
}
public DistanceType getDistanceType() {
return distanceType;
}
public Mode getMode() {
return mode;
}
public String getUnit() {
return unit;
}
public Boolean getIgnoreUnmapped() {
return ignoreUnmapped;
}
@Override
public GeoDistanceOrder withProperty(String property) {
return new GeoDistanceOrder(property, getGeoPoint(), getDirection(), getDistanceType(), getMode(), getUnit(),
getIgnoreUnmapped());
}
@Override
public GeoDistanceOrder with(Sort.Direction direction) {
return new GeoDistanceOrder(getProperty(), getGeoPoint(), direction, getDistanceType(), getMode(), getUnit(),
getIgnoreUnmapped());
}
@Override
public GeoDistanceOrder with(Sort.NullHandling nullHandling) {
throw new UnsupportedOperationException("null handling is not supported for _geo_distance sorts");
}
public GeoDistanceOrder with(DistanceType distanceType) {
return new GeoDistanceOrder(getProperty(), getGeoPoint(), getDirection(), distanceType, getMode(), getUnit(),
getIgnoreUnmapped());
}
public GeoDistanceOrder with(Mode mode) {
return new GeoDistanceOrder(getProperty(), getGeoPoint(), getDirection(), getDistanceType(), mode, getUnit(),
getIgnoreUnmapped());
}
public GeoDistanceOrder withUnit(String unit) {
return new GeoDistanceOrder(getProperty(), getGeoPoint(), getDirection(), getDistanceType(), getMode(), unit,
getIgnoreUnmapped());
}
public GeoDistanceOrder withIgnoreUnmapped(Boolean ignoreUnmapped) {
return new GeoDistanceOrder(getProperty(), getGeoPoint(), getDirection(), getDistanceType(), getMode(), getUnit(),
ignoreUnmapped);
}
@Override
public String toString() {
return "GeoDistanceOrder{" + "geoPoint=" + geoPoint + ", distanceType=" + distanceType + ", mode=" + mode
+ ", unit='" + unit + '\'' + ", ignoreUnmapped=" + ignoreUnmapped + "} " + super.toString();
}
public enum DistanceType {
arc, plane
}
public enum Mode {
min, max, median, avg
}
}

View File

@ -36,7 +36,7 @@ import org.springframework.data.elasticsearch.core.query.CriteriaQuery;
/**
* Tests for the mapping of {@link CriteriaQuery} by a
* {@link org.springframework.data.elasticsearch.core.convert.MappingElasticsearchConverter}. In the same package as
* {@link CriteriaQueryProcessor} as this is needed to get the String represenation to assert.
* {@link CriteriaQueryProcessor} as this is needed to get the String representation to assert.
*
* @author Peter-Josef Meisch
*/
@ -59,8 +59,8 @@ public class CriteriaQueryMappingTests {
void shouldMapNamesAndConvertValuesInCriteriaQuery() throws JSONException {
// use POJO properties and types in the query building
CriteriaQuery criteriaQuery = new CriteriaQuery(
new Criteria("birthDate").between(LocalDate.of(1989, 11, 9), LocalDate.of(1990, 11, 9)).or("birthDate").is(LocalDate.of(2019, 12, 28)));
CriteriaQuery criteriaQuery = new CriteriaQuery(new Criteria("birthDate")
.between(LocalDate.of(1989, 11, 9), LocalDate.of(1990, 11, 9)).or("birthDate").is(LocalDate.of(2019, 12, 28)));
// mapped field name and converted parameter
String expected = '{' + //

View File

@ -0,0 +1,111 @@
/*
* Copyright 2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.elasticsearch.core;
import static org.skyscreamer.jsonassert.JSONAssert.*;
import java.util.Collections;
import org.json.JSONException;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.springframework.core.convert.support.GenericConversionService;
import org.springframework.data.annotation.Id;
import org.springframework.data.domain.Sort;
import org.springframework.data.elasticsearch.annotations.Field;
import org.springframework.data.elasticsearch.core.convert.MappingElasticsearchConverter;
import org.springframework.data.elasticsearch.core.geo.GeoPoint;
import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates;
import org.springframework.data.elasticsearch.core.mapping.SimpleElasticsearchMappingContext;
import org.springframework.data.elasticsearch.core.query.Criteria;
import org.springframework.data.elasticsearch.core.query.CriteriaQuery;
import org.springframework.data.elasticsearch.core.query.GeoDistanceOrder;
/**
* @author Peter-Josef Meisch
*/
class RequestFactoryTest {
private static RequestFactory requestFactory;
private static MappingElasticsearchConverter converter;
@BeforeAll
static void setUpAll() {
SimpleElasticsearchMappingContext mappingContext = new SimpleElasticsearchMappingContext();
mappingContext.setInitialEntitySet(Collections.singleton(Person.class));
mappingContext.afterPropertiesSet();
converter = new MappingElasticsearchConverter(mappingContext, new GenericConversionService());
converter.afterPropertiesSet();
requestFactory = new RequestFactory((converter));
}
@Test // FPI-734
void shouldBuildSearchWithGeoSortSort() throws JSONException {
CriteriaQuery query = new CriteriaQuery(new Criteria("lastName").is("Smith"));
Sort sort = Sort.by(new GeoDistanceOrder("location", new GeoPoint(49.0, 8.4)));
query.addSort(sort);
converter.updateQuery(query, Person.class);
String expected = '{' + //
" \"query\": {" + //
" \"bool\": {" + //
" \"must\": [" + //
" {" + //
" \"query_string\": {" + //
" \"query\": \"Smith\"," + //
" \"fields\": [" + //
" \"first-name^1.0\"" + //
" ]" + //
" }" + //
" }" + //
" ]" + //
" }" + //
" }," + //
" \"sort\": [" + //
" {" + //
" \"_geo_distance\": {" + //
" \"current-location\": [" + //
" {" + //
" \"lat\": 49.0," + //
" \"lon\": 8.4" + //
" }" + //
" ]," + //
" \"unit\": \"m\"," + //
" \"distance_type\": \"arc\"," + //
" \"order\": \"asc\"," + //
" \"mode\": \"min\"," + //
" \"ignore_unmapped\": false" + //
" }" + //
" }" + //
" ]" + //
'}';
String searchRequest = requestFactory.searchRequest(query, Person.class, IndexCoordinates.of("persons")).source()
.toString();
assertEquals(expected, searchRequest, false);
}
static class Person {
@Id String id;
@Field(name = "last-name") String lastName;
@Field(name = "current-location") GeoPoint location;
}
}

View File

@ -54,6 +54,7 @@ import org.springframework.data.elasticsearch.core.SearchHit;
import org.springframework.data.elasticsearch.core.SearchHits;
import org.springframework.data.elasticsearch.core.geo.GeoBox;
import org.springframework.data.elasticsearch.core.geo.GeoPoint;
import org.springframework.data.elasticsearch.core.query.GeoDistanceOrder;
import org.springframework.data.elasticsearch.junit.jupiter.SpringIntegrationTest;
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;
import org.springframework.data.elasticsearch.utils.IndexInitializer;
@ -1441,6 +1442,40 @@ public abstract class CustomMethodRepositoryBaseTests {
assertThat(searchHit.getHighlightField("type")).hasSize(1).contains("<em>abc</em>");
}
@Test // DATAES-734
void shouldUseGeoSortParameter() {
GeoPoint munich = new GeoPoint(48.137154, 11.5761247);
GeoPoint berlin = new GeoPoint(52.520008, 13.404954);
GeoPoint vienna = new GeoPoint(48.20849, 16.37208);
GeoPoint oslo = new GeoPoint(59.9127, 10.7461);
List<SampleEntity> entities = new ArrayList<>();
SampleEntity entity1 = new SampleEntity();
entity1.setId("berlin");
entity1.setLocation(berlin);
entities.add(entity1);
SampleEntity entity2 = new SampleEntity();
entity2.setId("vienna");
entity2.setLocation(vienna);
entities.add(entity2);
SampleEntity entity3 = new SampleEntity();
entity3.setId("oslo");
entity3.setLocation(oslo);
entities.add(entity3);
repository.saveAll(entities);
SearchHits<SampleEntity> searchHits = repository.searchBy(Sort.by(new GeoDistanceOrder("location", munich)));
assertThat(searchHits.getTotalHits()).isEqualTo(3);
assertThat(searchHits.getSearchHit(0).getId()).isEqualTo("vienna");
assertThat(searchHits.getSearchHit(1).getId()).isEqualTo("berlin");
assertThat(searchHits.getSearchHit(2).getId()).isEqualTo("oslo");
}
private List<SampleEntity> createSampleEntities(String type, int numberOfEntities) {
List<SampleEntity> entities = new ArrayList<>();
@ -1588,6 +1623,7 @@ public abstract class CustomMethodRepositoryBaseTests {
Stream<SearchHit<SampleEntity>> readByMessage(String message);
SearchHits<SampleEntity> searchBy(Sort sort);
}
/**