Custom Order class with specific parameters for Elasticsearch.

Original Pull Request #1955 
Closes #1911
This commit is contained in:
Peter-Josef Meisch 2021-10-08 13:38:22 +02:00 committed by GitHub
parent 7ae55b9e75
commit b8f8a60fd7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 179 additions and 28 deletions

View File

@ -7,7 +7,8 @@ It is recommended to add those operations as custom implementation as described
[[elasticsearc.misc.index.settings]] [[elasticsearc.misc.index.settings]]
== Index settings == Index settings
When creating Elasticsearch indices with Spring Data Elasticsearch different index settings can be defined by using the `@Setting` annotation. The following arguments are available: When creating Elasticsearch indices with Spring Data Elasticsearch different index settings can be defined by using the `@Setting` annotation.
The following arguments are available:
* `useServerConfiguration` does not send any settings parameters, so the Elasticsearch server configuration determines them. * `useServerConfiguration` does not send any settings parameters, so the Elasticsearch server configuration determines them.
* `settingPath` refers to a JSON file defining the settings that must be resolvable in the classpath * `settingPath` refers to a JSON file defining the settings that must be resolvable in the classpath
@ -42,6 +43,7 @@ class Entity {
// getter and setter... // getter and setter...
} }
---- ----
<.> when defining sort fields, use the name of the Java property (_firstField_), not the name that might be defined for Elasticsearch (_first_field_) <.> when defining sort fields, use the name of the Java property (_firstField_), not the name that might be defined for Elasticsearch (_first_field_)
<.> `sortModes`, `sortOrders` and `sortMissingValues` are optional, but if they are set, the number of entries must match the number of `sortFields` elements <.> `sortModes`, `sortOrders` and `sortMissingValues` are optional, but if they are set, the number of entries must match the number of `sortFields` elements
==== ====
@ -49,13 +51,16 @@ class Entity {
[[elasticsearch.misc.mappings]] [[elasticsearch.misc.mappings]]
== Index Mapping == Index Mapping
When Spring Data Elasticsearch creates the index mapping with the `IndexOperations.createMapping()` methods, it uses the annotations described in <<elasticsearch.mapping.meta-model.annotations>>, especially the `@Field` annotation. In addition to that it is possible to add the `@Mapping` annotation to a class. This annotation has the following properties: When Spring Data Elasticsearch creates the index mapping with the `IndexOperations.createMapping()` methods, it uses the annotations described in <<elasticsearch.mapping.meta-model.annotations>>, especially the `@Field` annotation.
In addition to that it is possible to add the `@Mapping` annotation to a class.
This annotation has the following properties:
* `mappingPath` a classpath resource in JSON format; if this is not empty it is used as the mapping, no other mapping processing is done. * `mappingPath` a classpath resource in JSON format; if this is not empty it is used as the mapping, no other mapping processing is done.
* `enabled` when set to false, this flag is written to the mapping and no further processing is done. * `enabled` when set to false, this flag is written to the mapping and no further processing is done.
* `dateDetection` and `numericDetection` set the corresponding properties in the mapping when not set to `DEFAULT`. * `dateDetection` and `numericDetection` set the corresponding properties in the mapping when not set to `DEFAULT`.
* `dynamicDateFormats` when this String array is not empty, it defines the date formats used for automatic date detection. * `dynamicDateFormats` when this String array is not empty, it defines the date formats used for automatic date detection.
* `runtimeFieldsPath` a classpath resource in JSON format containing the definition of runtime fields which is written to the index mappings, for example: * `runtimeFieldsPath` a classpath resource in JSON format containing the definition of runtime fields which is written to the index mappings, for example:
==== ====
[source,json] [source,json]
---- ----
@ -165,7 +170,10 @@ interface SampleEntityRepository extends Repository<SampleEntity, String> {
[[elasticsearch.misc.sorts]] [[elasticsearch.misc.sorts]]
== Sort options == 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. In addition to the default sort options described in <<repositories.paging-and-sorting>>, Spring Data Elasticsearch provides the class `org.springframework.data.elasticsearch.core.query.Order` which derives from `org.springframework.data.domain.Sort.Order`.
It offers additional parameters that can be sent to Elasticsearch when specifying the sorting of the result (see https://www.elastic.co/guide/en/elasticsearch/reference/7.15/sort-search-results.html).
There also is the `org.springframework.data.elasticsearch.core.query.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: 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:

View File

@ -1212,6 +1212,15 @@ class RequestFactory {
private SortBuilder<?> getSortBuilder(Sort.Order order, @Nullable ElasticsearchPersistentEntity<?> entity) { private SortBuilder<?> getSortBuilder(Sort.Order order, @Nullable ElasticsearchPersistentEntity<?> entity) {
SortOrder sortOrder = order.getDirection().isDescending() ? SortOrder.DESC : SortOrder.ASC; SortOrder sortOrder = order.getDirection().isDescending() ? SortOrder.DESC : SortOrder.ASC;
Order.Mode mode = Order.DEFAULT_MODE;
String unmappedType = null;
if (order instanceof Order) {
Order o = (Order) order;
mode = o.getMode();
unmappedType = o.getUnmappedType();
}
if (ScoreSortBuilder.NAME.equals(order.getProperty())) { if (ScoreSortBuilder.NAME.equals(order.getProperty())) {
return SortBuilders // return SortBuilders //
.scoreSort() // .scoreSort() //
@ -1229,14 +1238,23 @@ class RequestFactory {
geoDistanceOrder.getGeoPoint().getLon()); geoDistanceOrder.getGeoPoint().getLon());
sort.geoDistance(GeoDistance.fromString(geoDistanceOrder.getDistanceType().name())); sort.geoDistance(GeoDistance.fromString(geoDistanceOrder.getDistanceType().name()));
sort.ignoreUnmapped(geoDistanceOrder.getIgnoreUnmapped()); sort.sortMode(SortMode.fromString(mode.name()));
sort.sortMode(SortMode.fromString(geoDistanceOrder.getMode().name()));
sort.unit(DistanceUnit.fromString(geoDistanceOrder.getUnit())); sort.unit(DistanceUnit.fromString(geoDistanceOrder.getUnit()));
if (geoDistanceOrder.getIgnoreUnmapped() != GeoDistanceOrder.DEFAULT_IGNORE_UNMAPPED) {
sort.ignoreUnmapped(geoDistanceOrder.getIgnoreUnmapped());
}
return sort; return sort;
} else { } else {
FieldSortBuilder sort = SortBuilders // FieldSortBuilder sort = SortBuilders //
.fieldSort(fieldName) // .fieldSort(fieldName) //
.order(sortOrder); .order(sortOrder) //
.sortMode(SortMode.fromString(mode.name()));
if (unmappedType != null) {
sort.unmappedType(unmappedType);
}
if (order.getNullHandling() == Sort.NullHandling.NULLS_FIRST) { if (order.getNullHandling() == Sort.NullHandling.NULLS_FIRST) {
sort.missing("_first"); sort.missing("_first");

View File

@ -25,16 +25,14 @@ import org.springframework.data.elasticsearch.core.geo.GeoPoint;
* @author Peter-Josef Meisch * @author Peter-Josef Meisch
* @since 4.0 * @since 4.0
*/ */
public class GeoDistanceOrder extends Sort.Order { public class GeoDistanceOrder extends Order {
private static final DistanceType DEFAULT_DISTANCE_TYPE = DistanceType.arc; public static final DistanceType DEFAULT_DISTANCE_TYPE = DistanceType.arc;
private static final Mode DEFAULT_MODE = Mode.min; public static final String DEFAULT_UNIT = "m";
private static final String DEFAULT_UNIT = "m"; public static final Boolean DEFAULT_IGNORE_UNMAPPED = false;
private static final Boolean DEFAULT_IGNORE_UNMAPPED = false;
private final GeoPoint geoPoint; private final GeoPoint geoPoint;
private final DistanceType distanceType; private final DistanceType distanceType;
private final Mode mode;
private final String unit; private final String unit;
private final Boolean ignoreUnmapped; private final Boolean ignoreUnmapped;
@ -45,10 +43,9 @@ public class GeoDistanceOrder extends Sort.Order {
private GeoDistanceOrder(String property, GeoPoint geoPoint, Sort.Direction direction, DistanceType distanceType, private GeoDistanceOrder(String property, GeoPoint geoPoint, Sort.Direction direction, DistanceType distanceType,
Mode mode, String unit, Boolean ignoreUnmapped) { Mode mode, String unit, Boolean ignoreUnmapped) {
super(direction, property); super(direction, property, mode);
this.geoPoint = geoPoint; this.geoPoint = geoPoint;
this.distanceType = distanceType; this.distanceType = distanceType;
this.mode = mode;
this.unit = unit; this.unit = unit;
this.ignoreUnmapped = ignoreUnmapped; this.ignoreUnmapped = ignoreUnmapped;
} }
@ -119,8 +116,4 @@ public class GeoDistanceOrder extends Sort.Order {
public enum DistanceType { public enum DistanceType {
arc, plane arc, plane
} }
public enum Mode {
min, max, median, avg
}
} }

View File

@ -0,0 +1,107 @@
/*
* Copyright 2021 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.lang.Nullable;
/**
* Extends the {@link Sort.Order} with properties that can be set on Elasticsearch order options.
*
* @author Peter-Josef Meisch
* @since 4.3
*/
public class Order extends Sort.Order {
public static final Mode DEFAULT_MODE = Mode.min;
public static final Sort.NullHandling DEFAULT_NULL_HANDLING = Sort.NullHandling.NATIVE;
protected final Mode mode;
@Nullable protected final String unmappedType;
public Order(Sort.Direction direction, String property) {
this(direction, property, DEFAULT_MODE, null);
}
public Order(Sort.Direction direction, String property, Mode mode) {
this(direction, property, DEFAULT_NULL_HANDLING, mode, null);
}
public Order(Sort.Direction direction, String property, @Nullable String unmappedType) {
this(direction, property, DEFAULT_NULL_HANDLING, DEFAULT_MODE, unmappedType);
}
public Order(Sort.Direction direction, String property, Mode mode, @Nullable String unmappedType) {
this(direction, property, DEFAULT_NULL_HANDLING, mode, unmappedType);
}
public Order(Sort.Direction direction, String property, Sort.NullHandling nullHandlingHint) {
this(direction, property, nullHandlingHint, DEFAULT_MODE, null);
}
public Order(Sort.Direction direction, String property, Sort.NullHandling nullHandlingHint, Mode mode) {
this(direction, property, nullHandlingHint, mode, null);
}
public Order(Sort.Direction direction, String property, Sort.NullHandling nullHandlingHint,
@Nullable String unmappedType) {
this(direction, property, nullHandlingHint, DEFAULT_MODE, unmappedType);
}
public Order(Sort.Direction direction, String property, Sort.NullHandling nullHandlingHint, Mode mode,
@Nullable String unmappedType) {
super(direction, property, nullHandlingHint);
this.mode = mode;
this.unmappedType = unmappedType;
}
@Nullable
public String getUnmappedType() {
return unmappedType;
}
@Override
public Sort.Order with(Sort.Direction direction) {
return new Order(direction, getProperty(), getNullHandling(), mode, unmappedType);
}
@Override
public Sort.Order withProperty(String property) {
return new Order(getDirection(), property, getNullHandling(), mode, unmappedType);
}
@Override
public Sort.Order with(Sort.NullHandling nullHandling) {
return new Order(getDirection(), getProperty(), nullHandling, getMode(), unmappedType);
}
public Order withUnmappedType(@Nullable String unmappedType) {
return new Order(getDirection(), getProperty(), getNullHandling(), getMode(), unmappedType);
}
public Order with(Mode mode) {
return new Order(getDirection(), getProperty(), getNullHandling(), mode, unmappedType);
}
public Mode getMode() {
return mode;
}
public enum Mode {
min, max, median, avg
}
}

View File

@ -686,9 +686,11 @@ public abstract class ElasticsearchTemplateTests {
operations.bulkIndex(indexQueries, IndexCoordinates.of(indexNameProvider.indexName())); operations.bulkIndex(indexQueries, IndexCoordinates.of(indexNameProvider.indexName()));
NativeSearchQuery searchQuery = new NativeSearchQueryBuilder().withQuery(matchAllQuery()) NativeSearchQuery searchQuery = new NativeSearchQueryBuilder().withQuery(matchAllQuery()) //
.withSort(new FieldSortBuilder("rate").order(SortOrder.ASC)) .withSorts( //
.withSort(new FieldSortBuilder("message").order(SortOrder.ASC)).build(); new FieldSortBuilder("rate").order(SortOrder.ASC), //
new FieldSortBuilder("message").order(SortOrder.ASC)) //
.build();
// when // when
SearchHits<SampleEntity> searchHits = operations.search(searchQuery, SampleEntity.class, SearchHits<SampleEntity> searchHits = operations.search(searchQuery, SampleEntity.class,
@ -725,8 +727,8 @@ public abstract class ElasticsearchTemplateTests {
operations.bulkIndex(indexQueries, IndexCoordinates.of(indexNameProvider.indexName())); operations.bulkIndex(indexQueries, IndexCoordinates.of(indexNameProvider.indexName()));
NativeSearchQuery searchQuery = new NativeSearchQueryBuilder().withQuery(matchAllQuery()) Query searchQuery = operations.matchAllQuery();
.withPageable(PageRequest.of(0, 10, Sort.by(Sort.Order.asc("message").nullsFirst()))).build(); searchQuery.setPageable(PageRequest.of(0, 10, Sort.by(Sort.Order.asc("message").nullsFirst())));
// when // when
SearchHits<SampleEntity> searchHits = operations.search(searchQuery, SampleEntity.class, SearchHits<SampleEntity> searchHits = operations.search(searchQuery, SampleEntity.class,
@ -763,8 +765,8 @@ public abstract class ElasticsearchTemplateTests {
operations.bulkIndex(indexQueries, IndexCoordinates.of(indexNameProvider.indexName())); operations.bulkIndex(indexQueries, IndexCoordinates.of(indexNameProvider.indexName()));
NativeSearchQuery searchQuery = new NativeSearchQueryBuilder().withQuery(matchAllQuery()) Query searchQuery = operations.matchAllQuery();
.withPageable(PageRequest.of(0, 10, Sort.by(Sort.Order.asc("message").nullsLast()))).build(); searchQuery.setPageable(PageRequest.of(0, 10, Sort.by(Sort.Order.asc("message").nullsLast())));
// when // when
SearchHits<SampleEntity> searchHits = operations.search(searchQuery, SampleEntity.class, SearchHits<SampleEntity> searchHits = operations.search(searchQuery, SampleEntity.class,
@ -1139,8 +1141,8 @@ public abstract class ElasticsearchTemplateTests {
// then // then
NativeSearchQuery searchQuery = new NativeSearchQueryBuilder().withQuery(matchAllQuery()) Query searchQuery = operations.matchAllQuery();
.withPageable(PageRequest.of(0, 10)).build(); searchQuery.setPageable(PageRequest.of(0, 10));
SearchScrollHits<SampleEntity> scroll = ((AbstractElasticsearchTemplate) operations).searchScrollStart(1000, SearchScrollHits<SampleEntity> scroll = ((AbstractElasticsearchTemplate) operations).searchScrollStart(1000,
searchQuery, SampleEntity.class, IndexCoordinates.of(indexNameProvider.indexName())); searchQuery, SampleEntity.class, IndexCoordinates.of(indexNameProvider.indexName()));
@ -3591,6 +3593,29 @@ public abstract class ElasticsearchTemplateTests {
operations.index(query, IndexCoordinates.of(indexNameProvider.indexName())); operations.index(query, IndexCoordinates.of(indexNameProvider.indexName()));
} }
@Test // #1945
@DisplayName("should error on sort with unmapped field and default settings")
void shouldErrorOnSortWithUnmappedFieldAndDefaultSettings() {
Sort.Order order = new Sort.Order(Sort.Direction.ASC, "unmappedField");
Query query = operations.matchAllQuery().addSort(Sort.by(order));
assertThatThrownBy(() -> {
operations.search(query, SampleEntity.class);
});
}
@Test // #1945
@DisplayName("should not error on sort with unmapped field and unmapped_type settings")
void shouldNotErrorOnSortWithUnmappedFieldAndUnmappedTypeSettings() {
org.springframework.data.elasticsearch.core.query.Order order = new org.springframework.data.elasticsearch.core.query.Order(
Sort.Direction.ASC, "unmappedField").withUnmappedType("long");
Query query = operations.matchAllQuery().addSort(Sort.by(order));
operations.search(query, SampleEntity.class);
}
// region entities // region entities
@Document(indexName = "#{@indexNameProvider.indexName()}") @Document(indexName = "#{@indexNameProvider.indexName()}")
@Setting(shards = 1, replicas = 0, refreshInterval = "-1") @Setting(shards = 1, replicas = 0, refreshInterval = "-1")