Skip to content

Support has_child and has_parent queries. #2889

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Apr 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,14 @@
package org.springframework.data.elasticsearch.client.elc;

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 co.elastic.clients.elasticsearch._types.FieldValue;
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.Query;
import co.elastic.clients.elasticsearch.core.search.InnerHits;
import co.elastic.clients.json.JsonData;

import java.util.ArrayList;
Expand All @@ -30,7 +32,12 @@

import org.springframework.data.elasticsearch.annotations.FieldType;
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.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.util.Assert;

Expand All @@ -42,7 +49,7 @@
* @author Ezequiel Antúnez Camacho
* @since 4.4
*/
class CriteriaQueryProcessor {
class CriteriaQueryProcessor extends AbstractQueryProcessor {

/**
* creates a query from the criteria
Expand Down Expand Up @@ -343,6 +350,34 @@ private static Query.Builder queryFor(Criteria.CriteriaEntry entry, Field field,
.value(value.toString()) //
.boost(boost)); //
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:
throw new CriteriaQueryException("Could not build query for " + entry);
}
Expand Down Expand Up @@ -397,4 +432,19 @@ public static String escape(String s) {
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()));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@
* @since 4.4
*/
@SuppressWarnings("ClassCanBeRecord")
class RequestConverter {
class RequestConverter extends AbstractQueryProcessor {

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

Expand Down Expand Up @@ -1755,31 +1755,7 @@ private void prepareNativeSearch(NativeQuery query, MultisearchBody.Builder buil
@Nullable
co.elastic.clients.elasticsearch._types.query_dsl.Query getQuery(@Nullable Query query,
@Nullable Class<?> 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;
return getEsQuery(query, (q) -> elasticsearchConverter.updateQuery(q, clazz));
}

private void addFilter(Query query, SearchRequest.Builder builder) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import co.elastic.clients.elasticsearch._types.*;
import co.elastic.clients.elasticsearch._types.mapping.FieldType;
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.core.search.BoundaryScanner;
import co.elastic.clients.elasticsearch.core.search.HighlighterEncoder;
Expand All @@ -41,6 +42,7 @@
import org.springframework.data.elasticsearch.core.RefreshPolicy;
import org.springframework.data.elasticsearch.core.document.Document;
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.IndicesOptions;
import org.springframework.data.elasticsearch.core.query.Order;
Expand Down Expand Up @@ -527,4 +529,24 @@ static Operator operator(@Nullable OperatorType operator) {
static Conflicts conflicts(@Nullable ConflictsType conflicts) {
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;
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -816,6 +816,32 @@ public Criteria contains(GeoJson<?> geoShape) {
filterCriteriaEntries.add(new CriteriaEntry(OperationKey.GEO_CONTAINS, geoShape));
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

// region helper functions
Expand Down Expand Up @@ -977,7 +1003,12 @@ public enum OperationKey { //
/**
* @since 5.1
*/
REGEXP;
REGEXP,
/**
* @since 5.3
*/
HAS_CHILD,
HAS_PARENT;

/**
* @return true if this key does not have an associated value
Expand Down
Loading