Skip to content

Commit 2d5f8e8

Browse files
authored
Support has_child and has_parent queries.
Original Pull Request: #2889 Closes #1472
1 parent ad66510 commit 2d5f8e8

File tree

9 files changed

+795
-28
lines changed

9 files changed

+795
-28
lines changed
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/*
2+
* Copyright 2024 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.client.elc;
17+
18+
import org.springframework.data.elasticsearch.core.query.CriteriaQuery;
19+
import org.springframework.data.elasticsearch.core.query.Query;
20+
import org.springframework.data.elasticsearch.core.query.StringQuery;
21+
import org.springframework.lang.Nullable;
22+
import org.springframework.util.Assert;
23+
24+
import java.util.function.Consumer;
25+
26+
/**
27+
* An abstract class that serves as a base for query processors.
28+
* It provides a common interface and basic functionality for query processing.
29+
*
30+
* @author Aouichaoui Youssef
31+
* @since 5.3
32+
*/
33+
public abstract class AbstractQueryProcessor {
34+
35+
/**
36+
* Convert a spring-data-elasticsearch {@literal query} to an Elasticsearch {@literal query}.
37+
*
38+
* @param query spring-data-elasticsearch {@literal query}.
39+
* @param queryConverter correct mapped field names and the values to the converted values.
40+
* @return an Elasticsearch {@literal query}.
41+
*/
42+
@Nullable
43+
static co.elastic.clients.elasticsearch._types.query_dsl.Query getEsQuery(@Nullable Query query,
44+
@Nullable Consumer<Query> queryConverter) {
45+
if (query == null) {
46+
return null;
47+
}
48+
49+
if (queryConverter != null) {
50+
queryConverter.accept(query);
51+
}
52+
53+
co.elastic.clients.elasticsearch._types.query_dsl.Query esQuery = null;
54+
55+
if (query instanceof CriteriaQuery criteriaQuery) {
56+
esQuery = CriteriaQueryProcessor.createQuery(criteriaQuery.getCriteria());
57+
} else if (query instanceof StringQuery stringQuery) {
58+
esQuery = Queries.wrapperQueryAsQuery(stringQuery.getSource());
59+
} else if (query instanceof NativeQuery nativeQuery) {
60+
if (nativeQuery.getQuery() != null) {
61+
esQuery = nativeQuery.getQuery();
62+
} else if (nativeQuery.getSpringDataQuery() != null) {
63+
esQuery = getEsQuery(nativeQuery.getSpringDataQuery(), queryConverter);
64+
}
65+
} else {
66+
throw new IllegalArgumentException("unhandled Query implementation " + query.getClass().getName());
67+
}
68+
69+
return esQuery;
70+
}
71+
}

src/main/java/org/springframework/data/elasticsearch/client/elc/CriteriaQueryProcessor.java

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,14 @@
1616
package org.springframework.data.elasticsearch.client.elc;
1717

1818
import static org.springframework.data.elasticsearch.client.elc.Queries.*;
19+
import static org.springframework.data.elasticsearch.client.elc.TypeUtils.scoreMode;
1920
import static org.springframework.util.StringUtils.*;
2021

2122
import co.elastic.clients.elasticsearch._types.FieldValue;
2223
import co.elastic.clients.elasticsearch._types.query_dsl.ChildScoreMode;
2324
import co.elastic.clients.elasticsearch._types.query_dsl.Operator;
2425
import co.elastic.clients.elasticsearch._types.query_dsl.Query;
26+
import co.elastic.clients.elasticsearch.core.search.InnerHits;
2527
import co.elastic.clients.json.JsonData;
2628

2729
import java.util.ArrayList;
@@ -30,7 +32,12 @@
3032

3133
import org.springframework.data.elasticsearch.annotations.FieldType;
3234
import org.springframework.data.elasticsearch.core.query.Criteria;
35+
import org.springframework.data.elasticsearch.core.query.CriteriaQuery;
3336
import org.springframework.data.elasticsearch.core.query.Field;
37+
import org.springframework.data.elasticsearch.core.query.HasChildQuery;
38+
import org.springframework.data.elasticsearch.core.query.HasParentQuery;
39+
import org.springframework.data.elasticsearch.core.query.InnerHitsQuery;
40+
import org.springframework.data.elasticsearch.core.query.StringQuery;
3441
import org.springframework.lang.Nullable;
3542
import org.springframework.util.Assert;
3643

