Skip to content

Commit 5a36f5e

Browse files
Add Query by Example feature.
Original Pull Request spring-projects#2422 Closes spring-projects#2418
1 parent 44a5c75 commit 5a36f5e

16 files changed

+1951
-6
lines changed

src/main/java/org/springframework/data/elasticsearch/client/elc/CriteriaQueryProcessor.java

+8
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
* query.
4040
*
4141
* @author Peter-Josef Meisch
42+
* @author Ezequiel Antúnez Camacho
4243
* @since 4.4
4344
*/
4445
class CriteriaQueryProcessor {
@@ -329,6 +330,13 @@ private static Query.Builder queryFor(Criteria.CriteriaEntry entry, Field field,
329330
throw new CriteriaQueryException("value for " + fieldName + " is not an Iterable");
330331
}
331332
break;
333+
case REGEXP:
334+
queryBuilder //
335+
.regexp(rb -> rb //
336+
.field(fieldName) //
337+
.value(value.toString()) //
338+
.boost(boost)); //
339+
break;
332340
default:
333341
throw new CriteriaQueryException("Could not build query for " + entry);
334342
}

src/main/java/org/springframework/data/elasticsearch/client/erhlc/CriteriaQueryProcessor.java

+4
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
* @author Rasmus Faber-Espensen
4545
* @author James Bodkin
4646
* @author Peter-Josef Meisch
47+
* @author Ezequiel Antúnez Camacho
4748
* @deprecated since 5.0
4849
*/
4950
@Deprecated
@@ -248,6 +249,9 @@ private QueryBuilder queryFor(Criteria.CriteriaEntry entry, Field field) {
248249
}
249250
}
250251
break;
252+
case REGEXP:
253+
query = regexpQuery(fieldName, value.toString());
254+
break;
251255
}
252256
return query;
253257
}

src/main/java/org/springframework/data/elasticsearch/core/query/Criteria.java

