From af594c4c3864626c573dd362bfff9dc37345c15b Mon Sep 17 00:00:00 2001 From: Peter-Josef Meisch Date: Tue, 4 Aug 2020 21:23:22 +0200 Subject: [PATCH] DATAES-706 - CriteriaQueryProcessor must handle nested Criteria definitions. --- src/main/asciidoc/preface.adoc | 6 +- .../reference/elasticsearch-operations.adoc | 119 ++- .../core/CriteriaFilterProcessor.java | 74 +- .../core/CriteriaQueryProcessor.java | 124 +-- .../elasticsearch/core/RequestFactory.java | 4 +- .../MappingElasticsearchConverter.java | 55 +- .../elasticsearch/core/query/Criteria.java | 814 ++++++++++++------ .../elasticsearch/core/query/SimpleField.java | 13 +- .../parser/ElasticsearchQueryCreator.java | 7 +- .../core/CriteriaQueryMappingTests.java | 64 +- .../core/CriteriaQueryProcessorTests.java | 341 ++++++++ .../core/query/CriteriaQueryTests.java | 134 ++- ...tiveElasticsearchStringQueryUnitTests.java | 8 +- 13 files changed, 1279 insertions(+), 484 deletions(-) create mode 100644 src/test/java/org/springframework/data/elasticsearch/core/CriteriaQueryProcessorTests.java diff --git a/src/main/asciidoc/preface.adoc b/src/main/asciidoc/preface.adoc index 5168c08faf..48740c4beb 100644 --- a/src/main/asciidoc/preface.adoc +++ b/src/main/asciidoc/preface.adoc @@ -1,7 +1,8 @@ [[preface]] = Preface -The Spring Data Elasticsearch project applies core Spring concepts to the development of solutions using the Elasticsearch Search Engine. It provides: +The Spring Data Elasticsearch project applies core Spring concepts to the development of solutions using the Elasticsearch Search Engine. +It provides: * _Templates_ as a high-level abstraction for storing, searching, sorting documents and building aggregations. * _Repositories_ which for example enable the user to express queries by defining interfaces having customized method names (for basic information about repositories see <>). @@ -29,12 +30,13 @@ Requires an installation of https://www.elastic.co/products/elasticsearch[Elasti === Versions The following table shows the Elasticsearch versions that are used by Spring Data release trains and version of Spring Data Elasticsearch included in that, as well as the Spring Boot versions referring to that particular Spring Data release train: + [cols="^,^,^,^",options="header"] |=== | Spring Data Release Train |Spring Data Elasticsearch |Elasticsearch | Spring Boot | 2020.0.0footnote:cdv[Currently in development] |4.1.xfootnote:cdv[]|7.8.1 |2.3.xfootnote:cdv[] | Neumann | 4.0.x | 7.6.2 |2.3.x -| Moore | 3.2.x |6.8.12 | 2.2.x +| Moore | 3.2.x |6.8.10 | 2.2.x | Lovelace | 3.1.x | 6.2.2 |2.1.x | Kayfootnote:oom[Out of maintenance] | 3.0.xfootnote:oom[] | 5.5.0 | 2.0.xfootnote:oom[] | Ingallsfootnote:oom[] | 2.1.xfootnote:oom[] | 2.4.0 | 1.5.xfootnote:oom[] diff --git a/src/main/asciidoc/reference/elasticsearch-operations.adoc b/src/main/asciidoc/reference/elasticsearch-operations.adoc index 69850323fe..49580288b3 100644 --- a/src/main/asciidoc/reference/elasticsearch-operations.adoc +++ b/src/main/asciidoc/reference/elasticsearch-operations.adoc @@ -45,7 +45,8 @@ public class TransportClientConfig extends ElasticsearchConfigurationSupport { } } ---- -<1> Setting up the <>. Deprecated as of version 4.0. +<1> Setting up the <>. +Deprecated as of version 4.0. <2> Creating the `ElasticsearchTemplate` bean, offering both names, _elasticsearchOperations_ and _elasticsearchTemplate_. ==== @@ -75,7 +76,9 @@ public class RestClientConfig extends AbstractElasticsearchConfiguration { [[elasticsearch.operations.usage]] == Usage examples -As both `ElasticsearchTemplate` and `ElasticsearchRestTemplate` implement the `ElasticsearchOperations` interface, the code to use them is not different. The example shows how to use an injected `ElasticsearchOperations` instance in a Spring REST controller. The decision, if this is using the `TransportClient` or the `RestClient` is made by providing the corresponding Bean with one of the configurations shown above. +As both `ElasticsearchTemplate` and `ElasticsearchRestTemplate` implement the `ElasticsearchOperations` interface, the code to use them is not different. +The example shows how to use an injected `ElasticsearchOperations` instance in a Spring REST controller. +The decision, if this is using the `TransportClient` or the `RestClient` is made by providing the corresponding Bean with one of the configurations shown above. .ElasticsearchOperations usage ==== @@ -123,9 +126,12 @@ include::reactive-elasticsearch-operations.adoc[leveloffset=+1] [[elasticsearch.operations.searchresulttypes]] == Search Result Types -When a document is retrieved with the methods of the `DocumentOperations` interface, just the found entity will be returned. When searching with the methods of the `SearchOperations` interface, additional information is available for each entity, for example the _score_ or the _sortValues_ of the found entity. +When a document is retrieved with the methods of the `DocumentOperations` interface, just the found entity will be returned. +When searching with the methods of the `SearchOperations` interface, additional information is available for each entity, for example the _score_ or the _sortValues_ of the found entity. -In order to return this information, each entity is wrapped in a `SearchHit` object that contains this entity-specific additional information. These `SearchHit` objects themselves are returned within a `SearchHits` object which additionally contains informations about the whole search like the _maxScore_ or requested aggregations. The following classes and interfaces are now available: +In order to return this information, each entity is wrapped in a `SearchHit` object that contains this entity-specific additional information. +These `SearchHit` objects themselves are returned within a `SearchHits` object which additionally contains informations about the whole search like the _maxScore_ or requested aggregations. +The following classes and interfaces are now available: .SearchHit Contains the following information: @@ -155,3 +161,108 @@ Returned by the low level scroll API functions in `ElasticsearchRestTemplate`, i .SearchHitsIterator An Iterator returned by the streaming functions of the `SearchOperations` interface. +== Queries + +Almost all of the methods defined in the `SearchOperations` and `ReactiveSearchOperations` interface take a `Query` parameter that defines the query to execute for searching. `Query` is an interface and Spring Data Elasticsearch provides three implementations: `CriteriaQuery`, `StringQuery` and `NativeSearchQuery`. + +=== CriteriaQuery + +`CriteriaQuery` based queries allow the creation of queries to search for data without knowing the syntax or basics of Elasticsearch queries. They allow the user to build queries by simply chaining and combining `Criteria` objects that specifiy the criteria the searched documents must fulfill. + +NOTE: when talking about AND or OR when combining criteria keep in mind, that in Elasticsearch AND are converted to a **must** condition and OR to a **should** + +`Criteria` and their usage are best explained by example +(let's assume we have a `Book` entity with a `price` property): + +.Get books with a given price +==== +[source,java] +---- +Criteria criteria = new Criteria("price").is(42.0); +Query query = new CriteriaQuery(criteria); +---- +==== + +Conditions for the same field can be chained, they will be combined with a logical AND: + +.Get books with a given price +==== +[source,java] +---- +Criteria criteria = new Criteria("price").greaterThan(42.0).lessThan(34.0L); +Query query = new CriteriaQuery(criteria); +---- +==== + +When chaining `Criteria`, by default a AND logic is used: + +.Get all persons with first name _James_ and last name _Miller_: +==== +[source,java] +---- +Criteria criteria = new Criteria("lastname").is("Miller") <1> + .and("firstname").is("James") <2> +Query query = new CriteriaQuery(criteria); +---- +<1> the first `Criteria` +<2> the and() creates a new `Criteria` and chaines it to the first one. +==== + +If you want to create nested queries, you need to use subqueries for this. Let's assume we want to find all persons with a last name of _Miller_ and a first name of either _Jack_ or _John_: + +.Nested subqueries +==== +[source,java] +---- +Criteria miller = new Criteria("lastName").is("Miller") <.> + .subCriteria( <.> + new Criteria().or("firstName").is("John") <.> + .or("firstName").is("Jack") <.> + ); +Query query = new CriteriaQuery(criteria); +---- +<.> create a first `Criteria` for the last name +<.> this is combined with AND to a subCriteria +<.> This sub Criteria is an OR combination for the first name _John_ +<.> and the first name Jack +==== + +Please refer to the API documentation of the `Criteria` class for a complete overview of the different available operations. + +=== StringQuery + +This class takes an Elasticsearch query as JSON String. +The following code shows a query that searches for persons having the first name "Jack": + +==== +[source,java] +---- + +Query query = new SearchQuery("{ \"match\": { \"firstname\": { \"query\": \"Jack\" } } } "); +SearchHits searchHits = operations.search(query, Person.class); + +---- +==== + +Using `StringQuery` may be appropriate if you already have an Elasticsearch query to use. + +=== NativeSearchQuery + +`NativeSearchQuery` is the class to use when you have a complex query, or a query that cannot be expressed by using the `Criteria` API, for example when building queries and using aggregates. +It allows to use all the different `QueryBuilder` implementations from the Elasticsearch library therefore named "native". + +The following code shows how to search for persons with a given firstname and for the found documents have a terms aggregation that counts the number of occurences of the lastnames for these persons: + +==== +[source,java] +---- +Query query = new NativeSearchQueryBuilder() + .addAggregation(terms("lastnames").field("lastname").size(10)) // + .withQuery(QueryBuilders.matchQuery("firstname", firstName)) + .build(); + +SearchHits searchHits = operations.search(query, Person.class); +---- +==== + + diff --git a/src/main/java/org/springframework/data/elasticsearch/core/CriteriaFilterProcessor.java b/src/main/java/org/springframework/data/elasticsearch/core/CriteriaFilterProcessor.java index 911a38b393..3fd3a6fc95 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/CriteriaFilterProcessor.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/CriteriaFilterProcessor.java @@ -17,9 +17,11 @@ import static org.springframework.data.elasticsearch.core.query.Criteria.*; +import java.util.ArrayList; import java.util.Iterator; import java.util.LinkedList; import java.util.List; +import java.util.stream.Collectors; import org.elasticsearch.common.geo.GeoDistance; import org.elasticsearch.index.query.BoolQueryBuilder; @@ -47,66 +49,56 @@ */ class CriteriaFilterProcessor { - QueryBuilder createFilterFromCriteria(Criteria criteria) { - List fbList = new LinkedList<>(); - QueryBuilder filter = null; + @Nullable + QueryBuilder createFilter(Criteria criteria) { + + List filterBuilders = new ArrayList<>(); for (Criteria chainedCriteria : criteria.getCriteriaChain()) { - QueryBuilder fb = null; + if (chainedCriteria.isOr()) { - fb = QueryBuilders.boolQuery(); - for (QueryBuilder f : createFilterFragmentForCriteria(chainedCriteria)) { - ((BoolQueryBuilder) fb).should(f); - } - fbList.add(fb); + BoolQueryBuilder boolQuery = QueryBuilders.boolQuery(); + queriesForEntries(chainedCriteria).forEach(boolQuery::should); + filterBuilders.add(boolQuery); } else if (chainedCriteria.isNegating()) { List negationFilters = buildNegationFilter(criteria.getField().getName(), criteria.getFilterCriteriaEntries().iterator()); - if (!negationFilters.isEmpty()) { - fbList.addAll(negationFilters); - } + filterBuilders.addAll(negationFilters); } else { - fbList.addAll(createFilterFragmentForCriteria(chainedCriteria)); + filterBuilders.addAll(queriesForEntries(chainedCriteria)); } } - if (!fbList.isEmpty()) { - if (fbList.size() == 1) { - filter = fbList.get(0); + QueryBuilder filter = null; + + if (!filterBuilders.isEmpty()) { + + if (filterBuilders.size() == 1) { + filter = filterBuilders.get(0); } else { - filter = QueryBuilders.boolQuery(); - for (QueryBuilder f : fbList) { - ((BoolQueryBuilder) filter).must(f); - } + BoolQueryBuilder boolQuery = QueryBuilders.boolQuery(); + filterBuilders.forEach(boolQuery::must); + filter = boolQuery; } } + return filter; } - private List createFilterFragmentForCriteria(Criteria chainedCriteria) { - Iterator it = chainedCriteria.getFilterCriteriaEntries().iterator(); - List filterList = new LinkedList<>(); + private List queriesForEntries(Criteria criteria) { - String fieldName = chainedCriteria.getField().getName(); + Assert.notNull(criteria.getField(), "criteria must have a field"); + String fieldName = criteria.getField().getName(); Assert.notNull(fieldName, "Unknown field"); - QueryBuilder filter = null; - while (it.hasNext()) { - Criteria.CriteriaEntry entry = it.next(); - filter = processCriteriaEntry(entry.getKey(), entry.getValue(), fieldName); - filterList.add(filter); - } - - return filterList; + return criteria.getFilterCriteriaEntries().stream() + .map(entry -> queryFor(entry.getKey(), entry.getValue(), fieldName)).collect(Collectors.toList()); } @Nullable - private QueryBuilder processCriteriaEntry(OperationKey key, Object value, String fieldName) { + private QueryBuilder queryFor(OperationKey key, Object value, String fieldName) { - if (value == null) { - return null; - } QueryBuilder filter = null; switch (key) { @@ -169,8 +161,7 @@ private QueryBuilder processCriteriaEntry(OperationKey key, Object value, String // 2x text twoParameterBBox((GeoBoundingBoxQueryBuilder) filter, valArray); } else { - // error - Assert.isTrue(false, + throw new IllegalArgumentException( "Geo distance filter takes a 1-elements array(GeoBox) or 2-elements array(GeoPoints or Strings(format lat,lon or geohash))."); } break; @@ -208,8 +199,7 @@ private void oneParameterBBox(GeoBoundingBoxQueryBuilder filter, Object value) { GeoBox geoBBox; if (value instanceof Box) { - Box sdbox = (Box) value; - geoBBox = GeoBox.fromBox(sdbox); + geoBBox = GeoBox.fromBox((Box) value); } else { geoBBox = (GeoBox) value; } @@ -218,7 +208,7 @@ private void oneParameterBBox(GeoBoundingBoxQueryBuilder filter, Object value) { geoBBox.getBottomRight().getLon()); } - private static boolean isType(Object[] array, Class clazz) { + private static boolean isType(Object[] array, Class clazz) { for (Object o : array) { if (!clazz.isInstance(o)) { return false; @@ -247,7 +237,7 @@ private List buildNegationFilter(String fieldName, Iterator shouldQueryBuilderList = new LinkedList<>(); - List mustNotQueryBuilderList = new LinkedList<>(); - List mustQueryBuilderList = new LinkedList<>(); - - ListIterator chainIterator = criteria.getCriteriaChain().listIterator(); + List shouldQueryBuilders = new ArrayList<>(); + List mustNotQueryBuilders = new ArrayList<>(); + List mustQueryBuilders = new ArrayList<>(); QueryBuilder firstQuery = null; boolean negateFirstQuery = false; - while (chainIterator.hasNext()) { - Criteria chainedCriteria = chainIterator.next(); - QueryBuilder queryFragmentForCriteria = createQueryFragmentForCriteria(chainedCriteria); - if (queryFragmentForCriteria != null) { + for (Criteria chainedCriteria : criteria.getCriteriaChain()) { + QueryBuilder queryFragment = queryForEntries(chainedCriteria); + + if (queryFragment != null) { + if (firstQuery == null) { - firstQuery = queryFragmentForCriteria; + firstQuery = queryFragment; negateFirstQuery = chainedCriteria.isNegating(); continue; } + if (chainedCriteria.isOr()) { - shouldQueryBuilderList.add(queryFragmentForCriteria); + shouldQueryBuilders.add(queryFragment); } else if (chainedCriteria.isNegating()) { - mustNotQueryBuilderList.add(queryFragmentForCriteria); + mustNotQueryBuilders.add(queryFragment); + } else { + mustQueryBuilders.add(queryFragment); + } + } + } + + for (Criteria subCriteria : criteria.getSubCriteria()) { + + QueryBuilder subQuery = createQuery(subCriteria); + + if (subQuery != null) { + if (criteria.isOr()) { + shouldQueryBuilders.add(subQuery); + } else if (criteria.isNegating()) { + mustNotQueryBuilders.add(subQuery); } else { - mustQueryBuilderList.add(queryFragmentForCriteria); + mustQueryBuilders.add(subQuery); } } } if (firstQuery != null) { - if (!shouldQueryBuilderList.isEmpty() && mustNotQueryBuilderList.isEmpty() && mustQueryBuilderList.isEmpty()) { - shouldQueryBuilderList.add(0, firstQuery); + + if (!shouldQueryBuilders.isEmpty() && mustNotQueryBuilders.isEmpty() && mustQueryBuilders.isEmpty()) { + shouldQueryBuilders.add(0, firstQuery); } else { + if (negateFirstQuery) { - mustNotQueryBuilderList.add(0, firstQuery); + mustNotQueryBuilders.add(0, firstQuery); } else { - mustQueryBuilderList.add(0, firstQuery); + mustQueryBuilders.add(0, firstQuery); } } } BoolQueryBuilder query = null; - if (!shouldQueryBuilderList.isEmpty() || !mustNotQueryBuilderList.isEmpty() || !mustQueryBuilderList.isEmpty()) { + if (!shouldQueryBuilders.isEmpty() || !mustNotQueryBuilders.isEmpty() || !mustQueryBuilders.isEmpty()) { query = boolQuery(); - for (QueryBuilder qb : shouldQueryBuilderList) { + for (QueryBuilder qb : shouldQueryBuilders) { query.should(qb); } - for (QueryBuilder qb : mustNotQueryBuilderList) { + for (QueryBuilder qb : mustNotQueryBuilders) { query.mustNot(qb); } - for (QueryBuilder qb : mustQueryBuilderList) { + for (QueryBuilder qb : mustQueryBuilders) { query.must(qb); } } @@ -111,46 +126,41 @@ QueryBuilder createQueryFromCriteria(Criteria criteria) { } @Nullable - private QueryBuilder createQueryFragmentForCriteria(Criteria chainedCriteria) { - if (chainedCriteria.getQueryCriteriaEntries().isEmpty()) + private QueryBuilder queryForEntries(Criteria criteria) { + + if (criteria.getField() == null || criteria.getQueryCriteriaEntries().isEmpty()) return null; - Iterator it = chainedCriteria.getQueryCriteriaEntries().iterator(); - boolean singeEntryCriteria = (chainedCriteria.getQueryCriteriaEntries().size() == 1); + String fieldName = criteria.getField().getName(); - String fieldName = chainedCriteria.getField().getName(); Assert.notNull(fieldName, "Unknown field"); - QueryBuilder query = null; - if (singeEntryCriteria) { - Criteria.CriteriaEntry entry = it.next(); - query = processCriteriaEntry(entry, fieldName); + Iterator it = criteria.getQueryCriteriaEntries().iterator(); + QueryBuilder query; + + if (criteria.getQueryCriteriaEntries().size() == 1) { + query = queryFor(it.next(), fieldName); } else { query = boolQuery(); while (it.hasNext()) { Criteria.CriteriaEntry entry = it.next(); - ((BoolQueryBuilder) query).must(processCriteriaEntry(entry, fieldName)); + ((BoolQueryBuilder) query).must(queryFor(entry, fieldName)); } } - addBoost(query, chainedCriteria.getBoost()); + addBoost(query, criteria.getBoost()); return query; } @Nullable - private QueryBuilder processCriteriaEntry(Criteria.CriteriaEntry entry, String fieldName) { + private QueryBuilder queryFor(Criteria.CriteriaEntry entry, String fieldName) { OperationKey key = entry.getKey(); - Object value = entry.getValue(); - if (value == null) { - - if (key == OperationKey.EXISTS) { - return existsQuery(fieldName); - } else { - return null; - } + if (key == OperationKey.EXISTS) { + return existsQuery(fieldName); } + Object value = entry.getValue(); String searchText = QueryParserUtil.escape(value.toString()); QueryBuilder query = null; @@ -190,11 +200,23 @@ private QueryBuilder processCriteriaEntry(Criteria.CriteriaEntry entry, String f case FUZZY: query = fuzzyQuery(fieldName, searchText); break; + case MATCHES: + query = matchQuery(fieldName, value).operator(org.elasticsearch.index.query.Operator.OR); + break; + case MATCHES_ALL: + query = matchQuery(fieldName, value).operator(org.elasticsearch.index.query.Operator.AND); + break; case IN: - query = boolQuery().must(termsQuery(fieldName, toStringList((Iterable) value))); + if (value instanceof Iterable) { + Iterable iterable = (Iterable) value; + query = boolQuery().must(termsQuery(fieldName, toStringList(iterable))); + } break; case NOT_IN: - query = boolQuery().mustNot(termsQuery(fieldName, toStringList((Iterable) value))); + if (value instanceof Iterable) { + Iterable iterable = (Iterable) value; + query = boolQuery().mustNot(termsQuery(fieldName, toStringList(iterable))); + } break; } return query; @@ -203,15 +225,17 @@ private QueryBuilder processCriteriaEntry(Criteria.CriteriaEntry entry, String f private static List toStringList(Iterable iterable) { List list = new ArrayList<>(); for (Object item : iterable) { - list.add(StringUtils.toString(item)); + list.add(item != null ? item.toString() : null); } return list; } - private void addBoost(QueryBuilder query, float boost) { - if (Float.isNaN(boost)) { + private void addBoost(@Nullable QueryBuilder query, float boost) { + + if (query == null || Float.isNaN(boost)) { return; } + query.boost(boost); } } diff --git a/src/main/java/org/springframework/data/elasticsearch/core/RequestFactory.java b/src/main/java/org/springframework/data/elasticsearch/core/RequestFactory.java index 79505b54bb..67e08c49ea 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/RequestFactory.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/RequestFactory.java @@ -1513,7 +1513,7 @@ private QueryBuilder getQuery(Query query) { elasticsearchQuery = searchQuery.getQuery(); } else if (query instanceof CriteriaQuery) { CriteriaQuery criteriaQuery = (CriteriaQuery) query; - elasticsearchQuery = new CriteriaQueryProcessor().createQueryFromCriteria(criteriaQuery.getCriteria()); + elasticsearchQuery = new CriteriaQueryProcessor().createQuery(criteriaQuery.getCriteria()); } else if (query instanceof StringQuery) { StringQuery stringQuery = (StringQuery) query; elasticsearchQuery = wrapperQuery(stringQuery.getSource()); @@ -1533,7 +1533,7 @@ private QueryBuilder getFilter(Query query) { elasticsearchFilter = searchQuery.getFilter(); } else if (query instanceof CriteriaQuery) { CriteriaQuery criteriaQuery = (CriteriaQuery) query; - elasticsearchFilter = new CriteriaFilterProcessor().createFilterFromCriteria(criteriaQuery.getCriteria()); + elasticsearchFilter = new CriteriaFilterProcessor().createFilter(criteriaQuery.getCriteria()); } else if (query instanceof StringQuery) { elasticsearchFilter = null; } else { diff --git a/src/main/java/org/springframework/data/elasticsearch/core/convert/MappingElasticsearchConverter.java b/src/main/java/org/springframework/data/elasticsearch/core/convert/MappingElasticsearchConverter.java index 910092a025..deda4c3fe2 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/convert/MappingElasticsearchConverter.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/convert/MappingElasticsearchConverter.java @@ -48,6 +48,7 @@ import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentEntity; import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentProperty; import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentPropertyConverter; +import org.springframework.data.elasticsearch.core.query.Criteria; import org.springframework.data.elasticsearch.core.query.CriteriaQuery; import org.springframework.data.elasticsearch.core.query.SeqNoPrimaryTerm; import org.springframework.data.mapping.PersistentPropertyAccessor; @@ -761,29 +762,39 @@ public void updateCriteriaQuery(CriteriaQuery criteriaQuery, Class domainClas ElasticsearchPersistentEntity persistentEntity = mappingContext.getPersistentEntity(domainClass); if (persistentEntity != null) { - criteriaQuery.getCriteria().getCriteriaChain().forEach(criteria -> { - String name = criteria.getField().getName(); - ElasticsearchPersistentProperty property = persistentEntity.getPersistentProperty(name); - - if (property != null && property.getName().equals(name)) { - criteria.getField().setName(property.getFieldName()); - - if (property.hasPropertyConverter()) { - ElasticsearchPersistentPropertyConverter propertyConverter = property.getPropertyConverter(); - criteria.getQueryCriteriaEntries().forEach(criteriaEntry -> { - Object value = criteriaEntry.getValue(); - if (value.getClass().isArray()) { - Object[] objects = (Object[]) value; - for (int i = 0; i < objects.length; i++) { - objects[i] = propertyConverter.write(objects[i]); - } - } else { - criteriaEntry.setValue(propertyConverter.write(value)); - } - }); + for (Criteria chainedCriteria : criteriaQuery.getCriteria().getCriteriaChain()) { + updateCriteria(chainedCriteria, persistentEntity); + } + } + } + + private void updateCriteria(Criteria criteria, ElasticsearchPersistentEntity persistentEntity) { + String name = criteria.getField().getName(); + ElasticsearchPersistentProperty property = persistentEntity.getPersistentProperty(name); + + if (property != null && property.getName().equals(name)) { + criteria.getField().setName(property.getFieldName()); + + if (property.hasPropertyConverter()) { + ElasticsearchPersistentPropertyConverter propertyConverter = property.getPropertyConverter(); + criteria.getQueryCriteriaEntries().forEach(criteriaEntry -> { + Object value = criteriaEntry.getValue(); + if (value.getClass().isArray()) { + Object[] objects = (Object[]) value; + for (int i = 0; i < objects.length; i++) { + objects[i] = propertyConverter.write(objects[i]); + } + } else { + criteriaEntry.setValue(propertyConverter.write(value)); } - } - }); + }); + } + } + + for (Criteria subCriteria : criteria.getSubCriteria()) { + for (Criteria chainedCriteria : subCriteria.getCriteriaChain()) { + updateCriteria(chainedCriteria, persistentEntity); + } } } // endregion diff --git a/src/main/java/org/springframework/data/elasticsearch/core/query/Criteria.java b/src/main/java/org/springframework/data/elasticsearch/core/query/Criteria.java index 0a2ed5a868..6b7d0fdb35 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/query/Criteria.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/query/Criteria.java @@ -15,12 +15,13 @@ */ package org.springframework.data.elasticsearch.core.query; -import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.LinkedHashSet; +import java.util.LinkedList; import java.util.List; +import java.util.Objects; import java.util.Set; import org.springframework.dao.InvalidDataAccessApiUsageException; @@ -31,12 +32,19 @@ import org.springframework.data.geo.Point; import org.springframework.lang.Nullable; import org.springframework.util.Assert; -import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; /** * Criteria is the central class when constructing queries. It follows more or less a fluent API style, which allows to - * easily chain together multiple criteria. + * easily chain together multiple criteria.
+ *
+ * A Criteria references a field and has {@link CriteriaEntry} sets for query and filter context. When building the + * query, the entries from the criteriaentries are combined in a bool must query (if more than one.
+ *
+ * A Criteria also has a {@link CriteriaChain} which is used to build a collection of Criteria with the fluent API. The + * value of {@link #isAnd()} and {@link #isOr()} describes whether the queries built from the criteria chain should be + * put in a must (and) or a should (or) clause when a query is built from it, it is not used to build some relationship + * between the elements of the criteria chain. * * @author Rizwan Idrees * @author Mohsin Husen @@ -45,564 +53,800 @@ */ public class Criteria { - @Override - public String toString() { - return "Criteria{" + "field=" + field.getName() + ", boost=" + boost + ", negating=" + negating + ", queryCriteria=" - + ObjectUtils.nullSafeToString(queryCriteria) + ", filterCriteria=" - + ObjectUtils.nullSafeToString(filterCriteria) + '}'; - } - - public static final String WILDCARD = "*"; - public static final String CRITERIA_VALUE_SEPERATOR = " "; - - private static final String OR_OPERATOR = " OR "; - private static final String AND_OPERATOR = " AND "; + public static final String CRITERIA_VALUE_SEPARATOR = " "; + /** @deprecated since 4.1, use {@link #CRITERIA_VALUE_SEPARATOR} */ + public static final String CRITERIA_VALUE_SEPERATOR = CRITERIA_VALUE_SEPARATOR; private @Nullable Field field; private float boost = Float.NaN; private boolean negating = false; - private List criteriaChain = new ArrayList<>(1); + private final CriteriaChain criteriaChain = new CriteriaChain(); + private final Set queryCriteriaEntries = new LinkedHashSet<>(); + private final Set filterCriteriaEntries = new LinkedHashSet<>(); + private final Set subCriteria = new LinkedHashSet<>(); + + // region criteria creation - private Set queryCriteria = new LinkedHashSet<>(); + /** + * @return factory method to create an and-Criteria that is not bound to a field + * @since 4.1 + */ + public static Criteria and() { + return new Criteria(); + } - private Set filterCriteria = new LinkedHashSet<>(); + /** + * @return factory method to create an or-Criteria that is not bound to a field + * @since 4.1 + */ + public static Criteria or() { + return new OrCriteria(); + } public Criteria() {} /** * Creates a new Criteria with provided field name * - * @param fieldname + * @param fieldName the field name */ - public Criteria(String fieldname) { - this(new SimpleField(fieldname)); + public Criteria(String fieldName) { + this(new SimpleField(fieldName)); } /** * Creates a new Criteria for the given field * - * @param field + * @param field field to create the Criteria for */ public Criteria(Field field) { Assert.notNull(field, "Field for criteria must not be null"); Assert.hasText(field.getName(), "Field.name for criteria must not be null/empty"); - this.criteriaChain.add(this); this.field = field; + this.criteriaChain.add(this); } - protected Criteria(List criteriaChain, String fieldname) { - this(criteriaChain, new SimpleField(fieldname)); + /** + * Creates a Criteria for the given field, sets it's criteriaChain to the given value and adds itself to the end of + * the chain. + * + * @param criteriaChain the chain to add to + * @param fieldName field to create the Criteria for + */ + protected Criteria(List criteriaChain, String fieldName) { + this(criteriaChain, new SimpleField(fieldName)); } + /** + * Creates a Criteria for the given field, sets it's criteriaChain to the given value and adds itself to the end of + * the chain. + * + * @param criteriaChain the chain to add to + * @param field field to create the Criteria for + */ protected Criteria(List criteriaChain, Field field) { Assert.notNull(criteriaChain, "CriteriaChain must not be null"); Assert.notNull(field, "Field for criteria must not be null"); Assert.hasText(field.getName(), "Field.name for criteria must not be null/empty"); + this.field = field; this.criteriaChain.addAll(criteriaChain); this.criteriaChain.add(this); - this.field = field; } /** * Static factory method to create a new Criteria for field with given name * - * @param field - * @return + * @param fieldName field to create the Criteria for */ - public static Criteria where(String field) { - return where(new SimpleField(field)); + public static Criteria where(String fieldName) { + return new Criteria(fieldName); } /** * Static factory method to create a new Criteria for provided field * - * @param field - * @return + * @param field field to create the Criteria for */ public static Criteria where(Field field) { return new Criteria(field); } + // endregion + + // region criteria attributes + /** + * @return the Field targeted by this Criteria + */ + @Nullable + public Field getField() { + return this.field; + } + + public Set getQueryCriteriaEntries() { + return Collections.unmodifiableSet(this.queryCriteriaEntries); + } + + public Set getFilterCriteriaEntries() { + return Collections.unmodifiableSet(this.filterCriteriaEntries); + } + + /** + * Conjunction to be used with this criteria (AND | OR) + * + * @deprecated since 4.1, use {@link #getOperator()} + */ + @Deprecated + public String getConjunctionOperator() { + return Operator.AND.name(); + } + + public Operator getOperator() { + return Operator.AND; + } + + public List getCriteriaChain() { + return Collections.unmodifiableList(this.criteriaChain); + } + + /** + * Sets the negating flag + * + * @return this object + */ + public Criteria not() { + this.negating = true; + return this; + } + + public boolean isNegating() { + return this.negating; + } + + /** + * Sets the boost factor. + * + * @param boost boost factor + * @return this object + */ + public Criteria boost(float boost) { + + Assert.isTrue(boost >= 0, "boost must not be negative"); + + this.boost = boost; + return this; + } + + public float getBoost() { + return this.boost; + } + + public boolean isAnd() { + return getOperator() == Operator.AND; + } + public boolean isOr() { + return getOperator() == Operator.OR; + } + + /** + * @return the set ob subCriteria + * @since 4.1 + */ + public Set getSubCriteria() { + return subCriteria; + } + + // endregion + + // region criteria chaining /** - * Chain using {@code AND} + * Chain a new and-Criteria * - * @param field - * @return + * @param field the field for the new Criteria + * @return the new chained Criteria */ public Criteria and(Field field) { - return new Criteria(this.criteriaChain, field); + return new Criteria(criteriaChain, field); } /** - * Chain using {@code AND} + * Chain a new and- Criteria * - * @param fieldName - * @return + * @param fieldName the field for the new Criteria + * @return the new chained Criteria */ public Criteria and(String fieldName) { - return new Criteria(this.criteriaChain, fieldName); + return new Criteria(criteriaChain, fieldName); } /** - * Chain using {@code AND} + * Chain a Criteria to this object. * - * @param criteria - * @return + * @param criteria the Criteria to add + * @return this object */ public Criteria and(Criteria criteria) { + + Assert.notNull(criteria, "Cannot chain 'null' criteria."); + this.criteriaChain.add(criteria); return this; } /** - * Chain using {@code AND} + * Chain an array of Criteria to this object. * - * @param criterias - * @return + * @param criterias the Criteria to add + * @return this object */ public Criteria and(Criteria... criterias) { + + Assert.notNull(criterias, "Cannot chain 'null' criterias."); + this.criteriaChain.addAll(Arrays.asList(criterias)); return this; } /** - * Chain using {@code OR} + * Chain a new or-Criteria * - * @param field - * @return + * @param field the field for the new Criteria + * @return the new chained Criteria */ public Criteria or(Field field) { return new OrCriteria(this.criteriaChain, field); } /** - * Chain using {@code OR} + * Chain a new or-Criteria + * + * @param fieldName the field for the new Criteria + * @return the new chained Criteria + */ + public Criteria or(String fieldName) { + return or(new SimpleField(fieldName)); + } + + /** + * Chain a new or-Criteria. The new Criteria uses the {@link #getField()}, {@link #getQueryCriteriaEntries()} and + * {@link #getFilterCriteriaEntries()} of the passed in parameter. the new created criteria is added to the criteria + * chain. * - * @param criteria - * @return + * @param criteria contains the information for the new Criteria + * @return the new chained criteria */ public Criteria or(Criteria criteria) { + Assert.notNull(criteria, "Cannot chain 'null' criteria."); + Assert.notNull(criteria.getField(), "Cannot chain Criteria with no field"); - Criteria orConnectedCritiera = new OrCriteria(this.criteriaChain, criteria.getField()); - orConnectedCritiera.queryCriteria.addAll(criteria.queryCriteria); - return orConnectedCritiera; + Criteria orCriteria = new OrCriteria(this.criteriaChain, criteria.getField()); + orCriteria.queryCriteriaEntries.addAll(criteria.queryCriteriaEntries); + orCriteria.filterCriteriaEntries.addAll(criteria.filterCriteriaEntries); + return orCriteria; } /** - * Chain using {@code OR} - * - * @param fieldName - * @return + * adds a Criteria as subCriteria + * + * @param criteria the criteria to add, must not be {@literal null} + * @return this object + * @since 4.1 */ - public Criteria or(String fieldName) { - return or(new SimpleField(fieldName)); + public Criteria subCriteria(Criteria criteria) { + + Assert.notNull(criteria, "criteria must not be null"); + + subCriteria.add(criteria); + return this; } + // endregion + + // region criteria entries - query /** - * Crates new CriteriaEntry without any wildcards + * Add a {@link OperationKey#EQUALS} entry to the {@link #queryCriteriaEntries} * - * @param o - * @return + * @param o the argument to the operation + * @return this object */ public Criteria is(Object o) { - queryCriteria.add(new CriteriaEntry(OperationKey.EQUALS, o)); + queryCriteriaEntries.add(new CriteriaEntry(OperationKey.EQUALS, o)); return this; } /** - * Creates a new CriteriaEntry for existence check. - * + * Add a {@link OperationKey#EXISTS} entry to the {@link #queryCriteriaEntries} + * * @return this object * @since 4.0 */ public Criteria exists() { - queryCriteria.add(new CriteriaEntry(OperationKey.EXISTS, null)); + queryCriteriaEntries.add(new CriteriaEntry(OperationKey.EXISTS)); return this; } /** - * Crates new CriteriaEntry with leading and trailing wildcards
- * NOTE: mind your schema as leading wildcards may not be supported and/or execution might be slow. + * Adds a OperationKey.BETWEEN entry to the {@link #queryCriteriaEntries}. Only one of the parameters may be null to + * define an unbounded end of the range. * - * @param s - * @return + * @param lowerBound the lower bound of the range, null for unbounded + * @param upperBound the upper bound of the range, null for unbounded + * @return this object */ - public Criteria contains(String s) { - assertNoBlankInWildcardedQuery(s, true, true); - queryCriteria.add(new CriteriaEntry(OperationKey.CONTAINS, s)); + public Criteria between(@Nullable Object lowerBound, @Nullable Object upperBound) { + + if (lowerBound == null && upperBound == null) { + throw new InvalidDataAccessApiUsageException("Range [* TO *] is not allowed"); + } + + queryCriteriaEntries.add(new CriteriaEntry(OperationKey.BETWEEN, new Object[] { lowerBound, upperBound })); return this; } /** - * Crates new CriteriaEntry with trailing wildcard + * Add a {@link OperationKey#STARTS_WITH} entry to the {@link #queryCriteriaEntries} * - * @param s - * @return + * @param s the argument to the operation + * @return this object */ public Criteria startsWith(String s) { - assertNoBlankInWildcardedQuery(s, true, false); - queryCriteria.add(new CriteriaEntry(OperationKey.STARTS_WITH, s)); + + Assert.notNull(s, "s may not be null"); + + assertNoBlankInWildcardQuery(s, true, false); + queryCriteriaEntries.add(new CriteriaEntry(OperationKey.STARTS_WITH, s)); return this; } /** - * Crates new CriteriaEntry with leading wildcard
- * NOTE: mind your schema and execution times as leading wildcards may not be supported. + * Add a {@link OperationKey#CONTAINS} entry to the {@link #queryCriteriaEntries}
+ * NOTE: mind your schema as leading wildcards may not be supported and/or execution might be slow. * - * @param s - * @return + * @param s the argument to the operation + * @return this object */ - public Criteria endsWith(String s) { - assertNoBlankInWildcardedQuery(s, false, true); - queryCriteria.add(new CriteriaEntry(OperationKey.ENDS_WITH, s)); + public Criteria contains(String s) { + + Assert.notNull(s, "s may not be null"); + + assertNoBlankInWildcardQuery(s, true, true); + queryCriteriaEntries.add(new CriteriaEntry(OperationKey.CONTAINS, s)); return this; } /** - * Crates new CriteriaEntry with trailing - + * Add a {@link OperationKey#ENDS_WITH} entry to the {@link #queryCriteriaEntries}
+ * NOTE: mind your schema as leading wildcards may not be supported and/or execution might be slow. * - * @return + * @param s the argument to the operation + * @return this object */ - public Criteria not() { - this.negating = true; + public Criteria endsWith(String s) { + + Assert.notNull(s, "s may not be null"); + + assertNoBlankInWildcardQuery(s, false, true); + queryCriteriaEntries.add(new CriteriaEntry(OperationKey.ENDS_WITH, s)); return this; } /** - * Crates new CriteriaEntry with trailing ~ + * Add a {@link OperationKey#IN} entry to the {@link #queryCriteriaEntries}. This will create a terms query, so don't + * use it with text fields as these are analyzed and changed by Elasticsearch (converted to lowercase with the default + * analyzer). If used for Strings, these should be marked a sfield type Keyword. * - * @param s - * @return + * @param values the argument to the operation + * @return this object */ - public Criteria fuzzy(String s) { - queryCriteria.add(new CriteriaEntry(OperationKey.FUZZY, s)); - return this; + public Criteria in(Object... values) { + return in(toCollection(values)); } /** - * Crates new CriteriaEntry allowing native elasticsearch expressions + * Add a {@link OperationKey#IN} entry to the {@link #queryCriteriaEntries}. See the comment at + * {@link Criteria#in(Object...)}. * - * @param s - * @return + * @param values the argument to the operation + * @return this object */ - public Criteria expression(String s) { - queryCriteria.add(new CriteriaEntry(OperationKey.EXPRESSION, s)); + public Criteria in(Iterable values) { + + Assert.notNull(values, "Collection of 'in' values must not be null"); + + queryCriteriaEntries.add(new CriteriaEntry(OperationKey.IN, values)); return this; } /** - * Boost positive hit with given factor. eg. ^2.3 + * Add a {@link OperationKey#NOT_IN} entry to the {@link #queryCriteriaEntries}. See the comment at + * {@link Criteria#in(Object...)}. * - * @param boost - * @return + * @param values the argument to the operation + * @return this object */ - public Criteria boost(float boost) { - if (boost < 0) { - throw new InvalidDataAccessApiUsageException("Boost must not be negative."); - } - this.boost = boost; - return this; + public Criteria notIn(Object... values) { + return notIn(toCollection(values)); } /** - * Crates new CriteriaEntry for {@code RANGE [lowerBound TO upperBound]} + * Add a {@link OperationKey#NOT_IN} entry to the {@link #queryCriteriaEntries}. See the comment at + * {@link Criteria#in(Object...)}. * - * @param lowerBound - * @param upperBound - * @return + * @param values the argument to the operation + * @return this object */ - public Criteria between(Object lowerBound, Object upperBound) { - if (lowerBound == null && upperBound == null) { - throw new InvalidDataAccessApiUsageException("Range [* TO *] is not allowed"); - } + public Criteria notIn(Iterable values) { - queryCriteria.add(new CriteriaEntry(OperationKey.BETWEEN, new Object[] { lowerBound, upperBound })); + Assert.notNull(values, "Collection of 'NotIn' values must not be null"); + + queryCriteriaEntries.add(new CriteriaEntry(OperationKey.NOT_IN, values)); return this; } /** - * Crates new CriteriaEntry for {@code RANGE [* TO upperBound]} + * Add a {@link OperationKey#EXPRESSION} entry to the {@link #queryCriteriaEntries} allowing native elasticsearch + * expressions * - * @param upperBound - * @return + * @param s the argument to the operation + * @return this object */ - public Criteria lessThanEqual(Object upperBound) { - if (upperBound == null) { - throw new InvalidDataAccessApiUsageException("UpperBound can't be null"); - } - queryCriteria.add(new CriteriaEntry(OperationKey.LESS_EQUAL, upperBound)); + public Criteria expression(String s) { + queryCriteriaEntries.add(new CriteriaEntry(OperationKey.EXPRESSION, s)); return this; } - public Criteria lessThan(Object upperBound) { - if (upperBound == null) { - throw new InvalidDataAccessApiUsageException("UpperBound can't be null"); - } - queryCriteria.add(new CriteriaEntry(OperationKey.LESS, upperBound)); + /** + * Add a {@link OperationKey#FUZZY} entry to the {@link #queryCriteriaEntries} + * + * @param s the argument to the operation + * @return this object + */ + public Criteria fuzzy(String s) { + queryCriteriaEntries.add(new CriteriaEntry(OperationKey.FUZZY, s)); return this; } /** - * Crates new CriteriaEntry for {@code RANGE [lowerBound TO *]} + * Add a {@link OperationKey#LESS_EQUAL} entry to the {@link #queryCriteriaEntries} * - * @param lowerBound - * @return + * @param upperBound the argument to the operation + * @return this object */ - public Criteria greaterThanEqual(Object lowerBound) { - if (lowerBound == null) { - throw new InvalidDataAccessApiUsageException("LowerBound can't be null"); - } - queryCriteria.add(new CriteriaEntry(OperationKey.GREATER_EQUAL, lowerBound)); - return this; - } + public Criteria lessThanEqual(Object upperBound) { - public Criteria greaterThan(Object lowerBound) { - if (lowerBound == null) { - throw new InvalidDataAccessApiUsageException("LowerBound can't be null"); - } - queryCriteria.add(new CriteriaEntry(OperationKey.GREATER, lowerBound)); + Assert.notNull(upperBound, "upperBound must not be null"); + + queryCriteriaEntries.add(new CriteriaEntry(OperationKey.LESS_EQUAL, upperBound)); return this; } /** - * Crates new CriteriaEntry for multiple values {@code (arg0 arg1 arg2 ...)} + * Add a {@link OperationKey#LESS} entry to the {@link #queryCriteriaEntries} * - * @param values - * @return + * @param upperBound the argument to the operation + * @return this object */ - public Criteria in(Object... values) { - return in(toCollection(values)); + public Criteria lessThan(Object upperBound) { + + Assert.notNull(upperBound, "upperBound must not be null"); + + queryCriteriaEntries.add(new CriteriaEntry(OperationKey.LESS, upperBound)); + return this; } /** - * Crates new CriteriaEntry for multiple values {@code (arg0 arg1 arg2 ...)} + * Add a {@link OperationKey#GREATER_EQUAL} entry to the {@link #queryCriteriaEntries} * - * @param values the collection containing the values to match against - * @return + * @param lowerBound the argument to the operation + * @return this object */ - public Criteria in(Iterable values) { - Assert.notNull(values, "Collection of 'in' values must not be null"); - queryCriteria.add(new CriteriaEntry(OperationKey.IN, values)); - return this; - } - - private List toCollection(Object... values) { - if (values.length == 0 || (values.length > 1 && values[1] instanceof Collection)) { - throw new InvalidDataAccessApiUsageException( - "At least one element " + (values.length > 0 ? ("of argument of type " + values[1].getClass().getName()) : "") - + " has to be present."); - } - return Arrays.asList(values); - } + public Criteria greaterThanEqual(Object lowerBound) { - public Criteria notIn(Object... values) { - return notIn(toCollection(values)); - } + Assert.notNull(lowerBound, "lowerBound must not be null"); - public Criteria notIn(Iterable values) { - Assert.notNull(values, "Collection of 'NotIn' values must not be null"); - queryCriteria.add(new CriteriaEntry(OperationKey.NOT_IN, values)); + queryCriteriaEntries.add(new CriteriaEntry(OperationKey.GREATER_EQUAL, lowerBound)); return this; } /** - * Creates new CriteriaEntry for {@code location WITHIN distance} + * Add a {@link OperationKey#GREATER} entry to the {@link #queryCriteriaEntries} * - * @param location {@link org.springframework.data.elasticsearch.core.geo.GeoPoint} center coordinates - * @param distance {@link String} radius as a string (e.g. : '100km'). Distance unit : either mi/miles or km can be - * set - * @return Criteria the chaind criteria with the new 'within' criteria included. + * @param lowerBound the argument to the operation + * @return this object */ - public Criteria within(GeoPoint location, String distance) { - Assert.notNull(location, "Location value for near criteria must not be null"); - Assert.notNull(location, "Distance value for near criteria must not be null"); - filterCriteria.add(new CriteriaEntry(OperationKey.WITHIN, new Object[] { location, distance })); + public Criteria greaterThan(Object lowerBound) { + + Assert.notNull(lowerBound, "lowerBound must not be null"); + + queryCriteriaEntries.add(new CriteriaEntry(OperationKey.GREATER, lowerBound)); return this; } /** - * Creates new CriteriaEntry for {@code location WITHIN distance} - * - * @param location {@link org.springframework.data.geo.Point} center coordinates - * @param distance {@link org.springframework.data.geo.Distance} radius . - * @return Criteria the chaind criteria with the new 'within' criteria included. + * Add a {@link OperationKey#MATCHES} entry to the {@link #queryCriteriaEntries}. This will build a match query with + * the OR operator. + * + * @param value the value to match + * @return this object + * @since 4.1 */ - public Criteria within(Point location, Distance distance) { - Assert.notNull(location, "Location value for near criteria must not be null"); - Assert.notNull(location, "Distance value for near criteria must not be null"); - filterCriteria.add(new CriteriaEntry(OperationKey.WITHIN, new Object[] { location, distance })); + public Criteria matches(Object value) { + + Assert.notNull(value, "value must not be null"); + + queryCriteriaEntries.add(new CriteriaEntry(OperationKey.MATCHES, value)); return this; } /** - * Creates new CriteriaEntry for {@code geoLocation WITHIN distance} + * Add a {@link OperationKey#MATCHES} entry to the {@link #queryCriteriaEntries}. This will build a match query with + * the AND operator. * - * @param geoLocation {@link String} center point supported formats: lat on = > "41.2,45.1", geohash = > "asd9as0d" - * @param distance {@link String} radius as a string (e.g. : '100km'). Distance unit : either mi/miles or km can be - * set - * @return + * @param value the value to match + * @return this object + * @since 4.1 */ - public Criteria within(String geoLocation, String distance) { - Assert.isTrue(!StringUtils.isEmpty(geoLocation), "geoLocation value must not be null"); - filterCriteria.add(new CriteriaEntry(OperationKey.WITHIN, new Object[] { geoLocation, distance })); + public Criteria matchesAll(Object value) { + + Assert.notNull(value, "value must not be null"); + + queryCriteriaEntries.add(new CriteriaEntry(OperationKey.MATCHES_ALL, value)); return this; } + // endregion + // region criteria entries - filter /** - * Creates new CriteriaEntry for {@code location GeoBox bounding box} + * Adds a new filter CriteriaEntry for {@code location GeoBox bounding box} * * @param boundingBox {@link org.springframework.data.elasticsearch.core.geo.GeoBox} bounding box(left top corner + * right bottom corner) - * @return Criteria the chaind criteria with the new 'boundingBox' criteria included. + * @return this object */ public Criteria boundedBy(GeoBox boundingBox) { + Assert.notNull(boundingBox, "boundingBox value for boundedBy criteria must not be null"); - filterCriteria.add(new CriteriaEntry(OperationKey.BBOX, new Object[] { boundingBox })); + + filterCriteriaEntries.add(new CriteriaEntry(OperationKey.BBOX, new Object[] { boundingBox })); return this; } /** - * Creates new CriteriaEntry for {@code location Box bounding box} + * Adds a new filter CriteriaEntry for {@code location Box bounding box} * * @param boundingBox {@link org.springframework.data.elasticsearch.core.geo.GeoBox} bounding box(left top corner + * right bottom corner) - * @return Criteria the chaind criteria with the new 'boundingBox' criteria included. + * @return this object */ public Criteria boundedBy(Box boundingBox) { + Assert.notNull(boundingBox, "boundingBox value for boundedBy criteria must not be null"); - filterCriteria + + filterCriteriaEntries .add(new CriteriaEntry(OperationKey.BBOX, new Object[] { boundingBox.getFirst(), boundingBox.getSecond() })); return this; } /** - * Creates new CriteriaEntry for bounding box created from points + * Adds a new filter CriteriaEntry for bounding box created from points * * @param topLeftGeohash left top corner of bounding box as geohash * @param bottomRightGeohash right bottom corner of bounding box as geohash - * @return Criteria the chaind criteria with the new 'boundedBy' criteria included. + * @return this object */ public Criteria boundedBy(String topLeftGeohash, String bottomRightGeohash) { + Assert.isTrue(!StringUtils.isEmpty(topLeftGeohash), "topLeftGeohash must not be empty"); Assert.isTrue(!StringUtils.isEmpty(bottomRightGeohash), "bottomRightGeohash must not be empty"); - filterCriteria.add(new CriteriaEntry(OperationKey.BBOX, new Object[] { topLeftGeohash, bottomRightGeohash })); + + filterCriteriaEntries + .add(new CriteriaEntry(OperationKey.BBOX, new Object[] { topLeftGeohash, bottomRightGeohash })); return this; } /** - * Creates new CriteriaEntry for bounding box created from points + * Adds a new filter CriteriaEntry for bounding box created from points * * @param topLeftPoint left top corner of bounding box * @param bottomRightPoint right bottom corner of bounding box - * @return Criteria the chaind criteria with the new 'boundedBy' criteria included. + * @return this object */ public Criteria boundedBy(GeoPoint topLeftPoint, GeoPoint bottomRightPoint) { + Assert.notNull(topLeftPoint, "topLeftPoint must not be null"); Assert.notNull(bottomRightPoint, "bottomRightPoint must not be null"); - filterCriteria.add(new CriteriaEntry(OperationKey.BBOX, new Object[] { topLeftPoint, bottomRightPoint })); + + filterCriteriaEntries.add(new CriteriaEntry(OperationKey.BBOX, new Object[] { topLeftPoint, bottomRightPoint })); return this; } + /** + * Adds a new filter CriteriaEntry for bounding box created from points + * + * @param topLeftPoint left top corner of bounding box + * @param bottomRightPoint right bottom corner of bounding box + * @return this object + */ public Criteria boundedBy(Point topLeftPoint, Point bottomRightPoint) { + Assert.notNull(topLeftPoint, "topLeftPoint must not be null"); Assert.notNull(bottomRightPoint, "bottomRightPoint must not be null"); - filterCriteria.add(new CriteriaEntry(OperationKey.BBOX, + + filterCriteriaEntries.add(new CriteriaEntry(OperationKey.BBOX, new Object[] { GeoPoint.fromPoint(topLeftPoint), GeoPoint.fromPoint(bottomRightPoint) })); return this; } - private void assertNoBlankInWildcardedQuery(String searchString, boolean leadingWildcard, boolean trailingWildcard) { - if (searchString != null && searchString.contains(CRITERIA_VALUE_SEPERATOR)) { - throw new InvalidDataAccessApiUsageException("Cannot constructQuery '" + (leadingWildcard ? "*" : "") + '"' - + searchString + '"' + (trailingWildcard ? "*" : "") + "'. Use expression or multiple clauses instead."); - } - } - /** - * Field targeted by this Criteria + * Adds a new filter CriteriaEntry for {@code location WITHIN distance} * - * @return + * @param location {@link org.springframework.data.elasticsearch.core.geo.GeoPoint} center coordinates + * @param distance {@link String} radius as a string (e.g. : '100km'). Distance unit : either mi/miles or km can be + * set + * @return this object */ - @Nullable - public Field getField() { - return this.field; - } + public Criteria within(GeoPoint location, String distance) { - public Set getQueryCriteriaEntries() { - return Collections.unmodifiableSet(this.queryCriteria); - } + Assert.notNull(location, "Location value for near criteria must not be null"); + Assert.notNull(location, "Distance value for near criteria must not be null"); - public Set getFilterCriteriaEntries() { - return Collections.unmodifiableSet(this.filterCriteria); + filterCriteriaEntries.add(new CriteriaEntry(OperationKey.WITHIN, new Object[] { location, distance })); + return this; } - public Set getFilterCriteria() { - return filterCriteria; + /** + * Adds a new filter CriteriaEntry for {@code location WITHIN distance} + * + * @param location {@link org.springframework.data.geo.Point} center coordinates + * @param distance {@link org.springframework.data.geo.Distance} radius . + * @return this object + */ + public Criteria within(Point location, Distance distance) { + + Assert.notNull(location, "Location value for near criteria must not be null"); + Assert.notNull(location, "Distance value for near criteria must not be null"); + + filterCriteriaEntries.add(new CriteriaEntry(OperationKey.WITHIN, new Object[] { location, distance })); + return this; } /** - * Conjunction to be used with this criteria (AND | OR) + * Adds a new filter CriteriaEntry for {@code geoLocation WITHIN distance} * - * @return + * @param geoLocation {@link String} center point supported formats: lat on = > "41.2,45.1", geohash = > "asd9as0d" + * @param distance {@link String} radius as a string (e.g. : '100km'). Distance unit : either mi/miles or km can be + * set + * @return this object */ - public String getConjunctionOperator() { - return AND_OPERATOR; + public Criteria within(String geoLocation, String distance) { + + Assert.isTrue(!StringUtils.isEmpty(geoLocation), "geoLocation value must not be null"); + + filterCriteriaEntries.add(new CriteriaEntry(OperationKey.WITHIN, new Object[] { geoLocation, distance })); + return this; } - public List getCriteriaChain() { - return Collections.unmodifiableList(this.criteriaChain); + // endregion + + // region helper functions + private void assertNoBlankInWildcardQuery(String searchString, boolean leadingWildcard, boolean trailingWildcard) { + + if (searchString.contains(CRITERIA_VALUE_SEPARATOR)) { + throw new InvalidDataAccessApiUsageException("Cannot constructQuery '" + (leadingWildcard ? "*" : "") + '"' + + searchString + '"' + (trailingWildcard ? "*" : "") + "'. Use expression or multiple clauses instead."); + } } - public boolean isNegating() { - return this.negating; + private List toCollection(Object... values) { + if (values.length == 0 || (values.length > 1 && values[1] instanceof Collection)) { + throw new InvalidDataAccessApiUsageException( + "At least one element " + (values.length > 0 ? ("of argument of type " + values[1].getClass().getName()) : "") + + " has to be present."); + } + return Arrays.asList(values); } - public boolean isAnd() { - return AND_OPERATOR == getConjunctionOperator(); + // endregion + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + + Criteria criteria = (Criteria) o; + + if (Float.compare(criteria.boost, boost) != 0) + return false; + if (negating != criteria.negating) + return false; + if (!Objects.equals(field, criteria.field)) + return false; + if (!queryCriteriaEntries.equals(criteria.queryCriteriaEntries)) + return false; + if (!filterCriteriaEntries.equals(criteria.filterCriteriaEntries)) + return false; + return subCriteria.equals(criteria.subCriteria); } - public boolean isOr() { - return OR_OPERATOR == getConjunctionOperator(); + @Override + public int hashCode() { + int result = field != null ? field.hashCode() : 0; + result = 31 * result + (boost != +0.0f ? Float.floatToIntBits(boost) : 0); + result = 31 * result + (negating ? 1 : 0); + result = 31 * result + queryCriteriaEntries.hashCode(); + result = 31 * result + filterCriteriaEntries.hashCode(); + result = 31 * result + subCriteria.hashCode(); + return result; } - public float getBoost() { - return this.boost; + @Override + public String toString() { + return "Criteria{" + // + "field=" + field + // + ", boost=" + boost + // + ", negating=" + negating + // + ", queryCriteriaEntries=" + queryCriteriaEntries + // + ", filterCriteriaEntries=" + filterCriteriaEntries + // + ", subCriteria=" + subCriteria + // + '}'; // } + @SuppressWarnings("unused") static class OrCriteria extends Criteria { public OrCriteria() { super(); } + public OrCriteria(String fieldName) { + super(fieldName); + } + public OrCriteria(Field field) { super(field); } - public OrCriteria(List criteriaChain, Field field) { - super(criteriaChain, field); + public OrCriteria(List criteriaChain, String fieldName) { + super(criteriaChain, fieldName); } - public OrCriteria(List criteriaChain, String fieldname) { - super(criteriaChain, fieldname); + public OrCriteria(List criteriaChain, Field field) { + super(criteriaChain, field); } - public OrCriteria(String fieldname) { - super(fieldname); + @Override + public String getConjunctionOperator() { + return Operator.OR.name(); } @Override - public String getConjunctionOperator() { - return OR_OPERATOR; + public Operator getOperator() { + return Operator.OR; } } + /** + * a list of {@link Criteria} objects that belong to one query. + * + * @since 4.1 + */ + public static class CriteriaChain extends LinkedList {} + + /** + * Operator to join the entries of the criteria chain + */ + public enum Operator { + AND, // + OR // + } + public enum OperationKey { // EQUALS, // CONTAINS, // @@ -611,11 +855,18 @@ public enum OperationKey { // EXPRESSION, // BETWEEN, // FUZZY, // + /** + * @since 4.1 + */ + MATCHES, // + /** + * @since 4.1 + */ + MATCHES_ALL, // IN, // NOT_IN, // WITHIN, // BBOX, // - NEAR, // LESS, // LESS_EQUAL, // GREATER, // @@ -623,16 +874,29 @@ public enum OperationKey { // /** * @since 4.0 */ - EXISTS + EXISTS // } + /** + * A class defining a single operation and it's argument value for the field of a {@link Criteria}. + */ public static class CriteriaEntry { - private OperationKey key; + private final OperationKey key; + @Nullable private Object value; + + protected CriteriaEntry(OperationKey key) { - private Object value; + Assert.isTrue(key == OperationKey.EXISTS, "key must be OperationKey.EXISTS for this call"); + + this.key = key; + } CriteriaEntry(OperationKey key, Object value) { + + Assert.notNull(key, "key must not be null"); + Assert.notNull(value, "value must not be null"); + this.key = key; this.value = value; } @@ -642,13 +906,41 @@ public OperationKey getKey() { } public void setValue(Object value) { + + Assert.notNull(value, "value must not be null"); + this.value = value; } public Object getValue() { + + Assert.isTrue(key != OperationKey.EXISTS, key.name() + " has no value"); + Assert.notNull(value, "unexpected null value"); + return value; } + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + + CriteriaEntry that = (CriteriaEntry) o; + + if (key != that.key) + return false; + return Objects.equals(value, that.value); + } + + @Override + public int hashCode() { + int result = key.hashCode(); + result = 31 * result + (value != null ? value.hashCode() : 0); + return result; + } + @Override public String toString() { return "CriteriaEntry{" + "key=" + key + ", value=" + value + '}'; diff --git a/src/main/java/org/springframework/data/elasticsearch/core/query/SimpleField.java b/src/main/java/org/springframework/data/elasticsearch/core/query/SimpleField.java index bf45034929..f9fed6a69a 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/query/SimpleField.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/query/SimpleField.java @@ -18,7 +18,8 @@ import org.springframework.util.Assert; /** - * The most trivial implementation of a Field + * The most trivial implementation of a Field. The {@link #name} is updateable, so it may be changed during query + * preparation by the {@link org.springframework.data.elasticsearch.core.convert.MappingElasticsearchConverter}. * * @author Rizwan Idrees * @author Mohsin Husen @@ -29,25 +30,27 @@ public class SimpleField implements Field { private String name; public SimpleField(String name) { - Assert.notNull(name, "name must not be null"); + + Assert.hasText(name, "name must not be null"); this.name = name; } @Override public void setName(String name) { - Assert.notNull(name, "name must not be null"); + + Assert.hasText(name, "name must not be null"); this.name = name; } @Override public String getName() { - return this.name; + return name; } @Override public String toString() { - return this.name; + return getName(); } } diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/query/parser/ElasticsearchQueryCreator.java b/src/main/java/org/springframework/data/elasticsearch/repository/query/parser/ElasticsearchQueryCreator.java index 8eac981322..97567dec3c 100644 --- a/src/main/java/org/springframework/data/elasticsearch/repository/query/parser/ElasticsearchQueryCreator.java +++ b/src/main/java/org/springframework/data/elasticsearch/repository/query/parser/ElasticsearchQueryCreator.java @@ -96,13 +96,10 @@ protected CriteriaQuery complete(@Nullable CriteriaQuery query, Sort sort) { return query.addSort(sort); } - private Criteria from(Part part, Criteria instance, Iterator parameters) { + private Criteria from(Part part, Criteria criteria, Iterator parameters) { + Part.Type type = part.getType(); - Criteria criteria = instance; - if (criteria == null) { - criteria = new Criteria(); - } switch (type) { case TRUE: return criteria.is(true); diff --git a/src/test/java/org/springframework/data/elasticsearch/core/CriteriaQueryMappingTests.java b/src/test/java/org/springframework/data/elasticsearch/core/CriteriaQueryMappingTests.java index d7a6c913f5..00d49da8cd 100644 --- a/src/test/java/org/springframework/data/elasticsearch/core/CriteriaQueryMappingTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/CriteriaQueryMappingTests.java @@ -56,12 +56,15 @@ void setUp() { } - @Test + @Test // DATAES-716 void shouldMapNamesAndConvertValuesInCriteriaQuery() throws JSONException { // use POJO properties and types in the query building - CriteriaQuery criteriaQuery = new CriteriaQuery(new Criteria("birthDate") - .between(LocalDate.of(1989, 11, 9), LocalDate.of(1990, 11, 9)).or("birthDate").is(LocalDate.of(2019, 12, 28))); + CriteriaQuery criteriaQuery = new CriteriaQuery( // + new Criteria("birthDate") // + .between(LocalDate.of(1989, 11, 9), LocalDate.of(1990, 11, 9)) // + .or("birthDate").is(LocalDate.of(2019, 12, 28)) // + ); // mapped field name and converted parameter String expected = '{' + // @@ -90,7 +93,60 @@ void shouldMapNamesAndConvertValuesInCriteriaQuery() throws JSONException { '}'; // mappingElasticsearchConverter.updateQuery(criteriaQuery, Person.class); - String queryString = new CriteriaQueryProcessor().createQueryFromCriteria(criteriaQuery.getCriteria()).toString(); + String queryString = new CriteriaQueryProcessor().createQuery(criteriaQuery.getCriteria()).toString(); + + assertEquals(expected, queryString, false); + } + + @Test // DATAES-706 + void shouldMapNamesAndValuesInSubCriteriaQuery() throws JSONException { + + CriteriaQuery criteriaQuery = new CriteriaQuery( // + new Criteria("firstName").matches("John") // + .subCriteria(new Criteria("birthDate") // + .between(LocalDate.of(1989, 11, 9), LocalDate.of(1990, 11, 9)) // + .or("birthDate").is(LocalDate.of(2019, 12, 28)))); + + String expected = "{\n" + // + " \"bool\": {\n" + // + " \"must\": [\n" + // + " {\n" + // + " \"match\": {\n" + // + " \"first-name\": {\n" + // + " \"query\": \"John\"\n" + // + " }\n" + // + " }\n" + // + " },\n" + // + " {\n" + // + " \"bool\": {\n" + // + " \"should\": [\n" + // + " {\n" + // + " \"range\": {\n" + // + " \"birth-date\": {\n" + // + " \"from\": \"09.11.1989\",\n" + // + " \"to\": \"09.11.1990\",\n" + // + " \"include_lower\": true,\n" + // + " \"include_upper\": true\n" + // + " }\n" + // + " }\n" + // + " },\n" + // + " {\n" + // + " \"query_string\": {\n" + // + " \"query\": \"28.12.2019\",\n" + // + " \"fields\": [\n" + // + " \"birth-date^1.0\"\n" + // + " ]\n" + // + " }\n" + // + " }\n" + // + " ]\n" + // + " }\n" + // + " }\n" + // + " ]\n" + // + " }\n" + // + "}\n"; // + + mappingElasticsearchConverter.updateQuery(criteriaQuery, Person.class); + String queryString = new CriteriaQueryProcessor().createQuery(criteriaQuery.getCriteria()).toString(); assertEquals(expected, queryString, false); } diff --git a/src/test/java/org/springframework/data/elasticsearch/core/CriteriaQueryProcessorTests.java b/src/test/java/org/springframework/data/elasticsearch/core/CriteriaQueryProcessorTests.java new file mode 100644 index 0000000000..ccf07c596b --- /dev/null +++ b/src/test/java/org/springframework/data/elasticsearch/core/CriteriaQueryProcessorTests.java @@ -0,0 +1,341 @@ +/* + * Copyright 2020 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; + +import static org.skyscreamer.jsonassert.JSONAssert.*; + +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.springframework.data.elasticsearch.core.query.Criteria; + +/** + * @author Peter-Josef Meisch + */ +class CriteriaQueryProcessorTests { + + private final CriteriaQueryProcessor queryProcessor = new CriteriaQueryProcessor(); + + @Test // DATAES-706 + void shouldProcessTwoCriteriaWithAnd() throws JSONException { + + String expected = "{\n" + // + " \"bool\": {\n" + // + " \"must\": [\n" + // + " {\n" + // + " \"query_string\": {\n" + // + " \"query\": \"value1\",\n" + // + " \"fields\": [\n" + // + " \"field1^1.0\"\n" + // + " ]\n" + // + " }\n" + // + " },\n" + // + " {\n" + // + " \"query_string\": {\n" + // + " \"query\": \"value2\",\n" + // + " \"fields\": [\n" + // + " \"field2^1.0\"\n" + // + " ]\n" + // + " }\n" + // + " }\n" + // + " ]\n" + // + " }\n" + // + "}"; // + + Criteria criteria = new Criteria("field1").is("value1").and("field2").is("value2"); + + String query = queryProcessor.createQuery(criteria).toString(); + + assertEquals(expected, query, false); + } + + @Test // DATAES-706 + void shouldProcessTwoCriteriaWithOr() throws JSONException { + + String expected = "{\n" + // + " \"bool\": {\n" + // + " \"should\": [\n" + // + " {\n" + // + " \"query_string\": {\n" + // + " \"query\": \"value1\",\n" + // + " \"fields\": [\n" + // + " \"field1^1.0\"\n" + // + " ]\n" + // + " }\n" + // + " },\n" + // + " {\n" + // + " \"query_string\": {\n" + // + " \"query\": \"value2\",\n" + // + " \"fields\": [\n" + // + " \"field2^1.0\"\n" + // + " ]\n" + // + " }\n" + // + " }\n" + // + " ]\n" + // + " }\n" + // + "}"; // + + Criteria criteria = new Criteria("field1").is("value1").or("field2").is("value2"); + + String query = queryProcessor.createQuery(criteria).toString(); + + assertEquals(expected, query, false); + } + + @Test // DATAES-706 + void shouldProcessMixedCriteriaWithOrAnd() throws JSONException { + + String expected = "{\n" + // + " \"bool\": {\n" + // + " \"must\": [\n" + // + " {\n" + // + " \"query_string\": {\n" + // + " \"query\": \"value1\",\n" + // + " \"fields\": [\n" + // + " \"field1^1.0\"\n" + // + " ]\n" + // + " }\n" + // + " },\n" + // + " {\n" + // + " \"query_string\": {\n" + // + " \"query\": \"value3\",\n" + // + " \"fields\": [\n" + // + " \"field3^1.0\"\n" + // + " ]\n" + // + " }\n" + // + " }\n" + // + " ],\n" + // + " \"should\": [\n" + // + " {\n" + // + " \"query_string\": {\n" + // + " \"query\": \"value2\",\n" + // + " \"fields\": [\n" + // + " \"field2^1.0\"\n" + // + " ]\n" + // + " }\n" + // + " },\n" + // + " {\n" + // + " \"query_string\": {\n" + // + " \"query\": \"value4\",\n" + // + " \"fields\": [\n" + // + " \"field4^1.0\"\n" + // + " ]\n" + // + " }\n" + // + " }\n" + // + " ]\n" + // + " }\n" + // + "}\n"; // + + Criteria criteria = new Criteria("field1").is("value1") // + .or("field2").is("value2") // + .and("field3").is("value3") // + .or("field4").is("value4"); // + + String query = queryProcessor.createQuery(criteria).toString(); + + assertEquals(expected, query, false); + } + + @Test // DATAES-706 + void shouldAddSubQuery() throws JSONException { + + String expected = "{\n" + // + " \"bool\": {\n" + // + " \"must\": [\n" + // + " {\n" + // + " \"query_string\": {\n" + // + " \"query\": \"Miller\",\n" + // + " \"fields\": [\n" + // + " \"lastName^1.0\"\n" + // + " ]\n" + // + " }\n" + // + " },\n" + // + " {\n" + // + " \"bool\": {\n" + // + " \"should\": [\n" + // + " {\n" + // + " \"query_string\": {\n" + // + " \"query\": \"John\",\n" + // + " \"fields\": [\n" + // + " \"firstName^1.0\"\n" + // + " ]\n" + // + " }\n" + // + " },\n" + // + " {\n" + // + " \"query_string\": {\n" + // + " \"query\": \"Jack\",\n" + // + " \"fields\": [\n" + // + " \"firstName^1.0\"\n" + // + " ]\n" + // + " }\n" + // + " }\n" + // + " ]\n" + // + " }\n" + // + " }\n" + // + " ]\n" + // + " }\n" + // + "}"; // + + Criteria criteria = new Criteria("lastName").is("Miller") + .subCriteria(new Criteria().or("firstName").is("John").or("firstName").is("Jack")); + + String query = queryProcessor.createQuery(criteria).toString(); + + assertEquals(expected, query, false); + } + + @Test // DATAES-706 + void shouldProcessNestedSubCriteria() throws JSONException { + + String expected = "{\n" + // + " \"bool\": {\n" + // + " \"should\": [\n" + // + " {\n" + // + " \"bool\": {\n" + // + " \"must\": [\n" + // + " {\n" + // + " \"query_string\": {\n" + // + " \"query\": \"Miller\",\n" + // + " \"fields\": [\n" + // + " \"lastName^1.0\"\n" + // + " ]\n" + // + " }\n" + // + " },\n" + // + " {\n" + // + " \"bool\": {\n" + // + " \"should\": [\n" + // + " {\n" + // + " \"query_string\": {\n" + // + " \"query\": \"Jack\",\n" + // + " \"fields\": [\n" + // + " \"firstName^1.0\"\n" + // + " ]\n" + // + " }\n" + // + " },\n" + // + " {\n" + // + " \"query_string\": {\n" + // + " \"query\": \"John\",\n" + // + " \"fields\": [\n" + // + " \"firstName^1.0\"\n" + // + " ]\n" + // + " }\n" + // + " }\n" + // + " ]\n" + // + " }\n" + // + " }\n" + // + " ]\n" + // + " }\n" + // + " },\n" + // + " {\n" + // + " \"bool\": {\n" + // + " \"must\": [\n" + // + " {\n" + // + " \"query_string\": {\n" + // + " \"query\": \"Smith\",\n" + // + " \"fields\": [\n" + // + " \"lastName^1.0\"\n" + // + " ]\n" + // + " }\n" + // + " },\n" + // + " {\n" + // + " \"bool\": {\n" + // + " \"should\": [\n" + // + " {\n" + // + " \"query_string\": {\n" + // + " \"query\": \"Emma\",\n" + // + " \"fields\": [\n" + // + " \"firstName^1.0\"\n" + // + " ]\n" + // + " }\n" + // + " },\n" + // + " {\n" + // + " \"query_string\": {\n" + // + " \"query\": \"Lucy\",\n" + // + " \"fields\": [\n" + // + " \"firstName^1.0\"\n" + // + " ]\n" + // + " }\n" + // + " }\n" + // + " ]\n" + // + " }\n" + // + " }\n" + // + " ]\n" + // + " }\n" + // + " }\n" + // + " ]\n" + // + " }\n" + // + "}"; // + + Criteria criteria = Criteria.or() + .subCriteria(new Criteria("lastName").is("Miller") + .subCriteria(new Criteria().or("firstName").is("John").or("firstName").is("Jack"))) + .subCriteria(new Criteria("lastName").is("Smith") + .subCriteria(new Criteria().or("firstName").is("Emma").or("firstName").is("Lucy"))); + + String query = queryProcessor.createQuery(criteria).toString(); + + assertEquals(expected, query, false); + } + + @Test // DATAES-706 + void shouldBuildMatchQuery() throws JSONException { + + String expected = "{\n" + // + " \"bool\" : {\n" + // + " \"must\" : [\n" + // + " {\n" + // + " \"match\" : {\n" + // + " \"field1\" : {\n" + // + " \"query\" : \"value1 value2\",\n" + // + " \"operator\" : \"OR\"\n" + // + " }\n" + // + " }\n" + // + " }\n" + // + " ]\n" + // + " }\n" + // + "}\n"; // + + Criteria criteria = new Criteria("field1").matches("value1 value2"); + + String query = queryProcessor.createQuery(criteria).toString(); + + assertEquals(expected, query, false); + } + + @Test // DATAES-706 + void shouldBuildMatchAllQuery() throws JSONException { + + String expected = "{\n" + // + " \"bool\" : {\n" + // + " \"must\" : [\n" + // + " {\n" + // + " \"match\" : {\n" + // + " \"field1\" : {\n" + // + " \"query\" : \"value1 value2\",\n" + // + " \"operator\" : \"AND\"\n" + // + " }\n" + // + " }\n" + // + " }\n" + // + " ]\n" + // + " }\n" + // + "}\n"; // + + Criteria criteria = new Criteria("field1").matchesAll("value1 value2"); + + String query = queryProcessor.createQuery(criteria).toString(); + + assertEquals(expected, query, false); + } +} diff --git a/src/test/java/org/springframework/data/elasticsearch/core/query/CriteriaQueryTests.java b/src/test/java/org/springframework/data/elasticsearch/core/query/CriteriaQueryTests.java index 516c64b5f7..149a01f1bd 100644 --- a/src/test/java/org/springframework/data/elasticsearch/core/query/CriteriaQueryTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/query/CriteriaQueryTests.java @@ -83,99 +83,72 @@ void after() { indexOperations.delete(); } - @Test - public void shouldPerformAndOperation() { + @Test // ,DATAES-706 + public void shouldPerformAndOperationOnCriteriaEntries() { // given - String documentId = nextIdAsString(); - SampleEntity sampleEntity = new SampleEntity(); - sampleEntity.setId(documentId); - sampleEntity.setMessage("some test message"); - sampleEntity.setVersion(System.currentTimeMillis()); - - IndexQuery indexQuery = new IndexQuery(); - indexQuery.setId(documentId); - indexQuery.setObject(sampleEntity); - operations.index(indexQuery, index); + SampleEntity sampleEntity1 = new SampleEntity(); + sampleEntity1.setId(nextIdAsString()); + sampleEntity1.setMessage("some test message"); + operations.save(sampleEntity1); + SampleEntity sampleEntity2 = new SampleEntity(); + sampleEntity2.setId(nextIdAsString()); + sampleEntity2.setMessage("some other message"); + operations.save(sampleEntity2); indexOperations.refresh(); - CriteriaQuery criteriaQuery = new CriteriaQuery( - new Criteria("message").contains("test").and("message").contains("some")); // when - SearchHit sampleEntity1 = operations.searchOne(criteriaQuery, SampleEntity.class, index); + CriteriaQuery criteriaQuery = new CriteriaQuery( + new Criteria("message").contains("test").and("message").contains("some")); + SearchHit searchHit = operations.searchOne(criteriaQuery, SampleEntity.class, index); // then - assertThat(sampleEntity1).isNotNull(); + assertThat(searchHit).isNotNull(); + assertThat(searchHit.getId()).isEqualTo(sampleEntity1.id); } - // @Ignore("DATAES-30") - @Test - public void shouldPerformOrOperation() { + @Test // ,DATAES-706 + public void shouldPerformOrOperationOnCriteriaEntries() { // given - List indexQueries = new ArrayList<>(); - - // first document - String documentId = nextIdAsString(); SampleEntity sampleEntity1 = new SampleEntity(); - sampleEntity1.setId(documentId); - sampleEntity1.setMessage("some message"); - sampleEntity1.setVersion(System.currentTimeMillis()); - - IndexQuery indexQuery1 = new IndexQuery(); - indexQuery1.setId(documentId); - indexQuery1.setObject(sampleEntity1); - indexQueries.add(indexQuery1); - - // second document - String documentId2 = nextIdAsString(); + sampleEntity1.setId(nextIdAsString()); + sampleEntity1.setMessage("some test message"); + operations.save(sampleEntity1); SampleEntity sampleEntity2 = new SampleEntity(); - sampleEntity2.setId(documentId2); - sampleEntity2.setMessage("test message"); - sampleEntity2.setVersion(System.currentTimeMillis()); - - IndexQuery indexQuery2 = new IndexQuery(); - indexQuery2.setId(documentId2); - indexQuery2.setObject(sampleEntity2); - - indexQueries.add(indexQuery2); - operations.bulkIndex(indexQueries, index); + sampleEntity2.setId(nextIdAsString()); + sampleEntity2.setMessage("some other message"); + operations.save(sampleEntity2); indexOperations.refresh(); - CriteriaQuery criteriaQuery = new CriteriaQuery( - new Criteria("message").contains("some").or("message").contains("test")); // when + CriteriaQuery criteriaQuery = new CriteriaQuery( + new Criteria("message").contains("test").or("message").contains("other")); SearchHits searchHits = operations.search(criteriaQuery, SampleEntity.class, index); // then assertThat(searchHits).isNotNull(); - assertThat(searchHits.getTotalHits()).isGreaterThanOrEqualTo(1); + assertThat(searchHits.getSearchHits().stream().map(SearchHit::getId)).containsExactlyInAnyOrder(sampleEntity1.id, + sampleEntity2.id); } - @Test + @Test // ,DATAES-706 public void shouldPerformAndOperationWithinCriteria() { // given - List indexQueries = new ArrayList<>(); - - // first document - String documentId = nextIdAsString(); - SampleEntity sampleEntity = new SampleEntity(); - sampleEntity.setId(documentId); - sampleEntity.setMessage("some message"); - sampleEntity.setVersion(System.currentTimeMillis()); - - IndexQuery indexQuery = new IndexQuery(); - indexQuery.setId(documentId); - indexQuery.setObject(sampleEntity); - indexQueries.add(indexQuery); - - operations.bulkIndex(indexQueries, index); + SampleEntity sampleEntity1 = new SampleEntity(); + sampleEntity1.setId(nextIdAsString()); + sampleEntity1.setMessage("some test message"); + operations.save(sampleEntity1); + SampleEntity sampleEntity2 = new SampleEntity(); + sampleEntity2.setId(nextIdAsString()); + sampleEntity2.setMessage("some other message"); + operations.save(sampleEntity2); indexOperations.refresh(); - CriteriaQuery criteriaQuery = new CriteriaQuery(new Criteria().and(new Criteria("message").contains("some"))); // when - + CriteriaQuery criteriaQuery = new CriteriaQuery( + new Criteria("message").contains("test").and(new Criteria("message").contains("some"))); SearchHits searchHits = operations.search(criteriaQuery, SampleEntity.class, index); // then @@ -183,34 +156,29 @@ public void shouldPerformAndOperationWithinCriteria() { assertThat(searchHits.getTotalHits()).isGreaterThanOrEqualTo(1); } - @Test + @Test // ,DATAES-706 public void shouldPerformOrOperationWithinCriteria() { // given - List indexQueries = new ArrayList<>(); - - // first document - String documentId = nextIdAsString(); - SampleEntity sampleEntity = new SampleEntity(); - sampleEntity.setId(documentId); - sampleEntity.setMessage("some message"); - sampleEntity.setVersion(System.currentTimeMillis()); - - IndexQuery indexQuery = new IndexQuery(); - indexQuery.setId(documentId); - indexQuery.setObject(sampleEntity); - indexQueries.add(indexQuery); - - operations.bulkIndex(indexQueries, index); + SampleEntity sampleEntity1 = new SampleEntity(); + sampleEntity1.setId(nextIdAsString()); + sampleEntity1.setMessage("some test message"); + operations.save(sampleEntity1); + SampleEntity sampleEntity2 = new SampleEntity(); + sampleEntity2.setId(nextIdAsString()); + sampleEntity2.setMessage("some other message"); + operations.save(sampleEntity2); indexOperations.refresh(); - CriteriaQuery criteriaQuery = new CriteriaQuery(new Criteria().or(new Criteria("message").contains("some"))); // when + CriteriaQuery criteriaQuery = new CriteriaQuery( + new Criteria("message").contains("test").or(new Criteria("message").contains("other"))); SearchHits searchHits = operations.search(criteriaQuery, SampleEntity.class, index); // then assertThat(searchHits).isNotNull(); - assertThat(searchHits.getTotalHits()).isGreaterThanOrEqualTo(1); + assertThat(searchHits.getSearchHits().stream().map(SearchHit::getId)).containsExactlyInAnyOrder(sampleEntity1.id, + sampleEntity2.id); } @Test diff --git a/src/test/java/org/springframework/data/elasticsearch/repository/query/ReactiveElasticsearchStringQueryUnitTests.java b/src/test/java/org/springframework/data/elasticsearch/repository/query/ReactiveElasticsearchStringQueryUnitTests.java index df787404e6..dd55e40889 100644 --- a/src/test/java/org/springframework/data/elasticsearch/repository/query/ReactiveElasticsearchStringQueryUnitTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/repository/query/ReactiveElasticsearchStringQueryUnitTests.java @@ -78,9 +78,9 @@ public void setUp() { public void bindsSimplePropertyCorrectly() throws Exception { ReactiveElasticsearchStringQuery elasticsearchStringQuery = createQueryForMethod("findByName", String.class); - StubParameterAccessor accesor = new StubParameterAccessor("Luke"); + StubParameterAccessor accessor = new StubParameterAccessor("Luke"); - org.springframework.data.elasticsearch.core.query.Query query = elasticsearchStringQuery.createQuery(accesor); + org.springframework.data.elasticsearch.core.query.Query query = elasticsearchStringQuery.createQuery(accessor); StringQuery reference = new StringQuery("{ 'bool' : { 'must' : { 'term' : { 'name' : 'Luke' } } } }"); assertThat(query).isInstanceOf(StringQuery.class); @@ -93,9 +93,9 @@ public void bindsExpressionPropertyCorrectly() throws Exception { ReactiveElasticsearchStringQuery elasticsearchStringQuery = createQueryForMethod("findByNameWithExpression", String.class); - StubParameterAccessor accesor = new StubParameterAccessor("Luke"); + StubParameterAccessor accessor = new StubParameterAccessor("Luke"); - org.springframework.data.elasticsearch.core.query.Query query = elasticsearchStringQuery.createQuery(accesor); + org.springframework.data.elasticsearch.core.query.Query query = elasticsearchStringQuery.createQuery(accessor); StringQuery reference = new StringQuery("{ 'bool' : { 'must' : { 'term' : { 'name' : 'Luke' } } } }"); assertThat(query).isInstanceOf(StringQuery.class);