mirror of
https://github.com/spring-projects/spring-data-elasticsearch.git
synced 2025-06-22 20:12:11 +00:00
parent
11a6430a90
commit
dec5231a05
@ -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);
|
||||
|
@ -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 + '}';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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.
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user