DATAES-706 - CriteriaQueryProcessor must handle nested Criteria definitions.

Original PR: #505
This commit is contained in:
Peter-Josef Meisch 2020-08-18 20:59:35 +02:00 committed by GitHub
parent c8c6e7a646
commit 131f0318cc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 1426 additions and 631 deletions

View File

@ -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 <<repositories>>).
@ -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[]

View File

@ -45,7 +45,8 @@ public class TransportClientConfig extends ElasticsearchConfigurationSupport {
}
}
----
<1> Setting up the <<elasticsearch.clients.transport>>. Deprecated as of version 4.0.
<1> Setting up the <<elasticsearch.clients.transport>>.
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<T>
Contains the following information:
@ -155,3 +161,108 @@ Returned by the low level scroll API functions in `ElasticsearchRestTemplate`, i
.SearchHitsIterator<T>
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<Person> 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<Person> searchHits = operations.search(query, Person.class);
----
====

View File

@ -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<QueryBuilder> fbList = new LinkedList<>();
QueryBuilder filter = null;
@Nullable
QueryBuilder createFilter(Criteria criteria) {
List<QueryBuilder> 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<QueryBuilder> 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<QueryBuilder> createFilterFragmentForCriteria(Criteria chainedCriteria) {
Iterator<Criteria.CriteriaEntry> it = chainedCriteria.getFilterCriteriaEntries().iterator();
List<QueryBuilder> filterList = new LinkedList<>();
private List<QueryBuilder> 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);
}

View File

@ -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<QueryBuilder> shouldQueryBuilderList = new LinkedList<>();
List<QueryBuilder> mustNotQueryBuilderList = new LinkedList<>();
List<QueryBuilder> mustQueryBuilderList = new LinkedList<>();
ListIterator<Criteria> chainIterator = criteria.getCriteriaChain().listIterator();
List<QueryBuilder> shouldQueryBuilders = new ArrayList<>();
List<QueryBuilder> mustNotQueryBuilders = new ArrayList<>();
List<QueryBuilder> 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<Criteria.CriteriaEntry> 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<Criteria.CriteriaEntry> 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<Object>) 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<Object>) 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<String> toStringList(Iterable<?> iterable) {
List<String> 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);
}
}

View File

@ -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 {

View File

@ -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

View File

@ -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();
}
}

View File

@ -96,13 +96,10 @@ public class ElasticsearchQueryCreator extends AbstractQueryCreator<CriteriaQuer
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);

View File

@ -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);
}

View File

@ -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);
}
}

View File

@ -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<SampleEntity> sampleEntity1 = operations.searchOne(criteriaQuery, SampleEntity.class, index);
SearchHit<SampleEntity> 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<IndexQuery> 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<SampleEntity> 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<IndexQuery> 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<SampleEntity> 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<IndexQuery> 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<SampleEntity> 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

View File

@ -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);