From 131f0318cc02298e389c916d26554dd397ef755c Mon Sep 17 00:00:00 2001 From: Peter-Josef Meisch Date: Tue, 18 Aug 2020 20:59:35 +0200 Subject: [PATCH] DATAES-706 - CriteriaQueryProcessor must handle nested Criteria definitions. Original PR: #505 --- 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 | 51 +- .../elasticsearch/core/query/Criteria.java | 1112 +++++++++++------ .../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, 1426 insertions(+), 631 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 5168c08fa..48740c4be 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 69850323f..49580288b 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 911a38b39..3fd3a6fc9 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 @@ package org.springframework.data.elasticsearch.core; 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 @@ import org.springframework.util.Assert; */ 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 @@ class CriteriaFilterProcessor { // 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 @@ class CriteriaFilterProcessor { 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 @@ class CriteriaFilterProcessor { 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 @@ class CriteriaFilterProcessor { while (it.hasNext()) { Criteria.CriteriaEntry criteriaEntry = it.next(); QueryBuilder notFilter = QueryBuilders.boolQuery() - .mustNot(processCriteriaEntry(criteriaEntry.getKey(), criteriaEntry.getValue(), fieldName)); + .mustNot(queryFor(criteriaEntry.getKey(), criteriaEntry.getValue(), fieldName)); notFilterList.add(notFilter); } diff --git a/src/main/java/org/springframework/data/elasticsearch/core/CriteriaQueryProcessor.java b/src/main/java/org/springframework/data/elasticsearch/core/CriteriaQueryProcessor.java index a1f9c919c..f465f2201 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/CriteriaQueryProcessor.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/CriteriaQueryProcessor.java @@ -21,11 +21,8 @@ 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.ListIterator; -import org.apache.lucene.queryparser.flexible.core.util.StringUtils; import org.apache.lucene.queryparser.flexible.standard.QueryParserUtil; import org.elasticsearch.index.query.BoolQueryBuilder; import org.elasticsearch.index.query.QueryBuilder; @@ -46,63 +43,81 @@ import org.springframework.util.Assert; */ class CriteriaQueryProcessor { - QueryBuilder createQueryFromCriteria(Criteria criteria) { + @Nullable + QueryBuilder createQuery(Criteria criteria) { Assert.notNull(criteria, "criteria must not be null"); - List 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 { - mustQueryBuilderList.add(queryFragmentForCriteria); + 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 { + 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 @@ class CriteriaQueryProcessor { } @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 @@ class CriteriaQueryProcessor { 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 @@ class CriteriaQueryProcessor { 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 79505b54b..67e08c49e 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 @@ class RequestFactory { 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 @@ class RequestFactory { 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 910092a02..deda4c3fe 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.join.JoinField; 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 class MappingElasticsearchConverter ElasticsearchPersistentEntity persistentEntity = mappingContext.getPersistentEntity(domainClass); if (persistentEntity != null) { - criteriaQuery.getCriteria().getCriteriaChain().forEach(criteria -> { - String name = criteria.getField().getName(); - ElasticsearchPersistentProperty property = persistentEntity.getPersistentProperty(name); + for (Criteria chainedCriteria : criteriaQuery.getCriteria().getCriteriaChain()) { + updateCriteria(chainedCriteria, persistentEntity); + } + } + } - if (property != null && property.getName().equals(name)) { - criteria.getField().setName(property.getFieldName()); + private void updateCriteria(Criteria criteria, ElasticsearchPersistentEntity persistentEntity) { + String name = criteria.getField().getName(); + ElasticsearchPersistentProperty property = persistentEntity.getPersistentProperty(name); - 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)); - } - }); + 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 0a2ed5a86..6b7d0fdb3 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.Distance; 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,353 +53,695 @@ import org.springframework.util.StringUtils; */ 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<>(); - private Set queryCriteria = new LinkedHashSet<>(); + // region criteria creation - private Set filterCriteria = 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(); + } + + /** + * @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 /** - * Chain using {@code AND} - * - * @param field - * @return + * @return the Field targeted by this Criteria */ - public Criteria and(Field field) { - return new Criteria(this.criteriaChain, field); + @Nullable + public Field getField() { + return this.field; + } + + public Set getQueryCriteriaEntries() { + return Collections.unmodifiableSet(this.queryCriteriaEntries); + } + + public Set getFilterCriteriaEntries() { + return Collections.unmodifiableSet(this.filterCriteriaEntries); } /** - * Chain using {@code AND} + * Conjunction to be used with this criteria (AND | OR) * - * @param fieldName - * @return + * @deprecated since 4.1, use {@link #getOperator()} */ - public Criteria and(String fieldName) { - return new Criteria(this.criteriaChain, fieldName); + @Deprecated + public String getConjunctionOperator() { + return Operator.AND.name(); + } + + public Operator getOperator() { + return Operator.AND; + } + + public List getCriteriaChain() { + return Collections.unmodifiableList(this.criteriaChain); } /** - * Chain using {@code AND} + * Sets the negating flag * - * @param criteria - * @return - */ - public Criteria and(Criteria criteria) { - this.criteriaChain.add(criteria); - return this; - } - - /** - * Chain using {@code AND} - * - * @param criterias - * @return - */ - public Criteria and(Criteria... criterias) { - this.criteriaChain.addAll(Arrays.asList(criterias)); - return this; - } - - /** - * Chain using {@code OR} - * - * @param field - * @return - */ - public Criteria or(Field field) { - return new OrCriteria(this.criteriaChain, field); - } - - /** - * Chain using {@code OR} - * - * @param criteria - * @return - */ - public Criteria or(Criteria criteria) { - Assert.notNull(criteria, "Cannot chain 'null' criteria."); - - Criteria orConnectedCritiera = new OrCriteria(this.criteriaChain, criteria.getField()); - orConnectedCritiera.queryCriteria.addAll(criteria.queryCriteria); - return orConnectedCritiera; - } - - /** - * Chain using {@code OR} - * - * @param fieldName - * @return - */ - public Criteria or(String fieldName) { - return or(new SimpleField(fieldName)); - } - - /** - * Crates new CriteriaEntry without any wildcards - * - * @param o - * @return - */ - public Criteria is(Object o) { - queryCriteria.add(new CriteriaEntry(OperationKey.EQUALS, o)); - return this; - } - - /** - * Creates a new CriteriaEntry for existence check. - * * @return this object - * @since 4.0 - */ - public Criteria exists() { - queryCriteria.add(new CriteriaEntry(OperationKey.EXISTS, null)); - 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. - * - * @param s - * @return - */ - public Criteria contains(String s) { - assertNoBlankInWildcardedQuery(s, true, true); - queryCriteria.add(new CriteriaEntry(OperationKey.CONTAINS, s)); - return this; - } - - /** - * Crates new CriteriaEntry with trailing wildcard - * - * @param s - * @return - */ - public Criteria startsWith(String s) { - assertNoBlankInWildcardedQuery(s, true, false); - queryCriteria.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. - * - * @param s - * @return - */ - public Criteria endsWith(String s) { - assertNoBlankInWildcardedQuery(s, false, true); - queryCriteria.add(new CriteriaEntry(OperationKey.ENDS_WITH, s)); - return this; - } - - /** - * Crates new CriteriaEntry with trailing - - * - * @return */ public Criteria not() { this.negating = true; return this; } - /** - * Crates new CriteriaEntry with trailing ~ - * - * @param s - * @return - */ - public Criteria fuzzy(String s) { - queryCriteria.add(new CriteriaEntry(OperationKey.FUZZY, s)); - return this; + public boolean isNegating() { + return this.negating; } /** - * Crates new CriteriaEntry allowing native elasticsearch expressions + * Sets the boost factor. * - * @param s - * @return - */ - public Criteria expression(String s) { - queryCriteria.add(new CriteriaEntry(OperationKey.EXPRESSION, s)); - return this; - } - - /** - * Boost positive hit with given factor. eg. ^2.3 - * - * @param boost - * @return + * @param boost boost factor + * @return this object */ public Criteria boost(float boost) { - if (boost < 0) { - throw new InvalidDataAccessApiUsageException("Boost must not be negative."); - } + + 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; + } + /** - * Crates new CriteriaEntry for {@code RANGE [lowerBound TO upperBound]} - * - * @param lowerBound - * @param upperBound - * @return + * @return the set ob subCriteria + * @since 4.1 */ - public Criteria between(Object lowerBound, Object upperBound) { + public Set getSubCriteria() { + return subCriteria; + } + + // endregion + + // region criteria chaining + /** + * Chain a new and-Criteria + * + * @param field the field for the new Criteria + * @return the new chained Criteria + */ + public Criteria and(Field field) { + return new Criteria(criteriaChain, field); + } + + /** + * Chain a new and- Criteria + * + * @param fieldName the field for the new Criteria + * @return the new chained Criteria + */ + public Criteria and(String fieldName) { + return new Criteria(criteriaChain, fieldName); + } + + /** + * Chain a Criteria to this object. + * + * @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 an array of Criteria to this object. + * + * @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 a new or-Criteria + * + * @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 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 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 orCriteria = new OrCriteria(this.criteriaChain, criteria.getField()); + orCriteria.queryCriteriaEntries.addAll(criteria.queryCriteriaEntries); + orCriteria.filterCriteriaEntries.addAll(criteria.filterCriteriaEntries); + return orCriteria; + } + + /** + * adds a Criteria as subCriteria + * + * @param criteria the criteria to add, must not be {@literal null} + * @return this object + * @since 4.1 + */ + public Criteria subCriteria(Criteria criteria) { + + Assert.notNull(criteria, "criteria must not be null"); + + subCriteria.add(criteria); + return this; + } + + // endregion + + // region criteria entries - query + /** + * Add a {@link OperationKey#EQUALS} entry to the {@link #queryCriteriaEntries} + * + * @param o the argument to the operation + * @return this object + */ + public Criteria is(Object o) { + queryCriteriaEntries.add(new CriteriaEntry(OperationKey.EQUALS, o)); + return this; + } + + /** + * Add a {@link OperationKey#EXISTS} entry to the {@link #queryCriteriaEntries} + * + * @return this object + * @since 4.0 + */ + public Criteria exists() { + queryCriteriaEntries.add(new CriteriaEntry(OperationKey.EXISTS)); + return this; + } + + /** + * 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 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 between(@Nullable Object lowerBound, @Nullable Object upperBound) { + if (lowerBound == null && upperBound == null) { throw new InvalidDataAccessApiUsageException("Range [* TO *] is not allowed"); } - queryCriteria.add(new CriteriaEntry(OperationKey.BETWEEN, new Object[] { lowerBound, upperBound })); + queryCriteriaEntries.add(new CriteriaEntry(OperationKey.BETWEEN, new Object[] { lowerBound, upperBound })); return this; } /** - * Crates new CriteriaEntry for {@code RANGE [* TO upperBound]} + * Add a {@link OperationKey#STARTS_WITH} entry to the {@link #queryCriteriaEntries} * - * @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)); - return this; - } + public Criteria startsWith(String s) { - public Criteria lessThan(Object upperBound) { - if (upperBound == null) { - throw new InvalidDataAccessApiUsageException("UpperBound can't be null"); - } - queryCriteria.add(new CriteriaEntry(OperationKey.LESS, upperBound)); + 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 for {@code RANGE [lowerBound TO *]} + * 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 lowerBound - * @return + * @param s 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 contains(String s) { - 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(s, "s may not be null"); + + assertNoBlankInWildcardQuery(s, true, true); + queryCriteriaEntries.add(new CriteriaEntry(OperationKey.CONTAINS, s)); return this; } /** - * Crates new CriteriaEntry for multiple values {@code (arg0 arg1 arg2 ...)} + * 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. * - * @param values - * @return + * @param s the argument to the operation + * @return this object + */ + 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; + } + + /** + * 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 values the argument to the operation + * @return this object */ public Criteria in(Object... values) { return in(toCollection(values)); } /** - * Crates new CriteriaEntry for multiple values {@code (arg0 arg1 arg2 ...)} + * Add a {@link OperationKey#IN} entry to the {@link #queryCriteriaEntries}. See the comment at + * {@link Criteria#in(Object...)}. * - * @param values the collection containing the values to match against - * @return + * @param values 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)); + + queryCriteriaEntries.add(new CriteriaEntry(OperationKey.IN, values)); return this; } + /** + * Add a {@link OperationKey#NOT_IN} entry to the {@link #queryCriteriaEntries}. See the comment at + * {@link Criteria#in(Object...)}. + * + * @param values the argument to the operation + * @return this object + */ + public Criteria notIn(Object... values) { + return notIn(toCollection(values)); + } + + /** + * Add a {@link OperationKey#NOT_IN} entry to the {@link #queryCriteriaEntries}. See the comment at + * {@link Criteria#in(Object...)}. + * + * @param values the argument to the operation + * @return this object + */ + public Criteria notIn(Iterable values) { + + Assert.notNull(values, "Collection of 'NotIn' values must not be null"); + + queryCriteriaEntries.add(new CriteriaEntry(OperationKey.NOT_IN, values)); + return this; + } + + /** + * Add a {@link OperationKey#EXPRESSION} entry to the {@link #queryCriteriaEntries} allowing native elasticsearch + * expressions + * + * @param s the argument to the operation + * @return this object + */ + public Criteria expression(String s) { + queryCriteriaEntries.add(new CriteriaEntry(OperationKey.EXPRESSION, s)); + return this; + } + + /** + * 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; + } + + /** + * Add a {@link OperationKey#LESS_EQUAL} entry to the {@link #queryCriteriaEntries} + * + * @param upperBound the argument to the operation + * @return this object + */ + public Criteria lessThanEqual(Object upperBound) { + + Assert.notNull(upperBound, "upperBound must not be null"); + + queryCriteriaEntries.add(new CriteriaEntry(OperationKey.LESS_EQUAL, upperBound)); + return this; + } + + /** + * Add a {@link OperationKey#LESS} entry to the {@link #queryCriteriaEntries} + * + * @param upperBound the argument to the operation + * @return this object + */ + public Criteria lessThan(Object upperBound) { + + Assert.notNull(upperBound, "upperBound must not be null"); + + queryCriteriaEntries.add(new CriteriaEntry(OperationKey.LESS, upperBound)); + return this; + } + + /** + * Add a {@link OperationKey#GREATER_EQUAL} entry to the {@link #queryCriteriaEntries} + * + * @param lowerBound the argument to the operation + * @return this object + */ + public Criteria greaterThanEqual(Object lowerBound) { + + Assert.notNull(lowerBound, "lowerBound must not be null"); + + queryCriteriaEntries.add(new CriteriaEntry(OperationKey.GREATER_EQUAL, lowerBound)); + return this; + } + + /** + * Add a {@link OperationKey#GREATER} entry to the {@link #queryCriteriaEntries} + * + * @param lowerBound the argument to the operation + * @return this object + */ + public Criteria greaterThan(Object lowerBound) { + + Assert.notNull(lowerBound, "lowerBound must not be null"); + + queryCriteriaEntries.add(new CriteriaEntry(OperationKey.GREATER, lowerBound)); + return this; + } + + /** + * 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 matches(Object value) { + + Assert.notNull(value, "value must not be null"); + + queryCriteriaEntries.add(new CriteriaEntry(OperationKey.MATCHES, value)); + return this; + } + + /** + * Add a {@link OperationKey#MATCHES} entry to the {@link #queryCriteriaEntries}. This will build a match query with + * the AND operator. + * + * @param value the value to match + * @return this object + * @since 4.1 + */ + 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 + /** + * 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 this object + */ + public Criteria boundedBy(GeoBox boundingBox) { + + Assert.notNull(boundingBox, "boundingBox value for boundedBy criteria must not be null"); + + filterCriteriaEntries.add(new CriteriaEntry(OperationKey.BBOX, new Object[] { boundingBox })); + return this; + } + + /** + * 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 this object + */ + public Criteria boundedBy(Box boundingBox) { + + Assert.notNull(boundingBox, "boundingBox value for boundedBy criteria must not be null"); + + filterCriteriaEntries + .add(new CriteriaEntry(OperationKey.BBOX, new Object[] { boundingBox.getFirst(), boundingBox.getSecond() })); + return this; + } + + /** + * 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 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"); + + filterCriteriaEntries + .add(new CriteriaEntry(OperationKey.BBOX, new Object[] { topLeftGeohash, bottomRightGeohash })); + 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(GeoPoint topLeftPoint, GeoPoint bottomRightPoint) { + + Assert.notNull(topLeftPoint, "topLeftPoint must not be null"); + Assert.notNull(bottomRightPoint, "bottomRightPoint must not be null"); + + 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"); + + filterCriteriaEntries.add(new CriteriaEntry(OperationKey.BBOX, + new Object[] { GeoPoint.fromPoint(topLeftPoint), GeoPoint.fromPoint(bottomRightPoint) })); + return this; + } + + /** + * Adds a new filter CriteriaEntry for {@code location WITHIN distance} + * + * @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 + */ + 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"); + + filterCriteriaEntries.add(new CriteriaEntry(OperationKey.WITHIN, new Object[] { location, distance })); + return this; + } + + /** + * 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; + } + + /** + * Adds a new filter CriteriaEntry for {@code geoLocation WITHIN distance} + * + * @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 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; + } + + // 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."); + } + } + private List toCollection(Object... values) { if (values.length == 0 || (values.length > 1 && values[1] instanceof Collection)) { throw new InvalidDataAccessApiUsageException( @@ -401,208 +751,102 @@ public class Criteria { return Arrays.asList(values); } - public Criteria notIn(Object... values) { - return notIn(toCollection(values)); + // 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 Criteria notIn(Iterable values) { - Assert.notNull(values, "Collection of 'NotIn' values must not be null"); - queryCriteria.add(new CriteriaEntry(OperationKey.NOT_IN, values)); - return this; + @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; } - /** - * Creates new CriteriaEntry for {@code location WITHIN distance} - * - * @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. - */ - 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 })); - 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. - */ - 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 })); - return this; - } - - /** - * Creates new CriteriaEntry for {@code geoLocation WITHIN distance} - * - * @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 - */ - 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 })); - return this; - } - - /** - * Creates new 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. - */ - 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 })); - return this; - } - - /** - * Creates new 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. - */ - public Criteria boundedBy(Box boundingBox) { - Assert.notNull(boundingBox, "boundingBox value for boundedBy criteria must not be null"); - filterCriteria - .add(new CriteriaEntry(OperationKey.BBOX, new Object[] { boundingBox.getFirst(), boundingBox.getSecond() })); - return this; - } - - /** - * Creates new 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. - */ - 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 })); - return this; - } - - /** - * Creates new 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. - */ - 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 })); - return this; - } - - 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, - 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 - * - * @return - */ - @Nullable - public Field getField() { - return this.field; - } - - public Set getQueryCriteriaEntries() { - return Collections.unmodifiableSet(this.queryCriteria); - } - - public Set getFilterCriteriaEntries() { - return Collections.unmodifiableSet(this.filterCriteria); - } - - public Set getFilterCriteria() { - return filterCriteria; - } - - /** - * Conjunction to be used with this criteria (AND | OR) - * - * @return - */ - public String getConjunctionOperator() { - return AND_OPERATOR; - } - - public List getCriteriaChain() { - return Collections.unmodifiableList(this.criteriaChain); - } - - public boolean isNegating() { - return this.negating; - } - - public boolean isAnd() { - return AND_OPERATOR == getConjunctionOperator(); - } - - public boolean isOr() { - return OR_OPERATOR == getConjunctionOperator(); - } - - 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, String fieldName) { + super(criteriaChain, fieldName); + } + public OrCriteria(List criteriaChain, Field field) { super(criteriaChain, field); } - public OrCriteria(List criteriaChain, String fieldname) { - super(criteriaChain, fieldname); - } - - 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 class Criteria { 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 class Criteria { /** * @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; - private Object value; + protected CriteriaEntry(OperationKey key) { + + 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 class Criteria { } 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 bf4503492..f9fed6a69 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 @@ package org.springframework.data.elasticsearch.core.query; 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 8eac98132..97567dec3 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 @@ public class ElasticsearchQueryCreator extends AbstractQueryCreator 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 d7a6c913f..00d49da8c 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 @@ public class CriteriaQueryMappingTests { } - @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 @@ public class CriteriaQueryMappingTests { '}'; // 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 000000000..ccf07c596 --- /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 516c64b5f..149a01f1b 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 @@ public class CriteriaQueryTests { 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(); + + // when CriteriaQuery criteriaQuery = new CriteriaQuery( new Criteria("message").contains("test").and("message").contains("some")); - - // when - SearchHit sampleEntity1 = operations.searchOne(criteriaQuery, SampleEntity.class, index); + 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 class CriteriaQueryTests { 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 df787404e..dd55e4088 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 class ReactiveElasticsearchStringQueryUnitTests { 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 class ReactiveElasticsearchStringQueryUnitTests { 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);