|
| 1 | +[[elasticsearch.misc.scripted-and-runtime-fields]] |
| 2 | += Scripted and runtime fields |
| 3 | + |
| 4 | +Spring Data Elasticsearch supports scripted fields and runtime fields. |
| 5 | +Please refer to the Elasticsearch documentation about scripting (https://www.elastic.co/guide/en/elasticsearch/reference/current/modules-scripting.html) and runtime fields (https://www.elastic.co/guide/en/elasticsearch/reference/8.9/runtime.html) for detailed information about this. |
| 6 | +In the context of Spring Data Elasticsearch you can use |
| 7 | + |
| 8 | +* scripted fields that are used to return fields that are calculated on the result documents and added to the returned document. |
| 9 | +* runtime fields that are calculated on the stored documents and can be used in a query and/or be returned in the search result. |
| 10 | + |
| 11 | +The following code snippets will show what you can do (this show imperative code, but the reactive implementation works similar). |
| 12 | + |
| 13 | +== The person entity |
| 14 | + |
| 15 | +The enity that is used in these examples is a `Person` entity. |
| 16 | +This entity has a `birthDate` and an `age` property. |
| 17 | +Whereas the birthdate is fix, the age depends on the time when a query is issued and needs to be calculated dynamically. |
| 18 | + |
| 19 | +==== |
| 20 | +[source,java] |
| 21 | +---- |
| 22 | +import org.springframework.data.annotation.Id; |
| 23 | +import org.springframework.data.elasticsearch.annotations.DateFormat; |
| 24 | +import org.springframework.data.elasticsearch.annotations.Document; |
| 25 | +import org.springframework.data.elasticsearch.annotations.Field; |
| 26 | +import org.springframework.data.elasticsearch.annotations.ScriptedField; |
| 27 | +import org.springframework.lang.Nullable; |
| 28 | +
|
| 29 | +import java.time.LocalDate; |
| 30 | +import java.time.format.DateTimeFormatter; |
| 31 | +
|
| 32 | +import static org.springframework.data.elasticsearch.annotations.FieldType.*; |
| 33 | +
|
| 34 | +import java.lang.Integer; |
| 35 | +
|
| 36 | +@Document(indexName = "persons") |
| 37 | +public record Person( |
| 38 | + @Id |
| 39 | + @Nullable |
| 40 | + String id, |
| 41 | + @Field(type = Text) |
| 42 | + String lastName, |
| 43 | + @Field(type = Text) |
| 44 | + String firstName, |
| 45 | + @Field(type = Keyword) |
| 46 | + String gender, |
| 47 | + @Field(type = Date, format = DateFormat.basic_date) |
| 48 | + LocalDate birthDate, |
| 49 | + @Nullable |
| 50 | + @ScriptedField Integer age <.> |
| 51 | +) { |
| 52 | + public Person(String id,String lastName, String firstName, String gender, String birthDate) { |
| 53 | + this(id, <.> |
| 54 | + lastName, |
| 55 | + firstName, |
| 56 | + LocalDate.parse(birthDate, DateTimeFormatter.ISO_LOCAL_DATE), |
| 57 | + gender, |
| 58 | + null); |
| 59 | + } |
| 60 | +} |
| 61 | +
|
| 62 | +---- |
| 63 | +
|
| 64 | +<.> the `age` property will be calculated and filled in search results. |
| 65 | +<.> a convenience constructor to set up the test data |
| 66 | +==== |
| 67 | + |
| 68 | +Note that the `age` property is annotated with `@ScriptedField`. |
| 69 | +This inhibits the writing of a corresponding entry in the index mapping and marks the property as a target to put a calculated field from a search response. |
| 70 | + |
| 71 | +== The repository interface |
| 72 | + |
| 73 | +The repository used in this example: |
| 74 | + |
| 75 | +==== |
| 76 | +[source,java] |
| 77 | +---- |
| 78 | +public interface PersonRepository extends ElasticsearchRepository<Person, String> { |
| 79 | +
|
| 80 | + SearchHits<Person> findAllBy(ScriptedField scriptedField); |
| 81 | +
|
| 82 | + SearchHits<Person> findByGenderAndAgeLessThanEqual(String gender, Integer age, RuntimeField runtimeField); |
| 83 | +} |
| 84 | +
|
| 85 | +---- |
| 86 | +==== |
| 87 | + |
| 88 | +== The service class |
| 89 | + |
| 90 | +The service class has a repository injected and an `ElasticsearchOperations` instance to show several ways of poplauting and using the `age` property. |
| 91 | +We show the code split up in different pieces to put the explanations in |
| 92 | + |
| 93 | +==== |
| 94 | +[source,java] |
| 95 | +---- |
| 96 | +import org.springframework.data.elasticsearch.core.ElasticsearchOperations; |
| 97 | +import org.springframework.data.elasticsearch.core.SearchHits; |
| 98 | +import org.springframework.data.elasticsearch.core.query.Criteria; |
| 99 | +import org.springframework.data.elasticsearch.core.query.CriteriaQuery; |
| 100 | +import org.springframework.data.elasticsearch.core.query.FetchSourceFilter; |
| 101 | +import org.springframework.data.elasticsearch.core.query.RuntimeField; |
| 102 | +import org.springframework.data.elasticsearch.core.query.ScriptData; |
| 103 | +import org.springframework.data.elasticsearch.core.query.ScriptType; |
| 104 | +import org.springframework.data.elasticsearch.core.query.ScriptedField; |
| 105 | +import org.springframework.data.elasticsearch.core.query.StringQuery; |
| 106 | +import org.springframework.stereotype.Service; |
| 107 | +
|
| 108 | +import java.util.List; |
| 109 | +
|
| 110 | +@Service |
| 111 | +public class PersonService { |
| 112 | + private final ElasticsearchOperations operations; |
| 113 | + private final PersonRepository repository; |
| 114 | +
|
| 115 | + public PersonService(ElasticsearchOperations operations, SaRPersonRepository repository) { |
| 116 | + this.operations = operations; |
| 117 | + this.repository = repository; |
| 118 | + } |
| 119 | +
|
| 120 | + public void save() { <.> |
| 121 | + List<Person> persons = List.of( |
| 122 | + new Person("1", "Smith", "Mary", "f", "1987-05-03"), |
| 123 | + new Person("2", "Smith", "Joshua", "m", "1982-11-17"), |
| 124 | + new Person("3", "Smith", "Joanna", "f", "2018-03-27"), |
| 125 | + new Person("4", "Smith", "Alex", "m", "2020-08-01"), |
| 126 | + new Person("5", "McNeill", "Fiona", "f", "1989-04-07"), |
| 127 | + new Person("6", "McNeill", "Michael", "m", "1984-10-20"), |
| 128 | + new Person("7", "McNeill", "Geraldine", "f", "2020-03-02"), |
| 129 | + new Person("8", "McNeill", "Patrick", "m", "2022-07-04")); |
| 130 | +
|
| 131 | + repository.saveAll(persons); |
| 132 | + } |
| 133 | +---- |
| 134 | +
|
| 135 | +<.> a utility method to store some data in Elasticsearch. |
| 136 | +==== |
| 137 | + |
| 138 | +=== Scripted fields |
| 139 | + |
| 140 | +The next piece show how to use a scripted field to calculate and return the age of the persons. |
| 141 | +Scripted fields can only add something to the returned data, the age cannot be used in the query (see runtime fields for that).) |
| 142 | + |
| 143 | +==== |
| 144 | +[source,java] |
| 145 | +---- |
| 146 | + public SearchHits<Person> findAllWithAge() { |
| 147 | +
|
| 148 | + var scriptedField = ScriptedField.of("age", <.> |
| 149 | + ScriptData.of(b -> b |
| 150 | + .withType(ScriptType.INLINE) |
| 151 | + .withScript(""" |
| 152 | + Instant currentDate = Instant.ofEpochMilli(new Date().getTime()); |
| 153 | + Instant startDate = doc['birth-date'].value.toInstant(); |
| 154 | + return (ChronoUnit.DAYS.between(startDate, currentDate) / 365); |
| 155 | + """))); |
| 156 | +
|
| 157 | + // version 1: use a direct query |
| 158 | + var query = new StringQuery(""" |
| 159 | + { "match_all": {} } |
| 160 | + """); |
| 161 | + query.addScriptedField(scriptedField); <.> |
| 162 | + query.addSourceFilter(FetchSourceFilter.of(b -> b.withIncludes("*"))); <.> |
| 163 | +
|
| 164 | + var result1 = operations.search(query, Person.class); <.> |
| 165 | +
|
| 166 | + // version 2: use the repository |
| 167 | + var result2 = repository.findAllBy(scriptedField); <.> |
| 168 | +
|
| 169 | + return result1; |
| 170 | + } |
| 171 | +---- |
| 172 | +
|
| 173 | +<.> define the `ScriptedField` that calculates the age of a person. |
| 174 | +<.> when using a `Query`, add the scripted field to the query. |
| 175 | +<.> when adding a scripted field to a `Query`, an additional source filter is needed to also retrieve the _normal_ fields from the document source. |
| 176 | +<.> get the data where the `Person` entities now have the values set in their `age` property. |
| 177 | +<.> when using the repository, all that needs to be done is adding the scripted field as method parameter. |
| 178 | +==== |
| 179 | + |
| 180 | +=== Runtime fields |
| 181 | + |
| 182 | +When using runtime fields, the calculated value can be used in the query itself. |
| 183 | +In the following code this is used to run a query for a given gender and maximum age of persons: |
| 184 | + |
| 185 | +==== |
| 186 | +[source,java] |
| 187 | +---- |
| 188 | + public SearchHits<Person> findWithGenderAndMaxAge(String gender, Integer maxAge) { |
| 189 | +
|
| 190 | + var runtimeField = new RuntimeField("age", "long", """ <.> |
| 191 | + Instant currentDate = Instant.ofEpochMilli(new Date().getTime()); |
| 192 | + Instant startDate = doc['birth-date'].value.toInstant(); |
| 193 | + emit (ChronoUnit.DAYS.between(startDate, currentDate) / 365); |
| 194 | + """); |
| 195 | +
|
| 196 | + // variant 1 : use a direct query |
| 197 | + var query = CriteriaQuery.builder(Criteria |
| 198 | + .where("gender").is(gender) |
| 199 | + .and("age").lessThanEqual(maxAge)) |
| 200 | + .withRuntimeFields(List.of(runtimeField)) <.> |
| 201 | + .withFields("age") <.> |
| 202 | + .withSourceFilter(FetchSourceFilter.of(b -> b.withIncludes("*"))) <.> |
| 203 | + .build(); |
| 204 | +
|
| 205 | + var result1 = operations.search(query, Person.class); <.> |
| 206 | +
|
| 207 | + // variant 2: use the repository <.> |
| 208 | + var result2 = repository.findByGenderAndAgeLessThanEqual(gender, maxAge, runtimeField); |
| 209 | +
|
| 210 | + return result1; |
| 211 | + } |
| 212 | +} |
| 213 | +---- |
| 214 | +
|
| 215 | +<.> define the runtime field that caclulates the // see https://asciidoctor.org/docs/user-manual/#builtin-attributes for builtin attributes. |
| 216 | +<.> when using `Query`, add the runtime field. |
| 217 | +<.> when adding a scripted field to a `Query`, an additional field parameter is needed to have the calculated value returned. |
| 218 | +<.> when adding a scripted field to a `Query`, an additional source filter is needed to also retrieve the _normal_ fields from the document source. |
| 219 | +<.> get the data filtered with the query and where the returned entites have the age property set. |
| 220 | +<.> when using the repository, all that needs to be done is adding the runtime field as method parameter. |
| 221 | +==== |
| 222 | + |
| 223 | +In addition to define a runtime fields on a query, they can also be defined in the index by setting the `runtimeFIeldPath` property of the `@Mapping` annotation to point to a JSON file that contains the runtime field definitions. |
0 commit comments