Skip to content

Commit c37bbd5

Browse files
authored
Add possibility to define runtime fields in a search request.
Original Pull Request #1972 Closes #1971
1 parent 2b5ab64 commit c37bbd5

File tree

8 files changed

+385
-0
lines changed

8 files changed

+385
-0
lines changed

Diff for: src/main/asciidoc/reference/elasticsearch-misc.adoc

+78
Original file line numberDiff line numberDiff line change
@@ -183,3 +183,81 @@ If the class to be retrieved has a `GeoPoint` property named _location_, the fol
183183
Sort.by(new GeoDistanceOrder("location", new GeoPoint(48.137154, 11.5761247)))
184184
----
185185
====
186+
187+
[[elasticsearch.misc.runtime-fields]]
188+
== Runtime Fields
189+
190+
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).
191+
Spring Data Elasticsearch supports this in two ways:
192+
193+
=== Runtime field definitions in the index mappings
194+
195+
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).
196+
To use this approach in Spring Data Elasticsearch the user must provide a JSON file that contains the corresponding definition, for example:
197+
198+
.runtime-fields.json
199+
====
200+
[source,json]
201+
----
202+
{
203+
"day_of_week": {
204+
"type": "keyword",
205+
"script": {
206+
"source": "emit(doc['@timestamp'].value.dayOfWeekEnum.getDisplayName(TextStyle.FULL, Locale.ROOT))"
207+
}
208+
}
209+
}
210+
----
211+
====
212+
213+
The path to this JSON file, which must be present on the classpath, must then be set in the `@Mapping` annotation of the entity:
214+
215+
====
216+
[source,java]
217+
----
218+
@Document(indexName = "runtime-fields")
219+
@Mapping(runtimeFieldsPath = "/runtime-fields.json")
220+
public class RuntimeFieldEntity {
221+
// properties, getter, setter,...
222+
}
223+
224+
----
225+
====
226+
227+
=== Runtime fields definitions set on a Query
228+
229+
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).
230+
The following code example shows how to do this with Spring Data Elasticsearch :
231+
232+
The entity used is a simple object that has a `price` property:
233+
234+
====
235+
[source,java]
236+
----
237+
@Document(indexName = "some_index_name")
238+
public class SomethingToBuy {
239+
240+
private @Id @Nullable String id;
241+
@Nullable @Field(type = FieldType.Text) private String description;
242+
@Nullable @Field(type = FieldType.Double) private Double price;
243+
244+
// getter and setter
245+
}
246+
247+
----
248+
====
249+
250+
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:
251+
252+
====
253+
[source,java]
254+
----
255+
RuntimeField runtimeField = new RuntimeField("priceWithTax", "double", "emit(doc['price'].value * 1.19)");
256+
Query query = new CriteriaQuery(new Criteria("priceWithTax").greaterThanEqual(16.5));
257+
query.addRuntimeField(runtimeField);
258+
259+
SearchHits<SomethingToBuy> searchHits = operations.search(query, SomethingToBuy.class);
260+
----
261+
====
262+
263+
This works with every implementation of the `Query` interface.

Diff for: src/main/java/org/springframework/data/elasticsearch/core/RequestFactory.java

+19
Original file line numberDiff line numberDiff line change
@@ -1020,7 +1020,17 @@ private SearchRequest prepareSearchRequest(Query query, @Nullable Class<?> clazz
10201020
request.requestCache(query.getRequestCache());
10211021
}
10221022

1023+
if (!query.getRuntimeFields().isEmpty()) {
1024+
1025+
Map<String, Object> runtimeMappings = new HashMap<>();
1026+
query.getRuntimeFields().forEach(runtimeField -> {
1027+
runtimeMappings.put(runtimeField.getName(), runtimeField.getMapping());
1028+
});
1029+
sourceBuilder.runtimeMappings(runtimeMappings);
1030+
}
1031+
10231032
request.source(sourceBuilder);
1033+
10241034
return request;
10251035
}
10261036

@@ -1112,6 +1122,15 @@ private SearchRequestBuilder prepareSearchRequestBuilder(Query query, Client cli
11121122
searchRequestBuilder.setRequestCache(query.getRequestCache());
11131123
}
11141124

