committed by
Mark Paluch
parent
e8e110e31d
commit
2d7d7bf004
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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<>();
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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}.
|
||||
*
|
||||
|
||||
@@ -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<>());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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}.
|
||||
*
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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()}.
|
||||
*
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
====
|
||||
======
|
||||
|
||||
Reference in New Issue
Block a user