@@ -99,6 +99,8 @@ void shouldUseRuntimeFieldFromQueryInSearch() {
9999 @ DisplayName ("should use runtime-field without script" )
100100 void shouldUseRuntimeFieldWithoutScript () {
101101
102+ // a runtime field without a script can be used to redefine the type of a field for the search,
103+ // here we change the type from text to double
102104 insert ("1" , "11" , 10 );
103105 Query query = new CriteriaQuery (new Criteria ("description" ).matches (11.0 ));
104106 RuntimeField runtimeField = new RuntimeField ("description" , "double" );
@@ -133,6 +135,25 @@ void shouldReturnValueFromRuntimeFieldDefinedInMapping() {
133135 assertThat (foundPerson .getBirthDate ()).isEqualTo (birthDate );
134136 }
135137
138+ @ Test // #3076
139+ @ DisplayName ("should return scripted fields that are lists" )
140+ void shouldReturnScriptedFieldsThatAreLists () {
141+ var person = new Person ();
142+ person .setFirstName ("John" );
143+ person .setLastName ("Doe" );
144+ operations .save (person );
145+ var query = Query .findAll ();
146+ query .addFields ("allNames" );
147+ query .addSourceFilter (new FetchSourceFilterBuilder ().withIncludes ("*" ).build ());
148+
149+ var searchHits = operations .search (query , Person .class );
150+
151+ assertThat (searchHits .getTotalHits ()).isEqualTo (1 );
152+ var foundPerson = searchHits .getSearchHit (0 ).getContent ();
153+ // the painless script seems to return the data sorted no matter in which order the values are emitted
154+ assertThat (foundPerson .getAllNames ()).containsExactlyInAnyOrderElementsOf (List .of ("John" , "Doe" ));
155+ }
156+
136157 @ Test // #2035
137158 @ DisplayName ("should use repository method with ScriptedField parameters" )
138159 void shouldUseRepositoryMethodWithScriptedFieldParameters () {
@@ -143,9 +164,11 @@ void shouldUseRepositoryMethodWithScriptedFieldParameters() {
143164
144165 repository .save (entity );
145166
146- org .springframework .data .elasticsearch .core .query .ScriptedField scriptedField1 = getScriptedField ("scriptedValue1" ,
167+ org .springframework .data .elasticsearch .core .query .ScriptedField scriptedField1 = buildScriptedField (
168+ "scriptedValue1" ,
147169 2 );
148- org .springframework .data .elasticsearch .core .query .ScriptedField scriptedField2 = getScriptedField ("scriptedValue2" ,
170+ org .springframework .data .elasticsearch .core .query .ScriptedField scriptedField2 = buildScriptedField (
171+ "scriptedValue2" ,
149172 3 );
150173
151174 var searchHits = repository .findByValue (3 , scriptedField1 , scriptedField2 );
@@ -157,17 +180,6 @@ void shouldUseRepositoryMethodWithScriptedFieldParameters() {
157180 assertThat (foundEntity .getScriptedValue2 ()).isEqualTo (9 );
158181 }
159182
160- @ NotNull
161- private static org .springframework .data .elasticsearch .core .query .ScriptedField getScriptedField (String fieldName ,
162- int factor ) {
163- return org .springframework .data .elasticsearch .core .query .ScriptedField .of (
164- fieldName ,
165- ScriptData .of (b -> b
166- .withType (ScriptType .INLINE )
167- .withScript ("doc['value'].size() > 0 ? doc['value'].value * params['factor'] : 0" )
168- .withParams (Map .of ("factor" , factor ))));
169- }
170-
171183 @ Test // #2035
172184 @ DisplayName ("should use repository string query method with ScriptedField parameters" )
173185 void shouldUseRepositoryStringQueryMethodWithScriptedFieldParameters () {
@@ -178,9 +190,11 @@ void shouldUseRepositoryStringQueryMethodWithScriptedFieldParameters() {
178190
179191 repository .save (entity );
180192
181- org .springframework .data .elasticsearch .core .query .ScriptedField scriptedField1 = getScriptedField ("scriptedValue1" ,
193+ org .springframework .data .elasticsearch .core .query .ScriptedField scriptedField1 = buildScriptedField (
194+ "scriptedValue1" ,
182195 2 );
183- org .springframework .data .elasticsearch .core .query .ScriptedField scriptedField2 = getScriptedField ("scriptedValue2" ,
196+ org .springframework .data .elasticsearch .core .query .ScriptedField scriptedField2 = buildScriptedField (
197+ "scriptedValue2" ,
184198 3 );
185199
186200 var searchHits = repository .findWithScriptedFields (3 , scriptedField1 , scriptedField2 );
@@ -202,8 +216,8 @@ void shouldUseRepositoryMethodWithRuntimeFieldParameters() {
202216
203217 repository .save (entity );
204218
205- var runtimeField1 = getRuntimeField ("scriptedValue1" , 3 );
206- var runtimeField2 = getRuntimeField ("scriptedValue2" , 4 );
219+ var runtimeField1 = buildRuntimeField ("scriptedValue1" , 3 );
220+ var runtimeField2 = buildRuntimeField ("scriptedValue2" , 4 );
207221
208222 var searchHits = repository .findByValue (3 , runtimeField1 , runtimeField2 );
209223
@@ -214,14 +228,6 @@ void shouldUseRepositoryMethodWithRuntimeFieldParameters() {
214228 assertThat (foundEntity .getScriptedValue2 ()).isEqualTo (12 );
215229 }
216230
217- @ NotNull
218- private static RuntimeField getRuntimeField (String fieldName , int factor ) {
219- return new RuntimeField (
220- fieldName ,
221- "long" ,
222- String .format ("emit(doc['value'].size() > 0 ? doc['value'].value * %d : 0)" , factor ));
223- }
224-
225231 @ Test // #2035
226232 @ DisplayName ("should use repository string query method with RuntimeField parameters" )
227233 void shouldUseRepositoryStringQueryMethodWithRuntimeFieldParameters () {
@@ -232,8 +238,8 @@ void shouldUseRepositoryStringQueryMethodWithRuntimeFieldParameters() {
232238
233239 repository .save (entity );
234240
235- var runtimeField1 = getRuntimeField ("scriptedValue1" , 3 );
236- var runtimeField2 = getRuntimeField ("scriptedValue2" , 4 );
241+ var runtimeField1 = buildRuntimeField ("scriptedValue1" , 3 );
242+ var runtimeField2 = buildRuntimeField ("scriptedValue2" , 4 );
237243
238244 var searchHits = repository .findWithRuntimeFields (3 , runtimeField1 , runtimeField2 );
239245
@@ -263,8 +269,7 @@ void shouldUseParametersForRuntimeFieldsInSearchQueries() {
263269 "priceWithTax" ,
264270 "double" ,
265271 "emit(doc['price'].value * params.tax)" ,
266- Map .of ("tax" , 1.19 )
267- );
272+ Map .of ("tax" , 1.19 ));
268273 var query = CriteriaQuery .builder (
269274 Criteria .where ("priceWithTax" ).greaterThan (100.0 ))
270275 .withRuntimeFields (List .of (runtimeField ))
@@ -275,6 +280,56 @@ void shouldUseParametersForRuntimeFieldsInSearchQueries() {
275280 assertThat (searchHits ).hasSize (1 );
276281 }
277282
283+ @ Test // #3076
284+ @ DisplayName ("should use runtime fields in queries returning lists" )
285+ void shouldUseRuntimeFieldsInQueriesReturningLists () {
286+
287+ insert ("1" , "item 1" , 80.0 );
288+
289+ var runtimeField = new RuntimeField (
290+ "someStrings" ,
291+ "keyword" ,
292+ "emit('foo'); emit('bar');" ,
293+ null );
294+
295+ var query = Query .findAll ();
296+ query .addRuntimeField (runtimeField );
297+ query .addFields ("someStrings" );
298+ query .addSourceFilter (new FetchSourceFilterBuilder ().withIncludes ("*" ).build ());
299+
300+ var searchHits = operations .search (query , SomethingToBuy .class );
301+
302+ assertThat (searchHits ).hasSize (1 );
303+ var somethingToBuy = searchHits .getSearchHit (0 ).getContent ();
304+ assertThat (somethingToBuy .someStrings ).containsExactlyInAnyOrder ("foo" , "bar" );
305+ }
306+
307+ /**
308+ * build a {@link org.springframework.data.elasticsearch.core.query.ScriptedField} to return the product of the
309+ * document's value property and the given factor
310+ */
311+ @ NotNull
312+ private static org .springframework .data .elasticsearch .core .query .ScriptedField buildScriptedField (String fieldName ,
313+ int factor ) {
314+ return org .springframework .data .elasticsearch .core .query .ScriptedField .of (
315+ fieldName ,
316+ ScriptData .of (b -> b
317+ .withType (ScriptType .INLINE )
318+ .withScript ("doc['value'].size() > 0 ? doc['value'].value * params['factor'] : 0" )
319+ .withParams (Map .of ("factor" , factor ))));
320+ }
321+
322+ /**
323+ * build a {@link RuntimeField} to return the product of the document's value property and the given factor
324+ */
325+ @ NotNull
326+ private static RuntimeField buildRuntimeField (String fieldName , int factor ) {
327+ return new RuntimeField (
328+ fieldName ,
329+ "long" ,
330+ String .format ("emit(doc['value'].size() > 0 ? doc['value'].value * %d : 0)" , factor ));
331+ }
332+
278333 @ SuppressWarnings ("unused" )
279334 @ Document (indexName = "#{@indexNameProvider.indexName()}-something-to-by" )
280335 private static class SomethingToBuy {
@@ -286,6 +341,9 @@ private static class SomethingToBuy {
286341 @ Nullable
287342 @ Field (type = FieldType .Double ) private Double price ;
288343
344+ @ Nullable
345+ @ ScriptedField private List <String > someStrings ;
346+
289347 @ Nullable
290348 public String getId () {
291349 return id ;
@@ -312,6 +370,15 @@ public Double getPrice() {
312370 public void setPrice (@ Nullable Double price ) {
313371 this .price = price ;
314372 }
373+
374+ @ Nullable
375+ public List <String > getSomeStrings () {
376+ return someStrings ;
377+ }
378+
379+ public void setSomeStrings (@ Nullable List <String > someStrings ) {
380+ this .someStrings = someStrings ;
381+ }
315382 }
316383
317384 @ SuppressWarnings ("unused" )
@@ -320,6 +387,13 @@ public void setPrice(@Nullable Double price) {
320387 public static class Person {
321388 @ Nullable private String id ;
322389
390+ // need keywords as we are using them in the script
391+ @ Nullable
392+ @ Field (type = FieldType .Keyword ) private String firstName ;
393+ @ Nullable
394+ @ Field (type = FieldType .Keyword ) private String lastName ;
395+ @ ScriptedField private List <String > allNames = List .of ();
396+
323397 @ Field (type = FieldType .Date , format = DateFormat .basic_date )
324398 @ Nullable private LocalDate birthDate ;
325399
@@ -335,6 +409,24 @@ public void setId(@Nullable String id) {
335409 this .id = id ;
336410 }
337411
412+ @ Nullable
413+ public String getFirstName () {
414+ return firstName ;
415+ }
416+
417+ public void setFirstName (@ Nullable String firstName ) {
418+ this .firstName = firstName ;
419+ }
420+
421+ @ Nullable
422+ public String getLastName () {
423+ return lastName ;
424+ }
425+
426+ public void setLastName (@ Nullable String lastName ) {
427+ this .lastName = lastName ;
428+ }
429+
338430 @ Nullable
339431 public LocalDate getBirthDate () {
340432 return birthDate ;
@@ -352,6 +444,14 @@ public Integer getAge() {
352444 public void setAge (@ Nullable Integer age ) {
353445 this .age = age ;
354446 }
447+
448+ public List <String > getAllNames () {
449+ return allNames ;
450+ }
451+
452+ public void setAllNames (List <String > allNames ) {
453+ this .allNames = allNames ;
454+ }
355455 }
356456
357457 @ SuppressWarnings ("unused" )
0 commit comments