Skip to content

Commit ab73c68

Browse files
authored
Nested Criteria queries must consider sub-fields.
Original Pull Request #1760 Closes #1758
1 parent 2bd4ef7 commit ab73c68

File tree

7 files changed

+84
-16
lines changed

7 files changed

+84
-16
lines changed

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

+3-5
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import static org.elasticsearch.index.query.Operator.*;
1919
import static org.elasticsearch.index.query.QueryBuilders.*;
2020
import static org.springframework.data.elasticsearch.core.query.Criteria.*;
21+
import static org.springframework.util.StringUtils.*;
2122

2223
import java.util.ArrayList;
2324
import java.util.Iterator;
@@ -154,11 +155,8 @@ private QueryBuilder queryForEntries(Criteria criteria) {
154155

155156
addBoost(query, criteria.getBoost());
156157

157-
int dotPosition = fieldName.lastIndexOf('.');
158-
159-
if (dotPosition > 0) {
160-
String nestedPath = fieldName.substring(0, dotPosition);
161-
query = nestedQuery(nestedPath, query, ScoreMode.Avg);
158+
if (hasText(field.getPath())) {
159+
query = nestedQuery(field.getPath(), query, ScoreMode.Avg);
162160
}
163161

164162
return query;

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

+8-11
Original file line numberDiff line numberDiff line change
@@ -86,21 +86,18 @@ default Document mapObject(@Nullable Object source) {
8686

8787
// region query
8888
/**
89-
* Updates a query by renaming the property names in the query to the correct mapped field names and the values to the
90-
* converted values if the {@link ElasticsearchPersistentProperty} for a property has a
89+
* Updates a {@link CriteriaQuery} by renaming the property names in the query to the correct mapped field names and
90+
* the values to the converted values if the {@link ElasticsearchPersistentProperty} for a property has a
9191
* {@link org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentPropertyConverter}. If
92-
* domainClass is null, it's a noop; handling null here eliminates null checks in the caller.
93-
*
92+
* domainClass is null or query is not a {@link CriteriaQuery}, it's a noop.
93+
*
9494
* @param query the query that is internally updated
9595
* @param domainClass the class of the object that is searched with the query
9696
*/
9797
default void updateQuery(Query query, @Nullable Class<?> domainClass) {
9898

99-
if (domainClass != null) {
100-
101-
if (query instanceof CriteriaQuery) {
102-
updateCriteriaQuery((CriteriaQuery) query, domainClass);
103-
}
99+
if (domainClass != null && query instanceof CriteriaQuery) {
100+
updateCriteriaQuery((CriteriaQuery) query, domainClass);
104101
}
105102
}
106103

@@ -109,8 +106,8 @@ default void updateQuery(Query query, @Nullable Class<?> domainClass) {
109106
* the values to the converted values if the {@link ElasticsearchPersistentProperty} for a property has a
110107
* {@link org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentPropertyConverter}.
111108
*
112-
* @param criteriaQuery the query that is internally updated
113-
* @param domainClass the class of the object that is searched with the query
109+
* @param criteriaQuery the query that is internally updated, must not be {@literal null}
110+
* @param domainClass the class of the object that is searched with the query, must not be {@literal null}
114111
*/
115112
void updateCriteriaQuery(CriteriaQuery criteriaQuery, Class<?> domainClass);
116113
// endregion

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

+11
Original file line numberDiff line numberDiff line change
@@ -1025,6 +1025,9 @@ private static Collection<?> asCollection(Object source) {
10251025
@Override
10261026
public void updateCriteriaQuery(CriteriaQuery criteriaQuery, Class<?> domainClass) {
10271027

1028+
Assert.notNull(criteriaQuery, "criteriaQuery must not be null");
1029+
Assert.notNull(domainClass, "domainClass must not be null");
1030+
10281031
ElasticsearchPersistentEntity<?> persistentEntity = mappingContext.getPersistentEntity(domainClass);
10291032

10301033
if (persistentEntity != null) {
@@ -1048,12 +1051,15 @@ private void updateCriteria(Criteria criteria, ElasticsearchPersistentEntity<?>
10481051
}
10491052

10501053
String[] fieldNames = field.getName().split("\\.");
1054+
10511055
ElasticsearchPersistentEntity<?> currentEntity = persistentEntity;
10521056
ElasticsearchPersistentProperty persistentProperty = null;
1057+
int propertyCount = 0;
10531058
for (int i = 0; i < fieldNames.length; i++) {
10541059
persistentProperty = currentEntity.getPersistentProperty(fieldNames[i]);
10551060

10561061
if (persistentProperty != null) {
1062+
propertyCount++;
10571063
fieldNames[i] = persistentProperty.getFieldName();
10581064
try {
10591065
currentEntity = mappingContext.getPersistentEntity(persistentProperty.getActualType());
@@ -1071,6 +1077,11 @@ private void updateCriteria(Criteria criteria, ElasticsearchPersistentEntity<?>
10711077

10721078
field.setName(String.join(".", fieldNames));
10731079

1080+
if (propertyCount > 1) {
1081+
List<String> propertyNames = Arrays.asList(fieldNames);
1082+
field.setPath(String.join(".", propertyNames.subList(0, propertyCount - 1)));
1083+
}
1084+
10741085
if (persistentProperty != null) {
10751086

10761087
if (persistentProperty.hasPropertyConverter()) {

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

+13
Original file line numberDiff line numberDiff line change
@@ -41,4 +41,17 @@ public interface Field {
4141
*/
4242
@Nullable
4343
FieldType getFieldType();
44+
45+
/**
46+
* Sets the path if this field has a multi-part name that should be used in a nested query.
47+
* @param path the value to set
48+
* @since 4.2
49+
*/
50+
void setPath(@Nullable String path);
51+
52+
/**
53+
* @return the path if this is a field for a nested query
54+
* @since 4.2
55+
*/
56+
@Nullable String getPath();
4457
}

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

+12
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ public class SimpleField implements Field {
3131

3232
private String name;
3333
@Nullable private FieldType fieldType;
34+
@Nullable private String path;
3435

3536
public SimpleField(String name) {
3637

@@ -63,6 +64,17 @@ public FieldType getFieldType() {
6364
return fieldType;
6465
}
6566

67+
@Override
68+
public void setPath(@Nullable String path) {
69+
this.path = path;
70+
}
71+
72+
@Override
73+
@Nullable
74+
public String getPath() {
75+
return path;
76+
}
77+
6678
@Override
6779
public String toString() {
6880
return getName();

Diff for: src/test/java/org/springframework/data/elasticsearch/core/CriteriaQueryMappingUnitTests.java

+36
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@
3434
import org.springframework.data.elasticsearch.annotations.DateFormat;
3535
import org.springframework.data.elasticsearch.annotations.Field;
3636
import org.springframework.data.elasticsearch.annotations.FieldType;
37+
import org.springframework.data.elasticsearch.annotations.InnerField;
38+
import org.springframework.data.elasticsearch.annotations.MultiField;
3739
import org.springframework.data.elasticsearch.core.convert.GeoConverters;
3840
import org.springframework.data.elasticsearch.core.convert.MappingElasticsearchConverter;
3941
import org.springframework.data.elasticsearch.core.document.Document;
@@ -362,6 +364,39 @@ void shouldMapNamesAndValueInNestedEntities() throws JSONException {
362364

363365
assertEquals(expected, queryString, false);
364366
}
367+
368+
@Test // #1753
369+
@DisplayName("should map names and value in nested entities with sub-fields")
370+
void shouldMapNamesAndValueInNestedEntitiesWithSubfields() throws JSONException {
371+
372+
String expected = "{\n" + //
373+
" \"bool\": {\n" + //
374+
" \"must\": [\n" + //
375+
" {\n" + //
376+
" \"nested\": {\n" + //
377+
" \"query\": {\n" + //
378+
" \"query_string\": {\n" + //
379+
" \"query\": \"Foobar\",\n" + //
380+
" \"fields\": [\n" + //
381+
" \"per-sons.nick-name.keyword^1.0\"\n" + //
382+
" ]\n" + //
383+
" }\n" + //
384+
" },\n" + //
385+
" \"path\": \"per-sons\"\n" + //
386+
" }\n" + //
387+
" }\n" + //
388+
" ]\n" + //
389+
" }\n" + //
390+
"}\n"; //
391+
392+
CriteriaQuery criteriaQuery = new CriteriaQuery(
393+
new Criteria("persons.nickName.keyword").is("Foobar")
394+
);
395+
mappingElasticsearchConverter.updateQuery(criteriaQuery, House.class);
396+
String queryString = new CriteriaQueryProcessor().createQuery(criteriaQuery.getCriteria()).toString();
397+
398+
assertEquals(expected, queryString, false);
399+
}
365400
// endregion
366401

367402
// region helper functions
@@ -379,6 +414,7 @@ static class Person {
379414
@Nullable @Id String id;
380415
@Nullable @Field(name = "first-name") String firstName;
381416
@Nullable @Field(name = "last-name") String lastName;
417+
@Nullable @MultiField(mainField = @Field(name="nick-name"), otherFields = {@InnerField(suffix = "keyword", type = FieldType.Keyword)}) String nickName;
382418
@Nullable @Field(name = "created-date", type = FieldType.Date, format = DateFormat.epoch_millis) Date createdDate;
383419
@Nullable @Field(name = "birth-date", type = FieldType.Date, format = {},
384420
pattern = "dd.MM.uuuu") LocalDate birthDate;

Diff for: src/test/java/org/springframework/data/elasticsearch/core/CriteriaQueryProcessorUnitTests.java

+1
Original file line numberDiff line numberDiff line change
@@ -365,6 +365,7 @@ void shouldBuildNestedQuery() throws JSONException {
365365
"}"; //
366366

367367
Criteria criteria = new Criteria("houses.inhabitants.lastName").is("murphy");
368+
criteria.getField().setPath("houses.inhabitants");
368369

369370
String query = queryProcessor.createQuery(criteria).toString();
370371

0 commit comments

Comments
 (0)