Skip to content

Commit d57c5a9

Browse files
christophstroblmp911de
authored andcommittedJul 14, 2021
Add support for Wildcard Index.
Add WildcardIndexed annotation and the programatic WildcardIndex. Closes #3225 Original pull request: #3671.
1 parent 986ea39 commit d57c5a9

File tree

9 files changed

+654
-19
lines changed

9 files changed

+654
-19
lines changed
 

‎spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/IndexConverters.java

+4
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,10 @@ private static Converter<IndexDefinition, IndexOptions> getIndexDefinitionIndexO
115115
ops = ops.collation(fromDocument(indexOptions.get("collation", Document.class)));
116116
}
117117

118+
if(indexOptions.containsKey("wildcardProjection")) {
119+
ops.wildcardProjection(indexOptions.get("wildcardProjection", Document.class));
120+
}
121+
118122
return ops;
119123
};
120124
}

‎spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/IndexField.java

+23-2
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
public final class IndexField {
3030

3131
enum Type {
32-
GEO, TEXT, DEFAULT, HASH;
32+
GEO, TEXT, DEFAULT, HASH, WILDCARD;
3333
}
3434

3535
private final String key;
@@ -48,7 +48,7 @@ private IndexField(String key, @Nullable Direction direction, @Nullable Type typ
4848
if (Type.GEO.equals(type) || Type.TEXT.equals(type)) {
4949
Assert.isNull(direction, "Geo/Text indexes must not have a direction!");
5050
} else {
51-
if (!Type.HASH.equals(type)) {
51+
if (!(Type.HASH.equals(type) || Type.WILDCARD.equals(type))) {
5252
Assert.notNull(direction, "Default indexes require a direction");
5353
}
5454
}
@@ -77,6 +77,17 @@ static IndexField hashed(String key) {
7777
return new IndexField(key, null, Type.HASH);
7878
}
7979

80+
/**
81+
* Creates a {@literal wildcard} {@link IndexField} for the given key.
82+
*
83+
* @param key must not be {@literal null} or empty.
84+
* @return new instance of {@link IndexField}.
85+
* @since 3.3
86+
*/
87+
static IndexField wildcard(String key) {
88+
return new IndexField(key, null, Type.WILDCARD);
89+
}
90+
8091
/**
8192
* Creates a geo {@link IndexField} for the given key.
8293
*
@@ -142,6 +153,16 @@ public boolean isHashed() {
142153
return Type.HASH.equals(type);
143154
}
144155

156+
/**
157+
* Returns whether the {@link IndexField} is contains a {@literal wildcard} expression.
158+
*
159+
* @return {@literal true} if {@link IndexField} contains a wildcard {@literal $**}.
160+
* @since 3.3
161+
*/
162+
public boolean isWildcard() {
163+
return Type.WILDCARD.equals(type);
164+
}
165+
145166
/*
146167
* (non-Javadoc)
147168
* @see java.lang.Object#equals(java.lang.Object)

‎spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/IndexInfo.java

+26
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ public class IndexInfo {
5555
private @Nullable Duration expireAfter;
5656
private @Nullable String partialFilterExpression;
5757
private @Nullable Document collation;
58+
private @Nullable Document wildcardProjection;
5859

5960
public IndexInfo(List<IndexField> indexFields, String name, boolean unique, boolean sparse, String language) {
6061

@@ -99,6 +100,8 @@ public static IndexInfo indexInfoOf(Document sourceDocument) {
99100

100101
if (ObjectUtils.nullSafeEquals("hashed", value)) {
101102
indexFields.add(IndexField.hashed(key));
103+
} else if (key.contains("$**")) {
104+
indexFields.add(IndexField.wildcard(key));
102105
} else {
103106

104107
Double keyValue = new Double(value.toString());
@@ -131,6 +134,10 @@ public static IndexInfo indexInfoOf(Document sourceDocument) {
131134
info.expireAfter = Duration.ofSeconds(NumberUtils.convertNumberToTargetClass(expireAfterSeconds, Long.class));
132135
}
133136

137+
if (sourceDocument.containsKey("wildcardProjection")) {
138+
info.wildcardProjection = sourceDocument.get("wildcardProjection", Document.class);
139+
}
140+
134141
return info;
135142
}
136143

@@ -216,6 +223,16 @@ public Optional<Document> getCollation() {
216223
return Optional.ofNullable(collation);
217224
}
218225

226+
/**
227+
* Get {@literal wildcardProjection} information.
228+
*
229+
* @return {@link Optional#empty() empty} if not set.
230+
* @since 3.3
231+
*/
232+
public Optional<Document> getWildcardProjection() {
233+
return Optional.ofNullable(wildcardProjection);
234+
}
235+
219236
/**
220237
* Get the duration after which documents within the index expire.
221238
*
@@ -234,6 +251,14 @@ public boolean isHashed() {
234251
return getIndexFields().stream().anyMatch(IndexField::isHashed);
235252
}
236253

254+
/**
255+
* @return {@literal true} if a wildcard index field is present.
256+
* @since 3.3
257+
*/
258+
public boolean isWildcard() {
259+
return getIndexFields().stream().anyMatch(IndexField::isWildcard);
260+
}
261+
237262
@Override
238263
public String toString() {
239264

@@ -303,4 +328,5 @@ public boolean equals(Object obj) {
303328
}
304329
return true;
305330
}
331+
306332
}

‎spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexResolver.java

+91-15
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
import org.springframework.data.mongodb.core.mapping.Document;
4747
import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
4848
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
49+
import org.springframework.data.mongodb.core.query.Collation;
4950
import org.springframework.data.mongodb.util.BsonUtils;
5051
import org.springframework.data.mongodb.util.DotPath;
5152
import org.springframework.data.spel.EvaluationContextProvider;
@@ -121,6 +122,7 @@ public List<IndexDefinitionHolder> resolveIndexForEntity(MongoPersistentEntity<?
121122
List<IndexDefinitionHolder> indexInformation = new ArrayList<>();
122123
String collection = root.getCollection();
123124
indexInformation.addAll(potentiallyCreateCompoundIndexDefinitions("", collection, root));
125+
indexInformation.addAll(potentiallyCreateWildcardIndexDefinitions("", collection, root));
124126
indexInformation.addAll(potentiallyCreateTextIndexDefinition(root, collection));
125127

126128
root.doWithProperties((PropertyHandler<MongoPersistentProperty>) property -> this
@@ -162,17 +164,18 @@ private void potentiallyAddIndexForProperty(MongoPersistentEntity<?> root, Mongo
162164
* @return List of {@link IndexDefinitionHolder} representing indexes for given type and its referenced property
163165
* types. Will never be {@code null}.
164166
*/
165-
private List<IndexDefinitionHolder> resolveIndexForClass( TypeInformation<?> type, String dotPath,
166-
Path path, String collection, CycleGuard guard) {
167+
private List<IndexDefinitionHolder> resolveIndexForClass(TypeInformation<?> type, String dotPath, Path path,
168+
String collection, CycleGuard guard) {
167169

168170
return resolveIndexForEntity(mappingContext.getRequiredPersistentEntity(type), dotPath, path, collection, guard);
169171
}
170172

171-
private List<IndexDefinitionHolder> resolveIndexForEntity(MongoPersistentEntity<?> entity, String dotPath,
172-
Path path, String collection, CycleGuard guard) {
173+
private List<IndexDefinitionHolder> resolveIndexForEntity(MongoPersistentEntity<?> entity, String dotPath, Path path,
174+
String collection, CycleGuard guard) {
173175

174176
List<IndexDefinitionHolder> indexInformation = new ArrayList<>();
175177
indexInformation.addAll(potentiallyCreateCompoundIndexDefinitions(dotPath, collection, entity));
178+
indexInformation.addAll(potentiallyCreateWildcardIndexDefinitions(dotPath, collection, entity));
176179

177180
entity.doWithProperties((PropertyHandler<MongoPersistentProperty>) property -> this
178181
.guardAndPotentiallyAddIndexForProperty(property, dotPath, path, collection, indexInformation, guard));
@@ -196,15 +199,15 @@ private void guardAndPotentiallyAddIndexForProperty(MongoPersistentProperty pers
196199

197200
if (persistentProperty.isEntity()) {
198201
try {
199-
indexes.addAll(resolveIndexForEntity(mappingContext.getPersistentEntity(persistentProperty), propertyDotPath.toString(),
200-
propertyPath, collection, guard));
202+
indexes.addAll(resolveIndexForEntity(mappingContext.getPersistentEntity(persistentProperty),
203+
propertyDotPath.toString(), propertyPath, collection, guard));
201204
} catch (CyclicPropertyReferenceException e) {
202205
LOGGER.info(e.getMessage());
203206
}
204207
}
205208

206-
List<IndexDefinitionHolder> indexDefinitions = createIndexDefinitionHolderForProperty(propertyDotPath.toString(), collection,
207-
persistentProperty);
209+
List<IndexDefinitionHolder> indexDefinitions = createIndexDefinitionHolderForProperty(propertyDotPath.toString(),
210+
collection, persistentProperty);
208211

209212
if (!indexDefinitions.isEmpty()) {
210213
indexes.addAll(indexDefinitions);
@@ -232,6 +235,11 @@ private List<IndexDefinitionHolder> createIndexDefinitionHolderForProperty(Strin
232235
if (persistentProperty.isAnnotationPresent(HashIndexed.class)) {
233236
indices.add(createHashedIndexDefinition(dotPath, collection, persistentProperty));
234237
}
238+
if (persistentProperty.isAnnotationPresent(WildcardIndexed.class)) {
239+
indices.add(createWildcardIndexDefinition(dotPath, collection,
240+
persistentProperty.getRequiredAnnotation(WildcardIndexed.class),
241+
mappingContext.getPersistentEntity(persistentProperty)));
242+
}
235243

236244
return indices;
237245
}
@@ -246,6 +254,18 @@ private List<IndexDefinitionHolder> potentiallyCreateCompoundIndexDefinitions(St
246254
return createCompoundIndexDefinitions(dotPath, collection, entity);
247255
}
248256

257+
private List<IndexDefinitionHolder> potentiallyCreateWildcardIndexDefinitions(String dotPath, String collection,
258+
MongoPersistentEntity<?> entity) {
259+
260+
if (entity.findAnnotation(WildcardIndexed.class) == null) {
261+
return Collections.emptyList();
262+
}
263+
264+
return Collections.singletonList(new IndexDefinitionHolder(dotPath,
265+
createWildcardIndexDefinition(dotPath, collection, entity.getRequiredAnnotation(WildcardIndexed.class), entity),
266+
collection));
267+
}
268+
249269
private Collection<? extends IndexDefinitionHolder> potentiallyCreateTextIndexDefinition(
250270
MongoPersistentEntity<?> root, String collection) {
251271

@@ -292,9 +312,8 @@ private Collection<? extends IndexDefinitionHolder> potentiallyCreateTextIndexDe
292312

293313
}
294314

295-
private void appendTextIndexInformation(DotPath dotPath, Path path,
296-
TextIndexDefinitionBuilder indexDefinitionBuilder, MongoPersistentEntity<?> entity,
297-
TextIndexIncludeOptions includeOptions, CycleGuard guard) {
315+
private void appendTextIndexInformation(DotPath dotPath, Path path, TextIndexDefinitionBuilder indexDefinitionBuilder,
316+
MongoPersistentEntity<?> entity, TextIndexIncludeOptions includeOptions, CycleGuard guard) {
298317

299318
entity.doWithProperties(new PropertyHandler<MongoPersistentProperty>() {
300319

@@ -311,8 +330,7 @@ public void doWithPersistentProperty(MongoPersistentProperty persistentProperty)
311330

312331
if (includeOptions.isForce() || indexed != null || persistentProperty.isEntity()) {
313332

314-
DotPath propertyDotPath = dotPath
315-
.append(persistentProperty.getFieldName());
333+
DotPath propertyDotPath = dotPath.append(persistentProperty.getFieldName());
316334

317335
Path propertyPath = path.append(persistentProperty);
318336

@@ -406,6 +424,32 @@ protected IndexDefinitionHolder createCompoundIndexDefinition(String dotPath, St
406424
return new IndexDefinitionHolder(dotPath, indexDefinition, collection);
407425
}
408426

427+
protected IndexDefinitionHolder createWildcardIndexDefinition(String dotPath, String collection,
428+
WildcardIndexed index, @Nullable MongoPersistentEntity<?> entity) {
429+
430+
WildcardIndex indexDefinition = new WildcardIndex(dotPath);
431+
432+
if (StringUtils.hasText(index.wildcardProjection())) {
433+
indexDefinition.wildcardProjection(evaluateWildcardProjection(index.wildcardProjection(), entity));
434+
}
435+
436+
if (!index.useGeneratedName()) {
437+
indexDefinition.named(pathAwareIndexName(index.name(), dotPath, entity, null));
438+
}
439+
440+
if (StringUtils.hasText(index.partialFilter())) {
441+
indexDefinition.partial(evaluatePartialFilter(index.partialFilter(), entity));
442+
}
443+
444+
if (StringUtils.hasText(index.collation())) {
445+
indexDefinition.collation(evaluateCollation(index.collation(), entity));
446+
} else if (entity != null && entity.hasCollation()) {
447+
indexDefinition.collation(entity.getCollation());
448+
}
449+
450+
return new IndexDefinitionHolder(dotPath, indexDefinition, collection);
451+
}
452+
409453
private org.bson.Document resolveCompoundIndexKeyFromStringDefinition(String dotPath, String keyDefinitionString,
410454
PersistentEntity<?, ?> entity) {
411455

@@ -510,6 +554,33 @@ private PartialIndexFilter evaluatePartialFilter(String filterExpression, Persis
510554
return PartialIndexFilter.of(BsonUtils.parse(filterExpression, null));
511555
}
512556

557+
private org.bson.Document evaluateWildcardProjection(String projectionExpression, PersistentEntity<?, ?> entity) {
558+
559+
Object result = evaluate(projectionExpression, getEvaluationContextForProperty(entity));
560+
561+
if (result instanceof org.bson.Document) {
562+
return (org.bson.Document) result;
563+
}
564+
565+
return BsonUtils.parse(projectionExpression, null);
566+
}
567+
568+
private Collation evaluateCollation(String collationExpression, PersistentEntity<?, ?> entity) {
569+
570+
Object result = evaluate(collationExpression, getEvaluationContextForProperty(entity));
571+
if (result instanceof org.bson.Document) {
572+
return Collation.from((org.bson.Document) result);
573+
}
574+
if (result instanceof Collation) {
575+
return (Collation) result;
576+
}
577+
if (result instanceof String) {
578+
return Collation.parse(result.toString());
579+
}
580+
throw new IllegalStateException("Cannot parse collation " + result);
581+
582+
}
583+
513584
/**
514585
* Creates {@link HashedIndex} wrapped in {@link IndexDefinitionHolder} out of {@link HashIndexed} for a given
515586
* {@link MongoPersistentProperty}.
@@ -657,8 +728,8 @@ private void resolveAndAddIndexesForAssociation(Association<MongoPersistentPrope
657728
propertyDotPath));
658729
}
659730

660-
List<IndexDefinitionHolder> indexDefinitions = createIndexDefinitionHolderForProperty(propertyDotPath.toString(), collection,
661-
property);
731+
List<IndexDefinitionHolder> indexDefinitions = createIndexDefinitionHolderForProperty(propertyDotPath.toString(),
732+
collection, property);
662733

663734
if (!indexDefinitions.isEmpty()) {
664735
indexes.addAll(indexDefinitions);
@@ -998,6 +1069,11 @@ public org.bson.Document getIndexKeys() {
9981069
public org.bson.Document getIndexOptions() {
9991070
return indexDefinition.getIndexOptions();
10001071
}
1072+
1073+
@Override
1074+
public String toString() {
1075+
return "IndexDefinitionHolder{" + "indexKeys=" + getIndexKeys() + '}';
1076+
}
10011077
}
10021078

10031079
/**
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
/*
2+
* Copyright 2021 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.mongodb.core.index;
17+
18+
import java.time.Duration;
19+
import java.util.LinkedHashMap;
20+
import java.util.Map;
21+
import java.util.concurrent.TimeUnit;
22+
23+
import org.bson.Document;
24+
import org.springframework.lang.Nullable;
25+
import org.springframework.util.CollectionUtils;
26+
import org.springframework.util.StringUtils;
27+
28+
/**
29+
* {@link WildcardIndex} is a specific {@link Index} that can be used to include all fields into an index based on the
30+
* {@code $**" : 1} pattern on a root object (the one typically carrying the
31+
* {@link org.springframework.data.mongodb.core.mapping.Document} annotation). On those it is possible to use
32+
* {@link #wildcardProjectionInclude(String...)} and {@link #wildcardProjectionExclude(String...)} to define specific
33+
* paths for in-/exclusion.
34+
* <p />
35+
* It can also be used to define an index on a specific field path and its subfields, e.g.
36+
* {@code "path.to.field.$**" : 1}. <br />
37+
* Note that {@literal wildcardProjections} are not allowed in this case.
38+
* <p />
39+
* <strong>LIMITATIONS</strong><br />
40+
* <ul>
41+
* <li>{@link #unique() Unique} and {@link #expire(long) ttl} options are not supported.</li>
42+
* <li>Keys used for sharding must not be included</li>
43+
* <li>Cannot be used to generate any type of geo index.</li>
44+
* </ul>
45+
*
46+
* @author Christoph Strobl
47+
* @see <a href= "https://docs.mongodb.com/manual/core/index-wildcard/">MongoDB Reference Documentation: Wildcard
48+
* Indexes/</a>
49+
* @since 3.3
50+
*/
51+
public class WildcardIndex extends Index {
52+
53+
private @Nullable String fieldName;
54+
private Map<String, Object> wildcardProjection = new LinkedHashMap<>();
55+
56+
/**
57+
* Create a new instance of {@link WildcardIndex} using {@code $**}.
58+
*/
59+
public WildcardIndex() {}
60+
61+
/**
62+
* Create a new instance of {@link WildcardIndex} for the given {@literal path}. If no {@literal path} is provided the
63+
* index will be considered a root one using {@code $**}. <br />
64+
* <strong>NOTE</strong> {@link #wildcardProjectionInclude(String...)}, {@link #wildcardProjectionExclude(String...)}
65+
* can only be used for top level index definitions having an {@literal empty} or {@literal null} path.
66+
*
67+
* @param path can be {@literal null}. If {@literal null} all fields will be indexed.
68+
*/
69+
public WildcardIndex(@Nullable String path) {
70+
this.fieldName = path;
71+
}
72+
73+
/**
74+
* Include the {@code _id} field in {@literal wildcardProjection}.
75+
*
76+
* @return this.
77+
*/
78+
public WildcardIndex includeId() {
79+
80+
wildcardProjection.put("_id", 1);
81+
return this;
82+
}
83+
84+
/**
85+
* Set the index name to use.
86+
*
87+
* @param name
88+
* @return this.
89+
*/
90+
@Override
91+
public WildcardIndex named(String name) {
92+
93+
super.named(name);
94+
return this;
95+
}
96+
97+
/**
98+
* Unique option is not supported.
99+
*
100+
* @throws UnsupportedOperationException
101+
*/
102+
@Override
103+
public Index unique() {
104+
throw new UnsupportedOperationException("Wildcard Index does not support 'unique'.");
105+
}
106+
107+
/**
108+
* ttl option is not supported.
109+
*
110+
* @throws UnsupportedOperationException
111+
*/
112+
@Override
113+
public Index expire(long seconds) {
114+
throw new UnsupportedOperationException("Wildcard Index does not support 'ttl'.");
115+
}
116+
117+
/**
118+
* ttl option is not supported.
119+
*
120+
* @throws UnsupportedOperationException
121+
*/
122+
@Override
123+
public Index expire(long value, TimeUnit timeUnit) {
124+
throw new UnsupportedOperationException("Wildcard Index does not support 'ttl'.");
125+
}
126+
127+
/**
128+
* ttl option is not supported.
129+
*
130+
* @throws UnsupportedOperationException
131+
*/
132+
@Override
133+
public Index expire(Duration duration) {
134+
throw new UnsupportedOperationException("Wildcard Index does not support 'ttl'.");
135+
}
136+
137+
/**
138+
* Add fields to be included from indexing via {@code wildcardProjection}. <br />
139+
* This option is only allowed on {@link WildcardIndex#WildcardIndex() top level} wildcard indexes.
140+
*
141+
* @param paths must not be {@literal null}.
142+
* @return this.
143+
*/
144+
public WildcardIndex wildcardProjectionInclude(String... paths) {
145+
146+
for (String path : paths) {
147+
wildcardProjection.put(path, 1);
148+
}
149+
return this;
150+
}
151+
152+
/**
153+
* Add fields to be excluded from indexing via {@code wildcardProjection}. <br />
154+
* This option is only allowed on {@link WildcardIndex#WildcardIndex() top level} wildcard indexes.
155+
*
156+
* @param paths must not be {@literal null}.
157+
* @return this.
158+
*/
159+
public WildcardIndex wildcardProjectionExclude(String... paths) {
160+
161+
for (String path : paths) {
162+
wildcardProjection.put(path, 0);
163+
}
164+
return this;
165+
}
166+
167+
/**
168+
* Set the fields to be in-/excluded from indexing via {@code wildcardProjection}. <br />
169+
* This option is only allowed on {@link WildcardIndex#WildcardIndex() top level} wildcard indexes.
170+
*
171+
* @param includeExclude must not be {@literal null}.
172+
* @return this.
173+
*/
174+
public WildcardIndex wildcardProjection(Map<String, Object> includeExclude) {
175+
176+
wildcardProjection.putAll(includeExclude);
177+
return this;
178+
}
179+
180+
private String getTargetFieldName() {
181+
return StringUtils.hasText(fieldName) ? (fieldName + ".$**") : "$**";
182+
}
183+
184+
@Override
185+
public Document getIndexKeys() {
186+
return new Document(getTargetFieldName(), 1);
187+
}
188+
189+
@Override
190+
public Document getIndexOptions() {
191+
192+
Document options = new Document(super.getIndexOptions());
193+
if (!CollectionUtils.isEmpty(wildcardProjection)) {
194+
options.put("wildcardProjection", new Document(wildcardProjection));
195+
}
196+
return options;
197+
}
198+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
/*
2+
* Copyright 2021 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.mongodb.core.index;
17+
18+
import java.lang.annotation.Documented;
19+
import java.lang.annotation.ElementType;
20+
import java.lang.annotation.Retention;
21+
import java.lang.annotation.RetentionPolicy;
22+
import java.lang.annotation.Target;
23+
24+
/**
25+
* Annotation for an entity or property that should be used as key for a
26+
* <a href="https://docs.mongodb.com/manual/core/index-wildcard/">Wildcard Index</a>. <br />
27+
* If placed on a {@link ElementType#TYPE type} that is a root level domain entity (one having an
28+
* {@link org.springframework.data.mongodb.core.mapping.Document} annotation) will advise the index creator to create a
29+
* wildcard index for it.
30+
*
31+
* <pre class="code">
32+
*
33+
* &#64;Document
34+
* &#64;WildcardIndexed
35+
* public class Product {
36+
* ...
37+
* }
38+
*
39+
* db.product.createIndex({ "$**" : 1 } , {})
40+
* </pre>
41+
*
42+
* {@literal wildcardProjection} can be used to specify keys to in-/exclude in the index.
43+
*
44+
* <pre class="code">
45+
*
46+
* &#64;Document
47+
* &#64;WildcardIndexed(wildcardProjection = "{ 'userMetadata.age' : 0 }")
48+
* public class User {
49+
* private &#64;Id String id;
50+
* private UserMetadata userMetadata;
51+
* }
52+
*
53+
*
54+
* db.user.createIndex(
55+
* { "$**" : 1 },
56+
* { "wildcardProjection" :
57+
* { "userMetadata.age" : 0 }
58+
* }
59+
* )
60+
* </pre>
61+
*
62+
* Wildcard indexes can also be expressed by adding the annotation directly to the field. Please note that
63+
* {@literal wildcardProjection} is not allowed on nested paths.
64+
*
65+
* <pre class="code">
66+
* &#64;Document
67+
* public class User {
68+
*
69+
* private &#64;Id String id;
70+
*
71+
* &#64;WildcardIndexed
72+
* private UserMetadata userMetadata;
73+
* }
74+
*
75+
*
76+
* db.user.createIndex({ "userMetadata.$**" : 1 }, {})
77+
* </pre>
78+
*
79+
* @author Christoph Strobl
80+
* @since 3.3
81+
*/
82+
@Documented
83+
@Target({ ElementType.TYPE, ElementType.FIELD })
84+
@Retention(RetentionPolicy.RUNTIME)
85+
public @interface WildcardIndexed {
86+
87+
/**
88+
* Index name either as plain value or as {@link org.springframework.expression.spel.standard.SpelExpression template
89+
* expression}. <br />
90+
* <br />
91+
* The name will only be applied as is when defined on root level. For usage on nested or embedded structures the
92+
* provided name will be prefixed with the path leading to the entity. <br />
93+
*
94+
* @return
95+
*/
96+
String name() default "";
97+
98+
/**
99+
* If set to {@literal true} then MongoDB will ignore the given index name and instead generate a new name. Defaults
100+
* to {@literal false}.
101+
*
102+
* @return {@literal false} by default.
103+
*/
104+
boolean useGeneratedName() default false;
105+
106+
/**
107+
* Only index the documents in a collection that meet a specified {@link IndexFilter filter expression}. <br />
108+
*
109+
* @return empty by default.
110+
* @see <a href=
111+
* "https://docs.mongodb.com/manual/core/index-partial/">https://docs.mongodb.com/manual/core/index-partial/</a>
112+
*/
113+
String partialFilter() default "";
114+
115+
/**
116+
* Explicitly specify sub fields to be in-/excluded as a {@link org.bson.Document#parse(String) prasable} String.
117+
* <br />
118+
* <strong>NOTE: </strong>Can only be done on root level documents.
119+
*
120+
* @return empty by default.
121+
*/
122+
String wildcardProjection() default "";
123+
124+
/**
125+
* Defines the collation to apply.
126+
*
127+
* @return an empty {@link String} by default.
128+
*/
129+
String collation() default "";
130+
}

‎spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/index/IndexInfoUnitTests.java

+11
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ public class IndexInfoUnitTests {
3636
static final String INDEX_WITH_PARTIAL_FILTER = "{ \"v\" : 2, \"key\" : { \"k3y\" : 1 }, \"name\" : \"partial-filter-index\", \"ns\" : \"db.collection\", \"partialFilterExpression\" : { \"quantity\" : { \"$gte\" : 10 } } }";
3737
static final String INDEX_WITH_EXPIRATION_TIME = "{ \"v\" : 2, \"key\" : { \"lastModifiedDate\" : 1 },\"name\" : \"expire-after-last-modified\", \"ns\" : \"db.collectio\", \"expireAfterSeconds\" : 3600 }";
3838
static final String HASHED_INDEX = "{ \"v\" : 2, \"key\" : { \"score\" : \"hashed\" }, \"name\" : \"score_hashed\", \"ns\" : \"db.collection\" }";
39+
static final String WILDCARD_INDEX = "{ \"v\" : 2, \"key\" : { \"$**\" : 1 }, \"name\" : \"$**_1\", \"wildcardProjection\" : { \"fieldA\" : 0, \"fieldB.fieldC\" : 0 } }";
3940

4041
@Test
4142
public void isIndexForFieldsCorrectly() {
@@ -79,6 +80,16 @@ public void hashedIndexIsMarkedAsSuch() {
7980
assertThat(getIndexInfo(HASHED_INDEX).isHashed()).isTrue();
8081
}
8182

83+
@Test // GH-3225
84+
public void identifiesWildcardIndexCorrectly() {
85+
assertThat(getIndexInfo(WILDCARD_INDEX).isWildcard()).isTrue();
86+
}
87+
88+
@Test // GH-3225
89+
public void readsWildcardIndexProjectionCorrectly() {
90+
assertThat(getIndexInfo(WILDCARD_INDEX).getWildcardProjection()).contains(new Document("fieldA", 0).append("fieldB.fieldC", 0));
91+
}
92+
8293
private static IndexInfo getIndexInfo(String documentJson) {
8394
return IndexInfo.indexInfoOf(Document.parse(documentJson));
8495
}

‎spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexResolverUnitTests.java

+83-2
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,9 @@
1515
*/
1616
package org.springframework.data.mongodb.core.index;
1717

18+
import static org.assertj.core.api.Assertions.assertThat;
1819
import static org.mockito.Mockito.*;
19-
import static org.springframework.data.mongodb.test.util.Assertions.*;
20+
import static org.springframework.data.mongodb.test.util.Assertions.assertThatExceptionOfType;
2021

2122
import java.lang.annotation.ElementType;
2223
import java.lang.annotation.Retention;
@@ -25,6 +26,7 @@
2526
import java.util.Arrays;
2627
import java.util.LinkedHashSet;
2728
import java.util.List;
29+
import java.util.Map;
2830

2931
import org.junit.Test;
3032
import org.junit.runner.RunWith;
@@ -1323,6 +1325,49 @@ public void errorsOnIndexOnEmbedded() {
13231325

13241326
}
13251327

1328+
@Test // GH-3225
1329+
public void resolvesWildcardOnRoot() {
1330+
1331+
List<IndexDefinitionHolder> indices = prepareMappingContextAndResolveIndexForType(
1332+
WithWildCardIndexOnEntity.class);
1333+
assertThat(indices).hasSize(1);
1334+
assertThat(indices.get(0)).satisfies(it -> {
1335+
assertThat(it.getIndexKeys()).containsEntry("$**", 1);
1336+
});
1337+
}
1338+
1339+
@Test // GH-3225
1340+
public void resolvesWildcardOnProperty() {
1341+
1342+
List<IndexDefinitionHolder> indices = prepareMappingContextAndResolveIndexForType(
1343+
WithWildCardIndexOnProperty.class);
1344+
assertThat(indices).hasSize(3);
1345+
assertThat(indices.get(0)).satisfies(it -> {
1346+
assertThat(it.getIndexKeys()).containsEntry("value.$**", 1);
1347+
});
1348+
assertThat(indices.get(1)).satisfies(it -> {
1349+
assertThat(it.getIndexKeys()).containsEntry("the_field.$**", 1);
1350+
});
1351+
assertThat(indices.get(2)).satisfies(it -> {
1352+
assertThat(it.getIndexKeys()).containsEntry("withOptions.$**", 1);
1353+
assertThat(it.getIndexOptions()).containsEntry("name",
1354+
"withOptions.idx")
1355+
.containsEntry("collation", new org.bson.Document("locale", "en_US"))
1356+
.containsEntry("partialFilterExpression", new org.bson.Document("$eq", 1));
1357+
});
1358+
}
1359+
1360+
@Test // GH-3225
1361+
public void resolvesWildcardTypeOfNestedProperty() {
1362+
1363+
List<IndexDefinitionHolder> indices = prepareMappingContextAndResolveIndexForType(
1364+
WithWildCardOnEntityOfNested.class);
1365+
assertThat(indices).hasSize(1);
1366+
assertThat(indices.get(0)).satisfies(it -> {
1367+
assertThat(it.getIndexKeys()).containsEntry("value.$**", 1);
1368+
});
1369+
}
1370+
13261371
@Document
13271372
class MixedIndexRoot {
13281373

@@ -1533,7 +1578,7 @@ class InvalidIndexOnUnwrapped {
15331578

15341579
@Indexed //
15351580
@Unwrapped.Nullable //
1536-
UnwrappableType unwrappableType;
1581+
UnwrappableType unwrappableType;
15371582

15381583
}
15391584

@@ -1573,6 +1618,42 @@ class WithHashedIndex {
15731618
@HashIndexed String value;
15741619
}
15751620

1621+
@Document
1622+
@WildcardIndexed
1623+
class WithWildCardIndexOnEntity {
1624+
1625+
String value;
1626+
}
1627+
1628+
@Document
1629+
@WildcardIndexed(wildcardProjection = "{'_id' : 1, 'value' : 0}")
1630+
class WithWildCardIndexHavingProjectionOnEntity {
1631+
1632+
String value;
1633+
}
1634+
1635+
@Document
1636+
class WithWildCardIndexOnProperty {
1637+
1638+
@WildcardIndexed //
1639+
Map<String, String> value;
1640+
1641+
@WildcardIndexed //
1642+
@Field("the_field") //
1643+
Map<String, String> renamedField;
1644+
1645+
@WildcardIndexed(name = "idx", partialFilter = "{ '$eq' : 1 }", collation = "en_US") //
1646+
Map<String, String> withOptions;
1647+
1648+
}
1649+
1650+
@Document
1651+
class WithWildCardOnEntityOfNested {
1652+
1653+
WithWildCardIndexOnEntity value;
1654+
1655+
}
1656+
15761657
@Document
15771658
class WithHashedIndexAndIndex {
15781659

‎src/main/asciidoc/reference/mapping.adoc

+88
Original file line numberDiff line numberDiff line change
@@ -760,6 +760,94 @@ mongoOperations.indexOpsFor(Jedi.class)
760760
----
761761
====
762762

763+
[[mapping-usage-indexes.wildcard-index]]
764+
=== Wildcard Indexes
765+
766+
A `WildcardIndex` is an index that can be used to include all fields or specific ones based a given (wildcard) pattern.
767+
For details, refer to the https://docs.mongodb.com/manual/core/index-wildcard/[MongoDB Documentation].
768+
769+
The index can be set up programmatically using `WildcardIndex` via `IndexOperations`.
770+
771+
.Programmatic WildcardIndex setup
772+
====
773+
[source,java]
774+
----
775+
mongoOperations
776+
.indexOps(User.class)
777+
.ensureIndex(new WildcardIndex("userMetadata"));
778+
----
779+
[source,javascript]
780+
----
781+
db.user.createIndex({ "userMetadata.$**" : 1 }, {})
782+
----
783+
====
784+
785+
The `@WildcardIndex` annotation allows a declarative index setup an can be added on either a type or property.
786+
787+
If placed on a type that is a root level domain entity (one having an `@Document` annotation) will advise the index creator to create a
788+
wildcard index for it.
789+
790+
.Wildcard index on domain type
791+
====
792+
[source,java]
793+
----
794+
@Document
795+
@WildcardIndexed
796+
public class Product {
797+
...
798+
}
799+
----
800+
[source,javascript]
801+
----
802+
db.product.createIndex({ "$**" : 1 },{})
803+
----
804+
====
805+
806+
The `wildcardProjection` can be used to specify keys to in-/exclude in the index.
807+
808+
.Wildcard index with `wildcardProjection`
809+
====
810+
[source,java]
811+
----
812+
@Document
813+
@WildcardIndexed(wildcardProjection = "{ 'userMetadata.age' : 0 }")
814+
public class User {
815+
private @Id String id;
816+
private UserMetadata userMetadata;
817+
}
818+
----
819+
[source,javascript]
820+
----
821+
db.user.createIndex(
822+
{ "$**" : 1 },
823+
{ "wildcardProjection" :
824+
{ "userMetadata.age" : 0 }
825+
}
826+
)
827+
----
828+
====
829+
830+
Wildcard indexes can also be expressed by adding the annotation directly to the field.
831+
Please note that `wildcardProjection` is not allowed on nested paths.
832+
833+
.Wildcard index on property
834+
====
835+
[source,java]
836+
----
837+
@Document
838+
public class User {
839+
private @Id String id;
840+
841+
@WildcardIndexed
842+
private UserMetadata userMetadata;
843+
}
844+
----
845+
[source,javascript]
846+
----
847+
db.user.createIndex({ "userMetadata.$**" : 1 }, {})
848+
----
849+
====
850+
763851
[[mapping-usage-indexes.text-index]]
764852
=== Text Indexes
765853

0 commit comments

Comments
 (0)
Please sign in to comment.