Skip to content

Commit 29f824c

Browse files
committed
Add VectorIndex and SearchIndexDefinition abstraction.
1 parent 8dccbae commit 29f824c

18 files changed

+915
-727
lines changed

spring-data-mongodb/pom.xml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,13 @@
131131
<optional>true</optional>
132132
</dependency>
133133

134+
<dependency>
135+
<groupId>org.awaitility</groupId>
136+
<artifactId>awaitility</artifactId>
137+
<version>4.2.2</version>
138+
<scope>test</scope>
139+
</dependency>
140+
134141
<dependency>
135142
<groupId>io.reactivex.rxjava3</groupId>
136143
<artifactId>rxjava</artifactId>

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

Lines changed: 22 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -185,8 +185,8 @@
185185
* @author Michael Krog
186186
* @author Jakub Zurawa
187187
*/
188-
public class MongoTemplate
189-
implements MongoOperations, ApplicationContextAware, IndexOperationsProvider, SearchIndexOperationsProvider, ReadPreferenceAware {
188+
public class MongoTemplate implements MongoOperations, ApplicationContextAware, IndexOperationsProvider,
189+
SearchIndexOperationsProvider, ReadPreferenceAware {
190190

191191
private static final Log LOGGER = LogFactory.getLog(MongoTemplate.class);
192192
private static final WriteResultChecking DEFAULT_WRITE_RESULT_CHECKING = WriteResultChecking.NONE;
@@ -771,6 +771,21 @@ public IndexOperations indexOps(Class<?> entityClass) {
771771
return indexOps(getCollectionName(entityClass), entityClass);
772772
}
773773

774+
@Override
775+
public SearchIndexOperations searchIndexOps(String collectionName) {
776+
return searchIndexOps(null, collectionName);
777+
}
778+
779+
@Override
780+
public SearchIndexOperations searchIndexOps(Class<?> type) {
781+
return new DefaultSearchIndexOperations(this, type);
782+
}
783+
784+
@Override
785+
public SearchIndexOperations searchIndexOps(@Nullable Class<?> type, String collectionName) {
786+
return new DefaultSearchIndexOperations(this, collectionName, type);
787+
}
788+
774789
@Override
775790
public BulkOperations bulkOps(BulkMode mode, String collectionName) {
776791
return bulkOps(mode, null, collectionName);
@@ -1316,7 +1331,7 @@ private WriteConcern potentiallyForceAcknowledgedWrite(@Nullable WriteConcern wc
13161331

13171332
if (ObjectUtils.nullSafeEquals(WriteResultChecking.EXCEPTION, writeResultChecking)) {
13181333
if (wc == null || wc.getWObject() == null
1319-
|| (wc.getWObject()instanceof Number concern && concern.intValue() < 1)) {
1334+
|| (wc.getWObject() instanceof Number concern && concern.intValue() < 1)) {
13201335
return WriteConcern.ACKNOWLEDGED;
13211336
}
13221337
}
@@ -1968,7 +1983,8 @@ public <T> List<T> mapReduce(Query query, Class<?> domainType, String inputColle
19681983
}
19691984

19701985
if (mapReduceOptions.getOutputSharded().isPresent()) {
1971-
MongoCompatibilityAdapter.mapReduceIterableAdapter(mapReduce).sharded(mapReduceOptions.getOutputSharded().get());
1986+
MongoCompatibilityAdapter.mapReduceIterableAdapter(mapReduce)
1987+
.sharded(mapReduceOptions.getOutputSharded().get());
19721988
}
19731989

19741990
if (StringUtils.hasText(mapReduceOptions.getOutputCollection()) && !mapReduceOptions.usesInlineOutput()) {
@@ -2067,7 +2083,7 @@ public <T> List<T> findAllAndRemove(Query query, Class<T> entityClass, String co
20672083
}
20682084

20692085
@Override
2070-
public <T> UpdateResult replace(Query query, T replacement, ReplaceOptions options, String collectionName){
2086+
public <T> UpdateResult replace(Query query, T replacement, ReplaceOptions options, String collectionName) {
20712087

20722088
Assert.notNull(replacement, "Replacement must not be null");
20732089
return replace(query, (Class<T>) ClassUtils.getUserClass(replacement), replacement, options, collectionName);
@@ -2743,8 +2759,7 @@ protected <T> T doFindAndModify(CollectionPreparer collectionPreparer, String co
27432759
LOGGER.debug(String.format(
27442760
"findAndModify using query: %s fields: %s sort: %s for class: %s and update: %s in collection: %s",
27452761
serializeToJsonSafely(mappedQuery), fields, serializeToJsonSafely(sort), entityClass,
2746-
serializeToJsonSafely(mappedUpdate),
2747-
collectionName));
2762+
serializeToJsonSafely(mappedUpdate), collectionName));
27482763
}
27492764

27502765
return executeFindOneInternal(
@@ -3013,21 +3028,6 @@ static RuntimeException potentiallyConvertRuntimeException(RuntimeException ex,
30133028
return resolved == null ? ex : resolved;
30143029
}
30153030

3016-
@Override
3017-
public SearchIndexOperations searchIndexOps(String collectionName) {
3018-
return searchIndexOps(null, collectionName);
3019-
}
3020-
3021-
@Override
3022-
public SearchIndexOperations searchIndexOps(Class<?> type) {
3023-
return new DefaultSearchIndexOperations(this, type);
3024-
}
3025-
3026-
@Override
3027-
public SearchIndexOperations searchIndexOps(Class<?> type, String collectionName) {
3028-
return new DefaultSearchIndexOperations(this, collectionName, type);
3029-
}
3030-
30313031
// Callback implementations
30323032

30333033
/**

spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/QueryMapper.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1130,7 +1130,7 @@ public Class<?> getFieldType() {
11301130
* @author Oliver Gierke
11311131
* @author Thomas Darimont
11321132
*/
1133-
protected static class MetadataBackedField extends Field {
1133+
public static class MetadataBackedField extends Field {
11341134

11351135
private static final Pattern POSITIONAL_PARAMETER_PATTERN = Pattern.compile("\\.\\$(\\[.*?\\])?");
11361136
private static final Pattern NUMERIC_SEGMENT = Pattern.compile("\\d+");
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/*
2+
* Copyright 2014-2024 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+
* {@link ABetterNameForVector} marks a property as Vector Index Field that contains the actual vector.
26+
*
27+
* @author Mark Paluch
28+
* @since 3.5
29+
*/
30+
@Documented
31+
@Target({ ElementType.FIELD, ElementType.METHOD, ElementType.ANNOTATION_TYPE })
32+
@Retention(RetentionPolicy.RUNTIME)
33+
public @interface ABetterNameForVector {
34+
35+
/**
36+
* @return number of dimensions used for the vector index.
37+
*/
38+
int dimensions();
39+
40+
/**
41+
* @return similarity function used for distance computation.
42+
*/
43+
VectorIndex.SimilarityFunction similarity();
44+
45+
/**
46+
* @return quantization strategy used for the vector index. Defaults to none expecting {@code float} vectors.
47+
*/
48+
VectorIndex.Quantization quantization() default VectorIndex.Quantization.NONE;
49+
50+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/*
2+
* Copyright 2014-2024 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+
* {@link ABetterNameForVectorIndexed} marks a property for inclusion in the Vector Index for filtering.
26+
*
27+
* @author Mark Paluch
28+
* @since 3.5
29+
*/
30+
@Documented
31+
@Target({ ElementType.FIELD, ElementType.METHOD, ElementType.ANNOTATION_TYPE })
32+
@Retention(RetentionPolicy.RUNTIME)
33+
public @interface ABetterNameForVectorIndexed {
34+
35+
}

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

Lines changed: 37 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -21,33 +21,46 @@
2121
import org.apache.commons.logging.Log;
2222
import org.apache.commons.logging.LogFactory;
2323
import org.bson.Document;
24-
import org.springframework.data.mongodb.core.DefaultIndexOperations;
24+
25+
import org.springframework.data.mapping.context.MappingContext;
2526
import org.springframework.data.mongodb.core.MongoOperations;
2627
import org.springframework.data.mongodb.core.aggregation.Aggregation;
2728
import org.springframework.data.mongodb.core.aggregation.AggregationResults;
28-
import org.springframework.data.mongodb.core.convert.QueryMapper;
29-
import org.springframework.data.mongodb.core.index.SearchIndex.Filter;
3029
import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
31-
import org.springframework.lang.NonNull;
30+
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
31+
import org.springframework.data.util.TypeInformation;
3232
import org.springframework.lang.Nullable;
3333

3434
/**
3535
* @author Christoph Strobl
36+
* @author Mark Paluch
37+
* @since 3.5
3638
*/
37-
public class DefaultSearchIndexOperations extends DefaultIndexOperations implements SearchIndexOperations {
39+
public class DefaultSearchIndexOperations implements SearchIndexOperations {
40+
41+
private static final Log LOGGER = LogFactory.getLog(DefaultSearchIndexOperations.class);
3842

39-
private static final Log LOGGER = LogFactory.getLog(SearchIndexOperations.class);
43+
private final MongoOperations mongoOperations;
44+
private final String collectionName;
45+
private final TypeInformation<?> entityTypeInformation;
4046

4147
public DefaultSearchIndexOperations(MongoOperations mongoOperations, Class<?> type) {
4248
this(mongoOperations, mongoOperations.getCollectionName(type), type);
4349
}
4450

4551
public DefaultSearchIndexOperations(MongoOperations mongoOperations, String collectionName, @Nullable Class<?> type) {
46-
super(mongoOperations, collectionName, type);
47-
}
52+
this.collectionName = collectionName;
53+
54+
if (type != null) {
55+
56+
MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext = mongoOperations
57+
.getConverter().getMappingContext();
58+
entityTypeInformation = mappingContext.getRequiredPersistentEntity(type).getTypeInformation();
59+
} else {
60+
entityTypeInformation = null;
61+
}
4862

49-
private static String getMappedPath(String path, MongoPersistentEntity<?> entity, QueryMapper mapper) {
50-
return mapper.getMappedFields(new Document(path, 1), entity).entrySet().iterator().next().getKey();
63+
this.mongoOperations = mongoOperations;
5164
}
5265

5366
@Override
@@ -62,11 +75,10 @@ public boolean exists(String indexName) {
6275
}
6376

6477
@Override
65-
public void updateIndex(SearchIndex index) {
66-
67-
MongoPersistentEntity<?> entity = lookupPersistentEntity(type, collectionName);
78+
public void updateIndex(SearchIndexDefinition index) {
6879

69-
Document indexDocument = createIndexDocument(index, entity);
80+
Document indexDocument = index.getIndexDocument(entityTypeInformation,
81+
mongoOperations.getConverter().getMappingContext());
7082

7183
Document cmdResult = mongoOperations.execute(db -> {
7284

@@ -108,57 +120,40 @@ public List<IndexInfo> getIndexInfo() {
108120
@Override
109121
public String ensureIndex(SearchIndexDefinition indexDefinition) {
110122

111-
if (!(indexDefinition instanceof SearchIndex vsi)) {
123+
if (!(indexDefinition instanceof VectorIndex vsi)) {
112124
throw new IllegalStateException("Index definitions must be of type VectorIndex");
113125
}
114126

115-
MongoPersistentEntity<?> entity = lookupPersistentEntity(type, collectionName);
116-
117-
Document index = createIndexDocument(vsi, entity);
127+
Document index = indexDefinition.getIndexDocument(entityTypeInformation,
128+
mongoOperations.getConverter().getMappingContext());
118129

119130
Document cmdResult = mongoOperations.execute(db -> {
120131

121132
Document command = new Document().append("createSearchIndexes", collectionName).append("indexes", List.of(index));
133+
122134
if (LOGGER.isDebugEnabled()) {
123-
LOGGER.debug("Creating VectorIndex: db.runCommand(%s)".formatted(command.toJson()));
135+
LOGGER.debug("Creating SearchIndex: db.runCommand(%s)".formatted(command.toJson()));
124136
}
137+
125138
return db.runCommand(command);
126139
});
127140

128141
return cmdResult.get("ok").toString().equalsIgnoreCase("1.0") ? vsi.getName() : cmdResult.toJson();
129142
}
130143

131-
@NonNull
132-
private Document createIndexDocument(SearchIndex vsi, MongoPersistentEntity<?> entity) {
133-
134-
Document index = new Document(vsi.getIndexOptions());
135-
Document definition = new Document();
136-
137-
List<Document> fields = new ArrayList<>(vsi.getFilters().size() + 1);
138-
139-
Document vectorField = new Document("type", "vector");
140-
vectorField.append("path", getMappedPath(vsi.getPath(), entity, mapper));
141-
vectorField.append("numDimensions", vsi.getDimensions());
142-
vectorField.append("similarity", vsi.getSimilarity());
143-
144-
fields.add(vectorField);
145-
146-
for (Filter filter : vsi.getFilters()) {
147-
fields.add(new Document("type", "filter").append("path", getMappedPath(filter.path(), entity, mapper)));
148-
}
149-
150-
definition.append("fields", fields);
151-
index.append("definition", definition);
152-
return index;
144+
@Override
145+
public void dropAllIndexes() {
146+
getIndexInfo().forEach(indexInfo -> dropIndex(indexInfo.getName()));
153147
}
154148

155149
@Override
156150
public void dropIndex(String name) {
157151

158152
Document command = new Document().append("dropSearchIndex", collectionName).append("name", name);
159153
if (LOGGER.isDebugEnabled()) {
160-
LOGGER.debug("Dropping VectorIndex: db.runCommand(%s)".formatted(command.toJson()));
154+
LOGGER.debug("Dropping SearchIndex: db.runCommand(%s)".formatted(command.toJson()));
161155
}
162156
mongoOperations.execute(db -> db.runCommand(command));
163157
}
158+
164159
}

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,4 +78,14 @@ default Iterable<? extends IndexDefinition> resolveIndexFor(Class<?> entityType)
7878
return resolveIndexFor(TypeInformation.of(entityType));
7979
}
8080

81+
/**
82+
* Find and create {@link SearchIndexDefinition}s for properties of given {@link TypeInformation}.
83+
* {@link SearchIndexDefinition}s are created for properties and types with {@link VectorIndexed}.
84+
*
85+
* @param typeInformation must not be {@literal null}.
86+
* @return Empty {@link Iterable} in case no {@link IndexDefinition} could be resolved for type.
87+
* @since 3.5
88+
*/
89+
Iterable<? extends SearchIndexDefinition> resolveSearchIndexFor(TypeInformation<?> typeInformation);
90+
8191
}

0 commit comments

Comments
 (0)