Skip to content

Commit

Permalink
Add repository search for nullable or empty properties.
Browse files Browse the repository at this point in the history
Original Pull Request #1946 
Closes #1909
  • Loading branch information
sothawo authored Sep 25, 2021
1 parent b8ae9b4 commit 175e7b5
Show file tree
Hide file tree
Showing 7 changed files with 417 additions and 22 deletions.
24 changes: 20 additions & 4 deletions src/main/asciidoc/reference/elasticsearch-repository-queries.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -242,10 +242,6 @@ A list of supported keywords for Elasticsearch is shown below.
| `findByNameNotIn(Collection<String>names)`
| `{"query": {"bool": {"must": [{"query_string": {"query": "NOT(\"?\" \"?\")", "fields": ["name"]}}]}}}`

| `Near`
| `findByStoreNear`
| `Not Supported Yet !`

| `True`
| `findByAvailableTrue`
| `{ "query" : {
Expand Down Expand Up @@ -277,6 +273,26 @@ A list of supported keywords for Elasticsearch is shown below.
}, "sort":[{"name":{"order":"desc"}}]
}`

| `Exists`
| `findByNameExists`
| `{"query":{"bool":{"must":[{"exists":{"field":"name"}}]}}}`

| `IsNull`
| `findByNameIsNull`
| `{"query":{"bool":{"must_not":[{"exists":{"field":"name"}}]}}}`

| `IsNotNull`
| `findByNameIsNotNull`
| `{"query":{"bool":{"must":[{"exists":{"field":"name"}}]}}}`

| `IsEmpty`
| `findByNameIsEmpty`
| `{"query":{"bool":{"must":[{"bool":{"must":[{"exists":{"field":"name"}}],"must_not":[{"wildcard":{"name":{"wildcard":"*"}}}]}}]}}}`

| `IsNotEmpty`
| `findByNameIsNotEmpty`
| `{"query":{"bool":{"must":[{"wildcard":{"name":{"wildcard":"*"}}}]}}}`

|===