1125+
if (!query.getRuntimeFields().isEmpty()) {
1126+
1127+
Map<String, Object> runtimeMappings = new HashMap<>();
1128+
query.getRuntimeFields().forEach(runtimeField -> {
1129+
runtimeMappings.put(runtimeField.getName(), runtimeField.getMapping());
1130+
});
1131+
searchRequestBuilder.setRuntimeMappings(runtimeMappings);
1132+
}
1133+
11151134
return searchRequestBuilder;
11161135
}
11171136

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/*
2+
* Copyright 2021 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.elasticsearch.core;
17+
18+
import java.util.HashMap;
19+
import java.util.Map;
20+
21+
import org.springframework.util.Assert;
22+
23+
/**
24+
* Defines a runtime field to be added to a Query
25+
*
26+
* @author Peter-Josef Meisch
27+
* @since 4.3
28+
*/
29+
public class RuntimeField {
30+
31+
private final String name;
32+
private final String type;
33+
private final String script;
34+
35+
public RuntimeField(String name, String type, String script) {
36+
37+
Assert.notNull(name, "name must not be null");
38+
Assert.notNull(type, "type must not be null");
39+
Assert.notNull(script, "script must not be null");
40+
41+
this.name = name;
42+
this.type = type;
43+
this.script = script;
44+
}
45+
46+
public String getName() {
47+
return name;
48+
}
49+
50+
/**
51+
* @return the mapping as a Map like it is needed for the Elasticsearch client
52+
*/
53+
public Map<String, Object> getMapping() {
54+
55+
Map<String, Object> map = new HashMap<>();
56+
map.put("type", type);
57+
map.put("script", script);
58+
return map;
59+
}
60+
}

Diff for: src/main/java/org/springframework/data/elasticsearch/core/query/BaseQuery.java

+14
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828

2929
import org.springframework.data.domain.Pageable;
3030
import org.springframework.data.domain.Sort;
31+
import org.springframework.data.elasticsearch.core.RuntimeField;
3132
import org.springframework.lang.Nullable;
3233
import org.springframework.util.Assert;
3334

@@ -67,6 +68,7 @@ public class BaseQuery implements Query {
6768
protected List<RescorerQuery> rescorerQueries = new ArrayList<>();
6869
@Nullable protected Boolean requestCache;
6970
private List<IdWithRouting> idsWithRouting = Collections.emptyList();
71+
private final List<RuntimeField> runtimeFields = new ArrayList<>();
7072

7173
@Override
7274
@Nullable
@@ -374,4 +376,16 @@ public Boolean getRequestCache() {
374376
return this.requestCache;
375377
}
376378

379+
@Override
380+
public void addRuntimeField(RuntimeField runtimeField) {
381+
382+
Assert.notNull(runtimeField, "runtimeField must not be null");
383+
384+
this.runtimeFields.add(runtimeField);
385+
}
386+
387+
@Override
388+
public List<RuntimeField> getRuntimeFields() {
389+
return runtimeFields;
390+
}
377391
}

Diff for: src/main/java/org/springframework/data/elasticsearch/core/query/Query.java

