Skip to content

Commit 2d7d7bf

Browse files
christophstroblmp911de
authored andcommitted
Extend integration tests.
See #4706 Original pull request: #4882
1 parent e8e110e commit 2d7d7bf

26 files changed

+1119
-387
lines changed

Diff for: spring-data-mongodb/pom.xml

+14
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,20 @@
273273
<scope>test</scope>
274274
</dependency>
275275

276+
<dependency>
277+
<groupId>org.testcontainers</groupId>
278+
<artifactId>junit-jupiter</artifactId>
279+
<version>${testcontainers}</version>
280+
<scope>test</scope>
281+
</dependency>
282+
283+
<dependency>
284+
<groupId>org.testcontainers</groupId>
285+
<artifactId>mongodb</artifactId>
286+
<version>${testcontainers}</version>
287+
<scope>test</scope>
288+
</dependency>
289+
276290
<dependency>
277291
<groupId>jakarta.transaction</groupId>
278292
<artifactId>jakarta.transaction-api</artifactId>

Diff for: spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultIndexOperations.java

+5-5
Original file line numberDiff line numberDiff line change
@@ -50,11 +50,11 @@ public class DefaultIndexOperations implements IndexOperations {
5050

5151
private static final String PARTIAL_FILTER_EXPRESSION_KEY = "partialFilterExpression";
5252

53-
protected final String collectionName;
54-
protected final QueryMapper mapper;
55-
protected final @Nullable Class<?> type;
53+
private final String collectionName;
54+
private final QueryMapper mapper;
55+
private final @Nullable Class<?> type;
5656

57-
protected final MongoOperations mongoOperations;
57+
private final MongoOperations mongoOperations;
5858

5959
/**
6060
* Creates a new {@link DefaultIndexOperations}.
@@ -132,7 +132,7 @@ public String ensureIndex(IndexDefinition indexDefinition) {
132132
}
133133

134134
@Nullable
135-
protected MongoPersistentEntity<?> lookupPersistentEntity(@Nullable Class<?> entityType, String collection) {
135+
private MongoPersistentEntity<?> lookupPersistentEntity(@Nullable Class<?> entityType, String collection) {
136136

137137
if (entityType != null) {
138138
return mapper.getMappingContext().getRequiredPersistentEntity(entityType);

Diff for: spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MappingMongoJsonSchemaCreator.java

+4
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,10 @@ private JsonSchemaProperty computeSchemaForProperty(List<MongoPersistentProperty
185185
Class<?> rawTargetType = computeTargetType(property); // target type before conversion
186186
Class<?> targetType = converter.getTypeMapper().getWriteTargetTypeFor(rawTargetType); // conversion target type
187187

188+
if((rawTargetType.isPrimitive() || ClassUtils.isPrimitiveArray(rawTargetType)) && targetType == Object.class) {
189+
targetType = rawTargetType;
190+
}
191+
188192
if (!isCollection(property) && ObjectUtils.nullSafeEquals(rawTargetType, targetType)) {
189193
if (property.isEntity() || mergeProperties.containsKey(stringPath)) {
190194
List<JsonSchemaProperty> targetProperties = new ArrayList<>();

Diff for: spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/VectorSearchOperation.java

+12-2
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@
2525

2626
import org.bson.BinaryVector;
2727
import org.bson.Document;
28-
2928
import org.springframework.data.domain.Limit;
3029
import org.springframework.data.domain.Vector;
3130
import org.springframework.data.mongodb.core.mapping.MongoVector;
@@ -38,7 +37,6 @@
3837
/**
3938
* Performs a semantic search on data in your Atlas cluster. This stage is only available for Atlas Vector Search.
4039
* Vector data must be less than or equal to 4096 dimensions in width.
41-
* <p>
4240
* <h3>Limitations</h3> You cannot use this stage together with:
4341
* <ul>
4442
* <li>{@link org.springframework.data.mongodb.core.aggregation.LookupOperation Lookup} stages</li>
@@ -452,6 +450,18 @@ default LimitContributor vector(float... vector) {
452450
return vector(Vector.of(vector));
453451
}
454452

453+
/**
454+
* Array of byte numbers that represent the query vector. The number type must match the indexed field value type.
455+
* Otherwise, Atlas Vector Search doesn't return any results or errors.
456+
*
457+
* @param vector the query vector.
458+
* @return
459+
*/
460+
@Contract("_ -> this")
461+
default LimitContributor vector(byte... vector) {
462+
return vector(BinaryVector.int8Vector(vector));
463+
}
464+
455465
/**
456466
* Array of double numbers that represent the query vector. The number type must match the indexed field value type.
457467
* Otherwise, Atlas Vector Search doesn't return any results or errors.

Diff for: spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoConverters.java

+45-2
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
*/
1616
package org.springframework.data.mongodb.core.convert;
1717

18-
import static org.springframework.data.convert.ConverterBuilder.*;
18+
import static org.springframework.data.convert.ConverterBuilder.reading;
1919

2020
import java.math.BigDecimal;
2121
import java.math.BigInteger;
@@ -47,7 +47,6 @@
4747
import org.bson.types.Code;
4848
import org.bson.types.Decimal128;
4949
import org.bson.types.ObjectId;
50-
5150
import org.springframework.core.convert.ConversionFailedException;
5251
import org.springframework.core.convert.TypeDescriptor;
5352
import org.springframework.core.convert.converter.ConditionalConverter;
@@ -119,6 +118,8 @@ static Collection<Object> getConvertersToRegister() {
119118
converters.add(reading(BsonUndefined.class, Object.class, it -> null));
120119
converters.add(reading(String.class, URI.class, URI::create).andWriting(URI::toString));
121120

121+
converters.add(ByteArrayConverterFactory.INSTANCE);
122+
122123
return converters;
123124
}
124125

@@ -473,6 +474,48 @@ public Vector convert(BinaryVector source) {
473474
}
474475
}
475476

