Extend integration tests.

See #4706
Original pull request: #4882
This commit is contained in:
Christoph Strobl
2025-01-29 10:21:04 +01:00
committed by Mark Paluch
parent e8e110e31d
commit 2d7d7bf004
26 changed files with 1127 additions and 395 deletions

View File

@@ -273,6 +273,20 @@
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<version>${testcontainers}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>mongodb</artifactId>
<version>${testcontainers}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>jakarta.transaction</groupId>
<artifactId>jakarta.transaction-api</artifactId>

View File

@@ -50,11 +50,11 @@ public class DefaultIndexOperations implements IndexOperations {
private static final String PARTIAL_FILTER_EXPRESSION_KEY = "partialFilterExpression";
protected final String collectionName;
protected final QueryMapper mapper;
protected final @Nullable Class<?> type;
private final String collectionName;
private final QueryMapper mapper;
private final @Nullable Class<?> type;
protected final MongoOperations mongoOperations;
private final MongoOperations mongoOperations;
/**
* Creates a new {@link DefaultIndexOperations}.
@@ -132,7 +132,7 @@ public class DefaultIndexOperations implements IndexOperations {
}
@Nullable
protected MongoPersistentEntity<?> lookupPersistentEntity(@Nullable Class<?> entityType, String collection) {
private MongoPersistentEntity<?> lookupPersistentEntity(@Nullable Class<?> entityType, String collection) {
if (entityType != null) {
return mapper.getMappingContext().getRequiredPersistentEntity(entityType);

View File

@@ -185,6 +185,10 @@ class MappingMongoJsonSchemaCreator implements MongoJsonSchemaCreator {
Class<?> rawTargetType = computeTargetType(property); // target type before conversion
Class<?> targetType = converter.getTypeMapper().getWriteTargetTypeFor(rawTargetType); // conversion target type
if((rawTargetType.isPrimitive() || ClassUtils.isPrimitiveArray(rawTargetType)) && targetType == Object.class) {
targetType = rawTargetType;
}
if (!isCollection(property) && ObjectUtils.nullSafeEquals(rawTargetType, targetType)) {
if (property.isEntity() || mergeProperties.containsKey(stringPath)) {
List<JsonSchemaProperty> targetProperties = new ArrayList<>();

View File

@@ -25,7 +25,6 @@ import java.util.stream.Collectors;
import org.bson.BinaryVector;
import org.bson.Document;
import org.springframework.data.domain.Limit;
import org.springframework.data.domain.Vector;
import org.springframework.data.mongodb.core.mapping.MongoVector;
@@ -38,7 +37,6 @@ import org.springframework.util.StringUtils;
/**
* Performs a semantic search on data in your Atlas cluster. This stage is only available for Atlas Vector Search.
* Vector data must be less than or equal to 4096 dimensions in width.
* <p>
* <h3>Limitations</h3> You cannot use this stage together with:
* <ul>
* <li>{@link org.springframework.data.mongodb.core.aggregation.LookupOperation Lookup} stages</li>
@@ -452,6 +450,18 @@ public class VectorSearchOperation implements AggregationOperation {
return vector(Vector.of(vector));
}
/**
* Array of byte numbers that represent the query vector. The number type must match the indexed field value type.
* Otherwise, Atlas Vector Search doesn't return any results or errors.
*
* @param vector the query vector.
* @return
*/
@Contract("_ -> this")
default LimitContributor vector(byte... vector) {
return vector(BinaryVector.int8Vector(vector));
}
/**
* Array of double numbers that represent the query vector. The number type must match the indexed field value type.
* Otherwise, Atlas Vector Search doesn't return any results or errors.

View File

@@ -15,7 +15,7 @@
*/
package org.springframework.data.mongodb.core.convert;
import static org.springframework.data.convert.ConverterBuilder.*;
import static org.springframework.data.convert.ConverterBuilder.reading;
import java.math.BigDecimal;
import java.math.BigInteger;
@@ -47,7 +47,6 @@ import org.bson.types.Binary;
import org.bson.types.Code;
import org.bson.types.Decimal128;
import org.bson.types.ObjectId;
import org.springframework.core.convert.ConversionFailedException;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.core.convert.converter.ConditionalConverter;
@@ -119,6 +118,8 @@ abstract class MongoConverters {
converters.add(reading(BsonUndefined.class, Object.class, it -> null));
converters.add(reading(String.class, URI.class, URI::create).andWriting(URI::toString));
converters.add(ByteArrayConverterFactory.INSTANCE);
return converters;
}
@@ -473,6 +474,48 @@ abstract class MongoConverters {
}
}
@WritingConverter
enum ByteArrayConverterFactory implements ConverterFactory<byte[], Object>, ConditionalConverter {
INSTANCE;
@Override
public <T> Converter<byte[], T> getConverter(Class<T> targetType) {
return new ByteArrayConverter<>(targetType);
}
@Override
public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) {
return targetType.getType() != Object.class && !sourceType.equals(targetType);
}
private final static class ByteArrayConverter<T> implements Converter<byte[], T> {
private final Class<T> targetType;
/**
* Creates a new {@link ByteArrayConverter} for the given target type.
*
* @param targetType must not be {@literal null}.
*/
public ByteArrayConverter(Class<T> targetType) {
Assert.notNull(targetType, "Target type must not be null");
this.targetType = targetType;
}
@Override
public T convert(byte[] source) {
if (this.targetType == BinaryVector.class) {
return (T) BinaryVector.int8Vector(source);
}
return (T) source;
}
}
}
/**
* {@link ConverterFactory} implementation converting {@link AtomicLong} into {@link Long}.
*

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2024 the original author or authors.
* Copyright 2025 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.
@@ -18,16 +18,15 @@ package org.springframework.data.mongodb.core.index;
import java.util.ArrayList;
import java.util.List;
import org.bson.BsonString;
import org.bson.Document;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.mongodb.core.MongoOperations;
import org.springframework.data.mongodb.core.aggregation.Aggregation;
import org.springframework.data.mongodb.core.aggregation.AggregationResults;
import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
import org.springframework.data.util.TypeInformation;
import org.springframework.lang.Nullable;
import org.springframework.util.StringUtils;
import com.mongodb.client.model.SearchIndexModel;
import com.mongodb.client.model.SearchIndexType;
@@ -35,7 +34,7 @@ import com.mongodb.client.model.SearchIndexType;
/**
* @author Christoph Strobl
* @author Mark Paluch
* @since 3.5
* @since 4.5
*/
public class DefaultSearchIndexOperations implements SearchIndexOperations {
@@ -48,6 +47,7 @@ public class DefaultSearchIndexOperations implements SearchIndexOperations {
}
public DefaultSearchIndexOperations(MongoOperations mongoOperations, String collectionName, @Nullable Class<?> type) {
this.collectionName = collectionName;
if (type != null) {
@@ -63,80 +63,63 @@ public class DefaultSearchIndexOperations implements SearchIndexOperations {
}
@Override
public String ensureIndex(SearchIndexDefinition indexDefinition) {
if (!(indexDefinition instanceof VectorIndex vsi)) {
throw new IllegalStateException("Index definitions must be of type VectorIndex");
}
public String createIndex(SearchIndexDefinition indexDefinition) {
Document index = indexDefinition.getIndexDocument(entityTypeInformation,
mongoOperations.getConverter().getMappingContext());
mongoOperations.getCollection(collectionName).createSearchIndexes(List
.of(new SearchIndexModel(vsi.getName(), (Document) index.get("definition"), SearchIndexType.vectorSearch())));
mongoOperations.getCollection(collectionName)
.createSearchIndexes(List.of(new SearchIndexModel(indexDefinition.getName(),
index.get("definition", Document.class), SearchIndexType.of(new BsonString(indexDefinition.getType())))));
return vsi.getName();
return indexDefinition.getName();
}
@Override
public void updateIndex(SearchIndexDefinition index) {
public void updateIndex(SearchIndexDefinition indexDefinition) {
if (index instanceof VectorIndex) {
throw new UnsupportedOperationException("Vector Index definitions cannot be updated");
}
Document indexDocument = index.getIndexDocument(entityTypeInformation,
Document indexDocument = indexDefinition.getIndexDocument(entityTypeInformation,
mongoOperations.getConverter().getMappingContext());
mongoOperations.getCollection(collectionName).updateSearchIndex(index.getName(), indexDocument);
mongoOperations.getCollection(collectionName).updateSearchIndex(indexDefinition.getName(), indexDocument);
}
@Override
public boolean exists(String indexName) {
List<Document> indexes = mongoOperations.getCollection(collectionName).listSearchIndexes().into(new ArrayList<>());
for (Document index : indexes) {
if (index.getString("name").equals(indexName)) {
return true;
}
}
return false;
return getSearchIndex(indexName) != null;
}
@Override
public List<IndexInfo> getIndexInfo() {
public SearchIndexStatus status(String indexName) {
AggregationResults<Document> aggregate = mongoOperations.aggregate(
Aggregation.newAggregation(context -> new Document("$listSearchIndexes", new Document())), collectionName,
Document.class);
ArrayList<IndexInfo> result = new ArrayList<>();
for (Document doc : aggregate) {
List<IndexField> indexFields = new ArrayList<>();
String name = doc.getString("name");
for (Object field : doc.get("latestDefinition", Document.class).get("fields", List.class)) {
if (field instanceof Document fieldInfo) {
indexFields.add(IndexField.vector(fieldInfo.getString("path")));
}
}
result.add(new IndexInfo(indexFields, name, false, false, null, false));
}
return result;
Document searchIndex = getSearchIndex(indexName);
return searchIndex != null ? SearchIndexStatus.valueOf(searchIndex.getString("status"))
: SearchIndexStatus.DOES_NOT_EXIST;
}
@Override
public void dropAllIndexes() {
getIndexInfo().forEach(indexInfo -> dropIndex(indexInfo.getName()));
getSearchIndexes(null).forEach(indexInfo -> dropIndex(indexInfo.getString("name")));
}
@Override
public void dropIndex(String name) {
mongoOperations.getCollection(collectionName).dropSearchIndex(name);
public void dropIndex(String indexName) {
mongoOperations.getCollection(collectionName).dropSearchIndex(indexName);
}
@Nullable
private Document getSearchIndex(String indexName) {
List<Document> indexes = getSearchIndexes(indexName);
return indexes.isEmpty() ? null : indexes.iterator().next();
}
private List<Document> getSearchIndexes(@Nullable String indexName) {
Document filter = StringUtils.hasText(indexName) ? new Document("name", indexName) : new Document();
return mongoOperations.getCollection(collectionName).aggregate(List.of(new Document("$listSearchIndexes", filter)))
.into(new ArrayList<>());
}
}

View File

@@ -33,9 +33,23 @@ public interface IndexOperations {
*
* @param indexDefinition must not be {@literal null}.
* @return the index name.
* @deprecated in favor of {@link #createIndex(IndexDefinition)}.
*/
@Deprecated(since = "4.5", forRemoval = true)
String ensureIndex(IndexDefinition indexDefinition);
/**
* Create the index for the provided {@link IndexDefinition} exists for the collection indicated by the entity
* class. If not it will be created.
*
* @param indexDefinition must not be {@literal null}.
* @return the index name.
* @since 4.5
*/
default String createIndex(IndexDefinition indexDefinition) {
return ensureIndex(indexDefinition);
}
/**
* Alters the index with given {@literal name}.
*

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2011-2024 the original author or authors.
* Copyright 2025 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.
@@ -16,7 +16,6 @@
package org.springframework.data.mongodb.core.index;
import org.bson.Document;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
@@ -42,17 +41,28 @@ public interface SearchIndexDefinition {
*/
String getType();
/**
* Returns the index document for this index without any potential entity context resolving field name mappings. The
* resulting document contains the index name, type and {@link #getDefinition(TypeInformation, MappingContext)
* definition}.
*
* @return never {@literal null}.
*/
default Document getRawIndexDocument() {
return getIndexDocument(null, null);
}
/**
* Returns the index document for this index in the context of a potential entity to resolve field name mappings. The
* resulting document contains the index name, type and {@link #getDefinition(TypeInformation, MappingContext)
* definition}.
*
* @param entity
* @param entity can be {@literal null}.
* @param mappingContext
* @return
* @return never {@literal null}.
*/
default Document getIndexDocument(@Nullable TypeInformation<?> entity,
MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext) {
@Nullable MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext) {
Document document = new Document();
document.put("name", getName());
@@ -66,11 +76,10 @@ public interface SearchIndexDefinition {
* Returns the actual index definition for this index in the context of a potential entity to resolve field name
* mappings.
*
* @param entity
* @param entity can be {@literal null}.
* @param mappingContext
* @return
* @return never {@literal null}.
*/
Document getDefinition(@Nullable TypeInformation<?> entity,
MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext);
@Nullable MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext);
}

View File

@@ -0,0 +1,115 @@
/*
* Copyright 2025 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.util.function.Supplier;
import org.bson.Document;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
import org.springframework.data.util.Lazy;
import org.springframework.data.util.TypeInformation;
import org.springframework.lang.Nullable;
/**
* Index information for a MongoDB Search Index.
*
* @author Christoph Strobl
*/
public class SearchIndexInfo {
private final @Nullable Object id;
private final SearchIndexStatus status;
private final Lazy<SearchIndexDefinition> indexDefinition;
SearchIndexInfo(@Nullable Object id, SearchIndexStatus status, Supplier<SearchIndexDefinition> indexDefinition) {
this.id = id;
this.status = status;
this.indexDefinition = Lazy.of(indexDefinition);
}
public static SearchIndexInfo parse(String source) {
return of(Document.parse(source));
}
public static SearchIndexInfo of(Document indexDocument) {
Object id = indexDocument.get("id");
SearchIndexStatus status = SearchIndexStatus.valueOf(indexDocument.get("status", "DOES_NOT_EXIST"));
return new SearchIndexInfo(id, status, () -> readIndexDefinition(indexDocument));
}
/**
* The id of the index. Can be {@literal null}, eg. for an index not yet created.
*
* @return can be {@literal null}.
*/
@Nullable
public Object getId() {
return id;
}
/**
* @return the current status of the index.
*/
public SearchIndexStatus getStatus() {
return status;
}
/**
* @return the current index definition.
*/
public SearchIndexDefinition getIndexDefinition() {
return indexDefinition.get();
}
private static SearchIndexDefinition readIndexDefinition(Document document) {
String type = document.get("type", "search");
if (type.equals("vectorSearch")) {
return VectorIndex.of(document);
}
return new SearchIndexDefinition() {
@Override
public String getName() {
return document.getString("name");
}
@Override
public String getType() {
return type;
}
@Override
public Document getDefinition(@Nullable TypeInformation<?> entity,
@Nullable MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext) {
if (document.containsKey("latestDefinition")) {
return document.get("latestDefinition", new Document());
}
return document.get("definition", new Document());
}
@Override
public String toString() {
return getDefinition(null, null).toJson();
}
};
}
}

View File

@@ -1,11 +1,11 @@
/*
* Copyright 2024. the original author or authors.
* Copyright 2025 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
*
* http://www.apache.org/licenses/LICENSE-2.0
* 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,
@@ -15,7 +15,7 @@
*/
package org.springframework.data.mongodb.core.index;
import java.util.List;
import org.springframework.dao.DataAccessException;
/**
* Search Index operations on a collection for Atlas Search.
@@ -23,52 +23,65 @@ import java.util.List;
* @author Christoph Strobl
* @author Mark Paluch
* @since 4.5
* @see VectorIndex
*/
public interface SearchIndexOperations {
/**
* Ensure that an index for the provided {@link SearchIndexDefinition} exists for the collection indicated by the
* entity class. If not it will be created.
* Create the index for the given {@link SearchIndexDefinition} in the collection indicated by the entity class.
*
* @param indexDefinition must not be {@literal null}.
* @return the index name.
*/
String ensureIndex(SearchIndexDefinition indexDefinition);
// TODO: keep or just go with createIndex?
default String ensureIndex(SearchIndexDefinition indexDefinition) {
return createIndex(indexDefinition);
}
/**
* Alters the search {@code index}.
* <p>
* Note that Atlas Search does not support updating Vector Search Indices resulting in
* {@link UnsupportedOperationException}.
* Create the index for the given {@link SearchIndexDefinition} in the collection indicated by the entity class.
*
* @param index the index definition.
* @param indexDefinition must not be {@literal null}.
* @return the index name.
*/
void updateIndex(SearchIndexDefinition index);
String createIndex(SearchIndexDefinition indexDefinition);
/**
* Check whether an index with the {@code name} exists.
* Alters the search index matching the index {@link SearchIndexDefinition#getName() name}.
* <p>
* Atlas Search might not support updating indices which raises a {@link DataAccessException}.
*
* @param name name of index to check for presence.
* @param indexDefinition the index definition.
*/
// TODO: keep or remove since it does not work reliably?
void updateIndex(SearchIndexDefinition indexDefinition);
/**
* Check whether an index with the given {@code indexName} exists for the collection indicated by the entity class. To
* ensure an existing index is queryable it is recommended to check its {@link #status(String) status}.
*
* @param indexName name of index to check for presence.
* @return {@literal true} if the index exists; {@literal false} otherwise.
*/
boolean exists(String name);
boolean exists(String indexName);
/**
* Drops an index from this collection.
* Check the actual {@link SearchIndexStatus status} of an index.
*
* @param name name of index to drop.
* @param indexName name of index to get the status for.
* @return the current status of the index or {@link SearchIndexStatus#DOES_NOT_EXIST} if the index cannot be found.
*/
void dropIndex(String name);
SearchIndexStatus status(String indexName);
/**
* Drops all search indices from this collection.
* Drops an index from the collection indicated by the entity class.
*
* @param indexName name of index to drop.
*/
void dropIndex(String indexName);
/**
* Drops all search indices from the collection indicated by the entity class.
*/
void dropAllIndexes();
/**
* Returns the index information on the collection.
*
* @return index information on the collection
*/
List<IndexInfo> getIndexInfo();
}

View File

@@ -1,11 +1,11 @@
/*
* Copyright 2024. the original author or authors.
* Copyright 2025 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
*
* http://www.apache.org/licenses/LICENSE-2.0
* 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,

View File

@@ -0,0 +1,46 @@
/*
* Copyright 2025. 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;
/**
* Representation of different conditions a search index can be in.
*
* @author Christoph Strobl
* @since 4.5
*/
public enum SearchIndexStatus {
/** building or re-building the index - might be queryable */
BUILDING,
/** nothing to be seen here - not queryable */
DOES_NOT_EXIST,
/** will cease to exist - no longer queryable */
DELETING,
/** well, this one is broken - not queryable */
FAILED,
/** busy with other things, check back later - not queryable */
PENDING,
/** ask me anything - queryable */
READY,
/** ask me anything about outdated data - still queryable */
STALE
}

View File

@@ -36,7 +36,6 @@ import java.util.List;
import java.util.function.Consumer;
import org.bson.Document;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.mongodb.core.convert.QueryMapper;
import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
@@ -45,9 +44,10 @@ import org.springframework.data.util.TypeInformation;
import org.springframework.lang.Contract;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* {@link IndexDefinition} for creating MongoDB
* {@link SearchIndexDefinition} for creating MongoDB
* <a href="https://www.mongodb.com/docs/atlas/atlas-vector-search/vector-search-overview/">Vector Index</a> required to
* run {@code $vectorSearch} queries.
*
@@ -58,7 +58,7 @@ import org.springframework.util.Assert;
public class VectorIndex implements SearchIndexDefinition {
private final String name;
private final List<Object> fields = new ArrayList<>();
private final List<SearchField> fields = new ArrayList<>();
/**
* Create a new {@link VectorIndex} instance.
@@ -80,8 +80,7 @@ public class VectorIndex implements SearchIndexDefinition {
Assert.hasText(path, "Path must not be null or empty");
fields.add(new VectorFilterField(path, "filter"));
return this;
return addField(new VectorFilterField(path, "filter"));
}
/**
@@ -98,11 +97,7 @@ public class VectorIndex implements SearchIndexDefinition {
VectorFieldBuilder builder = new VectorFieldBuilder(path, "vector");
customizer.accept(builder);
fields.add(
new VectorIndexField(builder.path, builder.type, builder.dimensions, builder.similarity, builder.quantization));
return this;
return addField(builder.build());
}
@Override
@@ -117,46 +112,71 @@ public class VectorIndex implements SearchIndexDefinition {
@Override
public Document getDefinition(@Nullable TypeInformation<?> entity,
MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext) {
@Nullable MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext) {
if (fields.isEmpty()) {
throw new IllegalStateException("At least one vector or filter field must be added to the index");
}
MongoPersistentEntity<?> persistentEntity = entity != null ? mappingContext.getPersistentEntity(entity) : null;
MongoPersistentEntity<?> persistentEntity = entity != null
? (mappingContext != null ? mappingContext.getPersistentEntity(entity) : null)
: null;
Document definition = new Document();
List<Document> fields = new ArrayList<>();
definition.put("fields", fields);
for (Object field : this.fields) {
for (SearchField field : this.fields) {
if (field instanceof VectorFilterField vff) {
Document filter = new Document("type", "filter");
filter.put("path", resolvePath(vff.path(), persistentEntity, mappingContext));
fields.add(filter);
}
Document filter = new Document("type", field.type());
filter.put("path", resolvePath(field.path(), persistentEntity, mappingContext));
if (field instanceof VectorIndexField vif) {
Document filter = new Document("type", "vector");
filter.put("path", resolvePath(vif.path(), persistentEntity, mappingContext));
filter.put("numDimensions", vif.dimensions());
filter.put("similarity", vif.similarity());
filter.put("quantization", vif.quantization());
fields.add(filter);
if (StringUtils.hasText(vif.quantization)) {
filter.put("quantization", vif.quantization());
}
}
fields.add(filter);
}
return definition;
}
private String resolvePath(String path, @Nullable MongoPersistentEntity<?> persistentEntity,
MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext) {
@Contract("_ -> this")
private VectorIndex addField(SearchField filterField) {
if (persistentEntity == null) {
fields.add(filterField);
return this;
}
@Override
public String toString() {
return "VectorIndex{" + "name='" + name + '\'' + ", fields=" + fields + ", type='" + getType() + '\'' + '}';
}
// /** instead of index info */
static VectorIndex of(Document document) {
VectorIndex index = new VectorIndex(document.getString("name"));
String definitionKey = document.containsKey("latestDefinition") ? "latestDefinition" : "definition";
Document definition = document.get(definitionKey, Document.class);
for (Object entry : definition.get("fields", List.class)) {
if (entry instanceof Document field) {
if (field.get("type").equals("vector")) {
index.addField(new VectorIndexField(field.getString("path"), "vector", field.getInteger("numDimensions"),
field.getString("similarity"), field.getString("quantization")));
} else {
index.addField(new VectorFilterField(field.getString("path"), "filter"));
}
}
}
return index;
}
private String resolvePath(String path, @Nullable MongoPersistentEntity<?> persistentEntity,
@Nullable MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext) {
if (persistentEntity == null || mappingContext == null) {
return path;
}
@@ -165,12 +185,23 @@ public class VectorIndex implements SearchIndexDefinition {
return mbf.getMappedKey();
}
record VectorIndexField(String path, String type, int dimensions, String similarity, String quantization) {
interface SearchField {
String path();
String type();
}
record VectorFilterField(String path, String type) {
record VectorFilterField(String path, String type) implements SearchField {
}
record VectorIndexField(String path, String type, int dimensions, String similarity,
@Nullable String quantization) implements SearchField {
}
/**
* Builder to create a vector field
*/
public static class VectorFieldBuilder {
private final String path;
@@ -178,9 +209,10 @@ public class VectorIndex implements SearchIndexDefinition {
private int dimensions;
private @Nullable String similarity;
private String quantization = "none";
private @Nullable String quantization;
VectorFieldBuilder(String path, String type) {
this.path = path;
this.type = type;
}
@@ -204,7 +236,6 @@ public class VectorIndex implements SearchIndexDefinition {
*/
@Contract(" -> this")
public VectorFieldBuilder cosine() {
return similarity(SimilarityFunction.COSINE);
}
@@ -219,7 +250,6 @@ public class VectorIndex implements SearchIndexDefinition {
/**
* Use similarity based on both angle and magnitude of the vectors.
*
* @param name The name of the index.
* @return new instance of {@link VectorIndex}.
*/
@Contract(" -> this")
@@ -237,6 +267,7 @@ public class VectorIndex implements SearchIndexDefinition {
*/
@Contract("_ -> this")
public VectorFieldBuilder similarity(String similarity) {
this.similarity = similarity;
return this;
}
@@ -249,6 +280,7 @@ public class VectorIndex implements SearchIndexDefinition {
*/
@Contract("_ -> this")
public VectorFieldBuilder similarity(SimilarityFunction similarity) {
return similarity(similarity.getFunctionName());
}
@@ -261,12 +293,13 @@ public class VectorIndex implements SearchIndexDefinition {
* @see #quantization(Quantization)
*/
public VectorFieldBuilder quantization(String quantization) {
this.quantization = quantization;
return this;
}
/**
* Quntization used.
* Quantization used.
*
* @param quantization must not be {@literal null}.
* @return this.
@@ -274,9 +307,14 @@ public class VectorIndex implements SearchIndexDefinition {
public VectorFieldBuilder quantization(Quantization quantization) {
return quantization(quantization.getQuantizationName());
}
VectorIndexField build() {
return new VectorIndexField(this.path, this.type, this.dimensions, this.similarity, this.quantization);
}
}
public enum SimilarityFunction {
DOT_PRODUCT("dotProduct"), COSINE("cosine"), EUCLIDEAN("euclidean");
final String functionName;
@@ -290,7 +328,9 @@ public class VectorIndex implements SearchIndexDefinition {
}
}
/** make it nullable */
public enum Quantization {
NONE("none"), SCALAR("scalar"), BINARY("binary");
final String quantizationName;

View File

@@ -18,6 +18,8 @@ package org.springframework.data.mongodb.core.mapping;
import java.util.Date;
import java.util.regex.Pattern;
import org.bson.BinaryVector;
import org.bson.BsonBinary;
import org.bson.types.BSONTimestamp;
import org.bson.types.Binary;
import org.bson.types.Code;
@@ -55,7 +57,8 @@ public enum FieldType {
INT32(15, Integer.class), //
TIMESTAMP(16, BSONTimestamp.class), //
INT64(17, Long.class), //
DECIMAL128(18, Decimal128.class);
DECIMAL128(18, Decimal128.class),
VECTOR(5, BinaryVector.class);
private final int bsonType;
private final Class<?> javaClass;

View File

@@ -24,7 +24,7 @@ import org.springframework.data.domain.Vector;
import org.springframework.util.ObjectUtils;
/**
* MongoDB-specific extension to {@link Vector} based on Mongo's {@link Binary}. Note that only float32 and int8
* MongoDB-specific extension to {@link Vector} based on Mongo's {@link BinaryVector}. Note that only float32 and int8
* variants can be represented as floating-point numbers. int1 returns an all-zero array for {@link #toFloatArray()} and
* {@link #toDoubleArray()}.
*

View File

@@ -301,11 +301,19 @@ public class BsonUtils {
case BOOLEAN -> value.asBoolean().getValue();
case OBJECT_ID -> value.asObjectId().getValue();
case DB_POINTER -> new DBRef(value.asDBPointer().getNamespace(), value.asDBPointer().getId());
case BINARY -> value.asBinary().getData();
case BINARY -> {
BsonBinary binary = value.asBinary();
if(binary.getType() != BsonBinarySubType.VECTOR.getValue()) {
yield binary.getData();
}
yield value.asBinary().asVector();
}
case DATE_TIME -> new Date(value.asDateTime().getValue());
case SYMBOL -> value.asSymbol().getSymbol();
case ARRAY -> value.asArray().toArray();
case DOCUMENT -> Document.parse(value.asDocument().toJson());
default -> value;
};
}

View File

@@ -15,261 +15,224 @@
*/
package org.springframework.data.mongodb.core.aggregation;
import static org.assertj.core.api.Assertions.assertThat;
import java.util.stream.IntStream;
import java.util.stream.Stream;
import org.assertj.core.api.InstanceOfAssertFactories;
import org.bson.BinaryVector;
import org.bson.Document;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.springframework.data.domain.Vector;
import org.springframework.data.mongodb.core.aggregation.VectorSearchOperation.SearchType;
import org.springframework.data.mongodb.core.index.VectorIndex;
import org.springframework.data.mongodb.test.util.EnableIfVectorSearchAvailable;
import org.springframework.data.mongodb.test.util.MongoTemplateExtension;
import org.springframework.data.mongodb.core.index.VectorIndex.SimilarityFunction;
import org.springframework.data.mongodb.core.mapping.Field;
import org.springframework.data.mongodb.core.mapping.FieldType;
import org.springframework.data.mongodb.core.mapping.MongoVector;
import org.springframework.data.mongodb.test.util.AtlasContainer;
import org.springframework.data.mongodb.test.util.MongoTestTemplate;
import org.springframework.data.mongodb.test.util.Template;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import com.mongodb.client.MongoClient;
import com.mongodb.client.MongoClients;
/**
* @author Christoph Strobl
*/
@EnableIfVectorSearchAvailable
@ExtendWith(MongoTemplateExtension.class)
@Testcontainers(disabledWithoutDocker = true)
public class VectorSearchTests {
static final String COLLECTION_NAME = "movies";
@Template(database = "mflix") //
public static final String SCORE_FIELD = "vector-search-tests";
static final String COLLECTION_NAME = "collection-1";
static MongoClient client;
static MongoTestTemplate template;
private static @Container AtlasContainer atlasLocal = AtlasContainer.bestMatch();
@Test
void xxx() {
@BeforeAll
static void beforeAll() throws InterruptedException {
// boolean hasIndex = template.indexOps(COLLECTION_NAME).getIndexInfo().stream()
// .anyMatch(it -> it.getName().endsWith("movie_vector_index"));
client = MongoClients.create(atlasLocal.getConnectionString());
template = new MongoTestTemplate(client, SCORE_FIELD);
// TODO: index conversion etc. is missing - should we combine the index info listing?
// boolean hasIndex = template.execute(db -> {
//
// Document doc = db.runCommand(new Document("listSearchIndexes", COLLECTION_NAME));
// Object searchIndexes = BsonUtils.resolveValue(BsonUtils.asMap(doc), "cursor.firstBatch");
// if(searchIndexes instanceof Collection<?> indexes) {
// return indexes.stream().anyMatch(it -> it instanceof Document idx && idx.get("name",
// String.class).equalsIgnoreCase("vector_index"));
// }
// return false;
// });
Thread.sleep(250); // just wait a little or the index will be broken
if (!template.collectionExists(COLLECTION_NAME)) {
template.createCollection(COLLECTION_NAME);
}
boolean hasIndex = template.searchIndexOps(COLLECTION_NAME).exists("movie_vector_index");
if (!hasIndex) {
System.out.print("Creating index: ");
VectorIndex vectorIndex = new VectorIndex("movie_vector_index").addVector("plot_embedding",
field -> field.dimensions(1536).similarity(VectorIndex.SimilarityFunction.COSINE)).addFilter("language");
String s = template.searchIndexOps(COLLECTION_NAME).ensureIndex(vectorIndex);
}
VectorSearchOperation $vectorSearch = VectorSearchOperation.search("movie_vector_index").path("plot_embedding")
.vector(vectors).limit(10).numCandidates(150).withSearchScore();
Aggregation agg = Aggregation.newAggregation($vectorSearch, Aggregation.project("plot", "title"));
AggregationResults<Document> aggregate = template.aggregate(agg, COLLECTION_NAME, Document.class);
aggregate.forEach(System.out::println);
initDocuments();
initIndexes();
}
static double[] vectors = { -0.0016261312, -0.028070757, -0.011342932, -0.012775794, -0.0027440966, 0.008683807,
-0.02575152, -0.02020668, -0.010283281, -0.0041719596, 0.021392956, 0.028657231, -0.006634482, 0.007490867,
0.018593878, 0.0038187427, 0.029590257, -0.01451522, 0.016061379, 0.00008528442, -0.008943722, 0.01627464,
0.024311995, -0.025911469, 0.00022596726, -0.008863748, 0.008823762, -0.034921836, 0.007910728, -0.01515501,
0.035801545, -0.0035688248, -0.020299982, -0.03145631, -0.032256044, -0.028763862, -0.0071576433, -0.012769129,
0.012322609, -0.006621153, 0.010583182, 0.024085402, -0.001623632, 0.007864078, -0.021406285, 0.002554159,
0.012229307, -0.011762793, 0.0051682983, 0.0048484034, 0.018087378, 0.024325324, -0.037694257, -0.026537929,
-0.008803768, -0.017767483, -0.012642504, -0.0062712682, 0.0009771782, -0.010409906, 0.017754154, -0.004671795,
-0.030469967, 0.008477209, -0.005218282, -0.0058480743, -0.020153364, -0.0032805866, 0.004248601, 0.0051449724,
0.006791097, 0.007650814, 0.003458861, -0.0031223053, -0.01932697, -0.033615597, 0.00745088, 0.006321252,
-0.0038154104, 0.014555207, 0.027697546, -0.02828402, 0.0066711367, 0.0077107945, 0.01794076, 0.011349596,
-0.0052715978, 0.014755142, -0.019753495, -0.011156326, 0.011202978, 0.022126047, 0.00846388, 0.030549942,
-0.0041386373, 0.018847128, -0.00033655585, 0.024925126, -0.003555496, -0.019300312, 0.010749794, 0.0075308536,
-0.018287312, -0.016567878, -0.012869096, -0.015528221, 0.0078107617, -0.011156326, 0.013522214, -0.020646535,
-0.01211601, 0.055928253, 0.011596181, -0.017247654, 0.0005939711, -0.026977783, -0.003942035, -0.009583511,
-0.0055248477, -0.028737204, 0.023179034, 0.003995351, 0.0219661, -0.008470545, 0.023392297, 0.010469886,
-0.015874773, 0.007890735, -0.009690142, -0.00024970944, 0.012775794, 0.0114762215, 0.013422247, 0.010429899,
-0.03686786, -0.006717788, -0.027484283, 0.011556195, -0.036068123, -0.013915418, -0.0016327957, 0.0151016945,
-0.020473259, 0.004671795, -0.012555866, 0.0209531, 0.01982014, 0.024485271, 0.0105431955, -0.005178295,
0.033162415, -0.013795458, 0.007150979, 0.010243294, 0.005644808, 0.017260984, -0.0045618312, 0.0024725192,
0.004305249, -0.008197301, 0.0014203656, 0.0018460588, 0.005015015, -0.011142998, 0.01439526, 0.022965772,
0.02552493, 0.007757446, -0.0019726837, 0.009503538, -0.032042783, 0.008403899, -0.04609149, 0.013808787,
0.011749465, 0.036388017, 0.016314628, 0.021939443, -0.0250051, -0.017354285, -0.012962398, 0.00006107364,
0.019113706, 0.03081652, -0.018114036, -0.0084572155, 0.009643491, -0.0034721901, 0.0072642746, -0.0090636825,
0.01642126, 0.013428912, 0.027724205, 0.0071243206, -0.6858542, -0.031029783, -0.014595194, -0.011449563,
0.017514233, 0.01743426, 0.009950057, 0.0029706885, -0.015714826, -0.001806072, 0.011856096, 0.026444625,
-0.0010663156, -0.006474535, 0.0016161345, -0.020313311, 0.0148351155, -0.0018393943, 0.0057347785, 0.018300641,
-0.018647194, 0.03345565, -0.008070676, 0.0071443142, 0.014301958, 0.0044818576, 0.003838736, -0.007350913,
-0.024525259, -0.001142124, -0.018620536, 0.017247654, 0.007037683, 0.010236629, 0.06046009, 0.0138887605,
-0.012122675, 0.037694257, 0.0055081863, 0.042492677, 0.00021784494, -0.011656162, 0.010276617, 0.022325981,
0.005984696, -0.009496873, 0.013382261, -0.0010563189, 0.0026507939, -0.041639622, 0.008637156, 0.026471283,
-0.008403899, 0.024858482, -0.00066686375, -0.0016252982, 0.027590916, 0.0051449724, 0.0058647357, -0.008743787,
-0.014968405, 0.027724205, -0.011596181, 0.0047650975, -0.015381602, 0.0043718936, 0.002159289, 0.035908177,
-0.008243952, -0.030443309, 0.027564257, 0.042625964, -0.0033688906, 0.01843393, 0.019087048, 0.024578573,
0.03268257, -0.015608194, -0.014128681, -0.0033538956, -0.0028757197, -0.004121976, -0.032389335, 0.0034322033,
0.058807302, 0.010943064, -0.030523283, 0.008903735, 0.017500903, 0.00871713, -0.0029406983, 0.013995391,
-0.03132302, -0.019660193, -0.00770413, -0.0038853872, 0.0015894766, -0.0015294964, -0.006251275, -0.021099718,
-0.010256623, -0.008863748, 0.028550599, 0.02020668, -0.0012962399, -0.003415542, -0.0022509254, 0.0119360695,
0.027590916, -0.046971202, -0.0015194997, -0.022405956, 0.0016677842, -0.00018535563, -0.015421589, -0.031802863,
0.03814744, 0.0065411795, 0.016567878, -0.015621523, 0.022899127, -0.011076353, 0.02841731, -0.002679118,
-0.002342562, 0.015341615, 0.01804739, -0.020566562, -0.012989056, -0.002990682, 0.01643459, 0.00042527664,
0.008243952, -0.013715484, -0.004835075, -0.009803439, 0.03129636, -0.021432944, 0.0012087687, -0.015741484,
-0.0052016205, 0.00080890034, -0.01755422, 0.004811749, -0.017967418, -0.026684547, -0.014128681, 0.0041386373,
-0.013742141, -0.010056688, -0.013268964, -0.0110630235, -0.028337335, 0.015981404, -0.00997005, -0.02424535,
-0.013968734, -0.028310679, -0.027750863, -0.020699851, 0.02235264, 0.001057985, 0.00081639783, -0.0099367285,
0.013522214, -0.012016043, -0.00086471526, 0.013568865, 0.0019376953, -0.019020405, 0.017460918, -0.023045745,
0.008503866, 0.0064678704, -0.011509543, 0.018727167, -0.003372223, -0.0028690554, -0.0027024434, -0.011902748,
-0.012182655, -0.015714826, -0.0098634185, 0.00593138, 0.018753825, 0.0010146659, 0.013029044, 0.0003521757,
-0.017620865, 0.04102649, 0.00552818, 0.024485271, -0.009630162, -0.015608194, 0.0006718621, -0.0008418062,
0.012395918, 0.0057980907, 0.016221326, 0.010616505, 0.004838407, -0.012402583, 0.019900113, -0.0034521967,
0.000247002, -0.03153628, 0.0011038032, -0.020819811, 0.016234655, -0.00330058, -0.0032289368, 0.00078973995,
-0.021952773, -0.022459272, 0.03118973, 0.03673457, -0.021472929, 0.0072109587, -0.015075036, 0.004855068,
-0.0008151483, 0.0069643734, 0.010023367, -0.010276617, -0.023019087, 0.0068244194, -0.0012520878, -0.0015086699,
0.022046074, -0.034148756, -0.0022192693, 0.002427534, -0.0027124402, 0.0060346797, 0.015461575, 0.0137554705,
0.009230294, -0.009583511, 0.032629255, 0.015994733, -0.019167023, -0.009203636, 0.03393549, -0.017274313,
-0.012042701, -0.0009930064, 0.026777849, -0.013582194, -0.0027590916, -0.017594207, -0.026804507, -0.0014236979,
-0.022032745, 0.0091236625, -0.0042419364, -0.00858384, -0.0033905501, -0.020739838, 0.016821127, 0.022539245,
0.015381602, 0.015141681, 0.028817179, -0.019726837, -0.0051283115, -0.011489551, -0.013208984, -0.0047017853,
-0.0072309524, 0.01767418, 0.0025658219, -0.010323267, 0.012609182, -0.028097415, 0.026871152, -0.010276617,
0.021912785, 0.0022542577, 0.005124979, -0.0019710176, 0.004518512, -0.040360045, 0.010969722, -0.0031539614,
-0.020366628, -0.025778178, -0.0110030435, -0.016221326, 0.0036587953, 0.016207997, 0.003007343, -0.0032555948,
0.0044052163, -0.022046074, -0.0008822095, -0.009363583, 0.028230704, -0.024538586, 0.0029840174, 0.0016044717,
-0.014181997, 0.031349678, -0.014381931, -0.027750863, 0.02613806, 0.0004136138, -0.005748107, -0.01868718,
-0.0010138329, 0.0054348772, 0.010703143, -0.003682121, 0.0030856507, -0.004275259, -0.010403241, 0.021113047,
-0.022685863, -0.023032416, 0.031429652, 0.001792743, -0.005644808, -0.011842767, -0.04078657, -0.0026874484,
0.06915057, -0.00056939584, -0.013995391, 0.010703143, -0.013728813, -0.022939114, -0.015261642, -0.022485929,
0.016807798, 0.007964044, 0.0144219175, 0.016821127, 0.0076241563, 0.005461535, -0.013248971, 0.015301628,
0.0085171955, -0.004318578, 0.011136333, -0.0059047225, -0.010249958, -0.018207338, 0.024645219, 0.021752838,
0.0007614159, -0.013648839, 0.01111634, -0.010503208, -0.0038487327, -0.008203966, -0.00397869, 0.0029740208,
0.008530525, 0.005261601, 0.01642126, -0.0038753906, -0.013222313, 0.026537929, 0.024671877, -0.043505676,
0.014195326, 0.024778508, 0.0056914594, -0.025951454, 0.017620865, -0.0021359634, 0.008643821, 0.021299653,
0.0041686273, -0.009017031, 0.04044002, 0.024378639, -0.027777521, -0.014208655, 0.0028623908, 0.042119466,
0.005801423, -0.028124074, -0.03129636, 0.022139376, -0.022179363, -0.04067994, 0.013688826, 0.013328944,
0.0046184794, -0.02828402, -0.0063412455, -0.0046184794, -0.011756129, -0.010383247, -0.0018543894, -0.0018593877,
-0.00052024535, 0.004815081, 0.014781799, 0.018007403, 0.01306903, -0.020433271, 0.009043689, 0.033189073,
-0.006844413, -0.019766824, -0.018767154, 0.00533491, -0.0024575242, 0.018727167, 0.0058080875, -0.013835444,
0.0040719924, 0.004881726, 0.012029372, 0.005664801, 0.03193615, 0.0058047553, 0.002695779, 0.009290274,
0.02361889, 0.017834127, 0.0049017193, -0.0036388019, 0.010776452, -0.019793482, 0.0067777685, -0.014208655,
-0.024911797, 0.002385881, 0.0034988478, 0.020899786, -0.0025858153, -0.011849431, 0.033189073, -0.021312982,
0.024965113, -0.014635181, 0.014048708, -0.0035921505, -0.003347231, 0.030869836, -0.0017161017, -0.0061346465,
0.009203636, -0.025165047, 0.0068510775, 0.021499587, 0.013782129, -0.0024475274, -0.0051149824, -0.024445284,
0.006167969, 0.0068844, -0.00076183246, 0.030150073, -0.0055948244, -0.011162991, -0.02057989, -0.009703471,
-0.020646535, 0.008004031, 0.0066378145, -0.019900113, -0.012169327, -0.01439526, 0.0044252095, -0.004018677,
0.014621852, -0.025085073, -0.013715484, -0.017980747, 0.0071043274, 0.011456228, -0.01010334, -0.0035321703,
-0.03801415, -0.012036037, -0.0028990454, -0.05419549, -0.024058744, -0.024272008, 0.015221654, 0.027964126,
0.03182952, -0.015354944, 0.004855068, 0.011522872, 0.004771762, 0.0027874154, 0.023405626, 0.0004242353,
-0.03132302, 0.007057676, 0.008763781, -0.0027057757, 0.023005757, -0.0071176565, -0.005238275, 0.029110415,
-0.010989714, 0.013728813, -0.009630162, -0.029137073, -0.0049317093, -0.0008630492, -0.015248313, 0.0043219104,
-0.0055681667, -0.013175662, 0.029723546, 0.025098402, 0.012849103, -0.0009996708, 0.03118973, -0.0021709518,
0.0260181, -0.020526575, 0.028097415, -0.016141351, 0.010509873, -0.022965772, 0.002865723, 0.0020493253,
0.0020509914, -0.0041419696, -0.00039695262, 0.017287642, 0.0038987163, 0.014795128, -0.014661839, -0.008950386,
0.004431874, -0.009383577, 0.0012604183, -0.023019087, 0.0029273694, -0.033135757, 0.009176978, -0.011023037,
-0.002102641, 0.02663123, -0.03849399, -0.0044152127, 0.0004527676, -0.0026924468, 0.02828402, 0.017727496,
0.035135098, 0.02728435, -0.005348239, -0.001467017, -0.019766824, 0.014715155, 0.011982721, 0.0045651635,
0.023458943, -0.0010046692, -0.0031373003, -0.0006972704, 0.0019043729, -0.018967088, -0.024311995, 0.0011546199,
0.007977373, -0.004755101, -0.010016702, -0.02780418, -0.004688456, 0.013022379, -0.005484861, 0.0017227661,
-0.015394931, -0.028763862, -0.026684547, 0.0030589928, -0.018513903, 0.028363993, 0.0044818576, -0.009270281,
0.038920518, -0.016008062, 0.0093902415, 0.004815081, -0.021059733, 0.01451522, -0.0051583014, 0.023765508,
-0.017874114, -0.016821127, -0.012522544, -0.0028390652, 0.0040886537, 0.020259995, -0.031216389, -0.014115352,
-0.009176978, 0.010303274, 0.020313311, 0.0064112223, -0.02235264, -0.022872468, 0.0052449396, 0.0005723116,
0.0037321046, 0.016807798, -0.018527232, -0.009303603, 0.0024858483, -0.0012662497, -0.007110992, 0.011976057,
-0.007790768, -0.042999174, -0.006727785, -0.011829439, 0.007024354, 0.005278262, -0.017740825, -0.0041519664,
0.0085905045, 0.027750863, -0.038387362, 0.024391968, 0.00087721116, 0.010509873, -0.00038508154, -0.006857742,
0.0183273, -0.0037054466, 0.015461575, 0.0017394272, -0.0017944091, 0.014181997, -0.0052682655, 0.009023695,
0.00719763, -0.013522214, 0.0034422, 0.014941746, -0.0016711164, -0.025298337, -0.017634194, 0.0058714002,
-0.005321581, 0.017834127, 0.0110630235, -0.03369557, 0.029190388, -0.008943722, 0.009363583, -0.0034222065,
-0.026111402, -0.007037683, -0.006561173, 0.02473852, -0.007084334, -0.010110005, -0.008577175, 0.0030439978,
-0.022712521, 0.0054582027, -0.0012620845, -0.0011954397, -0.015741484, 0.0129557345, -0.00042111133, 0.00846388,
0.008930393, 0.016487904, 0.010469886, -0.007917393, -0.011762793, -0.0214596, 0.000917198, 0.021672864,
0.010269952, -0.007737452, -0.010243294, -0.0067244526, -0.015488233, -0.021552904, 0.017127695, 0.011109675,
0.038067464, 0.00871713, -0.0025591573, 0.021312982, -0.006237946, 0.034628596, -0.0045251767, 0.008357248,
0.020686522, 0.0010696478, 0.0076708077, 0.03772091, -0.018700508, -0.0020676525, -0.008923728, -0.023298996,
0.018233996, -0.010256623, 0.0017860786, 0.009796774, -0.00897038, -0.01269582, -0.018527232, 0.009190307,
-0.02372552, -0.042119466, 0.008097334, -0.0066778013, -0.021046404, 0.0019593548, 0.011083017, -0.0016028056,
0.012662497, -0.000059095124, 0.0071043274, -0.014675168, 0.024831824, -0.053582355, 0.038387362, 0.0005698124,
0.015954746, 0.021552904, 0.031589597, -0.009230294, -0.0006147976, 0.002625802, -0.011749465, -0.034362018,
-0.0067844326, -0.018793812, 0.011442899, -0.008743787, 0.017474247, -0.021619547, 0.01831397, -0.009037024,
-0.0057247817, -0.02728435, 0.010363255, 0.034415334, -0.024032086, -0.0020126705, -0.0045518344, -0.019353628,
-0.018340627, -0.03129636, -0.0034038792, -0.006321252, -0.0016161345, 0.033642255, -0.000056075285, -0.005005019,
0.004571828, -0.0024075406, -0.00010215386, 0.0098634185, 0.1980148, -0.003825407, -0.025191706, 0.035161756,
0.005358236, 0.025111731, 0.023485601, 0.0023342315, -0.011882754, 0.018287312, -0.0068910643, 0.003912045,
0.009243623, -0.001355387, -0.028603915, -0.012802451, -0.030150073, -0.014795128, -0.028630573, -0.0013487226,
0.002667455, 0.00985009, -0.0033972147, -0.021486258, 0.009503538, -0.017847456, 0.013062365, -0.014341944,
0.005078328, 0.025165047, -0.015594865, -0.025924796, -0.0018177348, 0.010996379, -0.02993681, 0.007324255,
0.014475234, -0.028577257, 0.005494857, 0.00011725306, -0.013315615, 0.015941417, 0.009376912, 0.0025158382,
0.008743787, 0.023832154, -0.008084005, -0.014195326, -0.008823762, 0.0033455652, -0.032362677, -0.021552904,
-0.0056081535, 0.023298996, -0.025444955, 0.0097301295, 0.009736794, 0.015274971, -0.0012937407, -0.018087378,
-0.0039387033, 0.008637156, -0.011189649, -0.00023846315, -0.011582852, 0.0066411467, -0.018220667, 0.0060846633,
0.0376676, -0.002709108, 0.0072776037, 0.0034188742, -0.010249958, -0.0007747449, -0.00795738, -0.022192692,
0.03910712, 0.032122757, 0.023898797, 0.0076241563, -0.007397564, -0.003655463, 0.011442899, -0.014115352,
-0.00505167, -0.031163072, 0.030336678, -0.006857742, -0.022259338, 0.004048667, 0.02072651, 0.0030156737,
-0.0042119464, 0.00041861215, -0.005731446, 0.011103011, 0.013822115, 0.021512916, 0.009216965, -0.006537847,
-0.027057758, -0.04054665, 0.010403241, -0.0056281467, -0.005701456, -0.002709108, -0.00745088, -0.0024841821,
0.009356919, -0.022659205, 0.004061996, -0.013175662, 0.017074378, -0.006141311, -0.014541878, 0.02993681,
-0.00028448965, -0.025271678, 0.011689484, -0.014528549, 0.004398552, -0.017274313, 0.0045751603, 0.012455898,
0.004121976, -0.025458284, -0.006744446, 0.011822774, -0.015035049, -0.03257594, 0.014675168, -0.0039187097,
0.019726837, -0.0047251107, 0.0022825818, 0.011829439, 0.005391558, -0.016781142, -0.0058747325, 0.010309938,
-0.013049036, 0.01186276, -0.0011246296, 0.0062112883, 0.0028190718, -0.021739509, 0.009883412, -0.0073175905,
-0.012715813, -0.017181009, -0.016607866, -0.042492677, -0.0014478565, -0.01794076, 0.012302616, -0.015194997,
-0.04433207, -0.020606548, 0.009696807, 0.010303274, -0.01694109, -0.004018677, 0.019353628, -0.001991011,
0.000058938927, 0.010536531, -0.17274313, 0.010143327, 0.014235313, -0.024152048, 0.025684876, -0.0012504216,
0.036601283, -0.003698782, 0.0007310093, 0.004165295, -0.0029157067, 0.017101036, -0.046891227, -0.017460918,
0.022965772, 0.020233337, -0.024072073, 0.017220996, 0.009370248, 0.0010363255, 0.0194336, -0.019606877,
0.01818068, -0.020819811, 0.007410893, 0.0019326969, 0.017887443, 0.006651143, 0.00067394477, -0.011889419,
-0.025058415, -0.008543854, 0.021579562, 0.0047484366, 0.014062037, 0.0075508473, -0.009510202, -0.009143656,
0.0046817916, 0.013982063, -0.0027990784, 0.011782787, 0.014541878, -0.015701497, -0.029350337, 0.021979429,
0.01332228, -0.026244693, -0.0123492675, -0.003895384, 0.0071576433, -0.035454992, -0.00046984528, 0.0033522295,
0.039347045, 0.0005119148, 0.00476843, -0.012995721, 0.0024042083, -0.006931051, -0.014461905, -0.0127558,
0.0034555288, -0.0074842023, -0.030256703, -0.007057676, -0.00807734, 0.007804097, -0.006957709, 0.017181009,
-0.034575284, -0.008603834, -0.005008351, -0.015834786, 0.02943031, 0.016861115, -0.0050849924, 0.014235313,
0.0051449724, 0.0025924798, -0.0025741523, 0.04289254, -0.002104307, 0.012969063, -0.008310596, 0.00423194,
0.0074975314, 0.0018810473, -0.014248641, -0.024725191, 0.0151016945, -0.017527562, 0.0018727167, 0.0002830318,
0.015168339, 0.0144219175, -0.004048667, -0.004358565, 0.011836103, -0.010343261, -0.005911387, 0.0022825818,
0.0073175905, 0.00403867, 0.013188991, 0.03334902, 0.006111321, 0.008597169, 0.030123414, -0.015474904,
0.0017877447, -0.024551915, 0.013155668, 0.023525586, -0.0255116, 0.017220996, 0.004358565, -0.00934359,
0.0099967085, 0.011162991, 0.03092315, -0.021046404, -0.015514892, 0.0011946067, -0.01816735, 0.010876419,
-0.10124666, -0.03550831, 0.0056348112, 0.013942076, 0.005951374, 0.020419942, -0.006857742, -0.020873128,
-0.021259667, 0.0137554705, 0.0057880944, -0.029163731, -0.018767154, -0.021392956, 0.030896494, -0.005494857,
-0.0027307675, -0.006801094, -0.014821786, 0.021392956, -0.0018110704, -0.0018843795, -0.012362596, -0.0072176233,
-0.017194338, -0.018713837, -0.024272008, 0.03801415, 0.00015880188, 0.0044951867, -0.028630573, -0.0014070367,
-0.00916365, -0.026537929, -0.009576847, -0.013995391, -0.0077107945, 0.0050016865, 0.00578143, -0.04467862,
0.008363913, 0.010136662, -0.0006268769, -0.006591163, 0.015341615, -0.027377652, -0.00093136, 0.029243704,
-0.020886457, -0.01041657, -0.02424535, 0.005291591, -0.02980352, -0.009190307, 0.019460259, -0.0041286405,
0.004801752, 0.0011787785, -0.001257086, -0.011216307, -0.013395589, 0.00088137644, -0.0051616337, 0.03876057,
-0.0033455652, 0.00075850025, -0.006951045, -0.0062112883, 0.018140694, -0.006351242, -0.008263946, 0.018154023,
-0.012189319, 0.0075508473, -0.044358727, -0.0040153447, 0.0093302615, -0.010636497, 0.032789204, -0.005264933,
-0.014235313, -0.018393943, 0.007297597, -0.016114693, 0.015021721, 0.020033404, 0.0137688, 0.0011046362,
0.010616505, -0.0039453674, 0.012109346, 0.021099718, -0.0072842683, -0.019153694, -0.003768759, 0.039320387,
-0.006747778, -0.0016852784, 0.018154023, 0.0010963057, -0.015035049, -0.021033075, -0.04345236, 0.017287642,
0.016341286, -0.008610498, 0.00236922, 0.009290274, 0.028950468, -0.014475234, -0.0035654926, 0.015434918,
-0.03372223, 0.004501851, -0.012929076, -0.008483873, -0.0044685286, -0.0102233, 0.01615468, 0.0022792495,
0.010876419, -0.0059647025, 0.01895376, -0.0069976957, -0.0042952523, 0.017207667, -0.00036133936, 0.0085905045,
0.008084005, 0.03129636, -0.016994404, -0.014915089, 0.020100048, -0.012009379, -0.006684466, 0.01306903,
0.00015765642, -0.00530492, 0.0005277429, 0.015421589, 0.015528221, 0.032202728, -0.003485519, -0.0014286962,
0.033908837, 0.001367883, 0.010509873, 0.025271678, -0.020993087, 0.019846799, 0.006897729, -0.010216636,
-0.00725761, 0.01818068, -0.028443968, -0.011242964, -0.014435247, -0.013688826, 0.006101324, -0.0022509254,
0.013848773, -0.0019077052, 0.017181009, 0.03422873, 0.005324913, -0.0035188415, 0.014128681, -0.004898387,
0.005038341, 0.0012320944, -0.005561502, -0.017847456, 0.0008538855, -0.0047884234, 0.011849431, 0.015421589,
-0.013942076, 0.0029790192, -0.013702155, 0.0001199605, -0.024431955, 0.019926772, 0.022179363, -0.016487904,
-0.03964028, 0.0050849924, 0.017487574, 0.022792496, 0.0012504216, 0.004048667, -0.00997005, 0.0076041627,
-0.014328616, -0.020259995, 0.0005598157, -0.010469886, 0.0016852784, 0.01716768, -0.008990373, -0.001987679,
0.026417969, 0.023792166, 0.0046917885, -0.0071909656, -0.00032051947, -0.023259008, -0.009170313, 0.02071318,
-0.03156294, -0.030869836, -0.006324584, 0.013795458, -0.00047151142, 0.016874444, 0.00947688, 0.00985009,
-0.029883493, 0.024205362, -0.013522214, -0.015075036, -0.030603256, 0.029270362, 0.010503208, 0.021539574,
0.01743426, -0.023898797, 0.022019416, -0.0068777353, 0.027857494, -0.021259667, 0.0025758184, 0.006197959,
0.006447877, -0.00025200035, -0.004941706, -0.021246338, -0.005504854, -0.008390571, -0.0097301295, 0.027244363,
-0.04446536, 0.05216949, 0.010243294, -0.016008062, 0.0122493, -0.0199401, 0.009077012, 0.019753495, 0.006431216,
-0.037960835, -0.027377652, 0.016381273, -0.0038620618, 0.022512587, -0.010996379, -0.0015211658, -0.0102233,
0.007071005, 0.008230623, -0.009490209, -0.010083347, 0.024431955, 0.002427534, 0.02828402, 0.0035721571,
-0.022192692, -0.011882754, 0.010056688, 0.0011904413, -0.01426197, -0.017500903, -0.00010985966, 0.005591492,
-0.0077707744, -0.012049366, 0.011869425, 0.00858384, -0.024698535, -0.030283362, 0.020140035, 0.011949399,
-0.013968734, 0.042732596, -0.011649498, -0.011982721, -0.016967745, -0.0060913274, -0.007130985, -0.013109017,
-0.009710136 };
@AfterAll
static void afterAll() {
template.dropCollection(WithVectorFields.class);
}
@ParameterizedTest // GH-4706
@MethodSource("vectorAggregations")
void searchUsingArraysAddingScore(VectorSearchOperation searchOperation) {
VectorSearchOperation $search = searchOperation.withSearchScore(SCORE_FIELD);
AggregationResults<Document> results = template.aggregate(Aggregation.newAggregation($search),
WithVectorFields.class, Document.class);
assertThat(results).hasSize(10);
assertScoreIsDecreasing(results);
assertThat(results.iterator().next()).containsKey(SCORE_FIELD)
.extracting(it -> it.get(SCORE_FIELD), InstanceOfAssertFactories.DOUBLE).isEqualByComparingTo(1D);
}
@ParameterizedTest // GH-4706
@MethodSource("binaryVectorAggregations")
void searchUsingBinaryVectorAddingScore(VectorSearchOperation searchOperation) {
VectorSearchOperation $search = searchOperation.withSearchScore(SCORE_FIELD);
AggregationResults<Document> results = template.aggregate(Aggregation.newAggregation($search),
WithVectorFields.class, Document.class);
assertThat(results).hasSize(10);
assertScoreIsDecreasing(results);
assertThat(results.iterator().next()).containsKey(SCORE_FIELD)
.extracting(it -> it.get(SCORE_FIELD), InstanceOfAssertFactories.DOUBLE).isEqualByComparingTo(1D);
}
private static Stream<Arguments> binaryVectorAggregations() {
return Stream.of(//
Arguments.arguments(VectorSearchOperation.search("raw-index").path("rawInt8vector") //
.vector(new byte[] { 0, 1, 2, 3, 4 }) //
.limit(10)//
.numCandidates(20) //
.searchType(SearchType.ANN)),
Arguments.arguments(VectorSearchOperation.search("wrapper-index").path("int8vector") //
.vector(BinaryVector.int8Vector(new byte[] { 0, 1, 2, 3, 4 })) //
.limit(10)//
.numCandidates(20) //
.searchType(SearchType.ANN)),
Arguments.arguments(VectorSearchOperation.search("wrapper-index").path("float32vector") //
.vector(BinaryVector.floatVector(new float[] { 0.0001f, 1.12345f, 2.23456f, 3.34567f, 4.45678f })) //
.limit(10)//
.numCandidates(20) //
.searchType(SearchType.ANN)));
}
private static Stream<Arguments> vectorAggregations() {
return Stream.of(//
Arguments.arguments(VectorSearchOperation.search("raw-index").path("rawFloat32vector") //
.vector(new float[] { 0.0001f, 1.12345f, 2.23456f, 3.34567f, 4.45678f }) //
.limit(10)//
.numCandidates(20) //
.searchType(SearchType.ANN)),
Arguments.arguments(VectorSearchOperation.search("raw-index").path("rawFloat64vector") //
.vector(new double[] { 1.0001d, 2.12345d, 3.23456d, 4.34567d, 5.45678d }) //
.limit(10)//
.numCandidates(20) //
.searchType(SearchType.ANN)),
Arguments.arguments(VectorSearchOperation.search("wrapper-index").path("float64vector") //
.vector(Vector.of(1.0001d, 2.12345d, 3.23456d, 4.34567d, 5.45678d)) //
.limit(10)//
.numCandidates(20) //
.searchType(SearchType.ANN)));
}
static void initDocuments() {
IntStream.range(0, 10).mapToObj(WithVectorFields::instance).forEach(template::save);
}
static void initIndexes() {
VectorIndex rawIndex = new VectorIndex("raw-index")
.addVector("rawInt8vector", it -> it.similarity(SimilarityFunction.COSINE).dimensions(5))
.addVector("rawFloat32vector", it -> it.similarity(SimilarityFunction.COSINE).dimensions(5))
.addVector("rawFloat64vector", it -> it.similarity(SimilarityFunction.COSINE).dimensions(5))
.addFilter("justSomeArgument");
VectorIndex wrapperIndex = new VectorIndex("wrapper-index")
.addVector("int8vector", it -> it.similarity(SimilarityFunction.COSINE).dimensions(5))
.addVector("float32vector", it -> it.similarity(SimilarityFunction.COSINE).dimensions(5))
.addVector("float64vector", it -> it.similarity(SimilarityFunction.COSINE).dimensions(5))
.addFilter("justSomeArgument");
template.searchIndexOps(WithVectorFields.class).ensureIndex(rawIndex);
template.searchIndexOps(WithVectorFields.class).ensureIndex(wrapperIndex);
template.awaitIndexCreation(WithVectorFields.class, rawIndex.getName());
template.awaitIndexCreation(WithVectorFields.class, wrapperIndex.getName());
}
private static void assertScoreIsDecreasing(Iterable<Document> documents) {
double previousScore = Integer.MAX_VALUE;
for (Document document : documents) {
Double vectorSearchScore = document.getDouble(SCORE_FIELD);
assertThat(vectorSearchScore).isGreaterThan(0D);
assertThat(vectorSearchScore).isLessThan(previousScore);
previousScore = vectorSearchScore;
}
}
@org.springframework.data.mongodb.core.mapping.Document(COLLECTION_NAME)
static class WithVectorFields {
String id;
Vector int8vector;
Vector float32vector;
Vector float64vector;
@Field(targetType = FieldType.VECTOR) //
byte[] rawInt8vector;
float[] rawFloat32vector;
double[] rawFloat64vector;
int justSomeArgument;
static WithVectorFields instance(int offset) {
WithVectorFields instance = new WithVectorFields();
instance.id = "id-%s".formatted(offset);
instance.rawInt8vector = new byte[5];
instance.rawFloat32vector = new float[5];
instance.rawFloat64vector = new double[5];
for (int i = 0; i < 5; i++) {
int v = i + offset;
instance.rawInt8vector[i] = (byte) v;
}
if (offset == 0) {
instance.rawFloat32vector[0] = 0.0001f;
instance.rawFloat64vector[0] = 0.0001d;
} else {
instance.rawFloat32vector[0] = Float.parseFloat("%s.000%s".formatted(offset, offset));
instance.rawFloat64vector[0] = Double.parseDouble("%s.000%s".formatted(offset, offset));
}
instance.rawFloat32vector[1] = Float.parseFloat("%s.12345".formatted(offset + 1));
instance.rawFloat64vector[1] = Double.parseDouble("%s.12345".formatted(offset + 1));
instance.rawFloat32vector[2] = Float.parseFloat("%s.23456".formatted(offset + 2));
instance.rawFloat64vector[2] = Double.parseDouble("%s.23456".formatted(offset + 2));
instance.rawFloat32vector[3] = Float.parseFloat("%s.34567".formatted(offset + 3));
instance.rawFloat64vector[3] = Double.parseDouble("%s.34567".formatted(offset + 3));
instance.rawFloat32vector[4] = Float.parseFloat("%s.45678".formatted(offset + 4));
instance.rawFloat64vector[4] = Double.parseDouble("%s.45678".formatted(offset + 4));
instance.justSomeArgument = offset;
instance.int8vector = MongoVector.of(BinaryVector.int8Vector(instance.rawInt8vector));
instance.float32vector = MongoVector.of(BinaryVector.floatVector(instance.rawFloat32vector));
instance.float64vector = Vector.of(instance.rawFloat64vector);
return instance;
}
}
}

View File

@@ -15,10 +15,23 @@
*/
package org.springframework.data.mongodb.core.convert;
import static java.time.ZoneId.*;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.Mockito.*;
import static org.springframework.data.mongodb.core.DocumentTestUtils.*;
import static java.time.ZoneId.systemDefault;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.assertThatNoException;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.assertj.core.api.Assertions.fail;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.springframework.data.mongodb.core.DocumentTestUtils.assertTypeHint;
import static org.springframework.data.mongodb.core.DocumentTestUtils.getAsDocument;
import java.math.BigDecimal;
import java.math.BigInteger;
@@ -27,12 +40,30 @@ import java.nio.ByteBuffer;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.EnumMap;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.UUID;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.stream.Stream;
import org.assertj.core.data.Percentage;
import org.bson.BinaryVector;
import org.bson.BsonDouble;
import org.bson.BsonUndefined;
import org.bson.types.Binary;
@@ -3325,7 +3356,8 @@ class MappingMongoConverterUnitTests {
org.bson.Document document = write(source);
assertThat(document).containsEntry("_id", source.abc).containsEntry("id", source.id);
WithRenamedIdPropertyAndAnotherPropertyNamedId target = converter.read(WithRenamedIdPropertyAndAnotherPropertyNamedId.class, document);
WithRenamedIdPropertyAndAnotherPropertyNamedId target = converter
.read(WithRenamedIdPropertyAndAnotherPropertyNamedId.class, document);
assertThat(target.abc).isEqualTo(source.abc);
assertThat(target.id).isEqualTo(source.id);
}
@@ -3348,6 +3380,31 @@ class MappingMongoConverterUnitTests {
assertThat(withVector.embeddings.toDoubleArray()).contains(1.1d, 2.2d, 3.3d);
}
@Test // GH-4706
void mapsByteArrayAsVectorWhenAnnotatedWithFieldTargetType() {
WithExplicitTargetTypes source = new WithExplicitTargetTypes();
source.asVector = new byte[] { 0, 1, 2 };
org.bson.Document target = new org.bson.Document();
converter.write(source, target);
assertThatNoException().isThrownBy(() -> target.get("asVector", BinaryVector.class));
}
@Test // GH-4706
void writesByteArrayAsIsIfNoFieldInstructionsGiven() {
WithArrays source = new WithArrays();
source.arrayOfPrimitiveBytes = new byte[] { 0, 1, 2 };
org.bson.Document target = new org.bson.Document();
converter.write(source, target);
assertThat(target.get("arrayOfPrimitiveBytes", byte[].class)).isSameAs(source.arrayOfPrimitiveBytes);
}
org.bson.Document write(Object source) {
org.bson.Document target = new org.bson.Document();
@@ -3891,6 +3948,7 @@ class MappingMongoConverterUnitTests {
static class WithArrays {
String[] arrayOfStrings;
byte[] arrayOfPrimitiveBytes;
}
// DATAMONGO-1898
@@ -4012,6 +4070,9 @@ class MappingMongoConverterUnitTests {
@Field(targetType = FieldType.OBJECT_ID) //
Date dateAsObjectId;
@Field(targetType = FieldType.VECTOR) //
byte[] asVector;
}
static class WrapperAroundWithUnwrapped {

View File

@@ -0,0 +1,90 @@
/*
* Copyright 2025 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 static org.assertj.core.api.Assertions.assertThat;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
/**
* @author Christoph Strobl
*/
class SearchIndexInfoUnitTests {
@ParameterizedTest
@ValueSource(strings = { """
{
"id": "679b7637a580c270015ef6fb",
"name": "vector_index",
"type": "vectorSearch",
"status": "READY",
"queryable": true,
"latestVersion": 0,
"latestDefinition": {
"fields": [
{
"type": "vector",
"path": "plot_embedding",
"numDimensions": 1536,
"similarity": "euclidean"
}
]
}
}""", """
{
id: '648b4ad4d697b73bf9d2e5e1',
name: 'search-index',
status: 'PENDING',
queryable: false,
latestDefinition: {
mappings: { dynamic: false, fields: { text: { type: 'string' } } }
}
}""", """
{
name: 'search-index-not-yet-created',
definition: {
mappings: { dynamic: false, fields: { text: { type: 'string' } } }
}
}""", """
{
name: 'vector-index-with-filter',
type: "vectorSearch",
definition: {
fields: [
{
type: "vector",
path: "plot_embedding",
numDimensions: 1536,
similarity: "euclidean"
}, {
type: "filter",
path: "year"
}
]
}
}""" })
void parsesIndexInfo(String indexInfoSource) {
SearchIndexInfo indexInfo = SearchIndexInfo.parse(indexInfoSource);
if (indexInfo.getId() != null) {
assertThat(indexInfo.getId()).isInstanceOf(String.class);
}
assertThat(indexInfo.getStatus()).isNotNull();
assertThat(indexInfo.getIndexDefinition()).isNotNull();
}
}

View File

@@ -15,8 +15,9 @@
*/
package org.springframework.data.mongodb.core.index;
import static org.awaitility.Awaitility.*;
import static org.springframework.data.mongodb.test.util.Assertions.*;
import static org.assertj.core.api.Assertions.assertThatRuntimeException;
import static org.awaitility.Awaitility.await;
import static org.springframework.data.mongodb.test.util.Assertions.assertThat;
import java.util.List;
@@ -26,14 +27,17 @@ import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.index.VectorIndex.SimilarityFunction;
import org.springframework.data.mongodb.core.mapping.Field;
import org.springframework.data.mongodb.test.util.EnableIfVectorSearchAvailable;
import org.springframework.data.mongodb.test.util.AtlasContainer;
import org.springframework.data.mongodb.test.util.MongoTestTemplate;
import org.springframework.data.mongodb.test.util.MongoTestUtils;
import org.springframework.lang.Nullable;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import com.mongodb.ConnectionString;
import com.mongodb.client.AggregateIterable;
/**
@@ -42,10 +46,15 @@ import com.mongodb.client.AggregateIterable;
* @author Christoph Strobl
* @author Mark Paluch
*/
@EnableIfVectorSearchAvailable
@Testcontainers(disabledWithoutDocker = true)
class VectorIndexIntegrationTests {
private static @Container AtlasContainer atlasLocal = AtlasContainer.bestMatch();
MongoTestTemplate template = new MongoTestTemplate(cfg -> {
cfg.configureDatabaseFactory(ctx -> {
ctx.client(MongoTestUtils.client(new ConnectionString(atlasLocal.getConnectionString())));
});
cfg.configureMappingContext(ctx -> {
ctx.initialEntitySet(Movie.class);
});
@@ -68,7 +77,7 @@ class VectorIndexIntegrationTests {
@ParameterizedTest // GH-4706
@ValueSource(strings = { "euclidean", "cosine", "dotProduct" })
void createsSimpleVectorIndex(String similarityFunction) throws InterruptedException {
void createsSimpleVectorIndex(String similarityFunction) {
VectorIndex idx = new VectorIndex("vector_index").addVector("plotEmbedding",
builder -> builder.dimensions(1536).similarity(similarityFunction));
@@ -86,6 +95,50 @@ class VectorIndexIntegrationTests {
});
}
@Test // GH-4706
void dropIndex() {
VectorIndex idx = new VectorIndex("vector_index").addVector("plotEmbedding",
builder -> builder.dimensions(1536).similarity("cosine"));
indexOps.ensureIndex(idx);
template.awaitIndexCreation(Movie.class, idx.getName());
indexOps.dropIndex(idx.getName());
assertThat(readRawIndexInfo(idx.getName())).isNull();
}
@Test // GH-4706
void statusChanges() {
String indexName = "vector_index";
assertThat(indexOps.status(indexName)).isEqualTo(SearchIndexStatus.DOES_NOT_EXIST);
VectorIndex idx = new VectorIndex(indexName).addVector("plotEmbedding",
builder -> builder.dimensions(1536).similarity("cosine"));
indexOps.ensureIndex(idx);
assertThat(indexOps.status(indexName)).isIn(SearchIndexStatus.PENDING, SearchIndexStatus.BUILDING,
SearchIndexStatus.READY);
}
@Test // GH-4706
void exists() {
String indexName = "vector_index";
assertThat(indexOps.exists(indexName)).isFalse();
VectorIndex idx = new VectorIndex(indexName).addVector("plotEmbedding",
builder -> builder.dimensions(1536).similarity("cosine"));
indexOps.ensureIndex(idx);
assertThat(indexOps.exists(indexName)).isTrue();
}
@Test // GH-4706
void updatesVectorIndex() {
@@ -107,7 +160,9 @@ class VectorIndexIntegrationTests {
VectorIndex updatedIdx = new VectorIndex(indexName).addVector("plotEmbedding",
builder -> builder.dimensions(1536).similarity(SimilarityFunction.DOT_PRODUCT));
assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> indexOps.updateIndex(idx));
// updating vector index does currently not work, one needs to delete and recreat
assertThatRuntimeException().isThrownBy(() -> indexOps.updateIndex(updatedIdx));
}
@Test // GH-4706
@@ -151,5 +206,4 @@ class VectorIndexIntegrationTests {
@Field("plot_embedding") Double[] plotEmbedding;
}
}

View File

@@ -0,0 +1,110 @@
/*
* Copyright 2024 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.test.util;
import java.util.List;
import org.bson.Document;
import org.springframework.core.env.StandardEnvironment;
import org.springframework.data.util.Lazy;
import org.springframework.util.StringUtils;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.wait.strategy.DockerHealthcheckWaitStrategy;
import org.testcontainers.containers.wait.strategy.WaitStrategy;
import org.testcontainers.utility.DockerImageName;
import com.mongodb.ConnectionString;
import com.mongodb.client.MongoClient;
import com.mongodb.client.MongoCollection;
/**
* @author Christoph Strobl
*/
public class AtlasContainer extends GenericContainer<AtlasContainer> {
private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse("mongodb/mongodb-atlas-local");
private static final String DEFAULT_TAG = "latest";
private static final String MONGODB_DATABASE_NAME_DEFAULT = "test";
private static final String READY_DB = "__db_ready_check";
private final Lazy<MongoClient> client;
public static AtlasContainer bestMatch() {
return tagged(new StandardEnvironment().getProperty("mongodb.atlas.version", DEFAULT_TAG));
}
public static AtlasContainer latest() {
return tagged(DEFAULT_TAG);
}
public static AtlasContainer version8() {
return tagged("8.0.0");
}
public static AtlasContainer tagged(String tag) {
return new AtlasContainer(DEFAULT_IMAGE_NAME.withTag(tag));
}
public AtlasContainer(String dockerImageName) {
this(DockerImageName.parse(dockerImageName));
}
public AtlasContainer(DockerImageName dockerImageName) {
super(dockerImageName);
dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME);
setExposedPorts(List.of(27017));
client = Lazy.of(() -> MongoTestUtils.client(new ConnectionString(getConnectionString())));
}
public String getConnectionString() {
return getConnectionString(MONGODB_DATABASE_NAME_DEFAULT);
}
/**
* Gets a connection string url.
*
* @return a connection url pointing to a mongodb instance
*/
public String getConnectionString(String database) {
return String.format("mongodb://%s:%d/%s?directConnection=true", getHost(), getMappedPort(27017),
StringUtils.hasText(database) ? database : MONGODB_DATABASE_NAME_DEFAULT);
}
@Override
public boolean isHealthy() {
MongoClient mongoClient = client.get();
MongoCollection<Document> ready = MongoTestUtils.createOrReplaceCollection(READY_DB, "ready", mongoClient);
boolean isReady = false;
try {
ready.aggregate(List.of(new Document("$listSearchIndexes", new Document()))).first();
isReady = true;
} catch (Exception e) {
// ok so the search service is not ready yet - sigh
}
if (isReady) {
mongoClient.getDatabase(READY_DB).drop();
mongoClient.close();
}
return isReady;
}
@Override
protected WaitStrategy getWaitStrategy() {
return new DockerHealthcheckWaitStrategy();
}
}

View File

@@ -15,7 +15,10 @@
*/
package org.springframework.data.mongodb.test.util;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Supplier;
import java.util.stream.Collectors;
@@ -26,6 +29,7 @@ import org.springframework.data.mapping.callback.EntityCallbacks;
import org.springframework.data.mapping.context.PersistentEntities;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.util.MongoCompatibilityAdapter;
import org.testcontainers.shaded.org.awaitility.Awaitility;
import com.mongodb.MongoWriteException;
import com.mongodb.client.MongoClient;
@@ -154,4 +158,25 @@ public class MongoTestTemplate extends MongoTemplate {
return null;
}));
}
public void awaitIndexCreation(Class<?> type, String indexName) {
awaitIndexCreation(getCollectionName(type), indexName, Duration.ofSeconds(10));
}
public void awaitIndexCreation(String collectionName, String indexName, Duration timeout) {
Awaitility.await().atMost(timeout).pollInterval(Duration.ofMillis(200)).until(() -> {
ArrayList<Document> execute = this.execute(collectionName,
coll -> coll
.aggregate(List.of(Document.parse("{'$listSearchIndexes': { 'name' : '%s'}}".formatted(indexName))))
.into(new ArrayList<>()));
for (Document doc : execute) {
if (doc.getString("name").equals(indexName)) {
return doc.getString("status").equals("READY");
}
}
return false;
});
}
}

View File

@@ -64,8 +64,10 @@ public class MongoTestUtils {
}
public static MongoClient client(String host, int port) {
return client(new ConnectionString(String.format(CONNECTION_STRING_PATTERN, host, port)));
}
ConnectionString connectionString = new ConnectionString(String.format(CONNECTION_STRING_PATTERN, host, port));
public static MongoClient client(ConnectionString connectionString) {
return com.mongodb.client.MongoClients.create(connectionString, SpringDataMongoDB.driverInformation());
}

View File

@@ -36,5 +36,5 @@ runtime:
format: pretty
ui:
bundle:
url: https://github.com/spring-io/antora-ui-spring/releases/download/v0.4.16/ui-bundle.zip
url: https://github.com/spring-io/antora-ui-spring/releases/download/v0.4.18/ui-bundle.zip
snapshot: true

View File

@@ -33,6 +33,7 @@
** xref:mongodb/change-streams.adoc[]
** xref:mongodb/tailable-cursors.adoc[]
** xref:mongodb/sharding.adoc[]
** xref:mongodb/mongo-search-indexes.adoc[]
** xref:mongodb/mongo-encryption.adoc[]
// Repository

View File

@@ -0,0 +1,124 @@
[[mongo.search]]
= MongoDB Search
MongoDB enables users to do keyword or lexical search as well as vector search data using dedicated search indexes.
[[mongo.search.vector]]
== Vector Search
MongoDB Vector Search uses the `$vectorSearch` aggregation stage to run queries against specialized indexes.
Please refer to the MongoDB documentation to learn more about requirements and restrictions of `vectorSearch` indexes.
[[mongo.search.vector.index]]
=== Managing Vector Indexes
`SearchIndexOperationsProvider` implemented by `MongoTemplate` are the entrypoint to `SearchIndexOperations` offering various methods for managing vector indexes.
The following snippet shows how to create a vector index for a collection
.Create a Vector Index
[tabs]
======
Java::
+
====
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
----
VectorIndex index = new VectorIndex("vector_index")
.addVector("plotEmbedding"), vector -> vector.dimensions(1536).similarity(COSINE)) <1>
.addFilter("year"); <2>
mongoTemplate.searchIndexOps(Movie.class) <3>
.createIndex(index);
----
<1> A vector index may cover multiple vector embeddings that can be added via the `addVector` method.
<2> Vector indexes can contain additional fields to narrow down search results when running queries.
<3> Obtain `SearchIndexOperations` bound to the `Movie` type which is used for field name mapping.
====
Mongo Shell::
+
====
[source,console,indent=0,subs="verbatim,quotes",role="secondary"]
----
db.movie.createSearchIndex("movie", "vector_index",
{
"fields": [
{
"type": "vector",
"numDimensions": 1536,
"path": "plot_embedding", <1>
"similarity": "cosine"
},
{
"type": "filter",
"path": "year"
}
]
}
)
----
<1> Field name `plotEmbedding` got mapped to `plot_embedding` considering a `@Field(name = "...")` annotation.
====
======
Once created, vector indexes are not immediately ready to use although the `exists` check returns `true`.
The actual status of a search index can be obtained via `SearchIndexOperations#status(...)`.
The `READY` state indicates the index is ready to accept queries.
[[mongo.search.vector.query]]
=== Querying Vector Indexes
Vector indexes can be queried by issuing an aggregation using a `VectorSearchOperation` via `MongoOperations` as shown in the following example
.Query a Vector Index
[tabs]
======
Java::
+
====
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
----
VectorSearchOperation search = VectorSearchOperation.search("vector_index") <1>
.path("plotEmbedding") <2>
.vector( ... )
.numCandidates(150)
.limit(10)
.quantization(SCALAR)
.withSearchScore("score"); <3>
AggregationResults<MovieWithSearchScore> results = mongoTemplate
.aggregate(newAggregation(Movie.class, search), MovieWithSearchScore.class);
----
<1> Provide the name of the vector index to query since a collection may hold multiple ones.
<2> The name of the path used for comparison.
<3> Optionally add the search score with given name to the result document.
====
Mongo Shell::
+
====
[source,console,indent=0,subs="verbatim,quotes",role="secondary"]
----
db.embedded_movies.aggregate([
{
"$vectorSearch": {
"index": "vector_index",
"path": "plot_embedding", <1>
"queryVector": [ ... ],
"numCandidates": 150,
"limit": 10,
"quantization": "scalar"
}
},
{
"$addFields": {
"score": { $meta: "vectorSearchScore" }
}
}
])
----
<1> Field name `plotEmbedding` got mapped to `plot_embedding` considering a `@Field(name = "...")` annotation.
====
======