+15
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import org.springframework.data.domain.PageRequest;
2525
import org.springframework.data.domain.Pageable;
2626
import org.springframework.data.domain.Sort;
27+
import org.springframework.data.elasticsearch.core.RuntimeField;
2728
import org.springframework.data.elasticsearch.core.SearchHit;
2829
import org.springframework.lang.Nullable;
2930
import org.springframework.util.Assert;
@@ -392,6 +393,20 @@ default List<RescorerQuery> getRescorerQueries() {
392393
@Nullable
393394
Boolean getRequestCache();
394395

396+
/**
397+
* Adds a runtime field to the query.
398+
*
399+
* @param runtimeField the runtime field definition, must not be {@literal null}
400+
* @since 4.3
401+
*/
402+
void addRuntimeField(RuntimeField runtimeField);
403+
404+
/**
405+
* @return the runtime fields for this query. May be empty but not null
406+
* @since 4.3
407+
*/
408+
List<RuntimeField> getRuntimeFields();
409+
395410
/**
396411
* @since 4.3
397412
*/
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
/*
2+
* Copyright 2021 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.elasticsearch.core;
17+
18+
import static org.assertj.core.api.Assertions.*;
19+
20+
import org.junit.jupiter.api.BeforeEach;
21+
import org.junit.jupiter.api.DisplayName;
22+
import org.junit.jupiter.api.Order;
23+
import org.junit.jupiter.api.Test;
24+
import org.springframework.beans.factory.annotation.Autowired;
25+
import org.springframework.data.annotation.Id;
26+
import org.springframework.data.elasticsearch.annotations.Document;
27+
import org.springframework.data.elasticsearch.annotations.Field;
28+
import org.springframework.data.elasticsearch.annotations.FieldType;
29+
import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates;
30+
import org.springframework.data.elasticsearch.core.query.Criteria;
31+
import org.springframework.data.elasticsearch.core.query.CriteriaQuery;
32+
import org.springframework.data.elasticsearch.core.query.Query;
33+
import org.springframework.data.elasticsearch.junit.jupiter.SpringIntegrationTest;
34+
import org.springframework.data.elasticsearch.utils.IndexNameProvider;
35+
import org.springframework.lang.Nullable;
36+
37+
/**
38+
* @author Peter-Josef Meisch
39+
*/
40+
@SpringIntegrationTest
41+
public abstract class RuntimeFieldsIntegrationTests {
42+
43+
@Autowired private ElasticsearchOperations operations;
44+
@Autowired protected IndexNameProvider indexNameProvider;
45+
private IndexOperations indexOperations;
46+
47+
@BeforeEach
48+
void setUp() {
49+
50+
indexNameProvider.increment();
51+
indexOperations = operations.indexOps(SomethingToBuy.class);
52+
indexOperations.createWithMapping();
53+
}
54+
55+
@Test
56+
@Order(java.lang.Integer.MAX_VALUE)
57+
void cleanup() {
58+
operations.indexOps(IndexCoordinates.of("*")).delete();
59+
}
60+
61+
@Test // #1971
62+
@DisplayName("should use runtime-field from query in search")
63+
void shouldUseRuntimeFieldFromQueryInSearch() {
64+
65+
insert("1", "item 1", 13.5);
66+
insert("2", "item 2", 15);
67+
Query query = new CriteriaQuery(new Criteria("priceWithTax").greaterThanEqual(16.5));
68+
RuntimeField runtimeField = new RuntimeField("priceWithTax", "double", "emit(doc['price'].value * 1.19)");
69+
query.addRuntimeField(runtimeField);
70+
71+
SearchHits<SomethingToBuy> searchHits = operations.search(query, SomethingToBuy.class);
72+
73+
assertThat(searchHits.getTotalHits()).isEqualTo(1);
74+
assertThat(searchHits.getSearchHit(0).getId()).isEqualTo("2");
75+
}
76+
77+
private void insert(String id, String description, double price) {
78+
SomethingToBuy entity = new SomethingToBuy();
79+
entity.setId(id);
80+
entity.setDescription(description);
81+
entity.setPrice(price);
82+
operations.save(entity);
83+
}
84+
85+
@Document(indexName = "#{@indexNameProvider.indexName()}")
86+
private static class SomethingToBuy {
87+
private @Id @Nullable String id;
88+
89+
@Nullable @Field(type = FieldType.Text) private String description;
90+
91+
@Nullable @Field(type = FieldType.Double) private Double price;
92+
93+
@Nullable
94+
public String getId() {
95+
return id;
96+
}
97+
98+
public void setId(@Nullable String id) {
99+
this.id = id;
100+
}
101+
102+
@Nullable
103+
public String getDescription() {
104+
return description;
105+
}
106+
107+
public void setDescription(@Nullable String description) {
108+
this.description = description;
109+
}
110+
111+
@Nullable
112+
public Double getPrice() {
113+
return price;
114+
}
115+
116+
public void setPrice(@Nullable Double price) {
117+
this.price = price;
118+
}
119+
}
120+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/*
2+
* Copyright 2021 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.elasticsearch.core;
17+
18+
import org.springframework.context.annotation.Bean;
19+
import org.springframework.context.annotation.Configuration;
20+
import org.springframework.context.annotation.Import;
21+
import org.springframework.data.elasticsearch.junit.jupiter.ElasticsearchRestTemplateConfiguration;
22+
import org.springframework.data.elasticsearch.utils.IndexNameProvider;
23+
import org.springframework.test.context.ContextConfiguration;
24+
25+
/**
26+
* @author Peter-Josef Meisch
27+
*/
28+
@ContextConfiguration(classes = { RuntimeFieldsRestTemplateIntegrationTests.Config.class })
29+
public class RuntimeFieldsRestTemplateIntegrationTests extends RuntimeFieldsIntegrationTests {
30+
31+
@Configuration
32+
@Import({ ElasticsearchRestTemplateConfiguration.class })
33+
static class Config {
34+
@Bean
35+
IndexNameProvider indexNameProvider() {
36+
return new IndexNameProvider("runtime-fields-rest-template");
37+
}
38+
}
39+
}

0 commit comments

Comments
 (0)