NOTE: Methods names to build Geo-shape queries taking `GeoJson` parameters are not supported.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -165,20 +165,35 @@ private QueryBuilder queryForEntries(Criteria criteria) {
@Nullable
private QueryBuilder queryFor(Criteria.CriteriaEntry entry, Field field) {

QueryBuilder query = null;
String fieldName = field.getName();
boolean isKeywordField = FieldType.Keyword == field.getFieldType();

OperationKey key = entry.getKey();

if (key == OperationKey.EXISTS) {
return existsQuery(fieldName);
// operations without a value
switch (key) {
case EXISTS:
query = existsQuery(fieldName);
break;
case EMPTY:
query = boolQuery().must(existsQuery(fieldName)).mustNot(wildcardQuery(fieldName, "*"));
break;
case NOT_EMPTY:
query = wildcardQuery(fieldName, "*");
break;
default:
break;
}

if (query != null) {
return query;
}

// now operation keys with a value
Object value = entry.getValue();
String searchText = QueryParserUtil.escape(value.toString());

QueryBuilder query = null;

switch (key) {
case EQUALS:
query = queryStringQuery(searchText).field(fieldName).defaultOperator(AND);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -586,6 +586,31 @@ public Criteria matchesAll(Object value) {
queryCriteriaEntries.add(new CriteriaEntry(OperationKey.MATCHES_ALL, value));
return this;
}

/**
* Add a {@link OperationKey#EMPTY} entry to the {@link #queryCriteriaEntries}.
*
* @return this object
* @since 4.3
*/
public Criteria empty() {

queryCriteriaEntries.add(new CriteriaEntry(OperationKey.EMPTY));
return this;
}

/**
* Add a {@link OperationKey#NOT_EMPTY} entry to the {@link #queryCriteriaEntries}.
*
* @return this object
* @since 4.3
*/
public Criteria notEmpty() {

queryCriteriaEntries.add(new CriteriaEntry(OperationKey.NOT_EMPTY));
return this;
}

// endregion

// region criteria entries - filter
Expand Down Expand Up @@ -921,7 +946,15 @@ public enum OperationKey { //
/**
* @since 4.1
*/
GEO_CONTAINS
GEO_CONTAINS, //
/**
* @since 4.3
*/
EMPTY, //
/**
* @since 4.3
*/
NOT_EMPTY
}

/**
Expand All @@ -934,7 +967,9 @@ public static class CriteriaEntry {

protected CriteriaEntry(OperationKey key) {

Assert.isTrue(key == OperationKey.EXISTS, "key must be OperationKey.EXISTS for this call");
boolean keyIsValid = key == OperationKey.EXISTS || key == OperationKey.EMPTY || key == OperationKey.NOT_EMPTY;
Assert.isTrue(keyIsValid,
"key must be OperationKey.EXISTS, OperationKey.EMPTY or OperationKey.EMPTY for this call");

this.key = key;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,15 @@ private Criteria from(Part part, Criteria criteria, Iterator<?> parameters) {
if (firstParameter instanceof String && secondParameter instanceof String)
return criteria.within((String) firstParameter, (String) secondParameter);
}

case EXISTS:
case IS_NOT_NULL:
return criteria.exists();
case IS_NULL:
return criteria.not().exists();
case IS_EMPTY:
return criteria.empty();
case IS_NOT_EMPTY:
return criteria.notEmpty();
default:
throw new InvalidDataAccessApiUsageException("Illegal criteria found '" + type + "'.");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
/**
* @author Peter-Josef Meisch
*/
@SuppressWarnings("ConstantConditions")
class CriteriaQueryProcessorUnitTests {

private final CriteriaQueryProcessor queryProcessor = new CriteriaQueryProcessor();
Expand Down Expand Up @@ -371,4 +372,67 @@ void shouldBuildNestedQuery() throws JSONException {

assertEquals(expected, query, false);
}

@Test // #1909
@DisplayName("should build query for empty property")
void shouldBuildQueryForEmptyProperty() throws JSONException {

String expected = "{\n" + //
" \"bool\" : {\n" + //
" \"must\" : [\n" + //
" {\n" + //
" \"bool\" : {\n" + //
" \"must\" : [\n" + //
" {\n" + //
" \"exists\" : {\n" + //
" \"field\" : \"lastName\"" + //
" }\n" + //
" }\n" + //
" ],\n" + //
" \"must_not\" : [\n" + //
" {\n" + //
" \"wildcard\" : {\n" + //
" \"lastName\" : {\n" + //
" \"wildcard\" : \"*\"" + //
" }\n" + //
" }\n" + //
" }\n" + //
" ]\n" + //
" }\n" + //
" }\n" + //
" ]\n" + //
" }\n" + //
"}"; //

Criteria criteria = new Criteria("lastName").empty();

String query = queryProcessor.createQuery(criteria).toString();

assertEquals(expected, query, false);
}

@Test // #1909
@DisplayName("should build query for non-empty property")
void shouldBuildQueryForNonEmptyProperty() throws JSONException {

String expected = "{\n" + //
" \"bool\" : {\n" + //
" \"must\" : [\n" + //
" {\n" + //
" \"wildcard\" : {\n" + //
" \"lastName\" : {\n" + //
" \"wildcard\" : \"*\"\n" + //
" }\n" + //
" }\n" + //
" }\n" + //
" ]\n" + //
" }\n" + //
"}\n"; //

Criteria criteria = new Criteria("lastName").notEmpty();

String query = queryProcessor.createQuery(criteria).toString();

assertEquals(expected, query, false);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
import org.springframework.data.elasticsearch.annotations.FieldType;
import org.springframework.data.elasticsearch.core.ElasticsearchOperations;
import org.springframework.data.elasticsearch.core.IndexOperations;
import org.springframework.data.elasticsearch.core.SearchHits;
import org.springframework.data.elasticsearch.junit.jupiter.ElasticsearchRestTemplateConfiguration;
import org.springframework.data.elasticsearch.junit.jupiter.SpringIntegrationTest;
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;
Expand Down Expand Up @@ -75,9 +76,10 @@ public void before() {
Product product3 = new Product("3", "Sugar", "Beet sugar", 1.1f, true, "sort3");
Product product4 = new Product("4", "Salt", "Rock salt", 1.9f, true, "sort2");
Product product5 = new Product("5", "Salt", "Sea salt", 2.1f, false, "sort1");
Product product6 = new Product("6", null, "no name", 3.4f, false, "sort0");
Product product6 = new Product("6", null, "no name", 3.4f, false, "sort6");
Product product7 = new Product("7", "", "empty name", 3.4f, false, "sort7");

repository.saveAll(Arrays.asList(product1, product2, product3, product4, product5, product6));
repository.saveAll(Arrays.asList(product1, product2, product3, product4, product5, product6, product7));
}

@AfterEach
Expand Down Expand Up @@ -118,7 +120,7 @@ public void shouldSupportTrueAndFalse() {

// then
assertThat(repository.findByAvailableTrue()).hasSize(3);
assertThat(repository.findByAvailableFalse()).hasSize(3);
assertThat(repository.findByAvailableFalse()).hasSize(4);
}

@Test
Expand All @@ -130,8 +132,8 @@ public void shouldSupportInAndNotInAndNot() {

// then
assertThat(repository.findByPriceIn(Arrays.asList(1.2f, 1.1f))).hasSize(2);
assertThat(repository.findByPriceNotIn(Arrays.asList(1.2f, 1.1f))).hasSize(4);
assertThat(repository.findByPriceNot(1.2f)).hasSize(5);
assertThat(repository.findByPriceNotIn(Arrays.asList(1.2f, 1.1f))).hasSize(5);
assertThat(repository.findByPriceNot(1.2f)).hasSize(6);
}

@Test // DATAES-171
Expand All @@ -142,7 +144,7 @@ public void shouldWorkWithNotIn() {
// when

// then
assertThat(repository.findByIdNotIn(Arrays.asList("2", "3"))).hasSize(4);
assertThat(repository.findByIdNotIn(Arrays.asList("2", "3"))).hasSize(5);
}

@Test
Expand All @@ -167,8 +169,8 @@ public void shouldSupportLessThanAndGreaterThan() {
assertThat(repository.findByPriceLessThan(1.1f)).hasSize(1);
assertThat(repository.findByPriceLessThanEqual(1.1f)).hasSize(2);

assertThat(repository.findByPriceGreaterThan(1.9f)).hasSize(2);
assertThat(repository.findByPriceGreaterThanEqual(1.9f)).hasSize(3);
assertThat(repository.findByPriceGreaterThan(1.9f)).hasSize(3);
assertThat(repository.findByPriceGreaterThanEqual(1.9f)).hasSize(4);
}

@Test // DATAES-615
Expand All @@ -193,7 +195,8 @@ public void shouldSupportSortOnStandardFieldWithoutCriteria() {
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", "no name");
assertThat(sortedIds).containsExactly("Beet sugar", "Cane sugar", "Cane sugar", "Rock salt", "Sea salt",
"empty name", "no name");
}

@Test // DATAES-615
Expand All @@ -202,7 +205,7 @@ public void shouldSupportSortOnFieldWithCustomFieldNameWithoutCriteria() {
List<String> sortedIds = repository.findAllByOrderBySortName().stream() //
.map(it -> it.id).collect(Collectors.toList());

assertThat(sortedIds).containsExactly("6", "5", "4", "3", "2", "1");
assertThat(sortedIds).containsExactly("5", "4", "3", "2", "1", "6", "7");
}

@Test // DATAES-178
Expand Down Expand Up @@ -252,7 +255,7 @@ void shouldDeleteWithNullValues() {
repository.deleteByName(null);

long count = repository.count();
assertThat(count).isEqualTo(5);
assertThat(count).isEqualTo(6);
}

@Test // DATAES-937
Expand All @@ -273,6 +276,52 @@ void shouldReturnEmptyListOnDerivedMethodWithEmptyInputList() {
assertThat(products).isEmpty();
}

@Test // #1909
@DisplayName("should find by property exists")
void shouldFindByPropertyExists() {

SearchHits<Product> searchHits = repository.findByNameExists();

assertThat(searchHits.getTotalHits()).isEqualTo(6);
}

@Test // #1909
@DisplayName("should find by property is not null")
void shouldFindByPropertyIsNotNull() {

SearchHits<Product> searchHits = repository.findByNameIsNotNull();

assertThat(searchHits.getTotalHits()).isEqualTo(6);
}

@Test // #1909
@DisplayName("should find by property is null")
void shouldFindByPropertyIsNull() {

SearchHits<Product> searchHits = repository.findByNameIsNull();

assertThat(searchHits.getTotalHits()).isEqualTo(1);
}

@Test // #1909
@DisplayName("should find by empty property")
void shouldFindByEmptyProperty() {

SearchHits<Product> searchHits = repository.findByNameEmpty();

assertThat(searchHits.getTotalHits()).isEqualTo(1);
}

@Test // #1909
@DisplayName("should find by non-empty property")
void shouldFindByNonEmptyProperty() {

SearchHits<Product> searchHits = repository.findByNameNotEmpty();

assertThat(searchHits.getTotalHits()).isEqualTo(5);
}

@SuppressWarnings("unused")
@Document(indexName = "test-index-product-query-keywords")
static class Product {
@Nullable @Id private String id;
Expand Down Expand Up @@ -346,6 +395,7 @@ public void setSortName(@Nullable String sortName) {
}
}

@SuppressWarnings({ "SpringDataRepositoryMethodParametersInspection", "SpringDataMethodInconsistencyInspection" })
interface ProductRepository extends ElasticsearchRepository<Product, String> {

List<Product> findByName(@Nullable String name);
Expand Down Expand Up @@ -399,6 +449,16 @@ interface ProductRepository extends ElasticsearchRepository<Product, String> {
void deleteByName(@Nullable String name);

List<Product> findAllByNameIn(List<String> names);

SearchHits<Product> findByNameExists();

SearchHits<Product> findByNameIsNull();

SearchHits<Product> findByNameIsNotNull();

SearchHits<Product> findByNameEmpty();

SearchHits<Product> findByNameNotEmpty();
}

}
Loading

0 comments on commit 175e7b5

Please # to comment.