diff --git a/src/main/asciidoc/reference/elasticsearch-misc.adoc b/src/main/asciidoc/reference/elasticsearch-misc.adoc index 8853ed66e..df0235ecf 100644 --- a/src/main/asciidoc/reference/elasticsearch-misc.adoc +++ b/src/main/asciidoc/reference/elasticsearch-misc.adoc @@ -7,7 +7,8 @@ It is recommended to add those operations as custom implementation as described [[elasticsearc.misc.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. * `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... } ---- + <.> 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 ==== @@ -49,13 +51,16 @@ class Entity { [[elasticsearch.misc.mappings]] == Index Mapping -When Spring Data Elasticsearch creates the index mapping with the `IndexOperations.createMapping()` methods, it uses the annotations described in <>, 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 <>, 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. * `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`. * `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: + ==== [source,json] ---- @@ -165,7 +170,10 @@ interface SampleEntityRepository extends Repository { [[elasticsearch.misc.sorts]] == Sort options -In addition to the default sort options described <> 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 <>, 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: diff --git a/src/main/java/org/springframework/data/elasticsearch/core/RequestFactory.java b/src/main/java/org/springframework/data/elasticsearch/core/RequestFactory.java index f991b46a0..dd69178d2 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/RequestFactory.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/RequestFactory.java @@ -1212,6 +1212,15 @@ private void prepareSort(Query query, SearchRequestBuilder searchRequestBuilder, private SortBuilder getSortBuilder(Sort.Order order, @Nullable ElasticsearchPersistentEntity entity) { 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())) { return SortBuilders // .scoreSort() // @@ -1229,14 +1238,23 @@ private SortBuilder getSortBuilder(Sort.Order order, @Nullable ElasticsearchP geoDistanceOrder.getGeoPoint().getLon()); sort.geoDistance(GeoDistance.fromString(geoDistanceOrder.getDistanceType().name())); - sort.ignoreUnmapped(geoDistanceOrder.getIgnoreUnmapped()); - sort.sortMode(SortMode.fromString(geoDistanceOrder.getMode().name())); + sort.sortMode(SortMode.fromString(mode.name())); sort.unit(DistanceUnit.fromString(geoDistanceOrder.getUnit())); + + if (geoDistanceOrder.getIgnoreUnmapped() != GeoDistanceOrder.DEFAULT_IGNORE_UNMAPPED) { + sort.ignoreUnmapped(geoDistanceOrder.getIgnoreUnmapped()); + } + return sort; } else { FieldSortBuilder sort = SortBuilders // .fieldSort(fieldName) // - .order(sortOrder); + .order(sortOrder) // + .sortMode(SortMode.fromString(mode.name())); + + if (unmappedType != null) { + sort.unmappedType(unmappedType); + } if (order.getNullHandling() == Sort.NullHandling.NULLS_FIRST) { sort.missing("_first"); diff --git a/src/main/java/org/springframework/data/elasticsearch/core/query/GeoDistanceOrder.java b/src/main/java/org/springframework/data/elasticsearch/core/query/GeoDistanceOrder.java index 8ce78723d..640db4d61 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/query/GeoDistanceOrder.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/query/GeoDistanceOrder.java @@ -21,20 +21,18 @@ /** * {@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 { +public class GeoDistanceOrder extends 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; + public static final DistanceType DEFAULT_DISTANCE_TYPE = DistanceType.arc; + public static final String DEFAULT_UNIT = "m"; + public 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; @@ -45,10 +43,9 @@ public GeoDistanceOrder(String property, GeoPoint geoPoint) { private GeoDistanceOrder(String property, GeoPoint geoPoint, Sort.Direction direction, DistanceType distanceType, Mode mode, String unit, Boolean ignoreUnmapped) { - super(direction, property); + super(direction, property, mode); this.geoPoint = geoPoint; this.distanceType = distanceType; - this.mode = mode; this.unit = unit; this.ignoreUnmapped = ignoreUnmapped; } @@ -119,8 +116,4 @@ public String toString() { public enum DistanceType { arc, plane } - - public enum Mode { - min, max, median, avg - } } diff --git a/src/main/java/org/springframework/data/elasticsearch/core/query/Order.java b/src/main/java/org/springframework/data/elasticsearch/core/query/Order.java new file mode 100644 index 000000000..d2e0c6a19 --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/core/query/Order.java @@ -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 + } + +} diff --git a/src/test/java/org/springframework/data/elasticsearch/core/ElasticsearchTemplateTests.java b/src/test/java/org/springframework/data/elasticsearch/core/ElasticsearchTemplateTests.java index df3e582ff..60a911f82 100755 --- a/src/test/java/org/springframework/data/elasticsearch/core/ElasticsearchTemplateTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/ElasticsearchTemplateTests.java @@ -686,9 +686,11 @@ public void shouldSortResultsGivenMultipleSortCriteria() { operations.bulkIndex(indexQueries, IndexCoordinates.of(indexNameProvider.indexName())); - NativeSearchQuery searchQuery = new NativeSearchQueryBuilder().withQuery(matchAllQuery()) - .withSort(new FieldSortBuilder("rate").order(SortOrder.ASC)) - .withSort(new FieldSortBuilder("message").order(SortOrder.ASC)).build(); + NativeSearchQuery searchQuery = new NativeSearchQueryBuilder().withQuery(matchAllQuery()) // + .withSorts( // + new FieldSortBuilder("rate").order(SortOrder.ASC), // + new FieldSortBuilder("message").order(SortOrder.ASC)) // + .build(); // when SearchHits searchHits = operations.search(searchQuery, SampleEntity.class, @@ -725,8 +727,8 @@ public void shouldSortResultsGivenNullFirstSortCriteria() { operations.bulkIndex(indexQueries, IndexCoordinates.of(indexNameProvider.indexName())); - NativeSearchQuery searchQuery = new NativeSearchQueryBuilder().withQuery(matchAllQuery()) - .withPageable(PageRequest.of(0, 10, Sort.by(Sort.Order.asc("message").nullsFirst()))).build(); + Query searchQuery = operations.matchAllQuery(); + searchQuery.setPageable(PageRequest.of(0, 10, Sort.by(Sort.Order.asc("message").nullsFirst()))); // when SearchHits searchHits = operations.search(searchQuery, SampleEntity.class, @@ -763,8 +765,8 @@ public void shouldSortResultsGivenNullLastSortCriteria() { operations.bulkIndex(indexQueries, IndexCoordinates.of(indexNameProvider.indexName())); - NativeSearchQuery searchQuery = new NativeSearchQueryBuilder().withQuery(matchAllQuery()) - .withPageable(PageRequest.of(0, 10, Sort.by(Sort.Order.asc("message").nullsLast()))).build(); + Query searchQuery = operations.matchAllQuery(); + searchQuery.setPageable(PageRequest.of(0, 10, Sort.by(Sort.Order.asc("message").nullsLast()))); // when SearchHits searchHits = operations.search(searchQuery, SampleEntity.class, @@ -1139,8 +1141,8 @@ public void shouldReturnResultsWithScanAndScrollForGivenSearchQuery() { // then - NativeSearchQuery searchQuery = new NativeSearchQueryBuilder().withQuery(matchAllQuery()) - .withPageable(PageRequest.of(0, 10)).build(); + Query searchQuery = operations.matchAllQuery(); + searchQuery.setPageable(PageRequest.of(0, 10)); SearchScrollHits scroll = ((AbstractElasticsearchTemplate) operations).searchScrollStart(1000, searchQuery, SampleEntity.class, IndexCoordinates.of(indexNameProvider.indexName())); @@ -3591,6 +3593,29 @@ void shouldIndexDocumentFromSourceWithVersion() { 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 @Document(indexName = "#{@indexNameProvider.indexName()}") @Setting(shards = 1, replicas = 0, refreshInterval = "-1")