mirror of
				https://github.com/spring-projects/spring-data-elasticsearch.git
				synced 2025-10-31 06:38:44 +00:00 
			
		
		
		
	Add repository search for nullable or empty properties.
Original Pull Request #1946 Closes #1909
This commit is contained in:
		
							parent
							
								
									b8ae9b4a83
								
							
						
					
					
						commit
						175e7b51ae
					
				| @ -242,10 +242,6 @@ A list of supported keywords for Elasticsearch is shown below. | |||||||
| | `findByNameNotIn(Collection<String>names)` | | `findByNameNotIn(Collection<String>names)` | ||||||
| | `{"query": {"bool": {"must": [{"query_string": {"query": "NOT(\"?\" \"?\")", "fields": ["name"]}}]}}}` | | `{"query": {"bool": {"must": [{"query_string": {"query": "NOT(\"?\" \"?\")", "fields": ["name"]}}]}}}` | ||||||
| 
 | 
 | ||||||
| | `Near` |  | ||||||
| | `findByStoreNear` |  | ||||||
| | `Not Supported Yet !` |  | ||||||
| 
 |  | ||||||
| | `True` | | `True` | ||||||
| | `findByAvailableTrue` | | `findByAvailableTrue` | ||||||
| | `{ "query" : { | | `{ "query" : { | ||||||
| @ -277,6 +273,26 @@ A list of supported keywords for Elasticsearch is shown below. | |||||||
| }, "sort":[{"name":{"order":"desc"}}] | }, "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. | NOTE: Methods names to build Geo-shape queries taking `GeoJson` parameters are not supported. | ||||||
|  | |||||||
| @ -165,20 +165,35 @@ class CriteriaQueryProcessor { | |||||||
| 	@Nullable | 	@Nullable | ||||||
| 	private QueryBuilder queryFor(Criteria.CriteriaEntry entry, Field field) { | 	private QueryBuilder queryFor(Criteria.CriteriaEntry entry, Field field) { | ||||||
| 
 | 
 | ||||||
|  | 		QueryBuilder query = null; | ||||||
| 		String fieldName = field.getName(); | 		String fieldName = field.getName(); | ||||||
| 		boolean isKeywordField = FieldType.Keyword == field.getFieldType(); | 		boolean isKeywordField = FieldType.Keyword == field.getFieldType(); | ||||||
| 
 | 
 | ||||||
| 		OperationKey key = entry.getKey(); | 		OperationKey key = entry.getKey(); | ||||||
| 
 | 
 | ||||||
| 		if (key == OperationKey.EXISTS) { | 		// operations without a value | ||||||
| 			return existsQuery(fieldName); | 		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(); | 		Object value = entry.getValue(); | ||||||
| 		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); | ||||||
|  | |||||||
| @ -586,6 +586,31 @@ public class Criteria { | |||||||
| 		queryCriteriaEntries.add(new CriteriaEntry(OperationKey.MATCHES_ALL, value)); | 		queryCriteriaEntries.add(new CriteriaEntry(OperationKey.MATCHES_ALL, value)); | ||||||
| 		return this; | 		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 | 	// endregion | ||||||
| 
 | 
 | ||||||
| 	// region criteria entries - filter | 	// region criteria entries - filter | ||||||
| @ -921,7 +946,15 @@ public class Criteria { | |||||||
| 		/** | 		/** | ||||||
| 		 * @since 4.1 | 		 * @since 4.1 | ||||||
| 		 */ | 		 */ | ||||||
| 		GEO_CONTAINS | 		GEO_CONTAINS, // | ||||||
|  | 		/** | ||||||
|  | 		 * @since 4.3 | ||||||
|  | 		 */ | ||||||
|  | 		EMPTY, // | ||||||
|  | 		/** | ||||||
|  | 		 * @since 4.3 | ||||||
|  | 		 */ | ||||||
|  | 		NOT_EMPTY | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	/** | 	/** | ||||||
| @ -934,7 +967,9 @@ public class Criteria { | |||||||
| 
 | 
 | ||||||
| 		protected CriteriaEntry(OperationKey key) { | 		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; | 			this.key = key; | ||||||
| 		} | 		} | ||||||
|  | |||||||
| @ -186,7 +186,15 @@ public class ElasticsearchQueryCreator extends AbstractQueryCreator<CriteriaQuer | |||||||
| 				if (firstParameter instanceof String && secondParameter instanceof String) | 				if (firstParameter instanceof String && secondParameter instanceof String) | ||||||
| 					return criteria.within((String) firstParameter, (String) secondParameter); | 					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: | 			default: | ||||||
| 				throw new InvalidDataAccessApiUsageException("Illegal criteria found '" + type + "'."); | 				throw new InvalidDataAccessApiUsageException("Illegal criteria found '" + type + "'."); | ||||||
| 		} | 		} | ||||||
|  | |||||||
| @ -25,6 +25,7 @@ import org.springframework.data.elasticsearch.core.query.Criteria; | |||||||
| /** | /** | ||||||
|  * @author Peter-Josef Meisch |  * @author Peter-Josef Meisch | ||||||
|  */ |  */ | ||||||
|  | @SuppressWarnings("ConstantConditions") | ||||||
| class CriteriaQueryProcessorUnitTests { | class CriteriaQueryProcessorUnitTests { | ||||||
| 
 | 
 | ||||||
| 	private final CriteriaQueryProcessor queryProcessor = new CriteriaQueryProcessor(); | 	private final CriteriaQueryProcessor queryProcessor = new CriteriaQueryProcessor(); | ||||||
| @ -371,4 +372,67 @@ class CriteriaQueryProcessorUnitTests { | |||||||
| 
 | 
 | ||||||
| 		assertEquals(expected, query, false); | 		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); | ||||||
|  | 	} | ||||||
| } | } | ||||||
|  | |||||||
| @ -35,6 +35,7 @@ import org.springframework.data.elasticsearch.annotations.Field; | |||||||
| import org.springframework.data.elasticsearch.annotations.FieldType; | import org.springframework.data.elasticsearch.annotations.FieldType; | ||||||
| import org.springframework.data.elasticsearch.core.ElasticsearchOperations; | import org.springframework.data.elasticsearch.core.ElasticsearchOperations; | ||||||
| import org.springframework.data.elasticsearch.core.IndexOperations; | 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.ElasticsearchRestTemplateConfiguration; | ||||||
| import org.springframework.data.elasticsearch.junit.jupiter.SpringIntegrationTest; | import org.springframework.data.elasticsearch.junit.jupiter.SpringIntegrationTest; | ||||||
| import org.springframework.data.elasticsearch.repository.ElasticsearchRepository; | import org.springframework.data.elasticsearch.repository.ElasticsearchRepository; | ||||||
| @ -75,9 +76,10 @@ class QueryKeywordsTests { | |||||||
| 		Product product3 = new Product("3", "Sugar", "Beet sugar", 1.1f, true, "sort3"); | 		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 product4 = new Product("4", "Salt", "Rock salt", 1.9f, true, "sort2"); | ||||||
| 		Product product5 = new Product("5", "Salt", "Sea salt", 2.1f, false, "sort1"); | 		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 | 	@AfterEach | ||||||
| @ -118,7 +120,7 @@ class QueryKeywordsTests { | |||||||
| 
 | 
 | ||||||
| 		// then | 		// then | ||||||
| 		assertThat(repository.findByAvailableTrue()).hasSize(3); | 		assertThat(repository.findByAvailableTrue()).hasSize(3); | ||||||
| 		assertThat(repository.findByAvailableFalse()).hasSize(3); | 		assertThat(repository.findByAvailableFalse()).hasSize(4); | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	@Test | 	@Test | ||||||
| @ -130,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(4); | 		assertThat(repository.findByPriceNotIn(Arrays.asList(1.2f, 1.1f))).hasSize(5); | ||||||
| 		assertThat(repository.findByPriceNot(1.2f)).hasSize(5); | 		assertThat(repository.findByPriceNot(1.2f)).hasSize(6); | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	@Test // DATAES-171 | 	@Test // DATAES-171 | ||||||
| @ -142,7 +144,7 @@ class QueryKeywordsTests { | |||||||
| 		// when | 		// when | ||||||
| 
 | 
 | ||||||
| 		// then | 		// then | ||||||
| 		assertThat(repository.findByIdNotIn(Arrays.asList("2", "3"))).hasSize(4); | 		assertThat(repository.findByIdNotIn(Arrays.asList("2", "3"))).hasSize(5); | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	@Test | 	@Test | ||||||
| @ -167,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(2); | 		assertThat(repository.findByPriceGreaterThan(1.9f)).hasSize(3); | ||||||
| 		assertThat(repository.findByPriceGreaterThanEqual(1.9f)).hasSize(3); | 		assertThat(repository.findByPriceGreaterThanEqual(1.9f)).hasSize(4); | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	@Test // DATAES-615 | 	@Test // DATAES-615 | ||||||
| @ -193,7 +195,8 @@ 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", "no name"); | 		assertThat(sortedIds).containsExactly("Beet sugar", "Cane sugar", "Cane sugar", "Rock salt", "Sea salt", | ||||||
|  | 				"empty name", "no name"); | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	@Test // DATAES-615 | 	@Test // DATAES-615 | ||||||
| @ -202,7 +205,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("6", "5", "4", "3", "2", "1"); | 		assertThat(sortedIds).containsExactly("5", "4", "3", "2", "1", "6", "7"); | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	@Test // DATAES-178 | 	@Test // DATAES-178 | ||||||
| @ -252,7 +255,7 @@ class QueryKeywordsTests { | |||||||
| 		repository.deleteByName(null); | 		repository.deleteByName(null); | ||||||
| 
 | 
 | ||||||
| 		long count = repository.count(); | 		long count = repository.count(); | ||||||
| 		assertThat(count).isEqualTo(5); | 		assertThat(count).isEqualTo(6); | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	@Test // DATAES-937 | 	@Test // DATAES-937 | ||||||
| @ -273,6 +276,52 @@ class QueryKeywordsTests { | |||||||
| 		assertThat(products).isEmpty(); | 		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") | 	@Document(indexName = "test-index-product-query-keywords") | ||||||
| 	static class Product { | 	static class Product { | ||||||
| 		@Nullable @Id private String id; | 		@Nullable @Id private String id; | ||||||
| @ -346,6 +395,7 @@ class QueryKeywordsTests { | |||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | 	@SuppressWarnings({ "SpringDataRepositoryMethodParametersInspection", "SpringDataMethodInconsistencyInspection" }) | ||||||
| 	interface ProductRepository extends ElasticsearchRepository<Product, String> { | 	interface ProductRepository extends ElasticsearchRepository<Product, String> { | ||||||
| 
 | 
 | ||||||
| 		List<Product> findByName(@Nullable String name); | 		List<Product> findByName(@Nullable String name); | ||||||
| @ -399,6 +449,16 @@ class QueryKeywordsTests { | |||||||
| 		void deleteByName(@Nullable String name); | 		void deleteByName(@Nullable String name); | ||||||
| 
 | 
 | ||||||
| 		List<Product> findAllByNameIn(List<String> names); | 		List<Product> findAllByNameIn(List<String> names); | ||||||
|  | 
 | ||||||
|  | 		SearchHits<Product> findByNameExists(); | ||||||
|  | 
 | ||||||
|  | 		SearchHits<Product> findByNameIsNull(); | ||||||
|  | 
 | ||||||
|  | 		SearchHits<Product> findByNameIsNotNull(); | ||||||
|  | 
 | ||||||
|  | 		SearchHits<Product> findByNameEmpty(); | ||||||
|  | 
 | ||||||
|  | 		SearchHits<Product> findByNameNotEmpty(); | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| } | } | ||||||
|  | |||||||
| @ -0,0 +1,197 @@ | |||||||
|  | /* | ||||||
|  |  * Copyright 2021 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.repository.query.keywords; | ||||||
|  | 
 | ||||||
|  | import static org.assertj.core.api.Assertions.*; | ||||||
|  | import static org.springframework.data.elasticsearch.annotations.FieldType.*; | ||||||
|  | 
 | ||||||
|  | import reactor.core.publisher.Flux; | ||||||
|  | import reactor.test.StepVerifier; | ||||||
|  | 
 | ||||||
|  | import org.junit.jupiter.api.BeforeEach; | ||||||
|  | import org.junit.jupiter.api.DisplayName; | ||||||
|  | import org.junit.jupiter.api.Order; | ||||||
|  | import org.junit.jupiter.api.Test; | ||||||
|  | import org.springframework.beans.factory.annotation.Autowired; | ||||||
|  | import org.springframework.context.annotation.Bean; | ||||||
|  | import org.springframework.context.annotation.Configuration; | ||||||
|  | import org.springframework.context.annotation.Import; | ||||||
|  | import org.springframework.data.annotation.Id; | ||||||
|  | import org.springframework.data.elasticsearch.annotations.Document; | ||||||
|  | import org.springframework.data.elasticsearch.annotations.Field; | ||||||
|  | import org.springframework.data.elasticsearch.core.ReactiveElasticsearchOperations; | ||||||
|  | import org.springframework.data.elasticsearch.core.SearchHit; | ||||||
|  | import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates; | ||||||
|  | import org.springframework.data.elasticsearch.junit.jupiter.ReactiveElasticsearchRestTemplateConfiguration; | ||||||
|  | import org.springframework.data.elasticsearch.junit.jupiter.SpringIntegrationTest; | ||||||
|  | import org.springframework.data.elasticsearch.repository.ReactiveElasticsearchRepository; | ||||||
|  | import org.springframework.data.elasticsearch.repository.config.EnableReactiveElasticsearchRepositories; | ||||||
|  | import org.springframework.data.elasticsearch.utils.IndexNameProvider; | ||||||
|  | import org.springframework.lang.Nullable; | ||||||
|  | import org.springframework.test.context.ContextConfiguration; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * @author Peter-Josef Meisch | ||||||
|  |  */ | ||||||
|  | @SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection") | ||||||
|  | @SpringIntegrationTest | ||||||
|  | @ContextConfiguration(classes = { ReactiveQueryKeywordsIntegrationTests.Config.class }) | ||||||
|  | public class ReactiveQueryKeywordsIntegrationTests { | ||||||
|  | 
 | ||||||
|  | 	@Configuration | ||||||
|  | 	@Import({ ReactiveElasticsearchRestTemplateConfiguration.class }) | ||||||
|  | 	@EnableReactiveElasticsearchRepositories(considerNestedRepositories = true) | ||||||
|  | 	static class Config { | ||||||
|  | 		@Bean | ||||||
|  | 		IndexNameProvider indexNameProvider() { | ||||||
|  | 			return new IndexNameProvider("reactive-template"); | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	@Autowired private IndexNameProvider indexNameProvider; | ||||||
|  | 	@Autowired private ReactiveElasticsearchOperations operations; | ||||||
|  | 	@Autowired private SampleRepository repository; | ||||||
|  | 
 | ||||||
|  | 	// region setup | ||||||
|  | 	@BeforeEach | ||||||
|  | 	void setUp() { | ||||||
|  | 		indexNameProvider.increment(); | ||||||
|  | 		operations.indexOps(SampleEntity.class).createWithMapping().block(); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	@Test | ||||||
|  | 	@Order(java.lang.Integer.MAX_VALUE) | ||||||
|  | 	void cleanup() { | ||||||
|  | 		operations.indexOps(IndexCoordinates.of("*")).delete().block(); | ||||||
|  | 	} | ||||||
|  | 	// endregion | ||||||
|  | 
 | ||||||
|  | 	@Test // #1909 | ||||||
|  | 	@DisplayName("should find by property exists") | ||||||
|  | 	void shouldFindByPropertyExists() { | ||||||
|  | 
 | ||||||
|  | 		loadEntities(); | ||||||
|  | 		repository.findByMessageExists().mapNotNull(SearchHit::getId).collectList() // | ||||||
|  | 				.as(StepVerifier::create) // | ||||||
|  | 				.assertNext(ids -> { // | ||||||
|  | 					assertThat(ids).containsExactlyInAnyOrder("empty-message", "with-message"); // | ||||||
|  | 				}).verifyComplete(); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	@Test // #1909 | ||||||
|  | 	@DisplayName("should find by property is not null") | ||||||
|  | 	void shouldFindByPropertyIsNotNull() { | ||||||
|  | 
 | ||||||
|  | 		loadEntities(); | ||||||
|  | 		repository.findByMessageIsNotNull().mapNotNull(SearchHit::getId).collectList() // | ||||||
|  | 				.as(StepVerifier::create) // | ||||||
|  | 				.assertNext(ids -> { // | ||||||
|  | 					assertThat(ids).containsExactlyInAnyOrder("empty-message", "with-message"); // | ||||||
|  | 				}).verifyComplete(); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	@Test // #1909 | ||||||
|  | 	@DisplayName("should find by property is null") | ||||||
|  | 	void shouldFindByPropertyIsNull() { | ||||||
|  | 
 | ||||||
|  | 		loadEntities(); | ||||||
|  | 		repository.findByMessageIsNull().mapNotNull(SearchHit::getId).collectList() // | ||||||
|  | 				.as(StepVerifier::create) // | ||||||
|  | 				.assertNext(ids -> { // | ||||||
|  | 					assertThat(ids).containsExactlyInAnyOrder("null-message"); // | ||||||
|  | 				}).verifyComplete(); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	@Test // #1909 | ||||||
|  | 	@DisplayName("should find by empty property ") | ||||||
|  | 	void shouldFindByEmptyProperty() { | ||||||
|  | 
 | ||||||
|  | 		loadEntities(); | ||||||
|  | 		repository.findByMessageIsEmpty().mapNotNull(SearchHit::getId).collectList() // | ||||||
|  | 				.as(StepVerifier::create) // | ||||||
|  | 				.assertNext(ids -> { // | ||||||
|  | 					assertThat(ids).containsExactlyInAnyOrder("empty-message"); // | ||||||
|  | 				}).verifyComplete(); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	@Test // #1909 | ||||||
|  | 	@DisplayName("should find by not empty property ") | ||||||
|  | 	void shouldFindByNotEmptyProperty() { | ||||||
|  | 
 | ||||||
|  | 		loadEntities(); | ||||||
|  | 		repository.findByMessageIsNotEmpty().mapNotNull(SearchHit::getId).collectList() // | ||||||
|  | 				.as(StepVerifier::create) // | ||||||
|  | 				.assertNext(ids -> { // | ||||||
|  | 					assertThat(ids).containsExactlyInAnyOrder("with-message"); // | ||||||
|  | 				}).verifyComplete(); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	@SuppressWarnings("SpringDataMethodInconsistencyInspection") | ||||||
|  | 	interface SampleRepository extends ReactiveElasticsearchRepository<SampleEntity, String> { | ||||||
|  | 		Flux<SearchHit<SampleEntity>> findByMessageExists(); | ||||||
|  | 
 | ||||||
|  | 		Flux<SearchHit<SampleEntity>> findByMessageIsNotNull(); | ||||||
|  | 
 | ||||||
|  | 		Flux<SearchHit<SampleEntity>> findByMessageIsNull(); | ||||||
|  | 
 | ||||||
|  | 		Flux<SearchHit<SampleEntity>> findByMessageIsNotEmpty(); | ||||||
|  | 
 | ||||||
|  | 		Flux<SearchHit<SampleEntity>> findByMessageIsEmpty(); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	private void loadEntities() { | ||||||
|  | 		repository.saveAll(Flux.just( // | ||||||
|  | 				new SampleEntity("with-message", "message"), // | ||||||
|  | 				new SampleEntity("empty-message", ""), // | ||||||
|  | 				new SampleEntity("null-message", null)) // | ||||||
|  | 		).blockLast(); // | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// region entities | ||||||
|  | 	@SuppressWarnings("unused") | ||||||
|  | 	@Document(indexName = "#{@indexNameProvider.indexName()}") | ||||||
|  | 	static class SampleEntity { | ||||||
|  | 		@Nullable @Id private String id; | ||||||
|  | 
 | ||||||
|  | 		@Nullable @Field(type = Text) private String message; | ||||||
|  | 
 | ||||||
|  | 		public SampleEntity() {} | ||||||
|  | 
 | ||||||
|  | 		public SampleEntity(@Nullable String id, @Nullable String message) { | ||||||
|  | 			this.id = id; | ||||||
|  | 			this.message = message; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		@Nullable | ||||||
|  | 		public String getId() { | ||||||
|  | 			return id; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		public void setId(@Nullable String id) { | ||||||
|  | 			this.id = id; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		@Nullable | ||||||
|  | 		public String getMessage() { | ||||||
|  | 			return message; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		public void setMessage(@Nullable String message) { | ||||||
|  | 			this.message = message; | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	// endregion | ||||||
|  | } | ||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user