mirror of
https://github.com/spring-projects/spring-data-elasticsearch.git
synced 2025-06-08 13:12:10 +00:00
Add possibility to define runtime fields in a search request.
Original Pull Request #1972 Closes #1971
This commit is contained in:
parent
2b5ab64724
commit
c37bbd5a7e
@ -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.
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
*/
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user