477+
@WritingConverter
478+
enum ByteArrayConverterFactory implements ConverterFactory<byte[], Object>, ConditionalConverter {
479+
480+
INSTANCE;
481+
482+
@Override
483+
public <T> Converter<byte[], T> getConverter(Class<T> targetType) {
484+
return new ByteArrayConverter<>(targetType);
485+
}
486+
487+
@Override
488+
public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) {
489+
return targetType.getType() != Object.class && !sourceType.equals(targetType);
490+
}
491+
492+
private final static class ByteArrayConverter<T> implements Converter<byte[], T> {
493+
494+
private final Class<T> targetType;
495+
496+
/**
497+
* Creates a new {@link ByteArrayConverter} for the given target type.
498+
*
499+
* @param targetType must not be {@literal null}.
500+
*/
501+
public ByteArrayConverter(Class<T> targetType) {
502+
503+
Assert.notNull(targetType, "Target type must not be null");
504+
505+
this.targetType = targetType;
506+
}
507+
508+
@Override
509+
public T convert(byte[] source) {
510+
511+
if (this.targetType == BinaryVector.class) {
512+
return (T) BinaryVector.int8Vector(source);
513+
}
514+
return (T) source;
515+
}
516+
}
517+
}
518+
476519
/**
477520
* {@link ConverterFactory} implementation converting {@link AtomicLong} into {@link Long}.
478521
*
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2024 the original author or authors.
2+
* Copyright 2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -18,24 +18,23 @@
1818
import java.util.ArrayList;
1919
import java.util.List;
2020

21+
import org.bson.BsonString;
2122
import org.bson.Document;
22-
2323
import org.springframework.data.mapping.context.MappingContext;
2424
import org.springframework.data.mongodb.core.MongoOperations;
25-
import org.springframework.data.mongodb.core.aggregation.Aggregation;
26-
import org.springframework.data.mongodb.core.aggregation.AggregationResults;
2725
import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
2826
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
2927
import org.springframework.data.util.TypeInformation;
3028
import org.springframework.lang.Nullable;
29+
import org.springframework.util.StringUtils;
3130

3231
import com.mongodb.client.model.SearchIndexModel;
3332
import com.mongodb.client.model.SearchIndexType;
3433

3534
/**
3635
* @author Christoph Strobl
3736
* @author Mark Paluch
38-
* @since 3.5
37+
* @since 4.5
3938
*/
4039
public class DefaultSearchIndexOperations implements SearchIndexOperations {
4140

@@ -48,6 +47,7 @@ public DefaultSearchIndexOperations(MongoOperations mongoOperations, Class<?> ty
4847
}
4948

5049
public DefaultSearchIndexOperations(MongoOperations mongoOperations, String collectionName, @Nullable Class<?> type) {
50+
5151
this.collectionName = collectionName;
5252

5353
if (type != null) {
@@ -63,80 +63,63 @@ public DefaultSearchIndexOperations(MongoOperations mongoOperations, String coll
6363
}
6464

6565
@Override
66-
public String ensureIndex(SearchIndexDefinition indexDefinition) {
67-
68-
if (!(indexDefinition instanceof VectorIndex vsi)) {
69-
throw new IllegalStateException("Index definitions must be of type VectorIndex");
70-
}
66+
public String createIndex(SearchIndexDefinition indexDefinition) {
7167

7268
Document index = indexDefinition.getIndexDocument(entityTypeInformation,
7369
mongoOperations.getConverter().getMappingContext());
7470

75-
mongoOperations.getCollection(collectionName).createSearchIndexes(List
76-
.of(new SearchIndexModel(vsi.getName(), (Document) index.get("definition"), SearchIndexType.vectorSearch())));
71+
mongoOperations.getCollection(collectionName)
72+
.createSearchIndexes(List.of(new SearchIndexModel(indexDefinition.getName(),
73+
index.get("definition", Document.class), SearchIndexType.of(new BsonString(indexDefinition.getType())))));
7774

78-
return vsi.getName();
75+
return indexDefinition.getName();
7976
}
8077

8178
@Override
82-
public void updateIndex(SearchIndexDefinition index) {
83-
84-
if (index instanceof VectorIndex) {
85-
throw new UnsupportedOperationException("Vector Index definitions cannot be updated");
86-
}
79+
public void updateIndex(SearchIndexDefinition indexDefinition) {
8780

88-
Document indexDocument = index.getIndexDocument(entityTypeInformation,
81+
Document indexDocument = indexDefinition.getIndexDocument(entityTypeInformation,
8982
mongoOperations.getConverter().getMappingContext());
9083

91-
mongoOperations.getCollection(collectionName).updateSearchIndex(index.getName(), indexDocument);
84+
mongoOperations.getCollection(collectionName).updateSearchIndex(indexDefinition.getName(), indexDocument);
9285
}
9386

9487
@Override
9588
public boolean exists(String indexName) {
96-
97-
List<Document> indexes = mongoOperations.getCollection(collectionName).listSearchIndexes().into(new ArrayList<>());
98-
99-
for (Document index : indexes) {
100-
if (index.getString("name").equals(indexName)) {
101-
return true;
102-
}
103-
}
104-
105-
return false;
89+
return getSearchIndex(indexName) != null;
10690
}
10791

10892
@Override
109-
public List<IndexInfo> getIndexInfo() {
110-
111-
AggregationResults<Document> aggregate = mongoOperations.aggregate(
112-
Aggregation.newAggregation(context -> new Document("$listSearchIndexes", new Document())), collectionName,
113-
Document.class);
93+
public SearchIndexStatus status(String indexName) {
11494

115-
ArrayList<IndexInfo> result = new ArrayList<>();
116-
for (Document doc : aggregate) {
117-
118-
List<IndexField> indexFields = new ArrayList<>();
119-
String name = doc.getString("name");
120-
for (Object field : doc.get("latestDefinition", Document.class).get("fields", List.class)) {
121-
122-
if (field instanceof Document fieldInfo) {
123-
indexFields.add(IndexField.vector(fieldInfo.getString("path")));
124-
}
125-
}
126-
127-
result.add(new IndexInfo(indexFields, name, false, false, null, false));
128-
}
129-
return result;
95+
Document searchIndex = getSearchIndex(indexName);
96+
return searchIndex != null ? SearchIndexStatus.valueOf(searchIndex.getString("status"))
97+
: SearchIndexStatus.DOES_NOT_EXIST;
13098
}
13199

132100
@Override
133101
public void dropAllIndexes() {
134-
getIndexInfo().forEach(indexInfo -> dropIndex(indexInfo.getName()));
102+
getSearchIndexes(null).forEach(indexInfo -> dropIndex(indexInfo.getString("name")));
135103
}
136104

137105
@Override
138-
public void dropIndex(String name) {
139-
mongoOperations.getCollection(collectionName).dropSearchIndex(name);
106+
public void dropIndex(String indexName) {
107+
mongoOperations.getCollection(collectionName).dropSearchIndex(indexName);
108+
}
109+
110+
@Nullable
111+
private Document getSearchIndex(String indexName) {
112+
113+
List<Document> indexes = getSearchIndexes(indexName);
114+
return indexes.isEmpty() ? null : indexes.iterator().next();
115+
}
116+
117+
private List<Document> getSearchIndexes(@Nullable String indexName) {
118+
119+
Document filter = StringUtils.hasText(indexName) ? new Document("name", indexName) : new Document();
120+
121+
return mongoOperations.getCollection(collectionName).aggregate(List.of(new Document("$listSearchIndexes", filter)))
122+
.into(new ArrayList<>());
140123
}
141124

142125
}

Diff for: spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/IndexOperations.java

+14
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,23 @@ public interface IndexOperations {
3333
*
3434
* @param indexDefinition must not be {@literal null}.
3535
* @return the index name.
36+
* @deprecated in favor of {@link #createIndex(IndexDefinition)}.
3637
*/
38+
@Deprecated(since = "4.5", forRemoval = true)
3739
String ensureIndex(IndexDefinition indexDefinition);
3840

41+
/**
42+
* Create the index for the provided {@link IndexDefinition} exists for the collection indicated by the entity
43+
* class. If not it will be created.
44+
*
45+
* @param indexDefinition must not be {@literal null}.
46+
* @return the index name.
47+
* @since 4.5
48+
*/
49+
default String createIndex(IndexDefinition indexDefinition) {
50+
return ensureIndex(indexDefinition);
51+
}
52+
3953
/**
4054
* Alters the index with given {@literal name}.
4155
*

0 commit comments

Comments
 (0)