mirror of
https://github.com/spring-projects/spring-data-elasticsearch.git
synced 2025-07-03 09:12:12 +00:00
DATAES-734 - Add Sort implementation that allows geo distance sorts.
Original PR: #382
This commit is contained in:
parent
bf13ed919f
commit
37f15853c0
@ -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)))
|
||||
----
|
||||
|
||||
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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 +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
}
|
@ -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 = '{' + //
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
Loading…
x
Reference in New Issue
Block a user