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 Artur Konczak
* @author Rasmus Faber-Espensen * @author Rasmus Faber-Espensen
* @author James Bodkin * @author James Bodkin
* @author Peter-Josef Meisch
*/ */
class CriteriaQueryProcessor { class CriteriaQueryProcessor {
@ -134,17 +135,23 @@ class CriteriaQueryProcessor {
return query; return query;
} }
private QueryBuilder processCriteriaEntry(Criteria.CriteriaEntry entry, private QueryBuilder processCriteriaEntry(Criteria.CriteriaEntry entry, String fieldName) {
/* OperationKey key, Object value,*/ String fieldName) {
Object value = entry.getValue();
if (value == null) {
return null;
}
OperationKey key = entry.getKey(); 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()); String searchText = QueryParserUtil.escape(value.toString());
QueryBuilder query = null;
switch (key) { switch (key) {
case EQUALS: case EQUALS:
query = queryStringQuery(searchText).field(fieldName).defaultOperator(AND); query = queryStringQuery(searchText).field(fieldName).defaultOperator(AND);

View File

@ -15,10 +15,14 @@
*/ */
package org.springframework.data.elasticsearch.core.query; 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.dao.InvalidDataAccessApiUsageException;
import org.springframework.data.elasticsearch.core.geo.GeoBox; import org.springframework.data.elasticsearch.core.geo.GeoBox;
import org.springframework.data.elasticsearch.core.geo.GeoPoint; 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.Distance;
import org.springframework.data.geo.Point; import org.springframework.data.geo.Point;
import org.springframework.util.Assert; 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 * 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 Rizwan Idrees
* @author Mohsin Husen * @author Mohsin Husen
* @author Franck Marchand * @author Franck Marchand
* @author Peter-Josef Meisch
*/ */
public class Criteria { public class Criteria {
@Override @Override
public String toString() { public String toString() {
return "Criteria{" + return "Criteria{" + "field=" + field.getName() + ", boost=" + boost + ", negating=" + negating + ", queryCriteria="
"field=" + field.getName() + + ObjectUtils.nullSafeToString(queryCriteria) + ", filterCriteria="
", boost=" + boost + + ObjectUtils.nullSafeToString(filterCriteria) + '}';
", negating=" + negating +
", queryCriteria=" + ObjectUtils.nullSafeToString(queryCriteria) +
", filterCriteria=" + ObjectUtils.nullSafeToString(filterCriteria) +
'}';
} }
public static final String WILDCARD = "*"; public static final String WILDCARD = "*";
@ -64,8 +67,7 @@ public class Criteria {
private Set<CriteriaEntry> filterCriteria = new LinkedHashSet<>(); private Set<CriteriaEntry> filterCriteria = new LinkedHashSet<>();
public Criteria() { public Criteria() {}
}
/** /**
* Creates a new Criteria with provided field name * Creates a new Criteria with provided field name
@ -209,6 +211,17 @@ public class Criteria {
return this; 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/> * 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. * <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"); 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; return this;
} }
@ -377,9 +390,9 @@ public class Criteria {
private List<Object> toCollection(Object... values) { private List<Object> toCollection(Object... values) {
if (values.length == 0 || (values.length > 1 && values[1] instanceof Collection)) { if (values.length == 0 || (values.length > 1 && values[1] instanceof Collection)) {
throw new InvalidDataAccessApiUsageException("At least one element " throw new InvalidDataAccessApiUsageException(
+ (values.length > 0 ? ("of argument of type " + values[1].getClass().getName()) : "") "At least one element " + (values.length > 0 ? ("of argument of type " + values[1].getClass().getName()) : "")
+ " has to be present."); + " has to be present.");
} }
return Arrays.asList(values); return Arrays.asList(values);
} }
@ -398,15 +411,14 @@ public class Criteria {
* Creates new CriteriaEntry for {@code location WITHIN distance} * Creates new CriteriaEntry for {@code location WITHIN distance}
* *
* @param location {@link org.springframework.data.elasticsearch.core.geo.GeoPoint} center coordinates * @param location {@link org.springframework.data.elasticsearch.core.geo.GeoPoint} center coordinates
* @param distance {@link String} radius as a string (e.g. : '100km'). * @param distance {@link String} radius as a string (e.g. : '100km'). Distance unit : either mi/miles or km can be
* Distance unit : * set
* either mi/miles or km can be set
* @return Criteria the chaind criteria with the new 'within' criteria included. * @return Criteria the chaind criteria with the new 'within' criteria included.
*/ */
public Criteria within(GeoPoint location, String distance) { public Criteria within(GeoPoint location, String distance) {
Assert.notNull(location, "Location value for near criteria must not be null"); Assert.notNull(location, "Location value for near criteria must not be null");
Assert.notNull(location, "Distance 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; return this;
} }
@ -414,56 +426,54 @@ public class Criteria {
* Creates new CriteriaEntry for {@code location WITHIN distance} * Creates new CriteriaEntry for {@code location WITHIN distance}
* *
* @param location {@link org.springframework.data.geo.Point} center coordinates * @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. * @return Criteria the chaind criteria with the new 'within' criteria included.
*/ */
public Criteria within(Point location, Distance distance) { public Criteria within(Point location, Distance distance) {
Assert.notNull(location, "Location value for near criteria must not be null"); Assert.notNull(location, "Location value for near criteria must not be null");
Assert.notNull(location, "Distance 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; return this;
} }
/** /**
* Creates new CriteriaEntry for {@code geoLocation WITHIN distance} * Creates new CriteriaEntry for {@code geoLocation WITHIN distance}
* *
* @param geoLocation {@link String} center point * @param geoLocation {@link String} center point supported formats: lat on = > "41.2,45.1", geohash = > "asd9as0d"
* supported formats: * @param distance {@link String} radius as a string (e.g. : '100km'). Distance unit : either mi/miles or km can be
* lat on = > "41.2,45.1", * set
* geohash = > "asd9as0d"
* @param distance {@link String} radius as a string (e.g. : '100km').
* Distance unit :
* either mi/miles or km can be set
* @return * @return
*/ */
public Criteria within(String geoLocation, String distance) { public Criteria within(String geoLocation, String distance) {
Assert.isTrue(!StringUtils.isEmpty(geoLocation), "geoLocation value must not be null"); 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; return this;
} }
/** /**
* Creates new CriteriaEntry for {@code location GeoBox bounding box} * 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. * @return Criteria the chaind criteria with the new 'boundingBox' criteria included.
*/ */
public Criteria boundedBy(GeoBox boundingBox) { public Criteria boundedBy(GeoBox boundingBox) {
Assert.notNull(boundingBox, "boundingBox value for boundedBy criteria must not be null"); 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; return this;
} }
/** /**
* Creates new CriteriaEntry for {@code location Box bounding box} * 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. * @return Criteria the chaind criteria with the new 'boundingBox' criteria included.
*/ */
public Criteria boundedBy(Box boundingBox) { public Criteria boundedBy(Box boundingBox) {
Assert.notNull(boundingBox, "boundingBox value for boundedBy criteria must not be null"); 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; return this;
} }
@ -477,7 +487,7 @@ public class Criteria {
public Criteria boundedBy(String topLeftGeohash, String bottomRightGeohash) { public Criteria boundedBy(String topLeftGeohash, String bottomRightGeohash) {
Assert.isTrue(!StringUtils.isEmpty(topLeftGeohash), "topLeftGeohash must not be empty"); Assert.isTrue(!StringUtils.isEmpty(topLeftGeohash), "topLeftGeohash must not be empty");
Assert.isTrue(!StringUtils.isEmpty(bottomRightGeohash), "bottomRightGeohash 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; return this;
} }
@ -491,14 +501,15 @@ public class Criteria {
public Criteria boundedBy(GeoPoint topLeftPoint, GeoPoint bottomRightPoint) { public Criteria boundedBy(GeoPoint topLeftPoint, GeoPoint bottomRightPoint) {
Assert.notNull(topLeftPoint, "topLeftPoint must not be null"); Assert.notNull(topLeftPoint, "topLeftPoint must not be null");
Assert.notNull(bottomRightPoint, "bottomRightPoint 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; return this;
} }
public Criteria boundedBy(Point topLeftPoint, Point bottomRightPoint) { public Criteria boundedBy(Point topLeftPoint, Point bottomRightPoint) {
Assert.notNull(topLeftPoint, "topLeftPoint must not be null"); Assert.notNull(topLeftPoint, "topLeftPoint must not be null");
Assert.notNull(bottomRightPoint, "bottomRightPoint 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; return this;
} }
@ -587,8 +598,27 @@ public class Criteria {
} }
} }
public enum OperationKey { public enum OperationKey { //
EQUALS, CONTAINS, STARTS_WITH, ENDS_WITH, EXPRESSION, BETWEEN, FUZZY, IN, NOT_IN, WITHIN, BBOX, NEAR, LESS, LESS_EQUAL, GREATER, GREATER_EQUAL; 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 { public static class CriteriaEntry {
@ -611,10 +641,7 @@ public class Criteria {
@Override @Override
public String toString() { public String toString() {
return "CriteriaEntry{" + return "CriteriaEntry{" + "key=" + key + ", value=" + value + '}';
"key=" + key +
", value=" + value +
'}';
} }
} }
} }

View File

@ -138,9 +138,14 @@ public class ElasticsearchQueryCreator extends AbstractQueryCreator<CriteriaQuer
Object firstParameter = parameters.next(); Object firstParameter = parameters.next();
Object secondParameter = null; Object secondParameter = null;
if (type == Part.Type.SIMPLE_PROPERTY) { if (type == Part.Type.SIMPLE_PROPERTY) {
if (part.getProperty().getType() != GeoPoint.class) if (part.getProperty().getType() != GeoPoint.class) {
return criteria.is(firstParameter); if (firstParameter != null) {
else { 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) // 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 // 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. // 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 lombok.Setter;
import java.util.Arrays; import java.util.Arrays;
import java.util.Date;
import java.util.List; import java.util.List;
import java.util.stream.Collectors; 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.ElasticsearchRepository;
import org.springframework.data.elasticsearch.repository.config.EnableElasticsearchRepositories; import org.springframework.data.elasticsearch.repository.config.EnableElasticsearchRepositories;
import org.springframework.data.elasticsearch.utils.IndexInitializer; import org.springframework.data.elasticsearch.utils.IndexInitializer;
import org.springframework.lang.Nullable;
import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.ContextConfiguration;
/** /**
@ -81,10 +81,10 @@ class QueryKeywordsTests {
.sortName("sort2").build(); .sortName("sort2").build();
Product product5 = Product.builder().id("5").name("Salt").text("Sea salt").price(2.1f).available(false) Product product5 = Product.builder().id("5").name("Salt").text("Sea salt").price(2.1f).available(false)
.sortName("sort1").build(); .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)); repository.saveAll(Arrays.asList(product1, product2, product3, product4, product5, product6));
elasticsearchTemplate.refresh(Product.class);
} }
@Test @Test
@ -120,7 +120,7 @@ class QueryKeywordsTests {
// then // then
assertThat(repository.findByAvailableTrue()).hasSize(3); assertThat(repository.findByAvailableTrue()).hasSize(3);
assertThat(repository.findByAvailableFalse()).hasSize(2); assertThat(repository.findByAvailableFalse()).hasSize(3);
} }
@Test @Test
@ -132,8 +132,8 @@ class QueryKeywordsTests {
// then // then
assertThat(repository.findByPriceIn(Arrays.asList(1.2f, 1.1f))).hasSize(2); assertThat(repository.findByPriceIn(Arrays.asList(1.2f, 1.1f))).hasSize(2);
assertThat(repository.findByPriceNotIn(Arrays.asList(1.2f, 1.1f))).hasSize(3); assertThat(repository.findByPriceNotIn(Arrays.asList(1.2f, 1.1f))).hasSize(4);
assertThat(repository.findByPriceNot(1.2f)).hasSize(4); assertThat(repository.findByPriceNot(1.2f)).hasSize(5);
} }
@Test // DATAES-171 @Test // DATAES-171
@ -144,7 +144,7 @@ class QueryKeywordsTests {
// when // when
// then // then
assertThat(repository.findByIdNotIn(Arrays.asList("2", "3"))).hasSize(3); assertThat(repository.findByIdNotIn(Arrays.asList("2", "3"))).hasSize(4);
} }
@Test @Test
@ -169,8 +169,8 @@ class QueryKeywordsTests {
assertThat(repository.findByPriceLessThan(1.1f)).hasSize(1); assertThat(repository.findByPriceLessThan(1.1f)).hasSize(1);
assertThat(repository.findByPriceLessThanEqual(1.1f)).hasSize(2); assertThat(repository.findByPriceLessThanEqual(1.1f)).hasSize(2);
assertThat(repository.findByPriceGreaterThan(1.9f)).hasSize(1); assertThat(repository.findByPriceGreaterThan(1.9f)).hasSize(2);
assertThat(repository.findByPriceGreaterThanEqual(1.9f)).hasSize(2); assertThat(repository.findByPriceGreaterThanEqual(1.9f)).hasSize(3);
} }
@Test // DATAES-615 @Test // DATAES-615
@ -195,7 +195,7 @@ class QueryKeywordsTests {
List<String> sortedIds = repository.findAllByOrderByText().stream() // List<String> sortedIds = repository.findAllByOrderByText().stream() //
.map(it -> it.text).collect(Collectors.toList()); .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 @Test // DATAES-615
@ -204,7 +204,7 @@ class QueryKeywordsTests {
List<String> sortedIds = repository.findAllByOrderBySortName().stream() // List<String> sortedIds = repository.findAllByOrderBySortName().stream() //
.map(it -> it.id).collect(Collectors.toList()); .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 @Test // DATAES-178
@ -241,6 +241,22 @@ class QueryKeywordsTests {
products.forEach(product -> assertThat(product.name).isEqualTo("Sugar")); 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 Mohsin Husen
* @author Artur Konczak * @author Artur Konczak
@ -256,28 +272,14 @@ class QueryKeywordsTests {
@Id private String id; @Id private String id;
private List<String> title;
private String name; private String name;
private String description;
@Field(type = FieldType.Keyword) private String text; @Field(type = FieldType.Keyword) private String text;
private List<String> categories;
private Float weight;
@Field(type = FieldType.Float) private Float price; @Field(type = FieldType.Float) private Float price;
private Integer popularity;
private boolean available; private boolean available;
private String location;
private Date lastModified;
@Field(name = "sort-name", type = FieldType.Keyword) private String sortName; @Field(name = "sort-name", type = FieldType.Keyword) private String sortName;
} }
@ -286,6 +288,8 @@ class QueryKeywordsTests {
*/ */
interface ProductRepository extends ElasticsearchRepository<Product, String> { interface ProductRepository extends ElasticsearchRepository<Product, String> {
List<Product> findByName(@Nullable String name);
List<Product> findByNameAndText(String name, String text); List<Product> findByNameAndText(String name, String text);
List<Product> findByNameAndPrice(String name, Float price); List<Product> findByNameAndPrice(String name, Float price);
@ -331,6 +335,8 @@ class QueryKeywordsTests {
List<Product> findFirst2ByName(String name); List<Product> findFirst2ByName(String name);
List<Product> findTop2ByName(String name); List<Product> findTop2ByName(String name);
void deleteByName(@Nullable String name);
} }
} }