Add possibility to define runtime fields in a search request.

Original Pull Request #1972
Closes #1971
This commit is contained in:
Peter-Josef Meisch 2021-10-24 20:01:43 +02:00 committed by GitHub
parent 2b5ab64724
commit c37bbd5a7e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 385 additions and 0 deletions

View File

@ -183,3 +183,81 @@ If the class to be retrieved has a `GeoPoint` property named _location_, the fol
Sort.by(new GeoDistanceOrder("location", new GeoPoint(48.137154, 11.5761247)))
----
====
[[elasticsearch.misc.runtime-fields]]
== Runtime Fields
From version 7.12 on Elasticsearch has added the feature of runtime fields (https://www.elastic.co/guide/en/elasticsearch/reference/7.12/runtime.html).
Spring Data Elasticsearch supports this in two ways:
=== Runtime field definitions in the index mappings
The first way to define runtime fields is by adding the definitions to the index mappings (see https://www.elastic.co/guide/en/elasticsearch/reference/7.12/runtime-mapping-fields.html).
To use this approach in Spring Data Elasticsearch the user must provide a JSON file that contains the corresponding definition, for example:
.runtime-fields.json
====
[source,json]
----
{
"day_of_week": {
"type": "keyword",
"script": {
"source": "emit(doc['@timestamp'].value.dayOfWeekEnum.getDisplayName(TextStyle.FULL, Locale.ROOT))"
}
}
}
----
====
The path to this JSON file, which must be present on the classpath, must then be set in the `@Mapping` annotation of the entity:
====
[source,java]
----
@Document(indexName = "runtime-fields")
@Mapping(runtimeFieldsPath = "/runtime-fields.json")
public class RuntimeFieldEntity {
// properties, getter, setter,...
}
----
====
=== Runtime fields definitions set on a Query
The second way to define runtime fields is by adding the definitions to a search query (see https://www.elastic.co/guide/en/elasticsearch/reference/7.12/runtime-search-request.html).
The following code example shows how to do this with Spring Data Elasticsearch :
The entity used is a simple object that has a `price` property:
====
[source,java]
----
@Document(indexName = "some_index_name")
public class SomethingToBuy {
private @Id @Nullable String id;
@Nullable @Field(type = FieldType.Text) private String description;
@Nullable @Field(type = FieldType.Double) private Double price;
// getter and setter
}
----
====
The following query uses a runtime field that calculates a `priceWithTax` value by adding 19% to the price and uses this value in the search query to find all entities where `priceWithTax` is higher or equal than a given value:
====
[source,java]
----
RuntimeField runtimeField = new RuntimeField("priceWithTax", "double", "emit(doc['price'].value * 1.19)");
Query query = new CriteriaQuery(new Criteria("priceWithTax").greaterThanEqual(16.5));
query.addRuntimeField(runtimeField);
SearchHits<SomethingToBuy> searchHits = operations.search(query, SomethingToBuy.class);
----
====
This works with every implementation of the `Query` interface.

View File

@ -1020,7 +1020,17 @@ class RequestFactory {
request.requestCache(query.getRequestCache());
}
if (!query.getRuntimeFields().isEmpty()) {
Map<String, Object> runtimeMappings = new HashMap<>();
query.getRuntimeFields().forEach(runtimeField -> {
runtimeMappings.put(runtimeField.getName(), runtimeField.getMapping());
});
sourceBuilder.runtimeMappings(runtimeMappings);
}
request.source(sourceBuilder);
return request;
}
@ -1112,6 +1122,15 @@ class RequestFactory {
searchRequestBuilder.setRequestCache(query.getRequestCache());
}
if (!query.getRuntimeFields().isEmpty()) {
Map<String, Object> runtimeMappings = new HashMap<>();
query.getRuntimeFields().forEach(runtimeField -> {
runtimeMappings.put(runtimeField.getName(), runtimeField.getMapping());
});
searchRequestBuilder.setRuntimeMappings(runtimeMappings);
}
return searchRequestBuilder;
}

View File

@ -0,0 +1,60 @@
/*
* 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;
import java.util.HashMap;
import java.util.Map;
import org.springframework.util.Assert;
/**
* Defines a runtime field to be added to a Query
*
* @author Peter-Josef Meisch
* @since 4.3
*/
public class RuntimeField {
private final String name;
private final String type;
private final String script;
public RuntimeField(String name, String type, String script) {
Assert.notNull(name, "name must not be null");
Assert.notNull(type, "type must not be null");
Assert.notNull(script, "script must not be null");
this.name = name;
this.type = type;
this.script = script;
}
public String getName() {
return name;
}
/**
* @return the mapping as a Map like it is needed for the Elasticsearch client
*/
public Map<String, Object> getMapping() {
Map<String, Object> map = new HashMap<>();
map.put("type", type);
map.put("script", script);
return map;
}
}

View File

@ -28,6 +28,7 @@ import java.util.stream.Collectors;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.elasticsearch.core.RuntimeField;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
@ -67,6 +68,7 @@ public class BaseQuery implements Query {
protected List<RescorerQuery> rescorerQueries = new ArrayList<>();
@Nullable protected Boolean requestCache;
private List<IdWithRouting> idsWithRouting = Collections.emptyList();
private final List<RuntimeField> runtimeFields = new ArrayList<>();
@Override
@Nullable
@ -374,4 +376,16 @@ public class BaseQuery implements Query {
return this.requestCache;
}
@Override
public void addRuntimeField(RuntimeField runtimeField) {
Assert.notNull(runtimeField, "runtimeField must not be null");
this.runtimeFields.add(runtimeField);
}
@Override
public List<RuntimeField> getRuntimeFields() {
return runtimeFields;
}
}

View File

@ -24,6 +24,7 @@ import java.util.Optional;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.elasticsearch.core.RuntimeField;
import org.springframework.data.elasticsearch.core.SearchHit;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
@ -392,6 +393,20 @@ public interface Query {
@Nullable
Boolean getRequestCache();
/**
* Adds a runtime field to the query.
*
* @param runtimeField the runtime field definition, must not be {@literal null}
* @since 4.3
*/
void addRuntimeField(RuntimeField runtimeField);
/**
* @return the runtime fields for this query. May be empty but not null
* @since 4.3
*/
List<RuntimeField> getRuntimeFields();
/**
* @since 4.3
*/

View File

@ -0,0 +1,120 @@
/*
* 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;
import static org.assertj.core.api.Assertions.*;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.annotation.Id;
import org.springframework.data.elasticsearch.annotations.Document;
import org.springframework.data.elasticsearch.annotations.Field;
import org.springframework.data.elasticsearch.annotations.FieldType;
import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates;
import org.springframework.data.elasticsearch.core.query.Criteria;
import org.springframework.data.elasticsearch.core.query.CriteriaQuery;
import org.springframework.data.elasticsearch.core.query.Query;
import org.springframework.data.elasticsearch.junit.jupiter.SpringIntegrationTest;
import org.springframework.data.elasticsearch.utils.IndexNameProvider;
import org.springframework.lang.Nullable;
/**
* @author Peter-Josef Meisch
*/
@SpringIntegrationTest
public abstract class RuntimeFieldsIntegrationTests {
@Autowired private ElasticsearchOperations operations;
@Autowired protected IndexNameProvider indexNameProvider;
private IndexOperations indexOperations;
@BeforeEach
void setUp() {
indexNameProvider.increment();
indexOperations = operations.indexOps(SomethingToBuy.class);
indexOperations.createWithMapping();
}
@Test
@Order(java.lang.Integer.MAX_VALUE)
void cleanup() {
operations.indexOps(IndexCoordinates.of("*")).delete();
}
@Test // #1971
@DisplayName("should use runtime-field from query in search")
void shouldUseRuntimeFieldFromQueryInSearch() {
insert("1", "item 1", 13.5);
insert("2", "item 2", 15);
Query query = new CriteriaQuery(new Criteria("priceWithTax").greaterThanEqual(16.5));
RuntimeField runtimeField = new RuntimeField("priceWithTax", "double", "emit(doc['price'].value * 1.19)");
query.addRuntimeField(runtimeField);
SearchHits<SomethingToBuy> searchHits = operations.search(query, SomethingToBuy.class);
assertThat(searchHits.getTotalHits()).isEqualTo(1);
assertThat(searchHits.getSearchHit(0).getId()).isEqualTo("2");
}
private void insert(String id, String description, double price) {
SomethingToBuy entity = new SomethingToBuy();
entity.setId(id);
entity.setDescription(description);
entity.setPrice(price);
operations.save(entity);
}
@Document(indexName = "#{@indexNameProvider.indexName()}")
private static class SomethingToBuy {
private @Id @Nullable String id;
@Nullable @Field(type = FieldType.Text) private String description;
@Nullable @Field(type = FieldType.Double) private Double price;
@Nullable
public String getId() {
return id;
}
public void setId(@Nullable String id) {
this.id = id;
}
@Nullable
public String getDescription() {
return description;
}
public void setDescription(@Nullable String description) {
this.description = description;
}
@Nullable
public Double getPrice() {
return price;
}
public void setPrice(@Nullable Double price) {
this.price = price;
}
}
}

View File

@ -0,0 +1,39 @@
/*
* 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;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.data.elasticsearch.junit.jupiter.ElasticsearchRestTemplateConfiguration;
import org.springframework.data.elasticsearch.utils.IndexNameProvider;
import org.springframework.test.context.ContextConfiguration;
/**
* @author Peter-Josef Meisch
*/
@ContextConfiguration(classes = { RuntimeFieldsRestTemplateIntegrationTests.Config.class })
public class RuntimeFieldsRestTemplateIntegrationTests extends RuntimeFieldsIntegrationTests {
@Configuration
@Import({ ElasticsearchRestTemplateConfiguration.class })
static class Config {
@Bean
IndexNameProvider indexNameProvider() {
return new IndexNameProvider("runtime-fields-rest-template");
}
}
}

View File

@ -0,0 +1,40 @@
/*
* 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;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.data.elasticsearch.junit.jupiter.ElasticsearchTemplateConfiguration;
import org.springframework.data.elasticsearch.utils.IndexNameProvider;
import org.springframework.test.context.ContextConfiguration;
/**
* @author Peter-Josef Meisch
*/
@ContextConfiguration(classes = { RuntimeFieldsTransportTemplateIntegrationTests.Config.class })
public class RuntimeFieldsTransportTemplateIntegrationTests extends RuntimeFieldsIntegrationTests {
@Configuration
@Import({ ElasticsearchTemplateConfiguration.class })
static class Config {
@Bean
IndexNameProvider indexNameProvider() {
return new IndexNameProvider("runtime-fields-transport-template");
}
}
}