DATAES-239 - Query for null values.

Original PR: #355
This commit is contained in:
Peter-Josef Meisch 2019-12-11 22:09:02 +01:00 committed by GitHub
parent 11a6430a90
commit dec5231a05
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 125 additions and 80 deletions

View File

@ -41,6 +41,7 @@ import org.springframework.util.Assert;
* @author Artur Konczak
* @author Rasmus Faber-Espensen
* @author James Bodkin
* @author Peter-Josef Meisch
*/
class CriteriaQueryProcessor {
@ -134,17 +135,23 @@ class CriteriaQueryProcessor {
return query;
}
private QueryBuilder processCriteriaEntry(Criteria.CriteriaEntry entry,
/* OperationKey key, Object value,*/ String fieldName) {
Object value = entry.getValue();
if (value == null) {
return null;
}
private QueryBuilder processCriteriaEntry(Criteria.CriteriaEntry entry, String fieldName) {
OperationKey key = entry.getKey();
QueryBuilder query = null;
Object value = entry.getValue();
if (value == null) {
if (key == OperationKey.EXISTS) {
return existsQuery(fieldName);
} else {
return null;
}
}
String searchText = QueryParserUtil.escape(value.toString());
QueryBuilder query = null;
switch (key) {
case EQUALS:
query = queryStringQuery(searchText).field(fieldName).defaultOperator(AND);

View File

@ -15,10 +15,14 @@
*/
package org.springframework.data.elasticsearch.core.query;
import java.util.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import org.springframework.dao.InvalidDataAccessApiUsageException;
import org.springframework.data.elasticsearch.core.geo.GeoBox;
import org.springframework.data.elasticsearch.core.geo.GeoPoint;
@ -26,6 +30,8 @@ import org.springframework.data.geo.Box;
import org.springframework.data.geo.Distance;
import org.springframework.data.geo.Point;
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
@ -34,18 +40,15 @@ import org.springframework.util.Assert;
* @author Rizwan Idrees
* @author Mohsin Husen
* @author Franck Marchand
* @author Peter-Josef Meisch
*/
public class Criteria {
@Override
public String toString() {
return "Criteria{" +
"field=" + field.getName() +
", boost=" + boost +
", negating=" + negating +
", queryCriteria=" + ObjectUtils.nullSafeToString(queryCriteria) +
", filterCriteria=" + ObjectUtils.nullSafeToString(filterCriteria) +
'}';
return "Criteria{" + "field=" + field.getName() + ", boost=" + boost + ", negating=" + negating + ", queryCriteria="
+ ObjectUtils.nullSafeToString(queryCriteria) + ", filterCriteria="
+ ObjectUtils.nullSafeToString(filterCriteria) + '}';
}
public static final String WILDCARD = "*";
@ -64,8 +67,7 @@ public class Criteria {
private Set<CriteriaEntry> filterCriteria = new LinkedHashSet<>();
public Criteria() {
}
public Criteria() {}
/**
* Creates a new Criteria with provided field name
@ -209,6 +211,17 @@ public class Criteria {
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 <br/>
* <strong>NOTE: </strong> mind your schema as leading wildcards may not be supported and/or execution might be slow.
@ -305,7 +318,7 @@ public class Criteria {
throw new InvalidDataAccessApiUsageException("Range [* TO *] is not allowed");
}
queryCriteria.add(new CriteriaEntry(OperationKey.BETWEEN, new Object[]{lowerBound, upperBound}));
queryCriteria.add(new CriteriaEntry(OperationKey.BETWEEN, new Object[] { lowerBound, upperBound }));
return this;
}
@ -377,9 +390,9 @@ public class Criteria {
private List<Object> toCollection(Object... values) {
if (values.length == 0 || (values.length > 1 && values[1] instanceof Collection)) {
throw new InvalidDataAccessApiUsageException("At least one element "
+ (values.length > 0 ? ("of argument of type " + values[1].getClass().getName()) : "")
+ " has to be present.");
throw new InvalidDataAccessApiUsageException(
"At least one element " + (values.length > 0 ? ("of argument of type " + values[1].getClass().getName()) : "")
+ " has to be present.");
}
return Arrays.asList(values);
}
@ -398,15 +411,14 @@ public class Criteria {
* 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
* @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}));
filterCriteria.add(new CriteriaEntry(OperationKey.WITHIN, new Object[] { location, distance }));
return this;
}
@ -414,56 +426,54 @@ public class Criteria {
* 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
* .
* @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}));
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
* @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}));
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)
* @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}));
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)
* @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()}));
filterCriteria
.add(new CriteriaEntry(OperationKey.BBOX, new Object[] { boundingBox.getFirst(), boundingBox.getSecond() }));
return this;
}
@ -477,7 +487,7 @@ public class Criteria {
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}));
filterCriteria.add(new CriteriaEntry(OperationKey.BBOX, new Object[] { topLeftGeohash, bottomRightGeohash }));
return this;
}
@ -491,14 +501,15 @@ public class Criteria {
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}));
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)}));
filterCriteria.add(new CriteriaEntry(OperationKey.BBOX,
new Object[] { GeoPoint.fromPoint(topLeftPoint), GeoPoint.fromPoint(bottomRightPoint) }));
return this;
}
@ -587,8 +598,27 @@ public class Criteria {
}
}
public enum OperationKey {
EQUALS, CONTAINS, STARTS_WITH, ENDS_WITH, EXPRESSION, BETWEEN, FUZZY, IN, NOT_IN, WITHIN, BBOX, NEAR, LESS, LESS_EQUAL, GREATER, GREATER_EQUAL;
public enum OperationKey { //
EQUALS, //
CONTAINS, //
STARTS_WITH, //
ENDS_WITH, //
EXPRESSION, //
BETWEEN, //
FUZZY, //
IN, //
NOT_IN, //
WITHIN, //
BBOX, //
NEAR, //
LESS, //
LESS_EQUAL, //
GREATER, //
GREATER_EQUAL, //
/**
* @since 4.0
*/
EXISTS;
}
public static class CriteriaEntry {
@ -611,10 +641,7 @@ public class Criteria {
@Override
public String toString() {
return "CriteriaEntry{" +
"key=" + key +
", value=" + value +
'}';
return "CriteriaEntry{" + "key=" + key + ", value=" + value + '}';
}
}
}

View File

@ -138,9 +138,14 @@ public class ElasticsearchQueryCreator extends AbstractQueryCreator<CriteriaQuer
Object firstParameter = parameters.next();
Object secondParameter = null;
if (type == Part.Type.SIMPLE_PROPERTY) {
if (part.getProperty().getType() != GeoPoint.class)
return criteria.is(firstParameter);
else {
if (part.getProperty().getType() != GeoPoint.class) {
if (firstParameter != null) {
return criteria.is(firstParameter);
} else {
// searching for null is a must_not (exists)
return criteria.exists().not();
}
} else {
// it means it's a simple find with exact geopoint matching (e.g. findByLocation)
// and because Elasticsearch does not have any kind of query with just a geopoint
// as argument we use a "geo distance" query with a distance of one meter.

View File

@ -24,7 +24,6 @@ import lombok.NoArgsConstructor;
import lombok.Setter;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.stream.Collectors;
@ -43,6 +42,7 @@ import org.springframework.data.elasticsearch.junit.jupiter.SpringIntegrationTes
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;
import org.springframework.data.elasticsearch.repository.config.EnableElasticsearchRepositories;
import org.springframework.data.elasticsearch.utils.IndexInitializer;
import org.springframework.lang.Nullable;
import org.springframework.test.context.ContextConfiguration;
/**
@ -81,10 +81,10 @@ class QueryKeywordsTests {
.sortName("sort2").build();
Product product5 = Product.builder().id("5").name("Salt").text("Sea salt").price(2.1f).available(false)
.sortName("sort1").build();
Product product6 = Product.builder().id("6").name(null).text("no name").price(3.4f).available(false)
.sortName("sort0").build();
repository.saveAll(Arrays.asList(product1, product2, product3, product4, product5));
elasticsearchTemplate.refresh(Product.class);
repository.saveAll(Arrays.asList(product1, product2, product3, product4, product5, product6));
}
@Test
@ -120,7 +120,7 @@ class QueryKeywordsTests {
// then
assertThat(repository.findByAvailableTrue()).hasSize(3);
assertThat(repository.findByAvailableFalse()).hasSize(2);
assertThat(repository.findByAvailableFalse()).hasSize(3);
}
@Test
@ -132,8 +132,8 @@ class QueryKeywordsTests {
// then
assertThat(repository.findByPriceIn(Arrays.asList(1.2f, 1.1f))).hasSize(2);
assertThat(repository.findByPriceNotIn(Arrays.asList(1.2f, 1.1f))).hasSize(3);
assertThat(repository.findByPriceNot(1.2f)).hasSize(4);
assertThat(repository.findByPriceNotIn(Arrays.asList(1.2f, 1.1f))).hasSize(4);
assertThat(repository.findByPriceNot(1.2f)).hasSize(5);
}
@Test // DATAES-171
@ -144,7 +144,7 @@ class QueryKeywordsTests {
// when
// then
assertThat(repository.findByIdNotIn(Arrays.asList("2", "3"))).hasSize(3);
assertThat(repository.findByIdNotIn(Arrays.asList("2", "3"))).hasSize(4);
}
@Test
@ -169,8 +169,8 @@ class QueryKeywordsTests {
assertThat(repository.findByPriceLessThan(1.1f)).hasSize(1);
assertThat(repository.findByPriceLessThanEqual(1.1f)).hasSize(2);
assertThat(repository.findByPriceGreaterThan(1.9f)).hasSize(1);
assertThat(repository.findByPriceGreaterThanEqual(1.9f)).hasSize(2);
assertThat(repository.findByPriceGreaterThan(1.9f)).hasSize(2);
assertThat(repository.findByPriceGreaterThanEqual(1.9f)).hasSize(3);
}
@Test // DATAES-615
@ -195,7 +195,7 @@ class QueryKeywordsTests {
List<String> sortedIds = repository.findAllByOrderByText().stream() //
.map(it -> it.text).collect(Collectors.toList());
assertThat(sortedIds).containsExactly("Beet sugar", "Cane sugar", "Cane sugar", "Rock salt", "Sea salt");
assertThat(sortedIds).containsExactly("Beet sugar", "Cane sugar", "Cane sugar", "Rock salt", "Sea salt", "no name");
}
@Test // DATAES-615
@ -204,7 +204,7 @@ class QueryKeywordsTests {
List<String> sortedIds = repository.findAllByOrderBySortName().stream() //
.map(it -> it.id).collect(Collectors.toList());
assertThat(sortedIds).containsExactly("5", "4", "3", "2", "1");
assertThat(sortedIds).containsExactly("6", "5", "4", "3", "2", "1");
}
@Test // DATAES-178
@ -241,6 +241,22 @@ class QueryKeywordsTests {
products.forEach(product -> assertThat(product.name).isEqualTo("Sugar"));
}
@Test
void shouldSearchForNullValues() {
final List<Product> products = repository.findByName(null);
assertThat(products).hasSize(1);
assertThat(products.get(0).getId()).isEqualTo("6");
}
@Test
void shouldDeleteWithNullValues() {
repository.deleteByName(null);
long count = repository.count();
assertThat(count).isEqualTo(5);
}
/**
* @author Mohsin Husen
* @author Artur Konczak
@ -256,28 +272,14 @@ class QueryKeywordsTests {
@Id private String id;
private List<String> title;
private String name;
private String description;
@Field(type = FieldType.Keyword) private String text;
private List<String> categories;
private Float weight;
@Field(type = FieldType.Float) private Float price;
private Integer popularity;
private boolean available;
private String location;
private Date lastModified;
@Field(name = "sort-name", type = FieldType.Keyword) private String sortName;
}
@ -286,6 +288,8 @@ class QueryKeywordsTests {
*/
interface ProductRepository extends ElasticsearchRepository<Product, String> {
List<Product> findByName(@Nullable String name);
List<Product> findByNameAndText(String name, String text);
List<Product> findByNameAndPrice(String name, Float price);
@ -331,6 +335,8 @@ class QueryKeywordsTests {
List<Product> findFirst2ByName(String name);
List<Product> findTop2ByName(String name);
void deleteByName(@Nullable String name);
}
}