mirror of
https://github.com/spring-projects/spring-data-elasticsearch.git
synced 2025-06-29 15:22:11 +00:00
Support has_child and has_parent queries.
Original Pull Request: #2889 Closes #1472
This commit is contained in:
parent
ad66510e9e
commit
2d5f8e8219
@ -0,0 +1,71 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2024 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.springframework.data.elasticsearch.client.elc;
|
||||||
|
|
||||||
|
import org.springframework.data.elasticsearch.core.query.CriteriaQuery;
|
||||||
|
import org.springframework.data.elasticsearch.core.query.Query;
|
||||||
|
import org.springframework.data.elasticsearch.core.query.StringQuery;
|
||||||
|
import org.springframework.lang.Nullable;
|
||||||
|
import org.springframework.util.Assert;
|
||||||
|
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An abstract class that serves as a base for query processors.
|
||||||
|
* It provides a common interface and basic functionality for query processing.
|
||||||
|
*
|
||||||
|
* @author Aouichaoui Youssef
|
||||||
|
* @since 5.3
|
||||||
|
*/
|
||||||
|
public abstract class AbstractQueryProcessor {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a spring-data-elasticsearch {@literal query} to an Elasticsearch {@literal query}.
|
||||||
|
*
|
||||||
|
* @param query spring-data-elasticsearch {@literal query}.
|
||||||
|
* @param queryConverter correct mapped field names and the values to the converted values.
|
||||||
|
* @return an Elasticsearch {@literal query}.
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
static co.elastic.clients.elasticsearch._types.query_dsl.Query getEsQuery(@Nullable Query query,
|
||||||
|
@Nullable Consumer<Query> queryConverter) {
|
||||||
|
if (query == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (queryConverter != null) {
|
||||||
|
queryConverter.accept(query);
|
||||||
|
}
|
||||||
|
|
||||||
|
co.elastic.clients.elasticsearch._types.query_dsl.Query esQuery = null;
|
||||||
|
|
||||||
|
if (query instanceof CriteriaQuery criteriaQuery) {
|
||||||
|
esQuery = CriteriaQueryProcessor.createQuery(criteriaQuery.getCriteria());
|
||||||
|
} else if (query instanceof StringQuery stringQuery) {
|
||||||
|
esQuery = Queries.wrapperQueryAsQuery(stringQuery.getSource());
|
||||||
|
} else if (query instanceof NativeQuery nativeQuery) {
|
||||||
|
if (nativeQuery.getQuery() != null) {
|
||||||
|
esQuery = nativeQuery.getQuery();
|
||||||
|
} else if (nativeQuery.getSpringDataQuery() != null) {
|
||||||
|
esQuery = getEsQuery(nativeQuery.getSpringDataQuery(), queryConverter);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new IllegalArgumentException("unhandled Query implementation " + query.getClass().getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
return esQuery;
|
||||||
|
}
|
||||||
|
}
|
@ -16,12 +16,14 @@
|
|||||||
package org.springframework.data.elasticsearch.client.elc;
|
package org.springframework.data.elasticsearch.client.elc;
|
||||||
|
|
||||||
import static org.springframework.data.elasticsearch.client.elc.Queries.*;
|
import static org.springframework.data.elasticsearch.client.elc.Queries.*;
|
||||||
|
import static org.springframework.data.elasticsearch.client.elc.TypeUtils.scoreMode;
|
||||||
import static org.springframework.util.StringUtils.*;
|
import static org.springframework.util.StringUtils.*;
|
||||||
|
|
||||||
import co.elastic.clients.elasticsearch._types.FieldValue;
|
import co.elastic.clients.elasticsearch._types.FieldValue;
|
||||||
import co.elastic.clients.elasticsearch._types.query_dsl.ChildScoreMode;
|
import co.elastic.clients.elasticsearch._types.query_dsl.ChildScoreMode;
|
||||||
import co.elastic.clients.elasticsearch._types.query_dsl.Operator;
|
import co.elastic.clients.elasticsearch._types.query_dsl.Operator;
|
||||||
import co.elastic.clients.elasticsearch._types.query_dsl.Query;
|
import co.elastic.clients.elasticsearch._types.query_dsl.Query;
|
||||||
|
import co.elastic.clients.elasticsearch.core.search.InnerHits;
|
||||||
import co.elastic.clients.json.JsonData;
|
import co.elastic.clients.json.JsonData;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@ -30,7 +32,12 @@ import java.util.List;
|
|||||||
|
|
||||||
import org.springframework.data.elasticsearch.annotations.FieldType;
|
import org.springframework.data.elasticsearch.annotations.FieldType;
|
||||||
import org.springframework.data.elasticsearch.core.query.Criteria;
|
import org.springframework.data.elasticsearch.core.query.Criteria;
|
||||||
|
import org.springframework.data.elasticsearch.core.query.CriteriaQuery;
|
||||||
import org.springframework.data.elasticsearch.core.query.Field;
|
import org.springframework.data.elasticsearch.core.query.Field;
|
||||||
|
import org.springframework.data.elasticsearch.core.query.HasChildQuery;
|
||||||
|
import org.springframework.data.elasticsearch.core.query.HasParentQuery;
|
||||||
|
import org.springframework.data.elasticsearch.core.query.InnerHitsQuery;
|
||||||
|
import org.springframework.data.elasticsearch.core.query.StringQuery;
|
||||||
import org.springframework.lang.Nullable;
|
import org.springframework.lang.Nullable;
|
||||||
import org.springframework.util.Assert;
|
import org.springframework.util.Assert;
|
||||||
|
|
||||||
@ -42,7 +49,7 @@ import org.springframework.util.Assert;
|
|||||||
* @author Ezequiel Antúnez Camacho
|
* @author Ezequiel Antúnez Camacho
|
||||||
* @since 4.4
|
* @since 4.4
|
||||||
*/
|
*/
|
||||||
class CriteriaQueryProcessor {
|
class CriteriaQueryProcessor extends AbstractQueryProcessor {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* creates a query from the criteria
|
* creates a query from the criteria
|
||||||
@ -343,6 +350,34 @@ class CriteriaQueryProcessor {
|
|||||||
.value(value.toString()) //
|
.value(value.toString()) //
|
||||||
.boost(boost)); //
|
.boost(boost)); //
|
||||||
break;
|
break;
|
||||||
|
case HAS_CHILD:
|
||||||
|
if (value instanceof HasChildQuery query) {
|
||||||
|
queryBuilder.hasChild(hcb -> hcb
|
||||||
|
.type(query.getType())
|
||||||
|
.query(getEsQuery(query.getQuery(), null))
|
||||||
|
.innerHits(getInnerHits(query.getInnerHitsQuery()))
|
||||||
|
.ignoreUnmapped(query.getIgnoreUnmapped())
|
||||||
|
.minChildren(query.getMinChildren())
|
||||||
|
.maxChildren(query.getMaxChildren())
|
||||||
|
.scoreMode(scoreMode(query.getScoreMode()))
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
throw new CriteriaQueryException("value for " + fieldName + " is not a has_child query");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case HAS_PARENT:
|
||||||
|
if (value instanceof HasParentQuery query) {
|
||||||
|
queryBuilder.hasParent(hpb -> hpb
|
||||||
|
.parentType(query.getParentType())
|
||||||
|
.query(getEsQuery(query.getQuery(), null))
|
||||||
|
.innerHits(getInnerHits(query.getInnerHitsQuery()))
|
||||||
|
.ignoreUnmapped(query.getIgnoreUnmapped())
|
||||||
|
.score(query.getScore())
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
throw new CriteriaQueryException("value for " + fieldName + " is not a has_parent query");
|
||||||
|
}
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
throw new CriteriaQueryException("Could not build query for " + entry);
|
throw new CriteriaQueryException("Could not build query for " + entry);
|
||||||
}
|
}
|
||||||
@ -397,4 +432,19 @@ class CriteriaQueryProcessor {
|
|||||||
return sb.toString();
|
return sb.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a spring-data-elasticsearch {@literal inner_hits} to an Elasticsearch {@literal inner_hits} query.
|
||||||
|
*
|
||||||
|
* @param query spring-data-elasticsearch {@literal inner_hits}.
|
||||||
|
* @return an Elasticsearch {@literal inner_hits} query.
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
private static InnerHits getInnerHits(@Nullable InnerHitsQuery query) {
|
||||||
|
if (query == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return InnerHits.of(iqb -> iqb.from(query.getFrom()).size(query.getSize()).name(query.getName()));
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -112,7 +112,7 @@ import org.springframework.util.StringUtils;
|
|||||||
* @since 4.4
|
* @since 4.4
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings("ClassCanBeRecord")
|
@SuppressWarnings("ClassCanBeRecord")
|
||||||
class RequestConverter {
|
class RequestConverter extends AbstractQueryProcessor {
|
||||||
|
|
||||||
private static final Log LOGGER = LogFactory.getLog(RequestConverter.class);
|
private static final Log LOGGER = LogFactory.getLog(RequestConverter.class);
|
||||||
|
|
||||||
@ -1755,31 +1755,7 @@ class RequestConverter {
|
|||||||
@Nullable
|
@Nullable
|
||||||
co.elastic.clients.elasticsearch._types.query_dsl.Query getQuery(@Nullable Query query,
|
co.elastic.clients.elasticsearch._types.query_dsl.Query getQuery(@Nullable Query query,
|
||||||
@Nullable Class<?> clazz) {
|
@Nullable Class<?> clazz) {
|
||||||
|
return getEsQuery(query, (q) -> elasticsearchConverter.updateQuery(q, clazz));
|
||||||
if (query == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
elasticsearchConverter.updateQuery(query, clazz);
|
|
||||||
|
|
||||||
co.elastic.clients.elasticsearch._types.query_dsl.Query esQuery = null;
|
|
||||||
|
|
||||||
if (query instanceof CriteriaQuery) {
|
|
||||||
esQuery = CriteriaQueryProcessor.createQuery(((CriteriaQuery) query).getCriteria());
|
|
||||||
} else if (query instanceof StringQuery) {
|
|
||||||
esQuery = Queries.wrapperQueryAsQuery(((StringQuery) query).getSource());
|
|
||||||
} else if (query instanceof NativeQuery nativeQuery) {
|
|
||||||
|
|
||||||
if (nativeQuery.getQuery() != null) {
|
|
||||||
esQuery = nativeQuery.getQuery();
|
|
||||||
} else if (nativeQuery.getSpringDataQuery() != null) {
|
|
||||||
esQuery = getQuery(nativeQuery.getSpringDataQuery(), clazz);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
throw new IllegalArgumentException("unhandled Query implementation " + query.getClass().getName());
|
|
||||||
}
|
|
||||||
|
|
||||||
return esQuery;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void addFilter(Query query, SearchRequest.Builder builder) {
|
private void addFilter(Query query, SearchRequest.Builder builder) {
|
||||||
|
@ -18,6 +18,7 @@ package org.springframework.data.elasticsearch.client.elc;
|
|||||||
import co.elastic.clients.elasticsearch._types.*;
|
import co.elastic.clients.elasticsearch._types.*;
|
||||||
import co.elastic.clients.elasticsearch._types.mapping.FieldType;
|
import co.elastic.clients.elasticsearch._types.mapping.FieldType;
|
||||||
import co.elastic.clients.elasticsearch._types.mapping.TypeMapping;
|
import co.elastic.clients.elasticsearch._types.mapping.TypeMapping;
|
||||||
|
import co.elastic.clients.elasticsearch._types.query_dsl.ChildScoreMode;
|
||||||
import co.elastic.clients.elasticsearch._types.query_dsl.Operator;
|
import co.elastic.clients.elasticsearch._types.query_dsl.Operator;
|
||||||
import co.elastic.clients.elasticsearch.core.search.BoundaryScanner;
|
import co.elastic.clients.elasticsearch.core.search.BoundaryScanner;
|
||||||
import co.elastic.clients.elasticsearch.core.search.HighlighterEncoder;
|
import co.elastic.clients.elasticsearch.core.search.HighlighterEncoder;
|
||||||
@ -41,6 +42,7 @@ import org.springframework.data.domain.Sort;
|
|||||||
import org.springframework.data.elasticsearch.core.RefreshPolicy;
|
import org.springframework.data.elasticsearch.core.RefreshPolicy;
|
||||||
import org.springframework.data.elasticsearch.core.document.Document;
|
import org.springframework.data.elasticsearch.core.document.Document;
|
||||||
import org.springframework.data.elasticsearch.core.query.GeoDistanceOrder;
|
import org.springframework.data.elasticsearch.core.query.GeoDistanceOrder;
|
||||||
|
import org.springframework.data.elasticsearch.core.query.HasChildQuery;
|
||||||
import org.springframework.data.elasticsearch.core.query.IndexQuery;
|
import org.springframework.data.elasticsearch.core.query.IndexQuery;
|
||||||
import org.springframework.data.elasticsearch.core.query.IndicesOptions;
|
import org.springframework.data.elasticsearch.core.query.IndicesOptions;
|
||||||
import org.springframework.data.elasticsearch.core.query.Order;
|
import org.springframework.data.elasticsearch.core.query.Order;
|
||||||
@ -527,4 +529,24 @@ final class TypeUtils {
|
|||||||
static Conflicts conflicts(@Nullable ConflictsType conflicts) {
|
static Conflicts conflicts(@Nullable ConflictsType conflicts) {
|
||||||
return conflicts != null ? Conflicts.valueOf(conflicts.name()) : null;
|
return conflicts != null ? Conflicts.valueOf(conflicts.name()) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a spring-data-elasticsearch {@literal scoreMode} to an Elasticsearch {@literal scoreMode}.
|
||||||
|
*
|
||||||
|
* @param scoreMode spring-data-elasticsearch {@literal scoreMode}.
|
||||||
|
* @return an Elasticsearch {@literal scoreMode}.
|
||||||
|
*/
|
||||||
|
static ChildScoreMode scoreMode(@Nullable HasChildQuery.ScoreMode scoreMode) {
|
||||||
|
if (scoreMode == null) {
|
||||||
|
return ChildScoreMode.None;
|
||||||
|
}
|
||||||
|
|
||||||
|
return switch (scoreMode) {
|
||||||
|
case Avg -> ChildScoreMode.Avg;
|
||||||
|
case Max -> ChildScoreMode.Max;
|
||||||
|
case Min -> ChildScoreMode.Min;
|
||||||
|
case Sum -> ChildScoreMode.Sum;
|
||||||
|
default -> ChildScoreMode.None;
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -816,6 +816,32 @@ public class Criteria {
|
|||||||
filterCriteriaEntries.add(new CriteriaEntry(OperationKey.GEO_CONTAINS, geoShape));
|
filterCriteriaEntries.add(new CriteriaEntry(OperationKey.GEO_CONTAINS, geoShape));
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a new filter CriteriaEntry for HAS_CHILD.
|
||||||
|
*
|
||||||
|
* @param query the has_child query.
|
||||||
|
* @return the current Criteria.
|
||||||
|
*/
|
||||||
|
public Criteria hasChild(HasChildQuery query) {
|
||||||
|
Assert.notNull(query, "has_child query must not be null.");
|
||||||
|
|
||||||
|
queryCriteriaEntries.add(new CriteriaEntry(OperationKey.HAS_CHILD, query));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a new filter CriteriaEntry for HAS_PARENT.
|
||||||
|
*
|
||||||
|
* @param query the has_parent query.
|
||||||
|
* @return the current Criteria.
|
||||||
|
*/
|
||||||
|
public Criteria hasParent(HasParentQuery query) {
|
||||||
|
Assert.notNull(query, "has_parent query must not be null.");
|
||||||
|
|
||||||
|
queryCriteriaEntries.add(new CriteriaEntry(OperationKey.HAS_PARENT, query));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
// endregion
|
// endregion
|
||||||
|
|
||||||
// region helper functions
|
// region helper functions
|
||||||
@ -977,7 +1003,12 @@ public class Criteria {
|
|||||||
/**
|
/**
|
||||||
* @since 5.1
|
* @since 5.1
|
||||||
*/
|
*/
|
||||||
REGEXP;
|
REGEXP,
|
||||||
|
/**
|
||||||
|
* @since 5.3
|
||||||
|
*/
|
||||||
|
HAS_CHILD,
|
||||||
|
HAS_PARENT;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return true if this key does not have an associated value
|
* @return true if this key does not have an associated value
|
||||||
|
@ -0,0 +1,203 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2024 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.springframework.data.elasticsearch.core.query;
|
||||||
|
|
||||||
|
import org.springframework.lang.Nullable;
|
||||||
|
import org.springframework.util.Assert;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines a has_child request.
|
||||||
|
*
|
||||||
|
* @author Aouichaoui Youssef
|
||||||
|
* @see <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-has-child-query.html">docs</a>
|
||||||
|
* @since 5.3
|
||||||
|
*/
|
||||||
|
public class HasChildQuery {
|
||||||
|
/**
|
||||||
|
* Name of the child relationship mapped for the join field.
|
||||||
|
*/
|
||||||
|
private final String type;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Query that specifies the documents to run on child documents of the {@link #type} field.
|
||||||
|
*/
|
||||||
|
private final Query query;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates whether to ignore an unmapped {@link #type} and not return any documents instead of an error.
|
||||||
|
* Default, this is set to {@code false}.
|
||||||
|
*/
|
||||||
|
@Nullable private final Boolean ignoreUnmapped;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Maximum number of child documents that match the {@link #query} allowed for a returned parent document.
|
||||||
|
* If the parent document exceeds this limit, it is excluded from the search results.
|
||||||
|
*/
|
||||||
|
@Nullable private final Integer maxChildren;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Minimum number of child documents that match the query required to match the {@link #query} for a returned parent document.
|
||||||
|
* If the parent document does not meet this limit, it is excluded from the search results.
|
||||||
|
*/
|
||||||
|
@Nullable private final Integer minChildren;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates how scores for matching child documents affect the root parent document’s relevance score.
|
||||||
|
*/
|
||||||
|
@Nullable private final ScoreMode scoreMode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtaining nested objects and documents that have a parent-child relationship.
|
||||||
|
*/
|
||||||
|
@Nullable private final InnerHitsQuery innerHitsQuery;
|
||||||
|
|
||||||
|
public static Builder builder(String type) {
|
||||||
|
return new Builder(type);
|
||||||
|
}
|
||||||
|
|
||||||
|
private HasChildQuery(Builder builder) {
|
||||||
|
this.type = builder.type;
|
||||||
|
this.query = builder.query;
|
||||||
|
this.innerHitsQuery = builder.innerHitsQuery;
|
||||||
|
|
||||||
|
this.ignoreUnmapped = builder.ignoreUnmapped;
|
||||||
|
|
||||||
|
this.maxChildren = builder.maxChildren;
|
||||||
|
this.minChildren = builder.minChildren;
|
||||||
|
|
||||||
|
this.scoreMode = builder.scoreMode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getType() {
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Query getQuery() {
|
||||||
|
return query;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public Boolean getIgnoreUnmapped() {
|
||||||
|
return ignoreUnmapped;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public Integer getMaxChildren() {
|
||||||
|
return maxChildren;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public Integer getMinChildren() {
|
||||||
|
return minChildren;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public ScoreMode getScoreMode() {
|
||||||
|
return scoreMode;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public InnerHitsQuery getInnerHitsQuery() {
|
||||||
|
return innerHitsQuery;
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum ScoreMode {
|
||||||
|
Default, Avg, Max, Min, Sum
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final class Builder {
|
||||||
|
private final String type;
|
||||||
|
private Query query;
|
||||||
|
|
||||||
|
@Nullable private Boolean ignoreUnmapped;
|
||||||
|
|
||||||
|
@Nullable private Integer maxChildren;
|
||||||
|
@Nullable private Integer minChildren;
|
||||||
|
|
||||||
|
@Nullable private ScoreMode scoreMode;
|
||||||
|
|
||||||
|
@Nullable private InnerHitsQuery innerHitsQuery;
|
||||||
|
|
||||||
|
private Builder(String type) {
|
||||||
|
Assert.notNull(type, "type must not be null");
|
||||||
|
|
||||||
|
this.type = type;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Query that specifies the documents to run on child documents of the {@link #type} field.
|
||||||
|
*/
|
||||||
|
public Builder withQuery(Query query) {
|
||||||
|
this.query = query;
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates whether to ignore an unmapped {@link #type} and not return any documents instead of an error.
|
||||||
|
* Default, this is set to {@code false}.
|
||||||
|
*/
|
||||||
|
public Builder withIgnoreUnmapped(@Nullable Boolean ignoreUnmapped) {
|
||||||
|
this.ignoreUnmapped = ignoreUnmapped;
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Maximum number of child documents that match the {@link #query} allowed for a returned parent document.
|
||||||
|
* If the parent document exceeds this limit, it is excluded from the search results.
|
||||||
|
*/
|
||||||
|
public Builder withMaxChildren(@Nullable Integer maxChildren) {
|
||||||
|
this.maxChildren = maxChildren;
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Minimum number of child documents that match the query required to match the {@link #query} for a returned parent document.
|
||||||
|
* If the parent document does not meet this limit, it is excluded from the search results.
|
||||||
|
*/
|
||||||
|
public Builder withMinChildren(@Nullable Integer minChildren) {
|
||||||
|
this.minChildren = minChildren;
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates how scores for matching child documents affect the root parent document’s relevance score.
|
||||||
|
*/
|
||||||
|
public Builder withScoreMode(@Nullable ScoreMode scoreMode) {
|
||||||
|
this.scoreMode = scoreMode;
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtaining nested objects and documents that have a parent-child relationship.
|
||||||
|
*/
|
||||||
|
public Builder withInnerHitsQuery(@Nullable InnerHitsQuery innerHitsQuery) {
|
||||||
|
this.innerHitsQuery = innerHitsQuery;
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public HasChildQuery build() {
|
||||||
|
Assert.notNull(query, "query must not be null.");
|
||||||
|
|
||||||
|
return new HasChildQuery(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,140 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2024 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.springframework.data.elasticsearch.core.query;
|
||||||
|
|
||||||
|
import org.springframework.lang.Nullable;
|
||||||
|
import org.springframework.util.Assert;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines a has_parent request.
|
||||||
|
*
|
||||||
|
* @author Aouichaoui Youssef
|
||||||
|
* @see <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-has-parent-query.html">docs</a>
|
||||||
|
* @since 5.3
|
||||||
|
*/
|
||||||
|
public class HasParentQuery {
|
||||||
|
/**
|
||||||
|
* Name of the parent relationship mapped for the join field.
|
||||||
|
*/
|
||||||
|
private final String parentType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Query that specifies the documents to run on parent documents of the {@link #parentType} field.
|
||||||
|
*/
|
||||||
|
private final Query query;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates whether the relevance score of a matching parent document is aggregated into its child documents.
|
||||||
|
* Default, this is set to {@code false}.
|
||||||
|
*/
|
||||||
|
@Nullable private final Boolean score;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates whether to ignore an unmapped {@link #parentType} and not return any documents instead of an error.
|
||||||
|
* Default, this is set to {@code false}.
|
||||||
|
*/
|
||||||
|
@Nullable private final Boolean ignoreUnmapped;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtaining nested objects and documents that have a parent-child relationship.
|
||||||
|
*/
|
||||||
|
@Nullable private final InnerHitsQuery innerHitsQuery;
|
||||||
|
|
||||||
|
public static Builder builder(String parentType) {
|
||||||
|
return new Builder(parentType);
|
||||||
|
}
|
||||||
|
|
||||||
|
private HasParentQuery(Builder builder) {
|
||||||
|
this.parentType = builder.parentType;
|
||||||
|
this.query = builder.query;
|
||||||
|
this.innerHitsQuery = builder.innerHitsQuery;
|
||||||
|
|
||||||
|
this.score = builder.score;
|
||||||
|
this.ignoreUnmapped = builder.ignoreUnmapped;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getParentType() {
|
||||||
|
return parentType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Query getQuery() {
|
||||||
|
return query;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public Boolean getScore() {
|
||||||
|
return score;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public Boolean getIgnoreUnmapped() {
|
||||||
|
return ignoreUnmapped;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public InnerHitsQuery getInnerHitsQuery() {
|
||||||
|
return innerHitsQuery;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Builder {
|
||||||
|
private final String parentType;
|
||||||
|
private Query query;
|
||||||
|
|
||||||
|
@Nullable private Boolean score;
|
||||||
|
@Nullable private Boolean ignoreUnmapped;
|
||||||
|
|
||||||
|
@Nullable private InnerHitsQuery innerHitsQuery;
|
||||||
|
|
||||||
|
private Builder(String parentType) {
|
||||||
|
Assert.notNull(parentType, "parent_type must not be null.");
|
||||||
|
|
||||||
|
this.parentType = parentType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder withQuery(Query query) {
|
||||||
|
this.query = query;
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder withScore(@Nullable Boolean score) {
|
||||||
|
this.score = score;
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder withIgnoreUnmapped(@Nullable Boolean ignoreUnmapped) {
|
||||||
|
this.ignoreUnmapped = ignoreUnmapped;
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtaining nested objects and documents that have a parent-child relationship.
|
||||||
|
*/
|
||||||
|
public Builder withInnerHitsQuery(@Nullable InnerHitsQuery innerHitsQuery) {
|
||||||
|
this.innerHitsQuery = innerHitsQuery;
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public HasParentQuery build() {
|
||||||
|
Assert.notNull(query, "query must not be null.");
|
||||||
|
|
||||||
|
return new HasParentQuery(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,108 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2024 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.springframework.data.elasticsearch.core.query;
|
||||||
|
|
||||||
|
import org.springframework.lang.Nullable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines an inner_hits request.
|
||||||
|
*
|
||||||
|
* @author Aouichaoui Youssef
|
||||||
|
* @see <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/inner-hits.html">docs</a>
|
||||||
|
* @since 5.3
|
||||||
|
*/
|
||||||
|
public class InnerHitsQuery {
|
||||||
|
/**
|
||||||
|
* The name to be used for the particular inner hit definition in the response.
|
||||||
|
*/
|
||||||
|
@Nullable private final String name;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The maximum number of hits to return.
|
||||||
|
*/
|
||||||
|
@Nullable private final Integer size;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The offset from where the first hit to fetch.
|
||||||
|
*/
|
||||||
|
@Nullable private final Integer from;
|
||||||
|
|
||||||
|
public static Builder builder() {
|
||||||
|
return new Builder();
|
||||||
|
}
|
||||||
|
|
||||||
|
private InnerHitsQuery(Builder builder) {
|
||||||
|
this.name = builder.name;
|
||||||
|
|
||||||
|
this.from = builder.from;
|
||||||
|
this.size = builder.size;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public String getName() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public Integer getSize() {
|
||||||
|
return size;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public Integer getFrom() {
|
||||||
|
return from;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final class Builder {
|
||||||
|
@Nullable private String name;
|
||||||
|
@Nullable private Integer size;
|
||||||
|
@Nullable private Integer from;
|
||||||
|
|
||||||
|
private Builder() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The name to be used for the particular inner hit definition in the response.
|
||||||
|
*/
|
||||||
|
public Builder withName(@Nullable String name) {
|
||||||
|
this.name = name;
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The maximum number of hits to return.
|
||||||
|
*/
|
||||||
|
public Builder withSize(@Nullable Integer size) {
|
||||||
|
this.size = size;
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The offset from where the first hit to fetch.
|
||||||
|
*/
|
||||||
|
public Builder withFrom(@Nullable Integer from) {
|
||||||
|
this.from = from;
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public InnerHitsQuery build() {
|
||||||
|
return new InnerHitsQuery(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -3827,6 +3827,68 @@ public abstract class ElasticsearchIntegrationTests {
|
|||||||
assertThat(result.getDeleted()).isEqualTo(0);
|
assertThat(result.getDeleted()).isEqualTo(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldGetOnlyDocumentsThatHasChild() {
|
||||||
|
// Given
|
||||||
|
String indexName = indexNameProvider.indexName() + "-join";
|
||||||
|
operations.indexOps(RootEntity.class).createWithMapping();
|
||||||
|
|
||||||
|
RootEntity parentEntity = RootEntity.builder()
|
||||||
|
.withId(nextIdAsString())
|
||||||
|
.withParent(new RootEntity.Parent())
|
||||||
|
.build();
|
||||||
|
IndexQuery indexQuery = new IndexQueryBuilder().withId(parentEntity.id).withObject(parentEntity).build();
|
||||||
|
operations.index(indexQuery, IndexCoordinates.of(indexName));
|
||||||
|
|
||||||
|
RootEntity childEntity = RootEntity.builder()
|
||||||
|
.withId(nextIdAsString())
|
||||||
|
.withChild(new RootEntity.Child())
|
||||||
|
.withRelation(new JoinField<>("child", parentEntity.id))
|
||||||
|
.build();
|
||||||
|
indexQuery = new IndexQueryBuilder().withId(childEntity.id).withObject(childEntity).build();
|
||||||
|
operations.index(indexQuery, IndexCoordinates.of(indexName));
|
||||||
|
|
||||||
|
HasChildQuery childQuery = HasChildQuery.builder("child").withQuery(operations.matchAllQuery()).build();
|
||||||
|
Query query = CriteriaQuery.builder(Criteria.where("child").hasChild(childQuery)).build();
|
||||||
|
|
||||||
|
// When
|
||||||
|
SearchHits<RootEntity> hits = operations.search(query, RootEntity.class);
|
||||||
|
|
||||||
|
// Then
|
||||||
|
assertThat(hits.getTotalHits()).isEqualTo(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldGetOnlyDocumentsThatHasParent() {
|
||||||
|
// Given
|
||||||
|
String indexName = indexNameProvider.indexName() + "-join";
|
||||||
|
operations.indexOps(RootEntity.class).createWithMapping();
|
||||||
|
|
||||||
|
RootEntity parentEntity = RootEntity.builder()
|
||||||
|
.withId(nextIdAsString())
|
||||||
|
.withParent(new RootEntity.Parent())
|
||||||
|
.build();
|
||||||
|
IndexQuery indexQuery = new IndexQueryBuilder().withId(parentEntity.id).withObject(parentEntity).build();
|
||||||
|
operations.index(indexQuery, IndexCoordinates.of(indexName));
|
||||||
|
|
||||||
|
RootEntity childEntity = RootEntity.builder()
|
||||||
|
.withId(nextIdAsString())
|
||||||
|
.withChild(new RootEntity.Child())
|
||||||
|
.withRelation(new JoinField<>("child", parentEntity.id))
|
||||||
|
.build();
|
||||||
|
indexQuery = new IndexQueryBuilder().withId(childEntity.id).withObject(childEntity).build();
|
||||||
|
operations.index(indexQuery, IndexCoordinates.of(indexName));
|
||||||
|
|
||||||
|
HasParentQuery childQuery = HasParentQuery.builder("parent").withQuery(operations.matchAllQuery()).build();
|
||||||
|
Query query = CriteriaQuery.builder(Criteria.where("parent").hasParent(childQuery)).build();
|
||||||
|
|
||||||
|
// When
|
||||||
|
SearchHits<RootEntity> hits = operations.search(query, RootEntity.class);
|
||||||
|
|
||||||
|
// Then
|
||||||
|
assertThat(hits.getTotalHits()).isEqualTo(1);
|
||||||
|
}
|
||||||
|
|
||||||
// region entities
|
// region entities
|
||||||
@Document(indexName = "#{@indexNameProvider.indexName()}")
|
@Document(indexName = "#{@indexNameProvider.indexName()}")
|
||||||
@Setting(shards = 1, replicas = 0, refreshInterval = "-1")
|
@Setting(shards = 1, replicas = 0, refreshInterval = "-1")
|
||||||
@ -4948,5 +5010,109 @@ public abstract class ElasticsearchIntegrationTests {
|
|||||||
this.indexedIndexName = indexedIndexName;
|
this.indexedIndexName = indexedIndexName;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Document(indexName = "#{@indexNameProvider.indexName()}-join")
|
||||||
|
private static class RootEntity {
|
||||||
|
@Id
|
||||||
|
private String id;
|
||||||
|
|
||||||
|
@Field(type = FieldType.Object)
|
||||||
|
private Child child;
|
||||||
|
|
||||||
|
@Field(type = FieldType.Object)
|
||||||
|
private Parent parent;
|
||||||
|
|
||||||
|
@JoinTypeRelations(relations = {
|
||||||
|
@JoinTypeRelation(parent = "parent", children = {"child"})
|
||||||
|
})
|
||||||
|
private JoinField<String> relation = new JoinField<>("parent");
|
||||||
|
|
||||||
|
private static final class Child {}
|
||||||
|
private static final class Parent {}
|
||||||
|
|
||||||
|
public static Builder builder() {
|
||||||
|
return new Builder();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setId(String id) {
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Child getChild() {
|
||||||
|
return child;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setChild(@Nullable Child child) {
|
||||||
|
this.child = child;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Parent getParent() {
|
||||||
|
return parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setParent(@Nullable Parent parent) {
|
||||||
|
this.parent = parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
public JoinField<String> getRelation() {
|
||||||
|
if (relation == null) {
|
||||||
|
relation = new JoinField<>("parent");
|
||||||
|
}
|
||||||
|
|
||||||
|
return relation;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRelation(JoinField<String> relation) {
|
||||||
|
this.relation = relation;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final class Builder {
|
||||||
|
@Nullable private String id;
|
||||||
|
|
||||||
|
@Nullable private Parent parent;
|
||||||
|
@Nullable private Child child;
|
||||||
|
private JoinField<String> relation = new JoinField<>("parent");
|
||||||
|
|
||||||
|
private Builder() {}
|
||||||
|
|
||||||
|
public Builder withId(@Nullable String id) {
|
||||||
|
this.id = id;
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder withParent(@Nullable Parent parent) {
|
||||||
|
this.parent = parent;
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder withChild(@Nullable Child child) {
|
||||||
|
this.child = child;
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder withRelation(JoinField<String> relation) {
|
||||||
|
this.relation = relation;
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public RootEntity build() {
|
||||||
|
RootEntity root = new RootEntity();
|
||||||
|
root.setId(id);
|
||||||
|
root.setParent(parent);
|
||||||
|
root.setChild(child);
|
||||||
|
root.setRelation(relation);
|
||||||
|
|
||||||
|
return root;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
// endregion
|
// endregion
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user