Add the filter parts of a CriteriaQuery to the query, not as post-filter.

Original Pull Request #2906
Closes #2857
This commit is contained in:
Peter-Josef Meisch 2024-05-04 09:46:41 +02:00 committed by GitHub
parent 1d89054d12
commit dc5bf5a606
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 77 additions and 59 deletions

View File

@ -70,9 +70,13 @@ class CriteriaFilterProcessor {
for (Criteria chainedCriteria : criteria.getCriteriaChain()) {
if (chainedCriteria.isOr()) {
BoolQuery.Builder boolQueryBuilder = QueryBuilders.bool();
queriesForEntries(chainedCriteria).forEach(boolQueryBuilder::should);
filterQueries.add(new Query(boolQueryBuilder.build()));
Collection<? extends Query> queriesForEntries = queriesForEntries(chainedCriteria);
if (!queriesForEntries.isEmpty()) {
BoolQuery.Builder boolQueryBuilder = QueryBuilders.bool();
queriesForEntries.forEach(boolQueryBuilder::should);
filterQueries.add(new Query(boolQueryBuilder.build()));
}
} else if (chainedCriteria.isNegating()) {
Assert.notNull(criteria.getField(), "criteria must have a field");

View File

@ -29,6 +29,7 @@ import co.elastic.clients.json.JsonData;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Objects;
import org.springframework.data.elasticsearch.annotations.FieldType;
import org.springframework.data.elasticsearch.core.query.Criteria;
@ -115,11 +116,18 @@ class CriteriaQueryProcessor extends AbstractQueryProcessor {
}
}
var filterQuery = CriteriaFilterProcessor.createQuery(criteria);
if (shouldQueries.isEmpty() && mustNotQueries.isEmpty() && mustQueries.isEmpty()) {
return null;
if (filterQuery.isEmpty()) {
return null;
}
// we need something to add the filter to
mustQueries.add(Query.of(qb -> qb.matchAll(m -> m)));
}
Query query = new Query.Builder().bool(boolQueryBuilder -> {
return new Query.Builder().bool(boolQueryBuilder -> {
if (!shouldQueries.isEmpty()) {
boolQueryBuilder.should(shouldQueries);
@ -133,10 +141,10 @@ class CriteriaQueryProcessor extends AbstractQueryProcessor {
boolQueryBuilder.must(mustQueries);
}
filterQuery.ifPresent(boolQueryBuilder::filter);
return boolQueryBuilder;
}).build();
return query;
}
@Nullable
@ -238,7 +246,7 @@ class CriteriaQueryProcessor extends AbstractQueryProcessor {
queryBuilder.queryString(queryStringQuery(fieldName, '*' + searchText, true, boost));
break;
case EXPRESSION:
queryBuilder.queryString(queryStringQuery(fieldName, value.toString(), boost));
queryBuilder.queryString(queryStringQuery(fieldName, Objects.requireNonNull(value).toString(), boost));
break;
case LESS:
queryBuilder //
@ -270,6 +278,7 @@ class CriteriaQueryProcessor extends AbstractQueryProcessor {
break;
case BETWEEN:
Object[] ranges = (Object[]) value;
Assert.notNull(value, "value for a between condition must not be null");
queryBuilder //
.range(rb -> {
rb.field(fieldName);
@ -293,10 +302,10 @@ class CriteriaQueryProcessor extends AbstractQueryProcessor {
.boost(boost)); //
break;
case MATCHES:
queryBuilder.match(matchQuery(fieldName, value.toString(), Operator.Or, boost));
queryBuilder.match(matchQuery(fieldName, Objects.requireNonNull(value).toString(), Operator.Or, boost));
break;
case MATCHES_ALL:
queryBuilder.match(matchQuery(fieldName, value.toString(), Operator.And, boost));
queryBuilder.match(matchQuery(fieldName, Objects.requireNonNull(value).toString(), Operator.And, boost));
break;
case IN:
@ -345,7 +354,7 @@ class CriteriaQueryProcessor extends AbstractQueryProcessor {
queryBuilder //
.regexp(rb -> rb //
.field(fieldName) //
.value(value.toString()) //
.value(Objects.requireNonNull(value).toString()) //
.boost(boost)); //
break;
case HAS_CHILD:

View File

@ -1210,7 +1210,7 @@ class RequestConverter extends AbstractQueryProcessor {
builder.routing(routing);
}
addFilter(query, builder);
addPostFilter(query, builder);
return builder.build();
}
@ -1758,22 +1758,18 @@ class RequestConverter extends AbstractQueryProcessor {
return getEsQuery(query, (q) -> elasticsearchConverter.updateQuery(q, clazz));
}
private void addFilter(Query query, SearchRequest.Builder builder) {
@SuppressWarnings("StatementWithEmptyBody")
private void addPostFilter(Query query, SearchRequest.Builder builder) {
if (query instanceof CriteriaQuery) {
CriteriaFilterProcessor.createQuery(((CriteriaQuery) query).getCriteria()).ifPresent(builder::postFilter);
} else // noinspection StatementWithEmptyBody
if (query instanceof StringQuery) {
// no filter for StringQuery
} else if (query instanceof NativeQuery nativeQuery) {
// we only need to handle NativeQuery here. filter from a CriteriaQuery are added into the query and not as post
// filter anymore, StringQuery do not have post filters
if (query instanceof NativeQuery nativeQuery) {
if (nativeQuery.getFilter() != null) {
builder.postFilter(nativeQuery.getFilter());
} else if (nativeQuery.getSpringDataQuery() != null) {
addFilter(nativeQuery.getSpringDataQuery(), builder);
addPostFilter(nativeQuery.getSpringDataQuery(), builder);
}
} else {
throw new IllegalArgumentException("unhandled Query implementation " + query.getClass().getName());
}
}

View File

@ -17,6 +17,11 @@ package org.springframework.data.elasticsearch.core.query;
import static org.assertj.core.api.Assertions.*;
import co.elastic.clients.elasticsearch._types.GeoHashPrecision;
import co.elastic.clients.elasticsearch._types.aggregations.Aggregation;
import java.util.List;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Order;
@ -26,6 +31,7 @@ import org.springframework.data.annotation.Id;
import org.springframework.data.elasticsearch.annotations.Document;
import org.springframework.data.elasticsearch.annotations.Field;
import org.springframework.data.elasticsearch.annotations.FieldType;
import org.springframework.data.elasticsearch.client.elc.ElasticsearchAggregation;
import org.springframework.data.elasticsearch.client.elc.NativeQuery;
import org.springframework.data.elasticsearch.core.ElasticsearchOperations;
import org.springframework.data.elasticsearch.core.geo.GeoPoint;
@ -98,6 +104,40 @@ public abstract class NativeQueryIntegrationTests {
assertThat(searchHits.getSearchHit(0).getId()).isEqualTo(entity1.getId());
}
@Test // #2857
@DisplayName("should apply CriteriaQuery with filter arguments in a NativeQuery to aggregations")
void shouldBeAbleToUseCriteriaQueryWithFilterArgumentsInANativeQueryToAggregations() {
var entity1 = new SampleEntity();
entity1.setId("60");
var location1 = new GeoPoint(60.0, 60.0);
entity1.setLocation(location1);
entity1.setText("60");
var entity2 = new SampleEntity();
entity2.setId("70");
entity2.setText("70");
var location70 = new GeoPoint(70.0, 70.0);
entity2.setLocation(location70);
operations.save(entity1, entity2);
var criteriaQuery = new CriteriaQuery(Criteria.where("location").within(location1, "10km"));
var nativeQuery = NativeQuery.builder()
.withQuery(criteriaQuery)
.withAggregation("geohashgrid", Aggregation.of(ab -> ab
.geohashGrid(ghg -> ghg
.field("location")
.precision(GeoHashPrecision.of(ghp -> ghp.distance("10000km"))))))
.build();
var searchHits = operations.search(nativeQuery, SampleEntity.class);
assertThat(searchHits.getTotalHits()).isEqualTo(1);
assertThat(searchHits.getSearchHit(0).getId()).isEqualTo(entity1.getId());
assertThat(searchHits.getAggregations()).isNotNull();
// noinspection unchecked
var aggregations = (List<ElasticsearchAggregation>) searchHits.getAggregations().aggregations();
assertThat(aggregations).hasSize(1);
}
@Test // #2391
@DisplayName("should be able to use StringQuery in a NativeQuery")
void shouldBeAbleToUseStringQueryInANativeQuery() {

View File

@ -79,11 +79,6 @@ abstract class QueryKeywordsIntegrationTests {
@Test
public void shouldSupportAND() {
// given
// when
// then
assertThat(repository.findByNameAndText("Sugar", "Cane sugar")).hasSize(2);
assertThat(repository.findByNameAndPrice("Sugar", 1.1f)).hasSize(1);
}
@ -91,11 +86,6 @@ abstract class QueryKeywordsIntegrationTests {
@Test
public void shouldSupportOR() {
// given
// when
// then
assertThat(repository.findByNameOrPrice("Sugar", 1.9f)).hasSize(4);
assertThat(repository.findByNameOrText("Salt", "Beet sugar")).hasSize(3);
}
@ -103,11 +93,6 @@ abstract class QueryKeywordsIntegrationTests {
@Test
public void shouldSupportTrueAndFalse() {
// given
// when
// then
assertThat(repository.findByAvailableTrue()).hasSize(3);
assertThat(repository.findByAvailableFalse()).hasSize(4);
}
@ -115,11 +100,6 @@ abstract class QueryKeywordsIntegrationTests {
@Test
public void shouldSupportInAndNotInAndNot() {
// given
// when
// then
assertThat(repository.findByPriceIn(Arrays.asList(1.2f, 1.1f))).hasSize(2);
assertThat(repository.findByPriceNotIn(Arrays.asList(1.2f, 1.1f))).hasSize(5);
assertThat(repository.findByPriceNot(1.2f)).hasSize(6);
@ -128,33 +108,18 @@ abstract class QueryKeywordsIntegrationTests {
@Test // DATAES-171
public void shouldWorkWithNotIn() {
// given
// when
// then
assertThat(repository.findByIdNotIn(Arrays.asList("2", "3"))).hasSize(5);
}
@Test
public void shouldSupportBetween() {
// given
// when
// then
assertThat(repository.findByPriceBetween(1.0f, 2.0f)).hasSize(4);
}
@Test
public void shouldSupportLessThanAndGreaterThan() {
// given
// when
// then
assertThat(repository.findByPriceLessThan(1.1f)).hasSize(1);
assertThat(repository.findByPriceLessThanEqual(1.1f)).hasSize(2);

View File

@ -16,7 +16,7 @@
package org.springframework.data.elasticsearch.utils;
/**
* Class providing an index name with a prefix and an index number
* Class providing an index name with a prefix, an index number and a random 6-digit number.
*
* @author Peter-Josef Meisch
*/
@ -35,7 +35,7 @@ public class IndexNameProvider {
}
public void increment() {
indexName = prefix + '-' + ++idx;
indexName = prefix + '-' + ++idx + '-' + sixDigits();
}
public String indexName() {
@ -48,4 +48,8 @@ public class IndexNameProvider {
public String getPrefix() {
return prefix;
}
private String sixDigits() {
return String.valueOf((int) (100000 + Math.random() * 900000));
}
}