@@ -42,7 +49,7 @@
4249
* @author Ezequiel Antúnez Camacho
4350
* @since 4.4
4451
*/
45-
class CriteriaQueryProcessor {
52+
class CriteriaQueryProcessor extends AbstractQueryProcessor {
4653

4754
/**
4855
* creates a query from the criteria
@@ -343,6 +350,34 @@ private static Query.Builder queryFor(Criteria.CriteriaEntry entry, Field field,
343350
.value(value.toString()) //
344351
.boost(boost)); //
345352
break;
353+
case HAS_CHILD:
354+
if (value instanceof HasChildQuery query) {
355+
queryBuilder.hasChild(hcb -> hcb
356+
.type(query.getType())
357+
.query(getEsQuery(query.getQuery(), null))
358+
.innerHits(getInnerHits(query.getInnerHitsQuery()))
359+
.ignoreUnmapped(query.getIgnoreUnmapped())
360+
.minChildren(query.getMinChildren())
361+
.maxChildren(query.getMaxChildren())
362+
.scoreMode(scoreMode(query.getScoreMode()))
363+
);
364+
} else {
365+
throw new CriteriaQueryException("value for " + fieldName + " is not a has_child query");
366+
}
367+
break;
368+
case HAS_PARENT:
369+
if (value instanceof HasParentQuery query) {
370+
queryBuilder.hasParent(hpb -> hpb
371+
.parentType(query.getParentType())
372+
.query(getEsQuery(query.getQuery(), null))
373+
.innerHits(getInnerHits(query.getInnerHitsQuery()))
374+
.ignoreUnmapped(query.getIgnoreUnmapped())
375+
.score(query.getScore())
376+
);
377+
} else {
378+
throw new CriteriaQueryException("value for " + fieldName + " is not a has_parent query");
379+
}
380+
break;
346381
default:
347382
throw new CriteriaQueryException("Could not build query for " + entry);
348383
}
@@ -397,4 +432,19 @@ public static String escape(String s) {
397432
return sb.toString();
398433
}
399434

435+
/**
436+
* Convert a spring-data-elasticsearch {@literal inner_hits} to an Elasticsearch {@literal inner_hits} query.
437+
*
438+
* @param query spring-data-elasticsearch {@literal inner_hits}.
439+
* @return an Elasticsearch {@literal inner_hits} query.
440+
*/
441+
@Nullable
442+
private static InnerHits getInnerHits(@Nullable InnerHitsQuery query) {
443+
if (query == null) {
444+
return null;
445+
}
446+
447+
return InnerHits.of(iqb -> iqb.from(query.getFrom()).size(query.getSize()).name(query.getName()));
448+
}
449+
400450
}

src/main/java/org/springframework/data/elasticsearch/client/elc/RequestConverter.java

Lines changed: 2 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@
112112
* @since 4.4
113113
*/
114114
@SuppressWarnings("ClassCanBeRecord")
115-
class RequestConverter {
115+
class RequestConverter extends AbstractQueryProcessor {
116116

117117
private static final Log LOGGER = LogFactory.getLog(RequestConverter.class);
118118

@@ -1755,31 +1755,7 @@ private void prepareNativeSearch(NativeQuery query, MultisearchBody.Builder buil
17551755
@Nullable
17561756
co.elastic.clients.elasticsearch._types.query_dsl.Query getQuery(@Nullable Query query,
17571757
@Nullable Class<?> clazz) {
1758-
1759-
if (query == null) {
1760-
return null;
1761-
}
1762-
1763-
elasticsearchConverter.updateQuery(query, clazz);
1764-
1765-
co.elastic.clients.elasticsearch._types.query_dsl.Query esQuery = null;
1766-
1767-
if (query instanceof CriteriaQuery) {
1768-
esQuery = CriteriaQueryProcessor.createQuery(((CriteriaQuery) query).getCriteria());
1769-
} else if (query instanceof StringQuery) {
1770-
esQuery = Queries.wrapperQueryAsQuery(((StringQuery) query).getSource());
1771-
} else if (query instanceof NativeQuery nativeQuery) {
1772-
1773-
if (nativeQuery.getQuery() != null) {
1774-
esQuery = nativeQuery.getQuery();
1775-
} else if (nativeQuery.getSpringDataQuery() != null) {
1776-
esQuery = getQuery(nativeQuery.getSpringDataQuery(), clazz);
1777-
}
1778-
} else {
1779-
throw new IllegalArgumentException("unhandled Query implementation " + query.getClass().getName());
1780-
}
1781-
1782-
return esQuery;
1758+
return getEsQuery(query, (q) -> elasticsearchConverter.updateQuery(q, clazz));
17831759
}
17841760

17851761
private void addFilter(Query query, SearchRequest.Builder builder) {

src/main/java/org/springframework/data/elasticsearch/client/elc/TypeUtils.java

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import co.elastic.clients.elasticsearch._types.*;
1919
import co.elastic.clients.elasticsearch._types.mapping.FieldType;
2020
import co.elastic.clients.elasticsearch._types.mapping.TypeMapping;
21+
import co.elastic.clients.elasticsearch._types.query_dsl.ChildScoreMode;
2122
import co.elastic.clients.elasticsearch._types.query_dsl.Operator;
2223
import co.elastic.clients.elasticsearch.core.search.BoundaryScanner;
2324
import co.elastic.clients.elasticsearch.core.search.HighlighterEncoder;
@@ -41,6 +42,7 @@
4142
import org.springframework.data.elasticsearch.core.RefreshPolicy;
4243
import org.springframework.data.elasticsearch.core.document.Document;
4344
import org.springframework.data.elasticsearch.core.query.GeoDistanceOrder;
45+
import org.springframework.data.elasticsearch.core.query.HasChildQuery;
4446
import org.springframework.data.elasticsearch.core.query.IndexQuery;
4547
import org.springframework.data.elasticsearch.core.query.IndicesOptions;
4648
import org.springframework.data.elasticsearch.core.query.Order;
@@ -527,4 +529,24 @@ static Operator operator(@Nullable OperatorType operator) {
527529
static Conflicts conflicts(@Nullable ConflictsType conflicts) {
528530
return conflicts != null ? Conflicts.valueOf(conflicts.name()) : null;
529531
}
532+
533+
/**
534+
* Convert a spring-data-elasticsearch {@literal scoreMode} to an Elasticsearch {@literal scoreMode}.
535+
*
536+
* @param scoreMode spring-data-elasticsearch {@literal scoreMode}.
537+
* @return an Elasticsearch {@literal scoreMode}.
538+
*/
539+
static ChildScoreMode scoreMode(@Nullable HasChildQuery.ScoreMode scoreMode) {
540+
if (scoreMode == null) {
541+
return ChildScoreMode.None;
542+
}
543+
544+
return switch (scoreMode) {
545+
case Avg -> ChildScoreMode.Avg;
546+
case Max -> ChildScoreMode.Max;
547+
case Min -> ChildScoreMode.Min;
548+
case Sum -> ChildScoreMode.Sum;
549+
default -> ChildScoreMode.None;
550+
};
551+
}
530552
}

src/main/java/org/springframework/data/elasticsearch/core/query/Criteria.java

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -816,6 +816,32 @@ public Criteria contains(GeoJson<?> geoShape) {
816816
filterCriteriaEntries.add(new CriteriaEntry(OperationKey.GEO_CONTAINS, geoShape));
817817
return this;
818818
}
819+
820+
/**
821+
* Adds a new filter CriteriaEntry for HAS_CHILD.
822+
*
823+
* @param query the has_child query.
824+
* @return the current Criteria.
825+
*/
826+
public Criteria hasChild(HasChildQuery query) {
827+
Assert.notNull(query, "has_child query must not be null.");
828+
829+
queryCriteriaEntries.add(new CriteriaEntry(OperationKey.HAS_CHILD, query));
830+
return this;
831+
}
832+
833+
/**
834+
* Adds a new filter CriteriaEntry for HAS_PARENT.
835+
*
836+
* @param query the has_parent query.
837+
* @return the current Criteria.
838+
*/
839+
public Criteria hasParent(HasParentQuery query) {
840+
Assert.notNull(query, "has_parent query must not be null.");
841+
842+
queryCriteriaEntries.add(new CriteriaEntry(OperationKey.HAS_PARENT, query));
843+
return this;
844+
}
819845
// endregion
820846

821847
// region helper functions
@@ -977,7 +1003,12 @@ public enum OperationKey { //
9771003
/**
9781004
* @since 5.1
9791005
*/
980-
REGEXP;
1006+
REGEXP,
1007+
/**
1008+
* @since 5.3
1009+
*/
1010+
HAS_CHILD,
1011+
HAS_PARENT;
9811012

9821013
/**
9831014
* @return true if this key does not have an associated value

0 commit comments

Comments
 (0)