+21-1
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
* @author Mohsin Husen
5151
* @author Franck Marchand
5252
* @author Peter-Josef Meisch
53+
* @author Ezequiel Antúnez Camacho
5354
*/
5455
public class Criteria {
5556

@@ -611,6 +612,21 @@ public Criteria notEmpty() {
611612
return this;
612613
}
613614

615+
/**
616+
* Add a {@link OperationKey#REGEXP} entry to the {@link #queryCriteriaEntries}.
617+
*
618+
* @param value the regexp value to match
619+
* @return this object
620+
* @since 5.1
621+
*/
622+
public Criteria regexp(String value) {
623+
624+
Assert.notNull(value, "value must not be null");
625+
626+
queryCriteriaEntries.add(new CriteriaEntry(OperationKey.REGEXP, value));
627+
return this;
628+
}
629+
614630
// endregion
615631

616632
// region criteria entries - filter
@@ -954,7 +970,11 @@ public enum OperationKey { //
954970
/**
955971
* @since 4.3
956972
*/
957-
NOT_EMPTY;
973+
NOT_EMPTY, //
974+
/**
975+
* @since 5.1
976+
*/
977+
REGEXP;
958978

959979
/**
960980
* @return true if this key does not have an associated value

src/main/java/org/springframework/data/elasticsearch/repository/support/ElasticsearchRepositoryFactory.java

+23-5
Original file line numberDiff line numberDiff line change
@@ -15,29 +15,33 @@
1515
*/
1616
package org.springframework.data.elasticsearch.repository.support;
1717

18-
import static org.springframework.data.querydsl.QuerydslUtils.*;
19-
20-
import java.lang.reflect.Method;
21-
import java.util.Optional;
22-
2318
import org.springframework.data.elasticsearch.core.ElasticsearchOperations;
2419
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;
2520
import org.springframework.data.elasticsearch.repository.query.ElasticsearchPartQuery;
2621
import org.springframework.data.elasticsearch.repository.query.ElasticsearchQueryMethod;
2722
import org.springframework.data.elasticsearch.repository.query.ElasticsearchStringQuery;
23+
import org.springframework.data.elasticsearch.repository.support.querybyexample.QueryByExampleElasticsearchExecutor;
2824
import org.springframework.data.projection.ProjectionFactory;
2925
import org.springframework.data.querydsl.QuerydslPredicateExecutor;
3026
import org.springframework.data.repository.core.NamedQueries;
3127
import org.springframework.data.repository.core.RepositoryInformation;
3228
import org.springframework.data.repository.core.RepositoryMetadata;
29+
import org.springframework.data.repository.core.support.RepositoryComposition;
3330
import org.springframework.data.repository.core.support.RepositoryFactorySupport;
31+
import org.springframework.data.repository.core.support.RepositoryFragment;
32+
import org.springframework.data.repository.query.QueryByExampleExecutor;
3433
import org.springframework.data.repository.query.QueryLookupStrategy;
3534
import org.springframework.data.repository.query.QueryLookupStrategy.Key;
3635
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
3736
import org.springframework.data.repository.query.RepositoryQuery;
3837
import org.springframework.lang.Nullable;
3938
import org.springframework.util.Assert;
4039

40+
import java.lang.reflect.Method;
41+
import java.util.Optional;
42+
43+
import static org.springframework.data.querydsl.QuerydslUtils.QUERY_DSL_PRESENT;
44+
4145
/**
4246
* Factory to create {@link ElasticsearchRepository}
4347
*
@@ -49,6 +53,7 @@
4953
* @author Christoph Strobl
5054
* @author Sascha Woo
5155
* @author Peter-Josef Meisch
56+
* @author Ezequiel Antúnez Camacho
5257
*/
5358
public class ElasticsearchRepositoryFactory extends RepositoryFactorySupport {
5459

@@ -122,4 +127,17 @@ public RepositoryQuery resolveQuery(Method method, RepositoryMetadata metadata,
122127
protected RepositoryMetadata getRepositoryMetadata(Class<?> repositoryInterface) {
123128
return new ElasticsearchRepositoryMetadata(repositoryInterface);
124129
}
130+
131+
@Override
132+
protected RepositoryComposition.RepositoryFragments getRepositoryFragments(RepositoryMetadata metadata) {
133+
RepositoryComposition.RepositoryFragments fragments = RepositoryComposition.RepositoryFragments.empty();
134+
135+
if (QueryByExampleExecutor.class.isAssignableFrom(metadata.getRepositoryInterface())) {
136+
fragments = fragments.append(RepositoryFragment.implemented(QueryByExampleExecutor.class,
137+
instantiateClass(QueryByExampleElasticsearchExecutor.class, elasticsearchOperations)));
138+
}
139+
140+
return fragments;
141+
}
142+
125143
}

src/main/java/org/springframework/data/elasticsearch/repository/support/ReactiveElasticsearchRepositoryFactory.java

+17
Original file line numberDiff line numberDiff line change
@@ -25,15 +25,19 @@
2525
import org.springframework.data.elasticsearch.repository.query.ReactiveElasticsearchQueryMethod;
2626
import org.springframework.data.elasticsearch.repository.query.ReactiveElasticsearchStringQuery;
2727
import org.springframework.data.elasticsearch.repository.query.ReactivePartTreeElasticsearchQuery;
28+
import org.springframework.data.elasticsearch.repository.support.querybyexample.ReactiveQueryByExampleElasticsearchExecutor;
2829
import org.springframework.data.mapping.context.MappingContext;
2930
import org.springframework.data.projection.ProjectionFactory;
3031
import org.springframework.data.repository.core.NamedQueries;
3132
import org.springframework.data.repository.core.RepositoryInformation;
3233
import org.springframework.data.repository.core.RepositoryMetadata;
3334
import org.springframework.data.repository.core.support.ReactiveRepositoryFactorySupport;
35+
import org.springframework.data.repository.core.support.RepositoryComposition;
36+
import org.springframework.data.repository.core.support.RepositoryFragment;
3437
import org.springframework.data.repository.query.QueryLookupStrategy;
3538
import org.springframework.data.repository.query.QueryLookupStrategy.Key;
3639
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
40+
import org.springframework.data.repository.query.ReactiveQueryByExampleExecutor;
3741
import org.springframework.data.repository.query.RepositoryQuery;
3842
import org.springframework.expression.spel.standard.SpelExpressionParser;
3943
import org.springframework.lang.Nullable;
@@ -45,6 +49,7 @@
4549
*
4650
* @author Christoph Strobl
4751
* @author Ivan Greene
52+
* @author Ezequiel Antúnez Camacho
4853
* @since 3.2
4954
*/
5055
public class ReactiveElasticsearchRepositoryFactory extends ReactiveRepositoryFactorySupport {
@@ -168,4 +173,16 @@ public RepositoryQuery resolveQuery(Method method, RepositoryMetadata metadata,
168173
}
169174
}
170175
}
176+
177+
@Override
178+
protected RepositoryComposition.RepositoryFragments getRepositoryFragments(RepositoryMetadata metadata) {
179+
RepositoryComposition.RepositoryFragments fragments = RepositoryComposition.RepositoryFragments.empty();
180+
181+
if (ReactiveQueryByExampleExecutor.class.isAssignableFrom(metadata.getRepositoryInterface())) {
182+
fragments = fragments.append(RepositoryFragment.implemented(ReactiveQueryByExampleExecutor.class,
183+
instantiateClass(ReactiveQueryByExampleElasticsearchExecutor.class, operations)));
184+
}
185+
186+
return fragments;
187+
}
171188
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
/*
2+
* Copyright 2023 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.elasticsearch.repository.support.querybyexample;
17+
18+
import java.util.Map;
19+
import java.util.Optional;
20+
21+
import org.springframework.dao.InvalidDataAccessApiUsageException;
22+
import org.springframework.data.domain.Example;
23+
import org.springframework.data.domain.ExampleMatcher;
24+
import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentEntity;
25+
import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentProperty;
26+
import org.springframework.data.elasticsearch.core.query.Criteria;
27+
import org.springframework.data.mapping.PersistentPropertyAccessor;
28+
import org.springframework.data.mapping.context.MappingContext;
29+
import org.springframework.data.support.ExampleMatcherAccessor;
30+
import org.springframework.lang.Nullable;
31+
import org.springframework.util.StringUtils;
32+
33+
/**
34+
* Maps a {@link Example} to a {@link org.springframework.data.elasticsearch.core.query.Criteria}
35+
*
36+
* @author Ezequiel Antúnez Camacho
37+
* @since 5.1
38+
*/
39+
class ExampleCriteriaMapper {
40+
41+
private final MappingContext<? extends ElasticsearchPersistentEntity<?>, ElasticsearchPersistentProperty> mappingContext;
42+
43+
/**
44+
* Builds a {@link ExampleCriteriaMapper}
45+
*
46+
* @param mappingContext mappingContext to use
47+
*/
48+
ExampleCriteriaMapper(
49+
MappingContext<? extends ElasticsearchPersistentEntity<?>, ElasticsearchPersistentProperty> mappingContext) {
50+
this.mappingContext = mappingContext;
51+
}
52+
53+
<S> Criteria criteria(Example<S> example) {
54+
return buildCriteria(example);
55+
}
56+
57+
private <S> Criteria buildCriteria(Example<S> example) {
58+
final ExampleMatcherAccessor matcherAccessor = new ExampleMatcherAccessor(example.getMatcher());
59+
60+
return applyPropertySpecs(new Criteria(), "", example.getProbe(),
61+
mappingContext.getRequiredPersistentEntity(example.getProbeType()), matcherAccessor,
62+
example.getMatcher().getMatchMode());
63+
}
64+
65+
private Criteria applyPropertySpecs(Criteria criteria, String path, @Nullable Object probe,
66+
ElasticsearchPersistentEntity<?> persistentEntity, ExampleMatcherAccessor exampleSpecAccessor,
67+
ExampleMatcher.MatchMode matchMode) {
68+
69+
if (probe == null) {
70+
return criteria;
71+
}
72+
73+
PersistentPropertyAccessor<?> propertyAccessor = persistentEntity.getPropertyAccessor(probe);
74+
75+
for (ElasticsearchPersistentProperty property : persistentEntity) {
76+
final String propertyName = property.getName();
77+
String propertyPath = StringUtils.hasText(path) ? (path + "." + propertyName) : propertyName;
78+
if (exampleSpecAccessor.isIgnoredPath(propertyPath) || property.isCollectionLike()
79+
|| property.isVersionProperty()) {
80+
continue;
81+
}
82+
83+
Object propertyValue = propertyAccessor.getProperty(property);
84+
if (property.isMap() && propertyValue != null) {
85+
for (Map.Entry<String, Object> entry : ((Map<String, Object>) propertyValue).entrySet()) {
86+
String key = entry.getKey();
87+
Object value = entry.getValue();
88+
criteria = applyPropertySpec(propertyPath + "." + key, value, exampleSpecAccessor, property, matchMode,
89+
criteria);
90+
}
91+
continue;
92+
}
93+
94+
criteria = applyPropertySpec(propertyPath, propertyValue, exampleSpecAccessor, property, matchMode, criteria);
95+
}
96+
return criteria;
97+
}
98+
99+
private Criteria applyPropertySpec(String path, Object propertyValue, ExampleMatcherAccessor exampleSpecAccessor,
100+
ElasticsearchPersistentProperty property, ExampleMatcher.MatchMode matchMode, Criteria criteria) {
101+
102+
if (exampleSpecAccessor.isIgnoreCaseForPath(path)) {
103+
throw new InvalidDataAccessApiUsageException(
104+
"Current implementation of Query-by-Example supports only case-sensitive matching.");
105+
}
106+
107+
final Object transformedValue = exampleSpecAccessor.getValueTransformerForPath(path)
108+
.apply(Optional.ofNullable(propertyValue)).orElse(null);
109+
110+
if (transformedValue == null) {
111+
criteria = tryToAppendMustNotSentence(criteria, path, exampleSpecAccessor);
112+
} else {
113+
if (property.isEntity()) {
114+
return applyPropertySpecs(criteria, path, transformedValue,
115+
mappingContext.getRequiredPersistentEntity(property), exampleSpecAccessor, matchMode);
116+
} else {
117+
return applyStringMatcher(applyMatchMode(criteria, path, matchMode), transformedValue,
118+
exampleSpecAccessor.getStringMatcherForPath(path));
119+
}
120+
}
121+
return criteria;
122+
}
123+
124+
private Criteria tryToAppendMustNotSentence(Criteria criteria, String path,
125+
ExampleMatcherAccessor exampleSpecAccessor) {
126+
if (ExampleMatcher.NullHandler.INCLUDE.equals(exampleSpecAccessor.getNullHandler())
127+
|| exampleSpecAccessor.hasPropertySpecifier(path)) {
128+
return criteria.and(path).not().exists();
129+
}
130+
return criteria;
131+
}
132+
133+
private Criteria applyMatchMode(Criteria criteria, String path, ExampleMatcher.MatchMode matchMode) {
134+
if (matchMode == ExampleMatcher.MatchMode.ALL) {
135+
return criteria.and(path);
136+
} else {
137+
return criteria.or(path);
138+
}
139+
}
140+
141+
private Criteria applyStringMatcher(Criteria criteria, Object value, ExampleMatcher.StringMatcher stringMatcher) {
142+
return switch (stringMatcher) {
143+
case DEFAULT -> criteria.is(value);
144+
case EXACT -> criteria.matchesAll(value);
145+
case STARTING -> criteria.startsWith(validateString(value));
146+
case ENDING -> criteria.endsWith(validateString(value));
147+
case CONTAINING -> criteria.contains(validateString(value));
148+
case REGEX -> criteria.regexp(validateString(value));
149+
};
150+
}
151+
152+
private String validateString(Object value) {
153+
if (value instanceof String) {
154+
return value.toString();
155+
}
156+
throw new IllegalArgumentException("This operation requires a String but got " + value.getClass());
157+
}
158+
159+
}

0 commit comments

Comments
 (0)