includeExclude) {
+
+ wildcardProjection.putAll(includeExclude);
+ return this;
+ }
+
+ private String getTargetFieldName() {
+ return StringUtils.hasText(fieldName) ? (fieldName + ".$**") : "$**";
+ }
+
+ @Override
+ public Document getIndexKeys() {
+ return new Document(getTargetFieldName(), 1);
+ }
+
+ @Override
+ public Document getIndexOptions() {
+
+ Document options = new Document(super.getIndexOptions());
+ if (!CollectionUtils.isEmpty(wildcardProjection)) {
+ options.put("wildcardProjection", new Document(wildcardProjection));
+ }
+ return options;
+ }
+}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/WildcardIndexed.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/WildcardIndexed.java
new file mode 100644
index 0000000000..5f32aaf45c
--- /dev/null
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/WildcardIndexed.java
@@ -0,0 +1,130 @@
+/*
+ * 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.mongodb.core.index;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Annotation for an entity or property that should be used as key for a
+ * Wildcard Index.
+ * If placed on a {@link ElementType#TYPE type} that is a root level domain entity (one having an
+ * {@link org.springframework.data.mongodb.core.mapping.Document} annotation) will advise the index creator to create a
+ * wildcard index for it.
+ *
+ *
+ *
+ * @Document
+ * @WildcardIndexed
+ * public class Product {
+ * ...
+ * }
+ *
+ * db.product.createIndex({ "$**" : 1 } , {})
+ *
+ *
+ * {@literal wildcardProjection} can be used to specify keys to in-/exclude in the index.
+ *
+ *
+ *
+ * @Document
+ * @WildcardIndexed(wildcardProjection = "{ 'userMetadata.age' : 0 }")
+ * public class User {
+ * private @Id String id;
+ * private UserMetadata userMetadata;
+ * }
+ *
+ *
+ * db.user.createIndex(
+ * { "$**" : 1 },
+ * { "wildcardProjection" :
+ * { "userMetadata.age" : 0 }
+ * }
+ * )
+ *
+ *
+ * Wildcard indexes can also be expressed by adding the annotation directly to the field. Please note that
+ * {@literal wildcardProjection} is not allowed on nested paths.
+ *
+ *
+ * @Document
+ * public class User {
+ *
+ * private @Id String id;
+ *
+ * @WildcardIndexed
+ * private UserMetadata userMetadata;
+ * }
+ *
+ *
+ * db.user.createIndex({ "userMetadata.$**" : 1 }, {})
+ *
+ *
+ * @author Christoph Strobl
+ * @since 3.3
+ */
+@Documented
+@Target({ ElementType.TYPE, ElementType.FIELD })
+@Retention(RetentionPolicy.RUNTIME)
+public @interface WildcardIndexed {
+
+ /**
+ * Index name either as plain value or as {@link org.springframework.expression.spel.standard.SpelExpression template
+ * expression}.
+ *
+ * The name will only be applied as is when defined on root level. For usage on nested or embedded structures the
+ * provided name will be prefixed with the path leading to the entity.
+ *
+ * @return
+ */
+ String name() default "";
+
+ /**
+ * If set to {@literal true} then MongoDB will ignore the given index name and instead generate a new name. Defaults
+ * to {@literal false}.
+ *
+ * @return {@literal false} by default.
+ */
+ boolean useGeneratedName() default false;
+
+ /**
+ * Only index the documents in a collection that meet a specified {@link IndexFilter filter expression}.
+ *
+ * @return empty by default.
+ * @see https://docs.mongodb.com/manual/core/index-partial/
+ */
+ String partialFilter() default "";
+
+ /**
+ * Explicitly specify sub fields to be in-/excluded as a {@link org.bson.Document#parse(String) prasable} String.
+ *
+ * NOTE: Can only be done on root level documents.
+ *
+ * @return empty by default.
+ */
+ String wildcardProjection() default "";
+
+ /**
+ * Defines the collation to apply.
+ *
+ * @return an empty {@link String} by default.
+ */
+ String collation() default "";
+}
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/index/IndexInfoUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/index/IndexInfoUnitTests.java
index 2026dfc644..3618e4c1f9 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/index/IndexInfoUnitTests.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/index/IndexInfoUnitTests.java
@@ -36,6 +36,7 @@ public class IndexInfoUnitTests {
static final String INDEX_WITH_PARTIAL_FILTER = "{ \"v\" : 2, \"key\" : { \"k3y\" : 1 }, \"name\" : \"partial-filter-index\", \"ns\" : \"db.collection\", \"partialFilterExpression\" : { \"quantity\" : { \"$gte\" : 10 } } }";
static final String INDEX_WITH_EXPIRATION_TIME = "{ \"v\" : 2, \"key\" : { \"lastModifiedDate\" : 1 },\"name\" : \"expire-after-last-modified\", \"ns\" : \"db.collectio\", \"expireAfterSeconds\" : 3600 }";
static final String HASHED_INDEX = "{ \"v\" : 2, \"key\" : { \"score\" : \"hashed\" }, \"name\" : \"score_hashed\", \"ns\" : \"db.collection\" }";
+ static final String WILDCARD_INDEX = "{ \"v\" : 2, \"key\" : { \"$**\" : 1 }, \"name\" : \"$**_1\", \"wildcardProjection\" : { \"fieldA\" : 0, \"fieldB.fieldC\" : 0 } }";
@Test
public void isIndexForFieldsCorrectly() {
@@ -79,6 +80,16 @@ public void hashedIndexIsMarkedAsSuch() {
assertThat(getIndexInfo(HASHED_INDEX).isHashed()).isTrue();
}
+ @Test // GH-3225
+ public void identifiesWildcardIndexCorrectly() {
+ assertThat(getIndexInfo(WILDCARD_INDEX).isWildcard()).isTrue();
+ }
+
+ @Test // GH-3225
+ public void readsWildcardIndexProjectionCorrectly() {
+ assertThat(getIndexInfo(WILDCARD_INDEX).getWildcardProjection()).contains(new Document("fieldA", 0).append("fieldB.fieldC", 0));
+ }
+
private static IndexInfo getIndexInfo(String documentJson) {
return IndexInfo.indexInfoOf(Document.parse(documentJson));
}
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexResolverUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexResolverUnitTests.java
index 489070548d..0a06561b67 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexResolverUnitTests.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexResolverUnitTests.java
@@ -15,8 +15,9 @@
*/
package org.springframework.data.mongodb.core.index;
+import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.*;
-import static org.springframework.data.mongodb.test.util.Assertions.*;
+import static org.springframework.data.mongodb.test.util.Assertions.assertThatExceptionOfType;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
@@ -25,6 +26,7 @@
import java.util.Arrays;
import java.util.LinkedHashSet;
import java.util.List;
+import java.util.Map;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -1323,6 +1325,49 @@ public void errorsOnIndexOnEmbedded() {
}
+ @Test // GH-3225
+ public void resolvesWildcardOnRoot() {
+
+ List indices = prepareMappingContextAndResolveIndexForType(
+ WithWildCardIndexOnEntity.class);
+ assertThat(indices).hasSize(1);
+ assertThat(indices.get(0)).satisfies(it -> {
+ assertThat(it.getIndexKeys()).containsEntry("$**", 1);
+ });
+ }
+
+ @Test // GH-3225
+ public void resolvesWildcardOnProperty() {
+
+ List indices = prepareMappingContextAndResolveIndexForType(
+ WithWildCardIndexOnProperty.class);
+ assertThat(indices).hasSize(3);
+ assertThat(indices.get(0)).satisfies(it -> {
+ assertThat(it.getIndexKeys()).containsEntry("value.$**", 1);
+ });
+ assertThat(indices.get(1)).satisfies(it -> {
+ assertThat(it.getIndexKeys()).containsEntry("the_field.$**", 1);
+ });
+ assertThat(indices.get(2)).satisfies(it -> {
+ assertThat(it.getIndexKeys()).containsEntry("withOptions.$**", 1);
+ assertThat(it.getIndexOptions()).containsEntry("name",
+ "withOptions.idx")
+ .containsEntry("collation", new org.bson.Document("locale", "en_US"))
+ .containsEntry("partialFilterExpression", new org.bson.Document("$eq", 1));
+ });
+ }
+
+ @Test // GH-3225
+ public void resolvesWildcardTypeOfNestedProperty() {
+
+ List indices = prepareMappingContextAndResolveIndexForType(
+ WithWildCardOnEntityOfNested.class);
+ assertThat(indices).hasSize(1);
+ assertThat(indices.get(0)).satisfies(it -> {
+ assertThat(it.getIndexKeys()).containsEntry("value.$**", 1);
+ });
+ }
+
@Document
class MixedIndexRoot {
@@ -1533,7 +1578,7 @@ class InvalidIndexOnUnwrapped {
@Indexed //
@Unwrapped.Nullable //
- UnwrappableType unwrappableType;
+ UnwrappableType unwrappableType;
}
@@ -1573,6 +1618,42 @@ class WithHashedIndex {
@HashIndexed String value;
}
+ @Document
+ @WildcardIndexed
+ class WithWildCardIndexOnEntity {
+
+ String value;
+ }
+
+ @Document
+ @WildcardIndexed(wildcardProjection = "{'_id' : 1, 'value' : 0}")
+ class WithWildCardIndexHavingProjectionOnEntity {
+
+ String value;
+ }
+
+ @Document
+ class WithWildCardIndexOnProperty {
+
+ @WildcardIndexed //
+ Map value;
+
+ @WildcardIndexed //
+ @Field("the_field") //
+ Map renamedField;
+
+ @WildcardIndexed(name = "idx", partialFilter = "{ '$eq' : 1 }", collation = "en_US") //
+ Map withOptions;
+
+ }
+
+ @Document
+ class WithWildCardOnEntityOfNested {
+
+ WithWildCardIndexOnEntity value;
+
+ }
+
@Document
class WithHashedIndexAndIndex {
diff --git a/src/main/asciidoc/reference/mapping.adoc b/src/main/asciidoc/reference/mapping.adoc
index f08d03d3f0..7caf1093b9 100644
--- a/src/main/asciidoc/reference/mapping.adoc
+++ b/src/main/asciidoc/reference/mapping.adoc
@@ -760,6 +760,94 @@ mongoOperations.indexOpsFor(Jedi.class)
----
====
+[[mapping-usage-indexes.wildcard-index]]
+=== Wildcard Indexes
+
+A `WildcardIndex` is an index that can be used to include all fields or specific ones based a given (wildcard) pattern.
+For details, refer to the https://docs.mongodb.com/manual/core/index-wildcard/[MongoDB Documentation].
+
+The index can be set up programmatically using `WildcardIndex` via `IndexOperations`.
+
+.Programmatic WildcardIndex setup
+====
+[source,java]
+----
+mongoOperations
+ .indexOps(User.class)
+ .ensureIndex(new WildcardIndex("userMetadata"));
+----
+[source,javascript]
+----
+db.user.createIndex({ "userMetadata.$**" : 1 }, {})
+----
+====
+
+The `@WildcardIndex` annotation allows a declarative index setup an can be added on either a type or property.
+
+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
+wildcard index for it.
+
+.Wildcard index on domain type
+====
+[source,java]
+----
+@Document
+@WildcardIndexed
+public class Product {
+ ...
+}
+----
+[source,javascript]
+----
+db.product.createIndex({ "$**" : 1 },{})
+----
+====
+
+The `wildcardProjection` can be used to specify keys to in-/exclude in the index.
+
+.Wildcard index with `wildcardProjection`
+====
+[source,java]
+----
+@Document
+@WildcardIndexed(wildcardProjection = "{ 'userMetadata.age' : 0 }")
+public class User {
+ private @Id String id;
+ private UserMetadata userMetadata;
+}
+----
+[source,javascript]
+----
+db.user.createIndex(
+ { "$**" : 1 },
+ { "wildcardProjection" :
+ { "userMetadata.age" : 0 }
+ }
+)
+----
+====
+
+Wildcard indexes can also be expressed by adding the annotation directly to the field.
+Please note that `wildcardProjection` is not allowed on nested paths.
+
+.Wildcard index on property
+====
+[source,java]
+----
+@Document
+public class User {
+ private @Id String id;
+
+ @WildcardIndexed
+ private UserMetadata userMetadata;
+}
+----
+[source,javascript]
+----
+db.user.createIndex({ "userMetadata.$**" : 1 }, {})
+----
+====
+
[[mapping-usage-indexes.text-index]]
=== Text Indexes