Add VectorIndex and SearchIndexDefinition abstraction.
See #4706 Original pull request: #4882
This commit is contained in:
@@ -131,6 +131,13 @@
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.awaitility</groupId>
|
||||
<artifactId>awaitility</artifactId>
|
||||
<version>4.2.2</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.reactivex.rxjava3</groupId>
|
||||
<artifactId>rxjava</artifactId>
|
||||
|
||||
@@ -185,8 +185,8 @@ import com.mongodb.client.result.UpdateResult;
|
||||
* @author Michael Krog
|
||||
* @author Jakub Zurawa
|
||||
*/
|
||||
public class MongoTemplate
|
||||
implements MongoOperations, ApplicationContextAware, IndexOperationsProvider, SearchIndexOperationsProvider, ReadPreferenceAware {
|
||||
public class MongoTemplate implements MongoOperations, ApplicationContextAware, IndexOperationsProvider,
|
||||
SearchIndexOperationsProvider, ReadPreferenceAware {
|
||||
|
||||
private static final Log LOGGER = LogFactory.getLog(MongoTemplate.class);
|
||||
private static final WriteResultChecking DEFAULT_WRITE_RESULT_CHECKING = WriteResultChecking.NONE;
|
||||
@@ -771,6 +771,21 @@ public class MongoTemplate
|
||||
return indexOps(getCollectionName(entityClass), entityClass);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SearchIndexOperations searchIndexOps(String collectionName) {
|
||||
return searchIndexOps(null, collectionName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SearchIndexOperations searchIndexOps(Class<?> type) {
|
||||
return new DefaultSearchIndexOperations(this, type);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SearchIndexOperations searchIndexOps(@Nullable Class<?> type, String collectionName) {
|
||||
return new DefaultSearchIndexOperations(this, collectionName, type);
|
||||
}
|
||||
|
||||
@Override
|
||||
public BulkOperations bulkOps(BulkMode mode, String collectionName) {
|
||||
return bulkOps(mode, null, collectionName);
|
||||
@@ -1316,7 +1331,7 @@ public class MongoTemplate
|
||||
|
||||
if (ObjectUtils.nullSafeEquals(WriteResultChecking.EXCEPTION, writeResultChecking)) {
|
||||
if (wc == null || wc.getWObject() == null
|
||||
|| (wc.getWObject()instanceof Number concern && concern.intValue() < 1)) {
|
||||
|| (wc.getWObject() instanceof Number concern && concern.intValue() < 1)) {
|
||||
return WriteConcern.ACKNOWLEDGED;
|
||||
}
|
||||
}
|
||||
@@ -1968,7 +1983,8 @@ public class MongoTemplate
|
||||
}
|
||||
|
||||
if (mapReduceOptions.getOutputSharded().isPresent()) {
|
||||
MongoCompatibilityAdapter.mapReduceIterableAdapter(mapReduce).sharded(mapReduceOptions.getOutputSharded().get());
|
||||
MongoCompatibilityAdapter.mapReduceIterableAdapter(mapReduce)
|
||||
.sharded(mapReduceOptions.getOutputSharded().get());
|
||||
}
|
||||
|
||||
if (StringUtils.hasText(mapReduceOptions.getOutputCollection()) && !mapReduceOptions.usesInlineOutput()) {
|
||||
@@ -2067,7 +2083,7 @@ public class MongoTemplate
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> UpdateResult replace(Query query, T replacement, ReplaceOptions options, String collectionName){
|
||||
public <T> UpdateResult replace(Query query, T replacement, ReplaceOptions options, String collectionName) {
|
||||
|
||||
Assert.notNull(replacement, "Replacement must not be null");
|
||||
return replace(query, (Class<T>) ClassUtils.getUserClass(replacement), replacement, options, collectionName);
|
||||
@@ -2743,8 +2759,7 @@ public class MongoTemplate
|
||||
LOGGER.debug(String.format(
|
||||
"findAndModify using query: %s fields: %s sort: %s for class: %s and update: %s in collection: %s",
|
||||
serializeToJsonSafely(mappedQuery), fields, serializeToJsonSafely(sort), entityClass,
|
||||
serializeToJsonSafely(mappedUpdate),
|
||||
collectionName));
|
||||
serializeToJsonSafely(mappedUpdate), collectionName));
|
||||
}
|
||||
|
||||
return executeFindOneInternal(
|
||||
@@ -3013,21 +3028,6 @@ public class MongoTemplate
|
||||
return resolved == null ? ex : resolved;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SearchIndexOperations searchIndexOps(String collectionName) {
|
||||
return searchIndexOps(null, collectionName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SearchIndexOperations searchIndexOps(Class<?> type) {
|
||||
return new DefaultSearchIndexOperations(this, type);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SearchIndexOperations searchIndexOps(Class<?> type, String collectionName) {
|
||||
return new DefaultSearchIndexOperations(this, collectionName, type);
|
||||
}
|
||||
|
||||
// Callback implementations
|
||||
|
||||
/**
|
||||
|
||||
@@ -381,9 +381,9 @@ public class Aggregation {
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory method to create a new {@link UnwindOperation} for the field with the given name, including the name of a new
|
||||
* field to hold the array index of the element as {@code arrayIndex} using {@code preserveNullAndEmptyArrays}. Note
|
||||
* that extended unwind is supported in MongoDB version 3.2+.
|
||||
* Factory method to create a new {@link UnwindOperation} for the field with the given name, including the name of a
|
||||
* new field to hold the array index of the element as {@code arrayIndex} using {@code preserveNullAndEmptyArrays}.
|
||||
* Note that extended unwind is supported in MongoDB version 3.2+.
|
||||
*
|
||||
* @param field must not be {@literal null} or empty.
|
||||
* @param arrayIndex must not be {@literal null} or empty.
|
||||
@@ -428,6 +428,20 @@ public class Aggregation {
|
||||
return GraphLookupOperation.builder().from(fromCollection);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@link VectorSearchOperation} by starting from the {@code indexName} to use.
|
||||
*
|
||||
* @param indexName must not be {@literal null} or empty.
|
||||
* @return new instance of {@link VectorSearchOperation.PathContributor}.
|
||||
* @since 4.5
|
||||
*/
|
||||
public static VectorSearchOperation.PathContributor vectorSearch(String indexName) {
|
||||
|
||||
Assert.hasText(indexName, "Index name must not be null or empty");
|
||||
|
||||
return VectorSearchOperation.search(indexName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory method to create a new {@link SortOperation} for the given {@link Sort}.
|
||||
*
|
||||
@@ -669,14 +683,14 @@ public class Aggregation {
|
||||
|
||||
/**
|
||||
* Entrypoint for creating {@link LookupOperation $lookup} using a fluent builder API.
|
||||
*
|
||||
* <pre class="code">
|
||||
* Aggregation.lookup().from("restaurants")
|
||||
* .localField("restaurant_name")
|
||||
* .foreignField("name")
|
||||
* .let(newVariable("orders_drink").forField("drink"))
|
||||
* .pipeline(match(ctx -> new Document("$expr", new Document("$in", List.of("$$orders_drink", "$beverages")))))
|
||||
* .as("matches")
|
||||
* Aggregation.lookup().from("restaurants").localField("restaurant_name").foreignField("name")
|
||||
* .let(newVariable("orders_drink").forField("drink"))
|
||||
* .pipeline(match(ctx -> new Document("$expr", new Document("$in", List.of("$$orders_drink", "$beverages")))))
|
||||
* .as("matches")
|
||||
* </pre>
|
||||
*
|
||||
* @return new instance of {@link LookupOperationBuilder}.
|
||||
* @since 4.1
|
||||
*/
|
||||
|
||||
@@ -23,18 +23,313 @@ import java.util.Set;
|
||||
import java.util.function.Consumer;
|
||||
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;
|
||||
import org.springframework.data.mongodb.core.query.Criteria;
|
||||
import org.springframework.data.mongodb.core.query.CriteriaDefinition;
|
||||
import org.springframework.lang.Contract;
|
||||
import org.springframework.lang.Nullable;
|
||||
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>
|
||||
* <li>{@link org.springframework.data.mongodb.core.aggregation.FacetOperation Facet} stage</li>
|
||||
* </ul>
|
||||
*
|
||||
* @author Christoph Strobl
|
||||
* @author Mark Paluch
|
||||
* @since 4.5
|
||||
*/
|
||||
public class VectorSearchOperation implements AggregationOperation {
|
||||
|
||||
private final SearchType searchType;
|
||||
private final @Nullable CriteriaDefinition filter;
|
||||
private final String indexName;
|
||||
private final Limit limit;
|
||||
private final @Nullable Integer numCandidates;
|
||||
private final QueryPaths path;
|
||||
private final Vector vector;
|
||||
private final String score;
|
||||
private final Consumer<Criteria> scoreCriteria;
|
||||
|
||||
private VectorSearchOperation(SearchType searchType, @Nullable CriteriaDefinition filter, String indexName,
|
||||
Limit limit, @Nullable Integer numCandidates, QueryPaths path, Vector vector, @Nullable String searchScore,
|
||||
Consumer<Criteria> scoreCriteria) {
|
||||
|
||||
this.searchType = searchType;
|
||||
this.filter = filter;
|
||||
this.indexName = indexName;
|
||||
this.limit = limit;
|
||||
this.numCandidates = numCandidates;
|
||||
this.path = path;
|
||||
this.vector = vector;
|
||||
this.score = searchScore;
|
||||
this.scoreCriteria = scoreCriteria;
|
||||
}
|
||||
|
||||
VectorSearchOperation(String indexName, QueryPaths path, Limit limit, Vector vector) {
|
||||
this(SearchType.DEFAULT, null, indexName, limit, null, path, vector, null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Entrypoint to build a {@link VectorSearchOperation} starting from the {@code index} name to search. Atlas Vector
|
||||
* Search doesn't return results if you misspell the index name or if the specified index doesn't already exist on the
|
||||
* cluster.
|
||||
*
|
||||
* @param index must not be {@literal null} or empty.
|
||||
* @return new instance of {@link VectorSearchOperation.PathContributor}.
|
||||
*/
|
||||
public static PathContributor search(String index) {
|
||||
return new VectorSearchBuilder().index(index);
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure the search type to use. {@link SearchType#ENN} leads to an exact search while {@link SearchType#ANN} uses
|
||||
* {@code exact=false}.
|
||||
*
|
||||
* @param searchType must not be null.
|
||||
* @return a new {@link VectorSearchOperation} with {@link SearchType} applied.
|
||||
*/
|
||||
@Contract("_ -> new")
|
||||
public VectorSearchOperation searchType(SearchType searchType) {
|
||||
return new VectorSearchOperation(searchType, filter, indexName, limit, numCandidates, path, vector, score,
|
||||
scoreCriteria);
|
||||
}
|
||||
|
||||
/**
|
||||
* Criteria expression that compares an indexed field with a boolean, date, objectId, number (not decimals), string,
|
||||
* or UUID to use as a pre-filter.
|
||||
* <p>
|
||||
* Atlas Vector Search supports only the filters for the following MQL match expressions:
|
||||
* <ul>
|
||||
* <li>$gt</li>
|
||||
* <li>$lt</li>
|
||||
* <li>$gte</li>
|
||||
* <li>$lte</li>
|
||||
* <li>$eq</li>
|
||||
* <li>$ne</li>
|
||||
* <li>$in</li>
|
||||
* <li>$nin</li>
|
||||
* <li>$nor</li>
|
||||
* <li>$not</li>
|
||||
* <li>$and</li>
|
||||
* <li>$or</li>
|
||||
* </ul>
|
||||
*
|
||||
* @param filter must not be null.
|
||||
* @return a new {@link VectorSearchOperation} with {@link CriteriaDefinition} applied.
|
||||
*/
|
||||
@Contract("_ -> new")
|
||||
public VectorSearchOperation filter(CriteriaDefinition filter) {
|
||||
return new VectorSearchOperation(searchType, filter, indexName, limit, numCandidates, path, vector, score,
|
||||
scoreCriteria);
|
||||
}
|
||||
|
||||
/**
|
||||
* Criteria expression that compares an indexed field with a boolean, date, objectId, number (not decimals), string,
|
||||
* or UUID to use as a pre-filter.
|
||||
* <p>
|
||||
* Atlas Vector Search supports only the filters for the following MQL match expressions:
|
||||
* <ul>
|
||||
* <li>$gt</li>
|
||||
* <li>$lt</li>
|
||||
* <li>$gte</li>
|
||||
* <li>$lte</li>
|
||||
* <li>$eq</li>
|
||||
* <li>$ne</li>
|
||||
* <li>$in</li>
|
||||
* <li>$nin</li>
|
||||
* <li>$nor</li>
|
||||
* <li>$not</li>
|
||||
* <li>$and</li>
|
||||
* <li>$or</li>
|
||||
* </ul>
|
||||
*
|
||||
* @param filter must not be null.
|
||||
* @return a new {@link VectorSearchOperation} with {@link CriteriaDefinition} applied.
|
||||
*/
|
||||
@Contract("_ -> new")
|
||||
public VectorSearchOperation filter(Document filter) {
|
||||
|
||||
return filter(new CriteriaDefinition() {
|
||||
@Override
|
||||
public Document getCriteriaObject() {
|
||||
return filter;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public String getKey() {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Number of nearest neighbors to use during the search. Value must be less than or equal to (<=) {@code 10000}. You
|
||||
* can't specify a number less than the number of documents to return (limit). This field is required if
|
||||
* {@link #searchType(SearchType)} is {@link SearchType#ANN} or {@link SearchType#DEFAULT}.
|
||||
*
|
||||
* @param numCandidates
|
||||
* @return a new {@link VectorSearchOperation} with {@code numCandidates} applied.
|
||||
*/
|
||||
@Contract("_ -> new")
|
||||
public VectorSearchOperation numCandidates(int numCandidates) {
|
||||
return new VectorSearchOperation(searchType, filter, indexName, limit, numCandidates, path, vector, score,
|
||||
scoreCriteria);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a {@link AddFieldsOperation} stage including the search score using {@code score} as field name.
|
||||
*
|
||||
* @return a new {@link VectorSearchOperation} with search score applied.
|
||||
* @see #withSearchScore(String)
|
||||
*/
|
||||
@Contract("-> new")
|
||||
public VectorSearchOperation withSearchScore() {
|
||||
return withSearchScore("score");
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a {@link AddFieldsOperation} stage including the search score using {@code scoreFieldName} as field name.
|
||||
*
|
||||
* @param scoreFieldName name of the score field.
|
||||
* @return a new {@link VectorSearchOperation} with {@code scoreFieldName} applied.
|
||||
* @see #withSearchScore()
|
||||
*/
|
||||
@Contract("_ -> new")
|
||||
public VectorSearchOperation withSearchScore(String scoreFieldName) {
|
||||
return new VectorSearchOperation(searchType, filter, indexName, limit, numCandidates, path, vector, scoreFieldName,
|
||||
scoreCriteria);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a {@link MatchOperation} stage targeting the score field name. Implies that the score field is present by
|
||||
* either reusing a previous {@link AddFieldsOperation} from {@link #withSearchScore()} or
|
||||
* {@link #withSearchScore(String)} or by adding a new {@link AddFieldsOperation} stage.
|
||||
*
|
||||
* @return a new {@link VectorSearchOperation} with search score filter applied.
|
||||
*/
|
||||
@Contract("_ -> new")
|
||||
public VectorSearchOperation withFilterBySore(Consumer<Criteria> score) {
|
||||
return new VectorSearchOperation(searchType, filter, indexName, limit, numCandidates, path, vector,
|
||||
StringUtils.hasText(this.score) ? this.score : "score", score);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Document toDocument(AggregationOperationContext context) {
|
||||
|
||||
Document $vectorSearch = new Document();
|
||||
|
||||
if (searchType != null && !searchType.equals(SearchType.DEFAULT)) {
|
||||
$vectorSearch.append("exact", searchType.equals(SearchType.ENN));
|
||||
}
|
||||
|
||||
if (filter != null) {
|
||||
$vectorSearch.append("filter", context.getMappedObject(filter.getCriteriaObject()));
|
||||
}
|
||||
|
||||
$vectorSearch.append("index", indexName);
|
||||
$vectorSearch.append("limit", limit.max());
|
||||
|
||||
if (numCandidates != null) {
|
||||
$vectorSearch.append("numCandidates", numCandidates);
|
||||
}
|
||||
|
||||
Object path = this.path.getPathObject();
|
||||
|
||||
if (path instanceof String pathFieldName) {
|
||||
Document mappedObject = context.getMappedObject(new Document(pathFieldName, 1));
|
||||
path = mappedObject.keySet().iterator().next();
|
||||
}
|
||||
|
||||
Object source = vector.getSource();
|
||||
|
||||
if (source instanceof float[]) {
|
||||
source = vector.toDoubleArray();
|
||||
}
|
||||
|
||||
if (source instanceof double[] ds) {
|
||||
source = Arrays.stream(ds).boxed().collect(Collectors.toList());
|
||||
}
|
||||
|
||||
$vectorSearch.append("path", path);
|
||||
$vectorSearch.append("queryVector", source);
|
||||
|
||||
return new Document(getOperator(), $vectorSearch);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Document> toPipelineStages(AggregationOperationContext context) {
|
||||
|
||||
if (!StringUtils.hasText(score)) {
|
||||
return List.of(toDocument(context));
|
||||
}
|
||||
|
||||
AddFieldsOperation $vectorSearchScore = Aggregation.addFields().addField(score)
|
||||
.withValueOfExpression("{\"$meta\":\"vectorSearchScore\"}").build();
|
||||
|
||||
if (scoreCriteria == null) {
|
||||
return List.of(toDocument(context), $vectorSearchScore.toDocument(context));
|
||||
}
|
||||
|
||||
Criteria criteria = Criteria.where(score);
|
||||
scoreCriteria.accept(criteria);
|
||||
MatchOperation $filterByScore = Aggregation.match(criteria);
|
||||
|
||||
return List.of(toDocument(context), $vectorSearchScore.toDocument(context), $filterByScore.toDocument(context));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getOperator() {
|
||||
return "$vectorSearch";
|
||||
}
|
||||
|
||||
/**
|
||||
* Builder helper to create a {@link VectorSearchOperation}.
|
||||
*/
|
||||
private static class VectorSearchBuilder implements PathContributor, VectorContributor, LimitContributor {
|
||||
|
||||
String index;
|
||||
QueryPath<String> paths;
|
||||
Vector vector;
|
||||
|
||||
PathContributor index(String index) {
|
||||
this.index = index;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public VectorContributor path(String path) {
|
||||
|
||||
this.paths = QueryPath.path(path);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public VectorSearchOperation limit(Limit limit) {
|
||||
return new VectorSearchOperation(index, QueryPaths.of(paths), limit, vector);
|
||||
}
|
||||
|
||||
@Override
|
||||
public LimitContributor vector(Vector vector) {
|
||||
this.vector = vector;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Search type, ANN as approximation or ENN for exact search.
|
||||
*/
|
||||
public enum SearchType {
|
||||
|
||||
/** MongoDB Server default (value will be omitted) */
|
||||
@@ -131,190 +426,102 @@ public class VectorSearchOperation implements AggregationOperation {
|
||||
}
|
||||
}
|
||||
|
||||
private SearchType searchType;
|
||||
private CriteriaDefinition filter;
|
||||
private String indexName;
|
||||
private Limit limit;
|
||||
private Integer numCandidates;
|
||||
private QueryPaths path;
|
||||
private List<Double> vector;
|
||||
|
||||
private String score;
|
||||
private Consumer<Criteria> scoreCriteria;
|
||||
|
||||
private VectorSearchOperation(SearchType searchType, CriteriaDefinition filter, String indexName, Limit limit,
|
||||
Integer numCandidates, QueryPaths path, List<Double> vector, String searchScore,
|
||||
Consumer<Criteria> scoreCriteria) {
|
||||
|
||||
this.searchType = searchType;
|
||||
this.filter = filter;
|
||||
this.indexName = indexName;
|
||||
this.limit = limit;
|
||||
this.numCandidates = numCandidates;
|
||||
this.path = path;
|
||||
this.vector = vector;
|
||||
this.score = searchScore;
|
||||
this.scoreCriteria = scoreCriteria;
|
||||
}
|
||||
|
||||
public VectorSearchOperation(String indexName, QueryPaths path, Limit limit, List<Double> vector) {
|
||||
this(SearchType.DEFAULT, null, indexName, limit, null, path, vector, null, null);
|
||||
}
|
||||
|
||||
static PathContributor search(String index) {
|
||||
return new VectorSearchBuilder().index(index);
|
||||
}
|
||||
|
||||
public VectorSearchOperation(String indexName, String path, Limit limit, List<Double> vector) {
|
||||
this(indexName, QueryPaths.of(QueryPath.path(path)), limit, vector);
|
||||
}
|
||||
|
||||
public VectorSearchOperation searchType(SearchType searchType) {
|
||||
return new VectorSearchOperation(searchType, filter, indexName, limit, numCandidates, path, vector, score,
|
||||
scoreCriteria);
|
||||
}
|
||||
|
||||
public VectorSearchOperation filter(Document filter) {
|
||||
|
||||
return filter(new CriteriaDefinition() {
|
||||
@Override
|
||||
public Document getCriteriaObject() {
|
||||
return filter;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public String getKey() {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public VectorSearchOperation filter(CriteriaDefinition filter) {
|
||||
return new VectorSearchOperation(searchType, filter, indexName, limit, numCandidates, path, vector, score,
|
||||
scoreCriteria);
|
||||
}
|
||||
|
||||
public VectorSearchOperation numCandidates(int numCandidates) {
|
||||
return new VectorSearchOperation(searchType, filter, indexName, limit, numCandidates, path, vector, score,
|
||||
scoreCriteria);
|
||||
}
|
||||
|
||||
public VectorSearchOperation searchScore() {
|
||||
return searchScore("score");
|
||||
}
|
||||
|
||||
public VectorSearchOperation searchScore(String scoreFieldName) {
|
||||
return new VectorSearchOperation(searchType, filter, indexName, limit, numCandidates, path, vector, scoreFieldName,
|
||||
scoreCriteria);
|
||||
}
|
||||
|
||||
public VectorSearchOperation filterBySore(Consumer<Criteria> score) {
|
||||
return new VectorSearchOperation(searchType, filter, indexName, limit, numCandidates, path, vector,
|
||||
StringUtils.hasText(this.score) ? this.score : "score", score);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Document toDocument(AggregationOperationContext context) {
|
||||
|
||||
Document $vectorSearch = new Document();
|
||||
|
||||
$vectorSearch.append("index", indexName);
|
||||
$vectorSearch.append("path", path.getPathObject());
|
||||
$vectorSearch.append("queryVector", vector);
|
||||
$vectorSearch.append("limit", limit.max());
|
||||
|
||||
if (searchType != null && !searchType.equals(SearchType.DEFAULT)) {
|
||||
$vectorSearch.append("exact", searchType.equals(SearchType.ENN));
|
||||
}
|
||||
|
||||
if (filter != null) {
|
||||
$vectorSearch.append("filter", context.getMappedObject(filter.getCriteriaObject()));
|
||||
}
|
||||
|
||||
if (numCandidates != null) {
|
||||
$vectorSearch.append("numCandidates", numCandidates);
|
||||
}
|
||||
|
||||
return new Document(getOperator(), $vectorSearch);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Document> toPipelineStages(AggregationOperationContext context) {
|
||||
|
||||
if (!StringUtils.hasText(score)) {
|
||||
return List.of(toDocument(context));
|
||||
}
|
||||
|
||||
AddFieldsOperation $vectorSearchScore = Aggregation.addFields().addField(score)
|
||||
.withValueOfExpression("{\"$meta\":\"vectorSearchScore\"}").build();
|
||||
|
||||
if (scoreCriteria == null) {
|
||||
return List.of(toDocument(context), $vectorSearchScore.toDocument(context));
|
||||
}
|
||||
|
||||
Criteria criteria = Criteria.where(score);
|
||||
scoreCriteria.accept(criteria);
|
||||
MatchOperation $filterByScore = Aggregation.match(criteria);
|
||||
|
||||
return List.of(toDocument(context), $vectorSearchScore.toDocument(context), $filterByScore.toDocument(context));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getOperator() {
|
||||
return "$vectorSearch";
|
||||
}
|
||||
|
||||
public static class VectorSearchBuilder implements PathContributor, VectorContributor, LimitContributor {
|
||||
|
||||
String index;
|
||||
QueryPaths paths;
|
||||
private List<Double> vector;
|
||||
|
||||
PathContributor index(String index) {
|
||||
this.index = index;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public VectorContributor path(QueryPaths paths) {
|
||||
this.paths = paths;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public VectorSearchOperation limit(Limit limit) {
|
||||
return new VectorSearchOperation(index, paths, limit, vector);
|
||||
}
|
||||
|
||||
@Override
|
||||
public LimitContributor vectors(List<Double> vectors) {
|
||||
this.vector = vectors;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
public interface PathContributor {
|
||||
default VectorContributor path(String path) {
|
||||
return path(QueryPaths.of(QueryPath.path(path)));
|
||||
}
|
||||
|
||||
VectorContributor path(QueryPaths paths);
|
||||
/**
|
||||
* Indexed vector type field to search.
|
||||
*
|
||||
* @param path name of the search path.
|
||||
* @return
|
||||
*/
|
||||
@Contract("_ -> this")
|
||||
VectorContributor path(String path);
|
||||
}
|
||||
|
||||
public interface VectorContributor {
|
||||
default LimitContributor vectors(Double... vectors) {
|
||||
return vectors(Arrays.asList(vectors));
|
||||
|
||||
/**
|
||||
* Array of float 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(float... vector) {
|
||||
return vector(Vector.of(vector));
|
||||
}
|
||||
|
||||
LimitContributor vectors(List<Double> vectors);
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @param vector the query vector.
|
||||
* @return
|
||||
*/
|
||||
@Contract("_ -> this")
|
||||
default LimitContributor vector(double... vector) {
|
||||
return vector(Vector.of(vector));
|
||||
}
|
||||
|
||||
/**
|
||||
* Array of 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(List<? extends Number> vector) {
|
||||
return vector(Vector.of(vector));
|
||||
}
|
||||
|
||||
/**
|
||||
* Binary vector (BSON BinData vector subtype float32, or BSON BinData vector subtype int1 or int8 type) 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(BinaryVector vector) {
|
||||
return vector(MongoVector.of(vector));
|
||||
}
|
||||
|
||||
/**
|
||||
* 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")
|
||||
LimitContributor vector(Vector vector);
|
||||
}
|
||||
|
||||
public interface LimitContributor {
|
||||
|
||||
/**
|
||||
* Number (of type int only) of documents to return in the results. This value can't exceed the value of
|
||||
* numCandidates if you specify numCandidates.
|
||||
*
|
||||
* @param limit
|
||||
* @return
|
||||
*/
|
||||
@Contract("_ -> this")
|
||||
default VectorSearchOperation limit(int limit) {
|
||||
return limit(Limit.of(limit));
|
||||
}
|
||||
|
||||
/**
|
||||
* Number (of type int only) of documents to return in the results. This value can't exceed the value of
|
||||
* numCandidates if you specify numCandidates.
|
||||
*
|
||||
* @param limit
|
||||
* @return
|
||||
*/
|
||||
@Contract("_ -> this")
|
||||
VectorSearchOperation limit(Limit limit);
|
||||
}
|
||||
|
||||
|
||||
@@ -31,6 +31,9 @@ import java.util.UUID;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
import org.bson.BinaryVector;
|
||||
import org.bson.BsonArray;
|
||||
import org.bson.BsonDouble;
|
||||
import org.bson.BsonReader;
|
||||
import org.bson.BsonTimestamp;
|
||||
import org.bson.BsonUndefined;
|
||||
@@ -44,6 +47,7 @@ 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;
|
||||
@@ -51,7 +55,9 @@ import org.springframework.core.convert.converter.Converter;
|
||||
import org.springframework.core.convert.converter.ConverterFactory;
|
||||
import org.springframework.data.convert.ReadingConverter;
|
||||
import org.springframework.data.convert.WritingConverter;
|
||||
import org.springframework.data.domain.Vector;
|
||||
import org.springframework.data.mongodb.core.mapping.FieldName;
|
||||
import org.springframework.data.mongodb.core.mapping.MongoVector;
|
||||
import org.springframework.data.mongodb.core.query.Term;
|
||||
import org.springframework.data.mongodb.core.script.NamedMongoScript;
|
||||
import org.springframework.util.Assert;
|
||||
@@ -106,6 +112,10 @@ abstract class MongoConverters {
|
||||
converters.add(BinaryToByteArrayConverter.INSTANCE);
|
||||
converters.add(BsonTimestampToInstantConverter.INSTANCE);
|
||||
|
||||
converters.add(VectorToBsonArrayConverter.INSTANCE);
|
||||
converters.add(ListToVectorConverter.INSTANCE);
|
||||
converters.add(BinaryVectorToMongoVectorConverter.INSTANCE);
|
||||
|
||||
converters.add(reading(BsonUndefined.class, Object.class, it -> null));
|
||||
converters.add(reading(String.class, URI.class, URI::create).andWriting(URI::toString));
|
||||
|
||||
@@ -417,6 +427,52 @@ abstract class MongoConverters {
|
||||
}
|
||||
}
|
||||
|
||||
@WritingConverter
|
||||
enum VectorToBsonArrayConverter implements Converter<Vector, Object> {
|
||||
|
||||
INSTANCE;
|
||||
|
||||
@Override
|
||||
public Object convert(Vector source) {
|
||||
|
||||
if (source instanceof MongoVector mv) {
|
||||
return mv.getSource();
|
||||
}
|
||||
|
||||
double[] doubleArray = source.toDoubleArray();
|
||||
|
||||
BsonArray array = new BsonArray(doubleArray.length);
|
||||
|
||||
for (double v : doubleArray) {
|
||||
array.add(new BsonDouble(v));
|
||||
}
|
||||
|
||||
return array;
|
||||
}
|
||||
}
|
||||
|
||||
@ReadingConverter
|
||||
enum ListToVectorConverter implements Converter<List<Number>, Vector> {
|
||||
|
||||
INSTANCE;
|
||||
|
||||
@Override
|
||||
public Vector convert(List<Number> source) {
|
||||
return Vector.of(source);
|
||||
}
|
||||
}
|
||||
|
||||
@ReadingConverter
|
||||
enum BinaryVectorToMongoVectorConverter implements Converter<BinaryVector, Vector> {
|
||||
|
||||
INSTANCE;
|
||||
|
||||
@Override
|
||||
public Vector convert(BinaryVector source) {
|
||||
return MongoVector.of(source);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link ConverterFactory} implementation converting {@link AtomicLong} into {@link Long}.
|
||||
*
|
||||
|
||||
@@ -1130,7 +1130,7 @@ public class QueryMapper {
|
||||
* @author Oliver Gierke
|
||||
* @author Thomas Darimont
|
||||
*/
|
||||
protected static class MetadataBackedField extends Field {
|
||||
public static class MetadataBackedField extends Field {
|
||||
|
||||
private static final Pattern POSITIONAL_PARAMETER_PATTERN = Pattern.compile("\\.\\$(\\[.*?\\])?");
|
||||
private static final Pattern NUMERIC_SEGMENT = Pattern.compile("\\d+");
|
||||
|
||||
@@ -18,67 +18,91 @@ package org.springframework.data.mongodb.core.index;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.bson.Document;
|
||||
import org.springframework.data.mongodb.core.DefaultIndexOperations;
|
||||
|
||||
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.convert.QueryMapper;
|
||||
import org.springframework.data.mongodb.core.index.SearchIndex.Filter;
|
||||
import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
|
||||
import org.springframework.data.util.TypeInformation;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
import com.mongodb.client.model.SearchIndexModel;
|
||||
import com.mongodb.client.model.SearchIndexType;
|
||||
|
||||
/**
|
||||
* @author Christoph Strobl
|
||||
* @author Mark Paluch
|
||||
* @since 3.5
|
||||
*/
|
||||
public class DefaultSearchIndexOperations extends DefaultIndexOperations implements SearchIndexOperations {
|
||||
public class DefaultSearchIndexOperations implements SearchIndexOperations {
|
||||
|
||||
private static final Log LOGGER = LogFactory.getLog(SearchIndexOperations.class);
|
||||
private final MongoOperations mongoOperations;
|
||||
private final String collectionName;
|
||||
private final TypeInformation<?> entityTypeInformation;
|
||||
|
||||
public DefaultSearchIndexOperations(MongoOperations mongoOperations, Class<?> type) {
|
||||
this(mongoOperations, mongoOperations.getCollectionName(type), type);
|
||||
}
|
||||
|
||||
public DefaultSearchIndexOperations(MongoOperations mongoOperations, String collectionName, @Nullable Class<?> type) {
|
||||
super(mongoOperations, collectionName, type);
|
||||
this.collectionName = collectionName;
|
||||
|
||||
if (type != null) {
|
||||
|
||||
MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext = mongoOperations
|
||||
.getConverter().getMappingContext();
|
||||
entityTypeInformation = mappingContext.getRequiredPersistentEntity(type).getTypeInformation();
|
||||
} else {
|
||||
entityTypeInformation = null;
|
||||
}
|
||||
|
||||
this.mongoOperations = mongoOperations;
|
||||
}
|
||||
|
||||
private static String getMappedPath(String path, MongoPersistentEntity<?> entity, QueryMapper mapper) {
|
||||
return mapper.getMappedFields(new Document(path, 1), entity).entrySet().iterator().next().getKey();
|
||||
@Override
|
||||
public String ensureIndex(SearchIndexDefinition indexDefinition) {
|
||||
|
||||
if (!(indexDefinition instanceof VectorIndex vsi)) {
|
||||
throw new IllegalStateException("Index definitions must be of type VectorIndex");
|
||||
}
|
||||
|
||||
Document index = indexDefinition.getIndexDocument(entityTypeInformation,
|
||||
mongoOperations.getConverter().getMappingContext());
|
||||
|
||||
mongoOperations.getCollection(collectionName).createSearchIndexes(List
|
||||
.of(new SearchIndexModel(vsi.getName(), (Document) index.get("definition"), SearchIndexType.vectorSearch())));
|
||||
|
||||
return vsi.getName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateIndex(SearchIndexDefinition index) {
|
||||
|
||||
if (index instanceof VectorIndex) {
|
||||
throw new UnsupportedOperationException("Vector Index definitions cannot be updated");
|
||||
}
|
||||
|
||||
Document indexDocument = index.getIndexDocument(entityTypeInformation,
|
||||
mongoOperations.getConverter().getMappingContext());
|
||||
|
||||
mongoOperations.getCollection(collectionName).updateSearchIndex(index.getName(), indexDocument);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean exists(String indexName) {
|
||||
|
||||
// https://www.mongodb.com/docs/manual/reference/operator/aggregation/listSearchIndexes/
|
||||
AggregationResults<Document> aggregate = mongoOperations.aggregate(
|
||||
Aggregation.newAggregation(context -> new Document("$listSearchIndexes", new Document("name", indexName))),
|
||||
collectionName, Document.class);
|
||||
List<Document> indexes = mongoOperations.getCollection(collectionName).listSearchIndexes().into(new ArrayList<>());
|
||||
|
||||
return aggregate.iterator().hasNext();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateIndex(SearchIndex index) {
|
||||
|
||||
MongoPersistentEntity<?> entity = lookupPersistentEntity(type, collectionName);
|
||||
|
||||
Document indexDocument = createIndexDocument(index, entity);
|
||||
|
||||
Document cmdResult = mongoOperations.execute(db -> {
|
||||
|
||||
Document command = new Document().append("updateSearchIndex", collectionName).append("name", index.getName());
|
||||
command.putAll(indexDocument);
|
||||
command.remove("type");
|
||||
|
||||
if (LOGGER.isDebugEnabled()) {
|
||||
LOGGER.debug("Updating VectorIndex: db.runCommand(%s)".formatted(command.toJson()));
|
||||
for (Document index : indexes) {
|
||||
if (index.getString("name").equals(indexName)) {
|
||||
return true;
|
||||
}
|
||||
return db.runCommand(command);
|
||||
});
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -106,59 +130,13 @@ public class DefaultSearchIndexOperations extends DefaultIndexOperations impleme
|
||||
}
|
||||
|
||||
@Override
|
||||
public String ensureIndex(SearchIndexDefinition indexDefinition) {
|
||||
|
||||
if (!(indexDefinition instanceof SearchIndex vsi)) {
|
||||
throw new IllegalStateException("Index definitions must be of type VectorIndex");
|
||||
}
|
||||
|
||||
MongoPersistentEntity<?> entity = lookupPersistentEntity(type, collectionName);
|
||||
|
||||
Document index = createIndexDocument(vsi, entity);
|
||||
|
||||
Document cmdResult = mongoOperations.execute(db -> {
|
||||
|
||||
Document command = new Document().append("createSearchIndexes", collectionName).append("indexes", List.of(index));
|
||||
if (LOGGER.isDebugEnabled()) {
|
||||
LOGGER.debug("Creating VectorIndex: db.runCommand(%s)".formatted(command.toJson()));
|
||||
}
|
||||
return db.runCommand(command);
|
||||
});
|
||||
|
||||
return cmdResult.get("ok").toString().equalsIgnoreCase("1.0") ? vsi.getName() : cmdResult.toJson();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private Document createIndexDocument(SearchIndex vsi, MongoPersistentEntity<?> entity) {
|
||||
|
||||
Document index = new Document(vsi.getIndexOptions());
|
||||
Document definition = new Document();
|
||||
|
||||
List<Document> fields = new ArrayList<>(vsi.getFilters().size() + 1);
|
||||
|
||||
Document vectorField = new Document("type", "vector");
|
||||
vectorField.append("path", getMappedPath(vsi.getPath(), entity, mapper));
|
||||
vectorField.append("numDimensions", vsi.getDimensions());
|
||||
vectorField.append("similarity", vsi.getSimilarity());
|
||||
|
||||
fields.add(vectorField);
|
||||
|
||||
for (Filter filter : vsi.getFilters()) {
|
||||
fields.add(new Document("type", "filter").append("path", getMappedPath(filter.path(), entity, mapper)));
|
||||
}
|
||||
|
||||
definition.append("fields", fields);
|
||||
index.append("definition", definition);
|
||||
return index;
|
||||
public void dropAllIndexes() {
|
||||
getIndexInfo().forEach(indexInfo -> dropIndex(indexInfo.getName()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dropIndex(String name) {
|
||||
|
||||
Document command = new Document().append("dropSearchIndex", collectionName).append("name", name);
|
||||
if (LOGGER.isDebugEnabled()) {
|
||||
LOGGER.debug("Dropping VectorIndex: db.runCommand(%s)".formatted(command.toJson()));
|
||||
}
|
||||
mongoOperations.execute(db -> db.runCommand(command));
|
||||
mongoOperations.getCollection(collectionName).dropSearchIndex(name);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ package org.springframework.data.mongodb.core.index;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
* Provider interface to obtain {@link IndexOperations} by MongoDB collection name.
|
||||
* Provider interface to obtain {@link IndexOperations} by MongoDB collection name or entity type.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
* @author Jens Schauder
|
||||
@@ -46,4 +46,5 @@ public interface IndexOperationsProvider {
|
||||
* @since 3.2
|
||||
*/
|
||||
IndexOperations indexOps(String collectionName, @Nullable Class<?> type);
|
||||
|
||||
}
|
||||
|
||||
@@ -1,255 +0,0 @@
|
||||
/*
|
||||
* 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
|
||||
*
|
||||
* http://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.
|
||||
*/
|
||||
|
||||
/*
|
||||
* 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
|
||||
*
|
||||
* http://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.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import org.bson.Document;
|
||||
|
||||
/**
|
||||
* {@link IndexDefinition} 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.
|
||||
*
|
||||
* @author Christoph Strobl
|
||||
*/
|
||||
public class SearchIndex implements SearchIndexDefinition {
|
||||
|
||||
private final String name;
|
||||
private String path;
|
||||
private int dimensions;
|
||||
private String similarity;
|
||||
private List<Filter> filters;
|
||||
private String quantization = Quantization.NONE.name();
|
||||
|
||||
/**
|
||||
* Create a new {@link SearchIndex} instance.
|
||||
*
|
||||
* @param name The name of the index.
|
||||
*/
|
||||
public SearchIndex(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link SearchIndex} instance using similarity based on the angle between vectors.
|
||||
*
|
||||
* @param name The name of the index.
|
||||
* @return new instance of {@link SearchIndex}.
|
||||
*/
|
||||
public static SearchIndex cosine(String name) {
|
||||
|
||||
SearchIndex idx = new SearchIndex(name);
|
||||
return idx.similarity(SimilarityFunction.COSINE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link SearchIndex} instance using similarity based the distance between vector ends.
|
||||
*
|
||||
* @param name The name of the index.
|
||||
* @return new instance of {@link SearchIndex}.
|
||||
*/
|
||||
public static SearchIndex euclidean(String name) {
|
||||
|
||||
SearchIndex idx = new SearchIndex(name);
|
||||
return idx.similarity(SimilarityFunction.EUCLIDEAN);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link SearchIndex} instance using similarity based on based on both angle and magnitude of the
|
||||
* vectors.
|
||||
*
|
||||
* @param name The name of the index.
|
||||
* @return new instance of {@link SearchIndex}.
|
||||
*/
|
||||
public static SearchIndex dotProduct(String name) {
|
||||
|
||||
SearchIndex idx = new SearchIndex(name);
|
||||
return idx.similarity(SimilarityFunction.DOT_PRODUCT);
|
||||
}
|
||||
|
||||
/**
|
||||
* The path to the field/property to index.
|
||||
*
|
||||
* @param path The path using dot notation.
|
||||
* @return this.
|
||||
*/
|
||||
public SearchIndex path(String path) {
|
||||
|
||||
this.path = path;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Number of vector dimensions enforced at index- & query-time.
|
||||
*
|
||||
* @param dimensions value between {@code 0} and {@code 4096}.
|
||||
* @return this.
|
||||
*/
|
||||
public SearchIndex dimensions(int dimensions) {
|
||||
this.dimensions = dimensions;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Similarity function used.
|
||||
*
|
||||
* @param similarity should be one of {@literal euclidean | cosine | dotProduct}.
|
||||
* @return this.
|
||||
* @see SimilarityFunction
|
||||
* @see #similarity(SimilarityFunction)
|
||||
*/
|
||||
public SearchIndex similarity(String similarity) {
|
||||
this.similarity = similarity;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Similarity function used.
|
||||
*
|
||||
* @param similarity must not be {@literal null}.
|
||||
* @return this.
|
||||
*/
|
||||
public SearchIndex similarity(SimilarityFunction similarity) {
|
||||
return similarity(similarity.getFunctionName());
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Quantization used.
|
||||
*
|
||||
* @param quantization should be one of {@literal none | scalar | binary}.
|
||||
* @return this.
|
||||
* @see Quantization
|
||||
* @see #quantization(Quantization)
|
||||
*/
|
||||
public SearchIndex quantization(String quantization) {
|
||||
this.quantization = quantization;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Quntization used.
|
||||
*
|
||||
* @param quantization must not be {@literal null}.
|
||||
* @return this.
|
||||
*/
|
||||
public SearchIndex quantization(Quantization quantization) {
|
||||
return similarity(quantization.getQuantizationName());
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a {@link Filter} that can be used to narrow search scope.
|
||||
*
|
||||
* @param filter must not be {@literal null}.
|
||||
* @return this.
|
||||
*/
|
||||
public SearchIndex filter(Filter filter) {
|
||||
|
||||
if (this.filters == null) {
|
||||
this.filters = new ArrayList<>(3);
|
||||
}
|
||||
|
||||
this.filters.add(filter);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a field that can be used to pre filter data.
|
||||
*
|
||||
* @param path Dot notation to field/property used for filtering.
|
||||
* @return this.
|
||||
* @see #filter(Filter)
|
||||
*/
|
||||
public SearchIndex filter(String path) {
|
||||
return filter(new Filter(path));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Document getIndexOptions() {
|
||||
return new Document("name", name).append("type", "vectorSearch");
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public String getPath() {
|
||||
return path;
|
||||
}
|
||||
|
||||
public int getDimensions() {
|
||||
return dimensions;
|
||||
}
|
||||
|
||||
public String getSimilarity() {
|
||||
return similarity;
|
||||
}
|
||||
|
||||
public List<Filter> getFilters() {
|
||||
return filters == null ? Collections.emptyList() : filters;
|
||||
}
|
||||
|
||||
public record Filter(String path) {
|
||||
|
||||
}
|
||||
|
||||
public enum SimilarityFunction {
|
||||
DOT_PRODUCT("dotProduct"), COSINE("cosine"), EUCLIDEAN("euclidean");
|
||||
|
||||
String functionName;
|
||||
|
||||
SimilarityFunction(String functionName) {
|
||||
this.functionName = functionName;
|
||||
}
|
||||
|
||||
public String getFunctionName() {
|
||||
return functionName;
|
||||
}
|
||||
}
|
||||
|
||||
public enum Quantization {
|
||||
NONE("none"), SCALAR("scalar"), BINARY("binary");
|
||||
|
||||
String quantizationName;
|
||||
|
||||
Quantization(String quantizationName) {
|
||||
this.quantizationName = quantizationName;
|
||||
}
|
||||
|
||||
public String getQuantizationName() {
|
||||
return quantizationName;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,15 +17,60 @@ 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;
|
||||
import org.springframework.data.util.TypeInformation;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
* Definition for an Atlas Search Index (Search Index or Vector Index).
|
||||
*
|
||||
* @author Marcin Grzejszczak
|
||||
* @author Mark Paluch
|
||||
* @since 4.5
|
||||
*/
|
||||
public interface SearchIndexDefinition {
|
||||
|
||||
/**
|
||||
* Get the index properties such as {@literal unique},...
|
||||
*
|
||||
* @return never {@literal null}.
|
||||
* @return the name of the index.
|
||||
*/
|
||||
Document getIndexOptions();
|
||||
String getName();
|
||||
|
||||
/**
|
||||
* @return the type of the index. Typically, {@code search} or {@code vectorSearch}.
|
||||
*/
|
||||
String getType();
|
||||
|
||||
/**
|
||||
* 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 mappingContext
|
||||
* @return
|
||||
*/
|
||||
default Document getIndexDocument(@Nullable TypeInformation<?> entity,
|
||||
MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext) {
|
||||
|
||||
Document document = new Document();
|
||||
document.put("name", getName());
|
||||
document.put("type", getType());
|
||||
document.put("definition", getDefinition(entity, mappingContext));
|
||||
|
||||
return document;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the actual index definition for this index in the context of a potential entity to resolve field name
|
||||
* mappings.
|
||||
*
|
||||
* @param entity
|
||||
* @param mappingContext
|
||||
* @return
|
||||
*/
|
||||
Document getDefinition(@Nullable TypeInformation<?> entity,
|
||||
MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext);
|
||||
|
||||
}
|
||||
|
||||
@@ -18,18 +18,53 @@ package org.springframework.data.mongodb.core.index;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Search Index operations on a collection for Atlas Search.
|
||||
*
|
||||
* @author Christoph Strobl
|
||||
* @author Mark Paluch
|
||||
* @since 4.5
|
||||
*/
|
||||
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.
|
||||
*
|
||||
* @param indexDefinition must not be {@literal null}.
|
||||
* @return the index name.
|
||||
*/
|
||||
String ensureIndex(SearchIndexDefinition indexDefinition);
|
||||
|
||||
void updateIndex(SearchIndex index);
|
||||
/**
|
||||
* Alters the search {@code index}.
|
||||
* <p>
|
||||
* Note that Atlas Search does not support updating Vector Search Indices resulting in
|
||||
* {@link UnsupportedOperationException}.
|
||||
*
|
||||
* @param index the index definition.
|
||||
*/
|
||||
void updateIndex(SearchIndexDefinition index);
|
||||
|
||||
boolean exists(String indexName);
|
||||
/**
|
||||
* Check whether an index with the {@code name} exists.
|
||||
*
|
||||
* @param name name of index to check for presence.
|
||||
* @return {@literal true} if the index exists; {@literal false} otherwise.
|
||||
*/
|
||||
boolean exists(String name);
|
||||
|
||||
/**
|
||||
* Drops an index from this collection.
|
||||
*
|
||||
* @param name name of index to drop.
|
||||
*/
|
||||
void dropIndex(String name);
|
||||
|
||||
/**
|
||||
* Drops all search indices from this collection.
|
||||
*/
|
||||
void dropAllIndexes();
|
||||
|
||||
/**
|
||||
* Returns the index information on the collection.
|
||||
*
|
||||
|
||||
@@ -16,13 +16,36 @@
|
||||
package org.springframework.data.mongodb.core.index;
|
||||
|
||||
/**
|
||||
* Provider interface to obtain {@link SearchIndexOperations} by MongoDB collection name or entity type.
|
||||
*
|
||||
* @author Christoph Strobl
|
||||
* @author Mark Paluch
|
||||
* @since 4.5
|
||||
*/
|
||||
public interface SearchIndexOperationsProvider {
|
||||
|
||||
SearchIndexOperations searchIndexOps(String collectionName);
|
||||
/**
|
||||
* Returns the operations that can be performed on search indexes.
|
||||
*
|
||||
* @param collectionName name of the MongoDB collection, must not be {@literal null}.
|
||||
* @return index operations on the named collection
|
||||
*/
|
||||
SearchIndexOperations searchIndexOps(String collectionName);
|
||||
|
||||
SearchIndexOperations searchIndexOps(Class<?> type);
|
||||
/**
|
||||
* Returns the operations that can be performed on search indexes.
|
||||
*
|
||||
* @param type the type used for field mapping.
|
||||
* @return index operations on the named collection
|
||||
*/
|
||||
SearchIndexOperations searchIndexOps(Class<?> type);
|
||||
|
||||
SearchIndexOperations searchIndexOps(Class<?> type, String collectionName);
|
||||
/**
|
||||
* Returns the operations that can be performed on search indexes.
|
||||
*
|
||||
* @param collectionName name of the MongoDB collection, must not be {@literal null}.
|
||||
* @param type the type used for field mapping. Can be {@literal null}.
|
||||
* @return index operations on the named collection
|
||||
*/
|
||||
SearchIndexOperations searchIndexOps(Class<?> type, String collectionName);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,306 @@
|
||||
/*
|
||||
* 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
|
||||
*
|
||||
* http://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.
|
||||
*/
|
||||
|
||||
/*
|
||||
* 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
|
||||
*
|
||||
* http://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.ArrayList;
|
||||
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;
|
||||
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
|
||||
import org.springframework.data.util.TypeInformation;
|
||||
import org.springframework.lang.Contract;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* {@link IndexDefinition} 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.
|
||||
*
|
||||
* @author Christoph Strobl
|
||||
* @author Mark Paluch
|
||||
* @since 4.5
|
||||
*/
|
||||
public class VectorIndex implements SearchIndexDefinition {
|
||||
|
||||
private final String name;
|
||||
private final List<Object> fields = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* Create a new {@link VectorIndex} instance.
|
||||
*
|
||||
* @param name The name of the index.
|
||||
*/
|
||||
public VectorIndex(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a filter field.
|
||||
*
|
||||
* @param path dot notation to field/property used for filtering.
|
||||
* @return this.
|
||||
*/
|
||||
@Contract("_ -> this")
|
||||
public VectorIndex addFilter(String path) {
|
||||
|
||||
Assert.hasText(path, "Path must not be null or empty");
|
||||
|
||||
fields.add(new VectorFilterField(path, "filter"));
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a vector field and accept a {@link VectorFieldBuilder} customizer.
|
||||
*
|
||||
* @param path dot notation to field/property used for filtering.
|
||||
* @param customizer customizer function.
|
||||
* @return this.
|
||||
*/
|
||||
@Contract("_, _ -> this")
|
||||
public VectorIndex addVector(String path, Consumer<VectorFieldBuilder> customizer) {
|
||||
|
||||
Assert.hasText(path, "Path must not be null or empty");
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getType() {
|
||||
return "vectorSearch";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Document getDefinition(@Nullable TypeInformation<?> entity,
|
||||
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;
|
||||
|
||||
Document definition = new Document();
|
||||
List<Document> fields = new ArrayList<>();
|
||||
definition.put("fields", fields);
|
||||
|
||||
for (Object 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);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return definition;
|
||||
}
|
||||
|
||||
private String resolvePath(String path, @Nullable MongoPersistentEntity<?> persistentEntity,
|
||||
MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext) {
|
||||
|
||||
if (persistentEntity == null) {
|
||||
return path;
|
||||
}
|
||||
|
||||
QueryMapper.MetadataBackedField mbf = new QueryMapper.MetadataBackedField(path, persistentEntity, mappingContext);
|
||||
|
||||
return mbf.getMappedKey();
|
||||
}
|
||||
|
||||
record VectorIndexField(String path, String type, int dimensions, String similarity, String quantization) {
|
||||
}
|
||||
|
||||
record VectorFilterField(String path, String type) {
|
||||
}
|
||||
|
||||
public static class VectorFieldBuilder {
|
||||
|
||||
private final String path;
|
||||
private final String type;
|
||||
|
||||
private int dimensions;
|
||||
private @Nullable String similarity;
|
||||
private String quantization = "none";
|
||||
|
||||
VectorFieldBuilder(String path, String type) {
|
||||
this.path = path;
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Number of vector dimensions enforced at index- & query-time.
|
||||
*
|
||||
* @param dimensions value between {@code 0} and {@code 4096}.
|
||||
* @return this.
|
||||
*/
|
||||
@Contract("_ -> this")
|
||||
public VectorFieldBuilder dimensions(int dimensions) {
|
||||
this.dimensions = dimensions;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Use similarity based on the angle between vectors.
|
||||
*
|
||||
* @return new instance of {@link VectorIndex}.
|
||||
*/
|
||||
@Contract(" -> this")
|
||||
public VectorFieldBuilder cosine() {
|
||||
|
||||
return similarity(SimilarityFunction.COSINE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Use similarity based the distance between vector ends.
|
||||
*/
|
||||
@Contract(" -> this")
|
||||
public VectorFieldBuilder euclidean() {
|
||||
return similarity(SimilarityFunction.EUCLIDEAN);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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")
|
||||
public VectorFieldBuilder dotProduct() {
|
||||
return similarity(SimilarityFunction.DOT_PRODUCT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Similarity function used.
|
||||
*
|
||||
* @param similarity should be one of {@literal euclidean | cosine | dotProduct}.
|
||||
* @return this.
|
||||
* @see SimilarityFunction
|
||||
* @see #similarity(SimilarityFunction)
|
||||
*/
|
||||
@Contract("_ -> this")
|
||||
public VectorFieldBuilder similarity(String similarity) {
|
||||
this.similarity = similarity;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Similarity function used.
|
||||
*
|
||||
* @param similarity must not be {@literal null}.
|
||||
* @return this.
|
||||
*/
|
||||
@Contract("_ -> this")
|
||||
public VectorFieldBuilder similarity(SimilarityFunction similarity) {
|
||||
return similarity(similarity.getFunctionName());
|
||||
}
|
||||
|
||||
/**
|
||||
* Quantization used.
|
||||
*
|
||||
* @param quantization should be one of {@literal none | scalar | binary}.
|
||||
* @return this.
|
||||
* @see Quantization
|
||||
* @see #quantization(Quantization)
|
||||
*/
|
||||
public VectorFieldBuilder quantization(String quantization) {
|
||||
this.quantization = quantization;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Quntization used.
|
||||
*
|
||||
* @param quantization must not be {@literal null}.
|
||||
* @return this.
|
||||
*/
|
||||
public VectorFieldBuilder quantization(Quantization quantization) {
|
||||
return quantization(quantization.getQuantizationName());
|
||||
}
|
||||
}
|
||||
|
||||
public enum SimilarityFunction {
|
||||
DOT_PRODUCT("dotProduct"), COSINE("cosine"), EUCLIDEAN("euclidean");
|
||||
|
||||
final String functionName;
|
||||
|
||||
SimilarityFunction(String functionName) {
|
||||
this.functionName = functionName;
|
||||
}
|
||||
|
||||
public String getFunctionName() {
|
||||
return functionName;
|
||||
}
|
||||
}
|
||||
|
||||
public enum Quantization {
|
||||
NONE("none"), SCALAR("scalar"), BINARY("binary");
|
||||
|
||||
final String quantizationName;
|
||||
|
||||
Quantization(String quantizationName) {
|
||||
this.quantizationName = quantizationName;
|
||||
}
|
||||
|
||||
public String getQuantizationName() {
|
||||
return quantizationName;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -53,13 +53,13 @@ public abstract class MongoSimpleTypes {
|
||||
public static final Set<Class<?>> AUTOGENERATED_ID_TYPES = Set.of(ObjectId.class, String.class, BigInteger.class);
|
||||
private static final Set<Class<?>> MONGO_SIMPLE_TYPES = Set.of(Binary.class, DBRef.class, Decimal128.class,
|
||||
org.bson.Document.class, Code.class, CodeWScope.class, CodeWithScope.class, ObjectId.class, Pattern.class,
|
||||
Symbol.class, UUID.class, Instant.class, BsonValue.class, BsonNumber.class, BsonType.class, BsonArray.class,
|
||||
BsonSymbol.class, BsonUndefined.class, BsonMinKey.class, BsonMaxKey.class, BsonNull.class, BsonBinary.class,
|
||||
BsonBoolean.class, BsonDateTime.class, BsonDbPointer.class, BsonDecimal128.class, BsonDocument.class,
|
||||
BsonDouble.class, BsonInt32.class, BsonInt64.class, BsonJavaScript.class, BsonJavaScriptWithScope.class,
|
||||
BsonObjectId.class, BsonRegularExpression.class, BsonString.class, BsonTimestamp.class, Geometry.class,
|
||||
GeometryCollection.class, LineString.class, MultiLineString.class, MultiPoint.class, MultiPolygon.class,
|
||||
Point.class, Polygon.class);
|
||||
Symbol.class, UUID.class, Instant.class, BinaryVector.class, BsonValue.class, BsonNumber.class, BsonType.class,
|
||||
BsonArray.class, BsonSymbol.class, BsonUndefined.class, BsonMinKey.class, BsonMaxKey.class, BsonNull.class,
|
||||
BsonBinary.class, BsonBoolean.class, BsonDateTime.class, BsonDbPointer.class, BsonDecimal128.class,
|
||||
BsonDocument.class, BsonDouble.class, BsonInt32.class, BsonInt64.class, BsonJavaScript.class,
|
||||
BsonJavaScriptWithScope.class, BsonObjectId.class, BsonRegularExpression.class, BsonString.class,
|
||||
BsonTimestamp.class, Geometry.class, GeometryCollection.class, LineString.class, MultiLineString.class,
|
||||
MultiPoint.class, MultiPolygon.class, Point.class, Polygon.class);
|
||||
|
||||
public static final SimpleTypeHolder HOLDER = new SimpleTypeHolder(MONGO_SIMPLE_TYPES, true) {
|
||||
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
/*
|
||||
* 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.mapping;
|
||||
|
||||
import org.bson.BinaryVector;
|
||||
import org.bson.Float32BinaryVector;
|
||||
import org.bson.Int8BinaryVector;
|
||||
import org.bson.PackedBitBinaryVector;
|
||||
|
||||
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
|
||||
* variants can be represented as floating-point numbers. int1 returns an all-zero array for {@link #toFloatArray()} and
|
||||
* {@link #toDoubleArray()}.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
* @since 4.5
|
||||
*/
|
||||
public class MongoVector implements Vector {
|
||||
|
||||
private final BinaryVector v;
|
||||
|
||||
MongoVector(BinaryVector v) {
|
||||
this.v = v;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@link MongoVector} from the given {@link BinaryVector}.
|
||||
*
|
||||
* @param v binary vector representation.
|
||||
* @return the {@link MongoVector} for the given vector values.
|
||||
*/
|
||||
public static MongoVector of(BinaryVector v) {
|
||||
return new MongoVector(v);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<? extends Number> getType() {
|
||||
|
||||
if (v instanceof Float32BinaryVector) {
|
||||
return Float.class;
|
||||
}
|
||||
|
||||
if (v instanceof Int8BinaryVector) {
|
||||
return Byte.class;
|
||||
}
|
||||
|
||||
if (v instanceof PackedBitBinaryVector) {
|
||||
return Byte.class;
|
||||
}
|
||||
|
||||
return Number.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BinaryVector getSource() {
|
||||
return v;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int size() {
|
||||
|
||||
if (v instanceof Float32BinaryVector f) {
|
||||
return f.getData().length;
|
||||
}
|
||||
|
||||
if (v instanceof Int8BinaryVector i) {
|
||||
return i.getData().length;
|
||||
}
|
||||
|
||||
if (v instanceof PackedBitBinaryVector p) {
|
||||
return p.getData().length;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public float[] toFloatArray() {
|
||||
|
||||
if (v instanceof Float32BinaryVector f) {
|
||||
|
||||
float[] result = new float[f.getData().length];
|
||||
System.arraycopy(f.getData(), 0, result, 0, result.length);
|
||||
return result;
|
||||
}
|
||||
|
||||
if (v instanceof Int8BinaryVector i) {
|
||||
|
||||
float[] result = new float[i.getData().length];
|
||||
System.arraycopy(i.getData(), 0, result, 0, result.length);
|
||||
return result;
|
||||
}
|
||||
|
||||
return new float[size()];
|
||||
}
|
||||
|
||||
@Override
|
||||
public double[] toDoubleArray() {
|
||||
|
||||
if (v instanceof Float32BinaryVector f) {
|
||||
|
||||
float[] data = f.getData();
|
||||
double[] result = new double[data.length];
|
||||
for (int i = 0; i < data.length; i++) {
|
||||
result[i] = data[i];
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
if (v instanceof Int8BinaryVector i) {
|
||||
|
||||
double[] result = new double[i.getData().length];
|
||||
System.arraycopy(i.getData(), 0, result, 0, result.length);
|
||||
return result;
|
||||
}
|
||||
|
||||
return new double[size()];
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (!(o instanceof MongoVector that)) {
|
||||
return false;
|
||||
}
|
||||
return ObjectUtils.nullSafeEquals(v, that.v);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return ObjectUtils.nullSafeHashCode(v);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "MV[" + v + "]";
|
||||
}
|
||||
}
|
||||
@@ -1,383 +0,0 @@
|
||||
/*
|
||||
* 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
|
||||
*
|
||||
* http://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.
|
||||
*/
|
||||
|
||||
/*
|
||||
* 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
|
||||
*
|
||||
* http://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;
|
||||
|
||||
import static org.springframework.data.mongodb.test.util.Assertions.assertThat;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.bson.Document;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
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.DefaultSearchIndexOperations;
|
||||
import org.springframework.data.mongodb.core.index.SearchIndex;
|
||||
import org.springframework.data.mongodb.core.index.SearchIndex.SimilarityFunction;
|
||||
import org.springframework.data.mongodb.core.index.SearchIndexOperations;
|
||||
import org.springframework.data.mongodb.core.mapping.Field;
|
||||
import org.springframework.data.mongodb.test.util.EnableIfVectorSearchAvailable;
|
||||
import org.springframework.data.mongodb.test.util.MongoTestTemplate;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
import com.mongodb.WriteConcern;
|
||||
import com.mongodb.client.AggregateIterable;
|
||||
|
||||
/**
|
||||
* @author Christoph Strobl
|
||||
*/
|
||||
@EnableIfVectorSearchAvailable
|
||||
class DefaultSearchIndexOperationsTests {
|
||||
|
||||
MongoTestTemplate template = new MongoTestTemplate(cfg -> {
|
||||
cfg.configureMappingContext(ctx -> {
|
||||
ctx.initialEntitySet(Movie.class);
|
||||
});
|
||||
});
|
||||
|
||||
SearchIndexOperations indexOps;
|
||||
|
||||
@BeforeEach
|
||||
void init() throws InterruptedException {
|
||||
|
||||
Movie movie = new Movie();
|
||||
movie.id = "573a1390f29313caabcd5293";
|
||||
movie.description = "Young Pauline is left a lot of money when her wealthy uncle dies. However, her uncle's secretary has been named as her guardian until she marries, at which time she will officially take ...";
|
||||
movie.title = "The Perils of Pauline";
|
||||
movie.year = 1914;
|
||||
movie.plotEmbedding = new Double[] { 0.00072939653, -0.026834568, 0.013515796, -0.033257525, -0.001295428,
|
||||
0.022092875, -0.015958885, 0.018283758, -0.030315313, -0.019479034, 0.019400224, 0.0106917955, -0.005001107,
|
||||
0.017981656, 0.0036416466, -0.012918158, 0.029816188, -0.00018706948, 0.013193991, -0.024483424, -0.016011424,
|
||||
0.0019275442, -0.007467182, -0.011768856, 0.012859052, -0.011722884, -0.002154121, -0.022539461, 0.0010910163,
|
||||
-0.017351182, -0.005122605, -0.010035052, 0.0073161307, -0.04103338, -0.021068355, 0.009877433, 0.023918625,
|
||||
-0.0037828467, 0.0067776004, 0.02159375, 0.018993042, 0.0034905956, 0.0053557493, 0.001825749, -0.026493061,
|
||||
0.021580614, 0.0004851698, -0.02837135, -0.00970668, 0.009279796, 0.021751368, 0.007834959, -0.0130495075,
|
||||
-0.02049042, -0.0009054861, -0.0011345256, 0.00089563493, 0.02842389, -0.012957564, 0.014133136, 0.035831966,
|
||||
-0.015538569, -0.0022296465, -0.0038419536, 0.005523219, -0.009240391, -0.012215442, 0.011447052, -0.032574512,
|
||||
0.017232968, 0.03985124, 0.009719814, 0.01255695, 0.0013964024, 0.014592856, -0.020319667, -0.022119146,
|
||||
0.013922977, -0.021948392, 0.0051423074, 0.024930011, -0.037014104, 0.0042688376, 0.0041407724, 0.009752652,
|
||||
0.0025235396, -0.02721548, 0.004038977, -0.02274962, -0.0015835745, 0.035884503, 0.029317062, -0.012727703,
|
||||
0.0074080746, -0.0012510978, 0.009844596, -0.003332977, 0.023432633, 0.00880694, -0.0066364002, -0.016773248,
|
||||
0.019531572, -0.0059632375, -0.00668894, -0.012898456, -0.023406364, -0.006025628, -0.02336696, 0.014908094,
|
||||
-0.0026089165, -0.017745228, 0.013581471, 0.032600783, -0.01761388, 0.024798661, -0.047338124, 0.0020211304,
|
||||
-0.00039219944, -0.0108691165, 0.008820075, 0.010704931, 0.019597247, 0.016142773, -0.005050363, 0.004790949,
|
||||
0.01661563, 0.01987308, -0.017732093, -0.00998908, 0.0045643724, 0.012373061, -0.012438736, 0.0018405257,
|
||||
0.021212839, -0.03286348, -0.00081066863, -0.02395803, 0.000641557, -0.009798624, -0.020608634, -0.004423172,
|
||||
0.027767146, -0.015210196, -0.0030111722, 0.022683945, -0.0047613955, 0.006061749, 0.012799945, 0.010612987,
|
||||
0.0033756653, 0.00623907, 0.01168348, 0.04665511, -0.021422997, 0.03060428, 0.0037762793, -0.002083521,
|
||||
-0.0009596675, 0.0055856095, -0.008123926, 0.0042097303, 0.033073638, 0.0053064935, -0.002037549, 0.0008192884,
|
||||
0.030683089, 0.0049124467, 0.013896707, -0.0118936375, 0.0032525258, -0.020319667, 0.016221583, -0.027845955,
|
||||
0.026335442, -0.0051587257, 0.017338047, 0.0003144163, -0.00998908, -0.018533321, 0.000037506252, -0.011341972,
|
||||
0.0033346189, -0.0022641257, 0.029133173, -0.022513192, -0.0020671024, -0.00998908, 0.007467182, 0.010586717,
|
||||
0.017955387, 0.0038518049, 0.013647145, 0.024010569, -0.023025453, -0.66620135, 0.0043312283, -0.0021968095,
|
||||
0.0011328838, -0.008820075, 0.015486029, 0.015105117, -0.007073135, -0.026020207, 0.0007257024, 0.005792484,
|
||||
0.020582363, -0.009332336, 0.0010105652, -0.007230754, -0.02213228, 0.005464112, -0.0375395, 0.0050832,
|
||||
-0.005523219, -0.0015006606, 0.0389318, 0.008465433, 0.016142773, 0.019965025, 0.016523685, 0.007979442,
|
||||
-0.009542493, -0.017390586, 0.0029454979, -0.0029537072, 0.023498308, -0.010376559, -0.008629619, 0.04190028,
|
||||
0.009798624, -0.004866475, -0.0096016005, 0.008301247, 0.024535963, -0.030000076, -0.014133136, 0.005920549,
|
||||
-0.016274123, -0.0017124605, 0.0025465258, 0.008110791, 0.0075919633, 0.0051160376, 0.02559989, 0.005657851,
|
||||
0.014553452, -0.009253526, -0.019019313, -0.005322912, -0.005096335, 0.01584067, -0.0318915, -0.02672949,
|
||||
0.014461508, -0.0033395444, -0.0020785953, -0.0273731, -0.007460614, 0.010796875, 0.015289006, -0.009726382,
|
||||
0.025928263, -0.020713713, -0.018572727, 0.0038944932, 0.010429098, 0.009838029, 0.017508801, 0.02718921,
|
||||
-0.0055856095, 0.0153415445, -0.017232968, -0.016957136, 0.007460614, 0.0051751444, -0.010015349, -0.03633109,
|
||||
0.018966774, 0.022670811, -0.0081764655, -0.035385378, 0.0013512513, 0.023695331, -0.0035529863, -0.004380484,
|
||||
0.018441377, -0.007027163, 0.009286364, -0.0018766467, 0.02834508, -0.022657676, -0.0043640654, 0.023275016,
|
||||
-0.03267959, 0.023222476, 0.00023047617, 0.0014349861, -0.0014029698, 0.03848521, 0.0038452374, 0.0012084093,
|
||||
0.0059960745, 0.03507014, -0.017692689, 0.025744373, -0.011979015, 0.007946605, -0.01815241, -0.033677842,
|
||||
-0.032574512, 0.022119146, 0.02320934, -0.0026975768, -0.011841098, -0.0030752048, 0.022736484, -0.006603563,
|
||||
-0.024220727, -0.002741907, 0.013476391, -0.017745228, -0.020345936, -0.0115586985, 0.009522791, 0.004649749,
|
||||
-0.015998289, 0.01656309, -0.011486457, 0.009516223, -0.003756577, 0.034938794, -0.030866979, 0.02675576,
|
||||
-0.017416857, -0.006665954, 0.0126488935, 0.024220727, -0.004708856, 0.011144949, -0.03499133, -0.022618271,
|
||||
-0.026125286, -0.053800486, 0.0034708933, -0.010961061, 0.008229005, -0.012878754, 0.007073135, 0.018507052,
|
||||
0.0033855163, -0.007710177, -0.0031031165, -0.016208448, -0.019492168, 0.008485136, 0.0036646328, -0.025324058,
|
||||
0.0047055725, -0.0037138886, -0.006298177, 0.011913341, 0.008274977, 0.0055626235, -0.008432596, -0.002649963,
|
||||
0.005723526, -0.007854661, -0.0009219047, 0.02506136, 0.028896745, 0.015433489, 0.010199238, -0.021974662,
|
||||
-0.008353787, -0.008563945, -0.012970698, -0.004649749, -0.0051620095, 0.032548245, 0.006876112, -0.016182177,
|
||||
0.03176015, 0.0046924376, -0.0038583723, 0.014605992, 0.010061322, 0.0065969955, 0.007637935, 0.0065477397,
|
||||
0.010015349, 0.017876578, -0.001625442, 0.020687442, 0.0073161307, -0.00079917564, -0.0018093303, 0.004413321,
|
||||
-0.0129378615, 0.014803015, -0.028056113, 0.016326662, -0.0186384, 0.03170761, 0.006393405, -0.0036876188,
|
||||
-0.003016098, -0.009430847, 0.00002683416, 0.024877472, 0.012662029, -0.008104224, -0.0035825397, 0.012432168,
|
||||
0.012182605, 0.008340651, -0.003121177, -0.006698791, -0.0067841676, -0.01771896, 0.002211586, 0.02611215,
|
||||
-0.023931758, -0.0045545213, -0.00544441, -0.020845061, -0.0083997585, -0.005664419, 0.03183896, 0.0041571907,
|
||||
-0.010632689, 0.0038583723, -0.029211983, 0.0069417865, -0.0032984978, 0.01255695, 0.009851163, 0.020700578,
|
||||
0.0004203163, 0.00067398377, 0.00683014, 0.032311816, 0.007854661, -0.0017026094, 0.01422508, -0.0005812186,
|
||||
0.01584067, -0.007454047, 0.011781991, 0.017968522, -0.025796913, 0.009030233, 0.02387922, 0.027924765,
|
||||
0.019176932, -0.0037500095, 0.002635186, -0.019702327, -0.0033855163, 0.019649787, 0.00087675353, 0.0081764655,
|
||||
-0.008866047, -0.007145377, -0.021698829, -0.01412, -0.009831461, -0.010396261, 0.0015843954, 0.01815241,
|
||||
-0.017679553, 0.007480317, 0.0027763862, 0.010961061, 0.005910698, -0.028318811, -0.021462401, 0.029658569,
|
||||
0.047968596, 0.0047778143, -0.025271518, 0.0077627166, -0.00033740234, -0.0019850093, 0.0055593397, 0.012281117,
|
||||
0.025166439, -0.013844168, -0.004590642, -0.012845917, 0.00383867, 0.013988652, -0.0053393305, 0.008938289,
|
||||
-0.034649827, 0.02062177, -0.0030226652, -0.01422508, -0.01535468, 0.013896707, 0.015459759, -0.013391014,
|
||||
0.006058465, -0.005004391, -0.021554345, -0.012950996, -0.0127605405, -0.011236894, -0.0045545213,
|
||||
-0.00080245937, -0.0051160376, 0.016260987, 0.014711071, 0.02675576, 0.013765359, -0.0012322164, -0.006002642,
|
||||
-0.03971989, 0.0053656003, 0.122732356, 0.039509732, 0.005405005, 0.017154159, -0.007690475, -0.0057563633,
|
||||
-0.0035661212, -0.016037693, 0.026190959, 0.010126996, 0.023038587, -0.005697256, 0.00068917096, 0.019584112,
|
||||
0.01422508, 0.00069450703, 0.007920335, -0.034676094, 0.009870865, -0.004439591, 0.035148952, 0.013581471,
|
||||
0.009404577, 0.023025453, -0.032574512, -0.009916837, 0.010251777, 0.013003536, -0.0122942515, 0.012662029,
|
||||
-0.015617377, -0.026690084, 0.004794233, 0.018901099, -0.011611238, -0.008117358, -0.0035923908, -0.0054575442,
|
||||
0.037119184, -0.0048008002, 0.011985582, 0.0048073675, -0.002815791, -0.005825321, 0.00929293, 0.00028322096,
|
||||
-0.0217251, 0.036803946, 0.016602494, -0.003848521, 0.035779424, -0.0014981978, -0.0005730093, -0.011033303,
|
||||
0.016655033, -0.0030883397, 0.0075197215, -0.0009604884, -0.0012642327, 0.039430924, -0.015998289, -0.027478179,
|
||||
0.009424279, -0.012616056, 0.025087629, -0.0071322424, -0.0045479536, -0.016418606, 0.000326525, -0.013154587,
|
||||
0.02210601, -0.018480781, -0.004393619, -0.016681302, 0.0014046117, 0.008557377, 0.018467648, -0.009995647,
|
||||
-0.007723312, 0.0048336373, -0.0020900886, -0.028108653, -0.012819647, -0.01702281, -0.008117358, 0.030972058,
|
||||
0.0010048187, -0.0070205955, -0.01817868, 0.015709322, 0.0077692843, 0.01876975, -0.002402042, -0.021344187,
|
||||
0.0023445769, 0.009870865, -0.008018847, 0.0008882466, -0.008156763, 0.007907201, 0.012281117, -0.0066002794,
|
||||
-0.04410694, -0.021015815, 0.006511619, 0.015367814, -0.00018768519, 0.024155052, 0.0024184606, -0.0070140283,
|
||||
0.007486884, -0.022276765, 0.0055593397, 0.01817868, -0.03619974, 0.023156801, 0.016707573, 0.02156748,
|
||||
0.016786382, 0.0025235396, -0.015551703, -0.012622624, 0.02939587, 0.02565243, -0.019886214, 0.0031786421,
|
||||
-0.0035102977, -0.024273267, 0.027057862, 0.008156763, 0.038879257, -0.017141024, 0.0037828467, -0.008406326,
|
||||
-0.026506197, -0.010330587, -0.0074212095, -0.02621723, -0.023353824, 0.005523219, -0.012583219, 0.008327517,
|
||||
-0.0021738233, -0.018887963, 0.012662029, -0.031970307, 0.0017058931, 0.0041342047, 0.0012921443, 0.033730384,
|
||||
-0.018296894, 0.0026171256, -0.009095907, 0.01825749, 0.011532429, -0.027898494, 0.004226149, -0.016287256,
|
||||
0.0019817257, -0.0010943001, 0.036042124, -0.0067776004, -0.0074474793, 0.017521936, 0.01165721, -0.0033493955,
|
||||
-0.019321416, -0.029474681, -0.026821434, 0.03267959, 0.00623907, 0.013128317, 0.021974662, -0.037224263,
|
||||
-0.0061569773, 0.017298643, 0.004226149, -0.008347219, -0.016050829, -0.03969362, -0.012399331, -0.0038747909,
|
||||
-0.016182177, -0.013949247, 0.0008053326, -0.016418606, -0.008537675, -0.014658531, -0.0008266768,
|
||||
-0.0007745477, 0.01871721, -0.006025628, 0.0025153304, -0.010626121, -0.015000038, -0.0037106047, 0.0023051722,
|
||||
-0.005510084, 0.0071782144, 0.017324913, 0.0132728005, 0.009358605, -0.0059993584, -0.007867795, -0.008202735,
|
||||
0.013410717, -0.0052112653, -0.038091164, 0.02387922, -0.011952745, -0.024759257, -0.01930828, 0.002837135,
|
||||
-0.035359107, 0.005710391, -0.011900205, -0.0057760654, 0.015394084, -0.029343331, -0.028581508, -0.004616912,
|
||||
-0.019754866, 0.007040298, 0.0033690978, 0.022329304, 0.03183896, -0.0015113326, -0.010665526, 0.010238643,
|
||||
0.003651498, 0.0028781816, 0.031287294, 0.02845016, -0.0012190815, 0.008951424, 0.0018536606, 0.012373061,
|
||||
-0.023472039, -0.024168188, -0.001153407, -0.007894065, 0.009424279, 0.0036646328, -0.010803442, 0.0043772003,
|
||||
0.028082382, -0.0075065866, 0.0011566908, -0.027346829, -0.017351182, -0.029264523, -0.008150196, 0.009759219,
|
||||
0.013121749, 0.0033477535, -0.008452298, 0.003625228, -0.021790773, -0.016720708, 0.020937005, -0.016366066,
|
||||
0.010028484, -0.001024521, 0.002543242, -0.005828605, -0.028581508, -0.005230968, 0.00468587, 0.0007215977,
|
||||
0.023563983, 0.01656309, -0.003638363, 0.010409396, -0.006278475, 0.0016861908, -0.02457537, -0.011650642,
|
||||
-0.025560485, 0.0018421676, -0.018966774, -0.0088003725, -0.0065969955, -0.0148292845, -0.01419881,
|
||||
-0.009273228, -0.009595033, -0.011250028, -0.004426456, 0.012780243, 0.0022674093, -0.014816149, -0.016852057,
|
||||
0.0067644655, -0.01137481, -0.0078021213, 0.00821587, 0.009969377, 0.014632261, -0.012642326, 0.012832782,
|
||||
-0.010718065, 0.0010491489, -0.015683051, 0.015669918, -0.00795974, 0.010619554, 0.041164726, -0.02038534,
|
||||
-0.017167294, 0.008314381, 0.016392335, 0.011427349, 0.0021968095, -0.004495414, -0.016576225, 0.0073424005,
|
||||
0.02221109, 0.0020391908, -0.0059238328, -0.016103368, -0.0020621768, -0.0018093303, 0.024352076, -0.025796913,
|
||||
-0.003628512, -0.008531108, 0.009352038, 0.0036843352, -0.013489527, 0.002732056, 0.0045972094, 0.012799945,
|
||||
0.008990828, -0.011834531, -0.027110402, -0.012103796, -0.0041243536, 0.02732056, 0.0039338977, -0.018704075,
|
||||
-0.0053294795, 0.019242605, 0.029632298, -0.006078168, -0.0023002466, 0.019071853, -0.011098977, -0.030131426,
|
||||
-0.013804764, -0.000812721, 0.0023987582, 0.01887483, 0.011637508, -0.025074495, -0.018546456, 0.012865619,
|
||||
-0.03168134, -0.008465433, -0.013515796, 0.023931758, 0.02148867, 0.013095479, 0.0034807443, 0.012300819,
|
||||
0.017246103, 0.024535963, -0.022434382, -0.02708413, 0.01941336, -0.009818326, -0.013647145, 0.004695721,
|
||||
-0.026125286, -0.021554345, 0.010987331, -0.023077993, -0.0011993791, 0.0039962884, -0.016392335, -0.021462401,
|
||||
-0.015591107, 0.020805657, 0.0067381957, 0.01419881, 0.009168149, -0.0078021213, 0.01021894, 0.011979015,
|
||||
0.0040783817, -0.035674345, 0.005230968, 0.0007872721, -0.010658959, 0.0017173862, -0.007283293, -0.0031983443,
|
||||
-0.029422142, -0.008760968, 0.05351152, -0.0025826467, 0.003651498, 0.00880694, 0.027162941, -0.0083997585,
|
||||
-0.011059573, 0.01419881, -0.023485173, -0.0194659, 0.01132227, -0.0027008606, 0.03299483, -0.017246103,
|
||||
0.007145377, -0.012267982, -0.0043377955, -0.0043870513, 0.001860228, -0.003779563, 0.0101795355, 0.015985154,
|
||||
-0.017311778, 0.022578867, -0.021764504, -0.014934364, -0.026690084, -0.039063144, 0.015183927, -0.027740875,
|
||||
-0.02724175, 0.001930828, 0.0049879723, -0.017285507, -0.0061372747, -0.008058252, -0.010442233, 0.038143706,
|
||||
0.21709336, 0.005309777, 0.0121366335, 0.03157626, -0.004590642, 0.008583647, 0.018493917, 0.0053590327,
|
||||
-0.0028059396, -0.02444402, -0.040429175, 0.0015827536, 0.0036186606, 0.0071191075, -0.0107574705, -0.028029844,
|
||||
-0.02423386, -0.013108615, 0.0010146698, -0.0150525775, -0.017232968, 0.014894959, -0.00939801, -0.02282843,
|
||||
0.030472932, 0.00025202558, -0.011821396, 0.004630047, 0.013003536, 0.02102895, -0.013391014, -0.011190921,
|
||||
-0.022907238, 0.015367814, -0.022421248, -0.019938754, -0.014408968, -0.010704931, 0.0013233396, 0.027451908,
|
||||
0.022907238, 0.0047515444, -0.0015819327, -0.009233824, 0.013463257, 0.020818792, -0.008064819, -0.0035726884,
|
||||
-0.045972094, 0.0026762327, -0.047285583, -0.031208485, 0.05319628, 0.0016155908, 0.00051349186, 0.019820541,
|
||||
0.015748726, 0.024115648, -0.047022887, -0.0014661815, -0.011250028, 0.0014924513, -0.013213694, 0.0443171,
|
||||
0.006275191, 0.030499201, -0.008491702, -0.022972913, 0.017246103, -0.017929116, 0.009437415, 0.0037040373,
|
||||
0.010823145, -0.0028716142, -0.002223079, -0.029684838, 0.029317062, 0.0053721676, 0.007900633, -0.0075722607,
|
||||
-0.007966307, 0.016878327, -0.008944856, 0.004213014, -0.0067316284, -0.0352803, 0.010632689, 0.009851163,
|
||||
0.0095556285, -0.0008430954, -0.0011755722, -0.025087629, -0.008537675, 0.011420782, -0.0020047117, 0.036593787,
|
||||
0.0034577583, 0.034912523, -0.024930011, 0.017810903, -0.014894959, -0.005470679, 0.010586717, 0.0018273907,
|
||||
-0.013857303, -0.0028666884, -0.0089776935, 0.029106904, 0.016536819, -0.021738233, -0.005654568, -0.021134028,
|
||||
0.014973768, -0.0065378887, 0.026979053, 0.0023560699, -0.01887483, -0.018441377, 0.020004429, -0.014487778,
|
||||
-0.022789024, -0.00878067, -0.0022903956, 0.018914234, 0.027688336, 0.0006395047, -0.015026308, 0.004344363,
|
||||
-0.005661135, -0.02565243, 0.020253992, -0.026952783, 0.015183927, -0.018283758, -0.03194404, 0.0059599536,
|
||||
0.005030661, -0.00570054, -0.0044789957, -0.0013569978, 0.021987796, -0.020359071, -0.0008504838, -0.008649321,
|
||||
0.004367349, -0.016944, 0.012405898, 0.004406754, 0.00031646862, 0.0020506838, -0.05558683, -0.028187461,
|
||||
-0.010639257, -0.011309136, 0.0007203663, -0.0163792, -0.016930865, -0.023051722, 0.0053951535, 0.007953173,
|
||||
-0.030578012, 0.029632298, 0.016878327, -0.012950996, 0.0053951535, -0.011571833, -0.16749604, 0.023708466,
|
||||
0.014737341, -0.02613842, 0.031155946, 0.021908987, 0.0043016747, -0.012300819, -0.032364354, -0.012392763,
|
||||
0.026571872, -0.004613628, 0.0065444564, -0.0041965954, -0.018625267, 0.021291647, -0.029211983, 0.019557843,
|
||||
0.033178717, 0.008839777, 0.017929116, -0.01871721, 0.0071059726, 0.0074343444, 0.013200559, 0.025928263,
|
||||
-0.020017564, -0.01476361, -0.012898456, -0.0069417865, -0.015315276, 0.015499163, 0.026046475, 0.005450977,
|
||||
0.012609489, 0.008662457, -0.011177787, -0.0072701587, -0.010744335, 0.012038122, 0.026348578, 0.028817937,
|
||||
0.006193098, 0.0018388839, -0.0033231257, 0.0477059, 0.01702281, 0.0066002794, 0.022933507, -0.0059960745,
|
||||
0.02732056, -0.038038626, 0.022316169, 0.009614735, -0.010658959, 0.008563945, 0.004380484, -0.003533284,
|
||||
-0.0034938792, 0.011171219, 0.003319842, -0.00708627, 0.0003302192, 0.012064392, 0.0020506838, -0.015367814,
|
||||
0.003096549, -0.0071716467, -0.029264523, 0.0108691165, 0.010067889, -0.021738233, 0.007782419, -0.034308318,
|
||||
0.0012396047, 0.0009851163, -0.007427777, 0.0011796769, -0.0030555024, 0.036672596, -0.0122942515, 0.03743442,
|
||||
0.0066364002, -0.015827537, 0.0023232326, -0.014684801, 0.007513154, 0.00083283376, 0.018231219, -0.028555239,
|
||||
-0.0130495075, -0.022526328, -0.0063375817, -0.01055388, 0.0063737025, 0.0065575913, 0.012379629, 0.005181712,
|
||||
-0.0208976, -0.011578401, -0.005470679, -0.0009136954, -0.020792522, 0.007690475, 0.05584953, 0.011933043,
|
||||
0.02226363, -0.015144521, 0.016326662, 0.00058819656, 0.00584174, 0.005969805, 0.00063252676, 0.02028026,
|
||||
-0.021239107, 0.03643617, -0.0069417865, -0.032521974, 0.01530214, -0.0307619, 0.066147275, 0.018007927,
|
||||
-0.004298391, 0.011762289, -0.020687442, 0.0014826001, -0.10318765, -0.0017830606, 0.011506159, 0.022999182,
|
||||
0.0039338977, -0.012602922, 0.01134854, 0.0046694516, 0.00021221048, 0.005299926, 0.0010031768, -0.010849414,
|
||||
0.0122482795, 0.0020900886, 0.004219582, -0.014159406, -0.005168577, -0.029001825, -0.0049058795, -0.008721563,
|
||||
-0.008183033, 0.018218085, -0.0019997861, -0.02226363, -0.01368655, -0.017377453, -0.03315245, 0.014737341,
|
||||
0.015932614, 0.015394084, -0.010672093, -0.010816577, -0.0027698188, -0.01882229, -0.003109684, -0.018441377,
|
||||
-0.013949247, -0.011144949, 0.012307387, -0.02603334, 0.0066823727, 0.011466754, 0.026834568, -0.026335442,
|
||||
0.007631368, -0.0022000931, -0.024063108, 0.00821587, 0.013909843, -0.007289861, -0.038327593, -0.005569191,
|
||||
-0.007322698, -0.010317451, 0.036042124, 0.0003907628, 0.014894959, 0.022316169, -0.0023035305, 0.0049551353,
|
||||
-0.0069746235, -0.008570512, -0.028134922, -0.0021344188, 0.009286364, 0.010514475, 0.02049042, -0.016300391,
|
||||
0.0330211, -0.015827537, -0.018375704, 0.026361713, -0.0071519446, 0.023655927, -0.026348578, -0.008826642,
|
||||
-0.0217251, 0.011624373, 0.008025414, -0.009726382, -0.011250028, -0.0101664, 0.015197061, 0.008511405,
|
||||
0.00033699188, 0.0051127537, -0.0067841676, 0.023669062, 0.018126141, -0.0007261128, 0.00071297796, 0.023012318,
|
||||
0.013515796, -0.005723526, 0.0012708, -0.0071519446, -0.011394512, -0.041348618, 0.0041374885, -0.01476361,
|
||||
-0.027583256, -0.010961061, -0.06246951, 0.027845955, 0.021409862, -0.0011025093, -0.010133564, 0.003756577,
|
||||
0.02269708, 0.009411145, -0.033362605, 0.0045578047, -0.02210601, 0.02436521, 0.013423852, 0.016957136,
|
||||
-0.023406364, -0.018979907, 0.008629619, 0.004452726, 0.014264485, 0.015604243, 0.013134885, -0.0021048652,
|
||||
0.02274962, 0.0002943035, 0.001519542, 0.011368242, -0.001366028, -0.01255695, 0.0035004467, -0.0070205955,
|
||||
0.0019570978, -0.034176968, 0.0069155167, 0.013233396, 0.015932614, -0.022316169, -0.0041276375, 0.00017988635,
|
||||
0.026296038, 0.01997816, -0.009141879, -0.042793453, 0.005460828, -0.0051193214, -0.002308456, 0.00801228,
|
||||
-0.012484708, -0.0022789023, 0.03160253, 0.00084227446, 0.033677842, 0.031261023, 0.004203163, -0.018835424,
|
||||
-0.0077167447, 0.010481638, 0.013180857, -0.014737341, -0.017508801, -0.03740815, 0.018086735, -0.00015197472,
|
||||
-0.0011640792, 0.015210196, 0.002648321, -0.022736484, -0.007880931, -0.01086255, 0.0033001397, -0.03785474,
|
||||
-0.03299483, -0.002600707, 0.005523219, 0.031523723, 0.012740837, 0.0066134143, -0.0036777677, 0.0018290327,
|
||||
-0.025941396, 0.028791666, 0.021331051, 0.025153304, -0.040849492, -0.014330159, 0.03541165, 0.0028075816,
|
||||
-0.009805191, -0.009122177, -0.0051718606, 0.021751368, 0.011644075, 0.022421248, -0.010737768, -0.017246103,
|
||||
-0.011637508, 0.0034741769, -0.03055174, -0.0051849955, -0.027031591, -0.010796875, 0.01163094, -0.002929079,
|
||||
0.005299926, -0.021580614, -0.016536819, -0.009588466, -0.011164652, -0.03309991, 0.006593712, -0.0011509443,
|
||||
0.007500019, -0.0016164117, -0.0029816187, 0.012740837, -0.018336298, -0.0067973025, 0.0049387165, -0.01417254,
|
||||
-0.009903703, 0.007657638, 0.0037598608, -0.004830354, 0.023734735, -0.0071322424, 0.0018109722, 0.014619126,
|
||||
-0.0033888002, -0.0364099, -0.0035267165, -0.028318811, 0.019991294, -0.001766642, -0.037828468, -0.012320521,
|
||||
0.0060913027, -0.009365172, -0.009430847, 0.027504448, -0.03160253, 0.047022887, -0.0053951535, -0.014934364,
|
||||
0.0005639791, -0.0055593397, 0.0027287721, 0.015814401, 0.0053853025, -0.025402866, 0.0052112653, -0.033940542,
|
||||
-0.021094624, 0.03296856, -0.013397582, -0.015065713, -0.0043148096, -0.015932614, 0.024102513, -0.014422103,
|
||||
0.016549954, 0.010599852, 0.0055954605, 0.004012707, -0.000038788956, -0.018007927, -0.0002528465, 0.0017502233,
|
||||
0.016957136, 0.026519332, -0.03746069, 0.0077627166, -0.0026565304, -0.006248921, -0.012090661, 0.023248745,
|
||||
-0.02441775, 0.01419881, -0.01640547, 0.00013750582, 0.006629833, -0.017154159, 0.024312671, -0.010875684,
|
||||
-0.025035089, -0.011946177, 0.00004302188, -0.0019981442, 0.004042261, -0.01163094 };
|
||||
|
||||
template.setWriteConcern(WriteConcern.ACKNOWLEDGED);
|
||||
template.save(movie);
|
||||
|
||||
Thread.sleep(5000);
|
||||
|
||||
indexOps = new DefaultSearchIndexOperations(template, Movie.class);
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void cleanup() {
|
||||
|
||||
template.searchIndexOps(Movie.class).dropIndex("vector_index");
|
||||
template.dropCollection(Movie.class);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = { "euclidean", "cosine", "dotProduct" })
|
||||
void createsSimpleVectorIndex(String similarityFunction) throws InterruptedException {
|
||||
|
||||
SearchIndex idx = new SearchIndex("vector_index").dimensions(1536).path("plotEmbedding")
|
||||
.similarity(similarityFunction);
|
||||
|
||||
indexOps.ensureIndex(idx);
|
||||
Thread.sleep(1000); // now that's quite some time to build the index
|
||||
|
||||
Document raw = readRawIndexInfo(idx.getName());
|
||||
assertThat(raw).containsEntry("name", idx.getName()) //
|
||||
.containsEntry("type", "vectorSearch") //
|
||||
.containsEntry("latestDefinition.fields.[0].type", "vector") //
|
||||
.containsEntry("latestDefinition.fields.[0].path", "plot_embedding") //
|
||||
.containsEntry("latestDefinition.fields.[0].numDimensions", 1536) //
|
||||
.containsEntry("latestDefinition.fields.[0].similarity", similarityFunction); //
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled("""
|
||||
The command is valid according to documentation but even
|
||||
db.movie.updateSearchIndex("vector_index", {"fields": [{"type": "vector", "path": "plot_embedding", "numDimensions": 1536, "similarity": "dotProduct"}]});
|
||||
fails con the shell missing user.mappings.
|
||||
""")
|
||||
void updatesVectorIndex() throws InterruptedException {
|
||||
|
||||
SearchIndex idx = new SearchIndex("vector_index").dimensions(1536).path("plotEmbedding").similarity("cosine");
|
||||
|
||||
indexOps.ensureIndex(idx);
|
||||
Thread.sleep(5000); // now that's quite some time to build the index
|
||||
|
||||
Document raw = readRawIndexInfo(idx.getName());
|
||||
assertThat(raw).containsEntry("name", idx.getName()) //
|
||||
.containsEntry("type", "vectorSearch") //
|
||||
.containsEntry("latestDefinition.fields.[0].type", "vector") //
|
||||
.containsEntry("latestDefinition.fields.[0].path", "plot_embedding") //
|
||||
.containsEntry("latestDefinition.fields.[0].numDimensions", 1536) //
|
||||
.containsEntry("latestDefinition.fields.[0].similarity", "cosine"); //
|
||||
|
||||
idx.similarity(SimilarityFunction.DOT_PRODUCT);
|
||||
indexOps.updateIndex(idx);
|
||||
Thread.sleep(5000);
|
||||
|
||||
raw = readRawIndexInfo(idx.getName());
|
||||
assertThat(raw).containsEntry("name", idx.getName()) //
|
||||
.containsEntry("type", "vectorSearch") //
|
||||
.containsEntry("latestDefinition.fields.[0].type", "vector") //
|
||||
.containsEntry("latestDefinition.fields.[0].path", "plot_embedding") //
|
||||
.containsEntry("latestDefinition.fields.[0].numDimensions", 1536) //
|
||||
.containsEntry("latestDefinition.fields.[0].similarity", "dotProduct"); //
|
||||
}
|
||||
|
||||
@Test
|
||||
void createsVectorIndexWithFilters() throws InterruptedException {
|
||||
|
||||
SearchIndex idx = SearchIndex.cosine("vector_index").dimensions(1536).path("plotEmbedding") //
|
||||
.filter("description") //
|
||||
.filter("year");
|
||||
|
||||
indexOps.ensureIndex(idx);
|
||||
Thread.sleep(5000); // now that's quite some time to build the index
|
||||
|
||||
Document raw = readRawIndexInfo(idx.getName());
|
||||
assertThat(raw).containsEntry("name", idx.getName()) //
|
||||
.containsEntry("type", "vectorSearch") //
|
||||
.containsEntry("latestDefinition.fields.[0].type", "vector") //
|
||||
.containsEntry("latestDefinition.fields.[1].type", "filter") //
|
||||
.containsEntry("latestDefinition.fields.[1].path", "plot") //
|
||||
.containsEntry("latestDefinition.fields.[2].type", "filter") //
|
||||
.containsEntry("latestDefinition.fields.[2].path", "year"); //
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private Document readRawIndexInfo(String name) {
|
||||
|
||||
AggregateIterable<Document> indexes = template.execute(Movie.class, collection -> {
|
||||
return collection.aggregate(List.of(new Document("$listSearchIndexes", new Document("name", name))));
|
||||
});
|
||||
|
||||
return indexes.first();
|
||||
}
|
||||
|
||||
static class Movie {
|
||||
|
||||
@Id String id;
|
||||
String title;
|
||||
|
||||
@Field("plot") String description;
|
||||
int year;
|
||||
|
||||
@Field("plot_embedding") Double[] plotEmbedding;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -32,9 +32,9 @@ import org.springframework.data.mongodb.util.aggregation.TestAggregationContext;
|
||||
class VectorSearchOperationUnitTests {
|
||||
|
||||
static final Document $VECTOR_SEARCH = Document.parse(
|
||||
"{'index' : 'vector_index', 'path' : 'plot_embedding', 'queryVector' : [-0.0016261312, -0.028070757, -0.011342932], 'limit' : 10}");
|
||||
"{'index' : 'vector_index', 'limit' : 10, 'path' : 'plot_embedding', 'queryVector' : [-0.0016261312, -0.028070757, -0.011342932]}");
|
||||
static final VectorSearchOperation SEARCH_OPERATION = VectorSearchOperation.search("vector_index")
|
||||
.path("plot_embedding").vectors(-0.0016261312, -0.028070757, -0.011342932).limit(10);
|
||||
.path("plot_embedding").vector(-0.0016261312, -0.028070757, -0.011342932).limit(10);
|
||||
|
||||
@Test // GH-4706
|
||||
void requiredArgs() {
|
||||
@@ -60,7 +60,7 @@ class VectorSearchOperationUnitTests {
|
||||
@Test // GH-4706
|
||||
void withScore() {
|
||||
|
||||
List<Document> stages = SEARCH_OPERATION.searchScore().toPipelineStages(Aggregation.DEFAULT_CONTEXT);
|
||||
List<Document> stages = SEARCH_OPERATION.withSearchScore().toPipelineStages(Aggregation.DEFAULT_CONTEXT);
|
||||
Assertions.assertThat(stages).containsExactly(new Document("$vectorSearch", $VECTOR_SEARCH),
|
||||
new Document("$addFields", new Document("score", new Document("$meta", "vectorSearchScore"))));
|
||||
}
|
||||
@@ -68,7 +68,7 @@ class VectorSearchOperationUnitTests {
|
||||
@Test // GH-4706
|
||||
void withScoreFilter() {
|
||||
|
||||
List<Document> stages = SEARCH_OPERATION.filterBySore(score -> score.gt(50))
|
||||
List<Document> stages = SEARCH_OPERATION.withFilterBySore(score -> score.gt(50))
|
||||
.toPipelineStages(Aggregation.DEFAULT_CONTEXT);
|
||||
Assertions.assertThat(stages).containsExactly(new Document("$vectorSearch", $VECTOR_SEARCH),
|
||||
new Document("$addFields", new Document("score", new Document("$meta", "vectorSearchScore"))),
|
||||
@@ -78,7 +78,7 @@ class VectorSearchOperationUnitTests {
|
||||
@Test // GH-4706
|
||||
void withScoreFilterOnCustomFieldName() {
|
||||
|
||||
List<Document> stages = SEARCH_OPERATION.filterBySore(score -> score.gt(50)).searchScore("s-c-o-r-e")
|
||||
List<Document> stages = SEARCH_OPERATION.withFilterBySore(score -> score.gt(50)).withSearchScore("s-c-o-r-e")
|
||||
.toPipelineStages(Aggregation.DEFAULT_CONTEXT);
|
||||
Assertions.assertThat(stages).containsExactly(new Document("$vectorSearch", $VECTOR_SEARCH),
|
||||
new Document("$addFields", new Document("s-c-o-r-e", new Document("$meta", "vectorSearchScore"))),
|
||||
|
||||
@@ -18,7 +18,7 @@ package org.springframework.data.mongodb.core.aggregation;
|
||||
import org.bson.Document;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.springframework.data.mongodb.core.index.SearchIndex;
|
||||
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.test.util.MongoTestTemplate;
|
||||
@@ -31,7 +31,7 @@ import org.springframework.data.mongodb.test.util.Template;
|
||||
@ExtendWith(MongoTemplateExtension.class)
|
||||
public class VectorSearchTests {
|
||||
|
||||
static final String COLLECTION_NAME = "embedded_movies";
|
||||
static final String COLLECTION_NAME = "movies";
|
||||
|
||||
@Template(database = "mflix") //
|
||||
static MongoTestTemplate template;
|
||||
@@ -39,38 +39,37 @@ public class VectorSearchTests {
|
||||
@Test
|
||||
void xxx() {
|
||||
|
||||
// boolean hasIndex = template.indexOps(COLLECTION_NAME).getIndexInfo().stream()
|
||||
// .anyMatch(it -> it.getName().endsWith("vector_index"));
|
||||
// boolean hasIndex = template.indexOps(COLLECTION_NAME).getIndexInfo().stream()
|
||||
// .anyMatch(it -> it.getName().endsWith("movie_vector_index"));
|
||||
|
||||
// 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;
|
||||
// });
|
||||
// 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;
|
||||
// });
|
||||
|
||||
boolean hasIndex = template.searchIndexOps(COLLECTION_NAME).exists("vector_index");
|
||||
|
||||
if(hasIndex) {
|
||||
System.out.println("found the index: vector_index");
|
||||
System.out.println(template.searchIndexOps(COLLECTION_NAME).getIndexInfo());
|
||||
template.searchIndexOps(COLLECTION_NAME).updateIndex(new SearchIndex("vector_index").path("plot_embedding").dimensions(1536).similarity("euclidean"));
|
||||
// template.indexOps(COLLECTION_NAME).vectorIndexOperations().dropIndex("vector_name");
|
||||
if (!template.collectionExists(COLLECTION_NAME)) {
|
||||
template.createCollection(COLLECTION_NAME);
|
||||
}
|
||||
else {
|
||||
|
||||
boolean hasIndex = template.searchIndexOps(COLLECTION_NAME).exists("movie_vector_index");
|
||||
|
||||
if (!hasIndex) {
|
||||
|
||||
System.out.print("Creating index: ");
|
||||
String s = template.searchIndexOps(COLLECTION_NAME).ensureIndex(
|
||||
new SearchIndex("vector_index").path("plot_embedding").dimensions(1536).similarity("cosine"));
|
||||
System.out.println(s);
|
||||
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("vector_index").path("plot_embedding")
|
||||
.vectors(vectors).limit(10).numCandidates(150).searchScore();
|
||||
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"));
|
||||
|
||||
@@ -79,7 +78,7 @@ public class VectorSearchTests {
|
||||
aggregate.forEach(System.out::println);
|
||||
}
|
||||
|
||||
static Double[] vectors = { -0.0016261312, -0.028070757, -0.011342932, -0.012775794, -0.0027440966, 0.008683807,
|
||||
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,
|
||||
|
||||
@@ -33,6 +33,7 @@ import java.util.function.Function;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.assertj.core.data.Percentage;
|
||||
import org.bson.BsonDouble;
|
||||
import org.bson.BsonUndefined;
|
||||
import org.bson.types.Binary;
|
||||
import org.bson.types.Code;
|
||||
@@ -70,6 +71,7 @@ import org.springframework.data.convert.PropertyValueConverterFactory;
|
||||
import org.springframework.data.convert.ReadingConverter;
|
||||
import org.springframework.data.convert.ValueConverter;
|
||||
import org.springframework.data.convert.WritingConverter;
|
||||
import org.springframework.data.domain.Vector;
|
||||
import org.springframework.data.geo.Box;
|
||||
import org.springframework.data.geo.Circle;
|
||||
import org.springframework.data.geo.Distance;
|
||||
@@ -3328,6 +3330,24 @@ class MappingMongoConverterUnitTests {
|
||||
assertThat(target.id).isEqualTo(source.id);
|
||||
}
|
||||
|
||||
@Test // GH-4706
|
||||
void shouldWriteVectorValues() {
|
||||
|
||||
WithVector source = new WithVector();
|
||||
source.embeddings = Vector.of(1.1d, 2.2d, 3.3d);
|
||||
|
||||
org.bson.Document document = write(source);
|
||||
assertThat(document.getList("embeddings", BsonDouble.class)).hasSize(3);
|
||||
}
|
||||
|
||||
@Test // GH-4706
|
||||
void shouldReadVectorValues() {
|
||||
|
||||
org.bson.Document document = new org.bson.Document("embeddings", List.of(1.1d, 2.2d, 3.3d));
|
||||
WithVector withVector = converter.read(WithVector.class, document);
|
||||
assertThat(withVector.embeddings.toDoubleArray()).contains(1.1d, 2.2d, 3.3d);
|
||||
}
|
||||
|
||||
org.bson.Document write(Object source) {
|
||||
|
||||
org.bson.Document target = new org.bson.Document();
|
||||
@@ -3335,6 +3355,11 @@ class MappingMongoConverterUnitTests {
|
||||
return target;
|
||||
}
|
||||
|
||||
static class WithVector {
|
||||
|
||||
Vector embeddings;
|
||||
}
|
||||
|
||||
static class GenericType<T> {
|
||||
T content;
|
||||
}
|
||||
|
||||
@@ -23,17 +23,22 @@ import java.util.Arrays;
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.bson.BinaryVector;
|
||||
import org.bson.types.Binary;
|
||||
import org.bson.types.ObjectId;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.springframework.data.annotation.Id;
|
||||
import org.springframework.data.domain.Vector;
|
||||
import org.springframework.data.mongodb.core.mapping.Document;
|
||||
import org.springframework.data.mongodb.core.mapping.MongoVector;
|
||||
import org.springframework.data.mongodb.core.query.Criteria;
|
||||
import org.springframework.data.mongodb.core.query.Query;
|
||||
import org.springframework.data.mongodb.test.util.MongoTemplateExtension;
|
||||
import org.springframework.data.mongodb.test.util.MongoTestTemplate;
|
||||
import org.springframework.data.mongodb.test.util.Template;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
|
||||
/**
|
||||
* Integration tests for {@link MongoConverters}.
|
||||
@@ -101,6 +106,78 @@ public class MongoConvertersIntegrationTests {
|
||||
assertThat(template.findOne(query(where("id").is(wbd.id)), WithBinaryDataType.class)).isEqualTo(wbd);
|
||||
}
|
||||
|
||||
@Test // GH-4706
|
||||
public void shouldReadAndWriteVectors() {
|
||||
|
||||
WithVectors source = new WithVectors();
|
||||
source.vector = Vector.of(1.1, 2.2, 3.3);
|
||||
|
||||
template.save(source);
|
||||
|
||||
WithVectors loaded = template.findOne(query(where("id").is(source.id)), WithVectors.class);
|
||||
assertThat(loaded).isEqualTo(source);
|
||||
}
|
||||
|
||||
@Test // GH-4706
|
||||
public void shouldReadAndWriteFloatVectors() {
|
||||
|
||||
WithVectors source = new WithVectors();
|
||||
source.vector = Vector.of(1.1f, 2.2f, 3.3f);
|
||||
|
||||
template.save(source);
|
||||
|
||||
WithVectors loaded = template.findOne(query(where("id").is(source.id)), WithVectors.class);
|
||||
|
||||
// top-level arrays are converted into doubles by MongoDB with all their conversion imprecisions
|
||||
assertThat(loaded.vector.getClass().getName()).contains("DoubleVector");
|
||||
assertThat(loaded.vector).isNotEqualTo(source.vector);
|
||||
}
|
||||
|
||||
@Test // GH-4706
|
||||
public void shouldReadAndWriteBinFloat32Vectors() {
|
||||
|
||||
WithVectors source = new WithVectors();
|
||||
source.binVector = BinaryVector.floatVector(new float[] { 1.1f, 2.2f, 3.3f });
|
||||
source.vector = MongoVector.of(source.binVector);
|
||||
|
||||
template.save(source);
|
||||
|
||||
WithVectors loaded = template.findOne(query(where("id").is(source.id)), WithVectors.class);
|
||||
|
||||
assertThat(loaded.vector).isEqualTo(source.vector);
|
||||
assertThat(loaded.binVector).isEqualTo(source.binVector);
|
||||
}
|
||||
|
||||
@Test // GH-4706
|
||||
public void shouldReadAndWriteBinInt8Vectors() {
|
||||
|
||||
WithVectors source = new WithVectors();
|
||||
source.binVector = BinaryVector.int8Vector(new byte[] { 1, 2, 3 });
|
||||
source.vector = MongoVector.of(source.binVector);
|
||||
|
||||
template.save(source);
|
||||
|
||||
WithVectors loaded = template.findOne(query(where("id").is(source.id)), WithVectors.class);
|
||||
|
||||
assertThat(loaded.vector).isEqualTo(source.vector);
|
||||
assertThat(loaded.binVector).isEqualTo(source.binVector);
|
||||
}
|
||||
|
||||
@Test // GH-4706
|
||||
public void shouldReadAndWriteBinPackedVectors() {
|
||||
|
||||
WithVectors source = new WithVectors();
|
||||
source.binVector = BinaryVector.packedBitVector(new byte[] { 1, 2, 3 }, (byte) 1);
|
||||
source.vector = MongoVector.of(source.binVector);
|
||||
|
||||
template.save(source);
|
||||
|
||||
WithVectors loaded = template.findOne(query(where("id").is(source.id)), WithVectors.class);
|
||||
|
||||
assertThat(loaded.vector).isEqualTo(source.vector);
|
||||
assertThat(loaded.binVector).isEqualTo(source.binVector);
|
||||
}
|
||||
|
||||
@Document(COLLECTION)
|
||||
static class Wrapper {
|
||||
|
||||
@@ -108,6 +185,33 @@ public class MongoConvertersIntegrationTests {
|
||||
UUID uuid;
|
||||
}
|
||||
|
||||
@Document(COLLECTION)
|
||||
static class WithVectors {
|
||||
|
||||
ObjectId id;
|
||||
Vector vector;
|
||||
BinaryVector binVector;
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (!(o instanceof WithVectors that)) {
|
||||
return false;
|
||||
}
|
||||
if (!ObjectUtils.nullSafeEquals(id, that.id)) {
|
||||
return false;
|
||||
}
|
||||
if (!ObjectUtils.nullSafeEquals(vector, that.vector)) {
|
||||
return false;
|
||||
}
|
||||
return ObjectUtils.nullSafeEquals(binVector, that.binVector);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return ObjectUtils.nullSafeHash(id, vector, binVector);
|
||||
}
|
||||
}
|
||||
|
||||
@Document(COLLECTION)
|
||||
static class WithBinaryDataInArray {
|
||||
|
||||
|
||||
@@ -15,9 +15,8 @@
|
||||
*/
|
||||
package org.springframework.data.mongodb.core.index;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.*;
|
||||
import static org.springframework.data.mongodb.test.util.Assertions.assertThatExceptionOfType;
|
||||
import static org.springframework.data.mongodb.test.util.Assertions.*;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
@@ -32,6 +31,7 @@ import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.Suite;
|
||||
import org.junit.runners.Suite.SuiteClasses;
|
||||
|
||||
import org.springframework.core.annotation.AliasFor;
|
||||
import org.springframework.dao.InvalidDataAccessApiUsageException;
|
||||
import org.springframework.data.annotation.Id;
|
||||
@@ -328,7 +328,8 @@ public class MongoPersistentEntityIndexResolverUnitTests {
|
||||
|
||||
class IndexOnLevelZeroWithExplicityNamedField {
|
||||
|
||||
@Indexed @Field("customFieldName") String namedProperty;
|
||||
@Indexed
|
||||
@Field("customFieldName") String namedProperty;
|
||||
}
|
||||
|
||||
@Document
|
||||
@@ -441,7 +442,8 @@ public class MongoPersistentEntityIndexResolverUnitTests {
|
||||
|
||||
@Document
|
||||
class IndexOnMetaAnnotatedField {
|
||||
@Field("_name") @IndexedFieldAnnotation String lastname;
|
||||
@Field("_name")
|
||||
@IndexedFieldAnnotation String lastname;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -839,7 +841,8 @@ public class MongoPersistentEntityIndexResolverUnitTests {
|
||||
class WithCompoundCollationFromDocument {}
|
||||
|
||||
@Document(collation = "{'locale': 'en_US', 'strength': 2}")
|
||||
@CompoundIndex(name = "compound_index_with_collation", def = "{'foo': 1}", collation = "#{{ 'locale' : 'de' + '_' + 'AT' }}")
|
||||
@CompoundIndex(name = "compound_index_with_collation", def = "{'foo': 1}",
|
||||
collation = "#{{ 'locale' : 'de' + '_' + 'AT' }}")
|
||||
class WithEvaluatedCollationFromCompoundIndex {}
|
||||
}
|
||||
|
||||
@@ -1474,9 +1477,9 @@ public class MongoPersistentEntityIndexResolverUnitTests {
|
||||
WithCollationFromIndexedAnnotation.class);
|
||||
|
||||
IndexDefinition indexDefinition = indexDefinitions.get(0).getIndexDefinition();
|
||||
assertThat(indexDefinition.getIndexOptions()).isEqualTo(new org.bson.Document().append("name", "value")
|
||||
.append("unique", true)
|
||||
.append("collation", new org.bson.Document().append("locale", "en_US").append("strength", 2)));
|
||||
assertThat(indexDefinition.getIndexOptions())
|
||||
.isEqualTo(new org.bson.Document().append("name", "value").append("unique", true).append("collation",
|
||||
new org.bson.Document().append("locale", "en_US").append("strength", 2)));
|
||||
}
|
||||
|
||||
@Test // GH-3002
|
||||
@@ -1486,9 +1489,9 @@ public class MongoPersistentEntityIndexResolverUnitTests {
|
||||
WithCollationFromDocumentAnnotation.class);
|
||||
|
||||
IndexDefinition indexDefinition = indexDefinitions.get(0).getIndexDefinition();
|
||||
assertThat(indexDefinition.getIndexOptions()).isEqualTo(new org.bson.Document().append("name", "value")
|
||||
.append("unique", true)
|
||||
.append("collation", new org.bson.Document().append("locale", "en_US").append("strength", 2)));
|
||||
assertThat(indexDefinition.getIndexOptions())
|
||||
.isEqualTo(new org.bson.Document().append("name", "value").append("unique", true).append("collation",
|
||||
new org.bson.Document().append("locale", "en_US").append("strength", 2)));
|
||||
}
|
||||
|
||||
@Test // GH-3002
|
||||
@@ -1591,7 +1594,8 @@ public class MongoPersistentEntityIndexResolverUnitTests {
|
||||
@Document
|
||||
class SimilarityHolingBean {
|
||||
|
||||
@Indexed @Field("norm") String normalProperty;
|
||||
@Indexed
|
||||
@Field("norm") String normalProperty;
|
||||
@Field("similarityL") private List<SimilaritySibling> listOfSimilarilyNamedEntities = null;
|
||||
}
|
||||
|
||||
@@ -1754,7 +1758,8 @@ public class MongoPersistentEntityIndexResolverUnitTests {
|
||||
@Document
|
||||
class WithHashedIndexOnId {
|
||||
|
||||
@HashIndexed @Id String id;
|
||||
@HashIndexed
|
||||
@Id String id;
|
||||
}
|
||||
|
||||
@Document
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
/*
|
||||
* 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.awaitility.Awaitility.*;
|
||||
import static org.springframework.data.mongodb.test.util.Assertions.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.bson.Document;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
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.MongoTestTemplate;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
import com.mongodb.client.AggregateIterable;
|
||||
|
||||
/**
|
||||
* Integration tests for vector index creation.
|
||||
*
|
||||
* @author Christoph Strobl
|
||||
* @author Mark Paluch
|
||||
*/
|
||||
@EnableIfVectorSearchAvailable
|
||||
class VectorIndexIntegrationTests {
|
||||
|
||||
MongoTestTemplate template = new MongoTestTemplate(cfg -> {
|
||||
cfg.configureMappingContext(ctx -> {
|
||||
ctx.initialEntitySet(Movie.class);
|
||||
});
|
||||
});
|
||||
|
||||
SearchIndexOperations indexOps;
|
||||
|
||||
@BeforeEach
|
||||
void init() {
|
||||
template.createCollection(Movie.class);
|
||||
indexOps = template.searchIndexOps(Movie.class);
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void cleanup() {
|
||||
|
||||
template.searchIndexOps(Movie.class).dropAllIndexes();
|
||||
template.dropCollection(Movie.class);
|
||||
}
|
||||
|
||||
@ParameterizedTest // GH-4706
|
||||
@ValueSource(strings = { "euclidean", "cosine", "dotProduct" })
|
||||
void createsSimpleVectorIndex(String similarityFunction) throws InterruptedException {
|
||||
|
||||
VectorIndex idx = new VectorIndex("vector_index").addVector("plotEmbedding",
|
||||
builder -> builder.dimensions(1536).similarity(similarityFunction));
|
||||
|
||||
indexOps.ensureIndex(idx);
|
||||
|
||||
await().untilAsserted(() -> {
|
||||
Document raw = readRawIndexInfo(idx.getName());
|
||||
assertThat(raw).containsEntry("name", idx.getName()) //
|
||||
.containsEntry("type", "vectorSearch") //
|
||||
.containsEntry("latestDefinition.fields.[0].type", "vector") //
|
||||
.containsEntry("latestDefinition.fields.[0].path", "plot_embedding") //
|
||||
.containsEntry("latestDefinition.fields.[0].numDimensions", 1536) //
|
||||
.containsEntry("latestDefinition.fields.[0].similarity", similarityFunction); //
|
||||
});
|
||||
}
|
||||
|
||||
@Test // GH-4706
|
||||
void updatesVectorIndex() {
|
||||
|
||||
String indexName = "vector_index";
|
||||
VectorIndex idx = new VectorIndex(indexName).addVector("plotEmbedding",
|
||||
builder -> builder.dimensions(1536).similarity("cosine"));
|
||||
|
||||
indexOps.ensureIndex(idx);
|
||||
|
||||
await().untilAsserted(() -> {
|
||||
Document raw = readRawIndexInfo(idx.getName());
|
||||
assertThat(raw).containsEntry("name", idx.getName()) //
|
||||
.containsEntry("type", "vectorSearch") //
|
||||
.containsEntry("latestDefinition.fields.[0].type", "vector") //
|
||||
.containsEntry("latestDefinition.fields.[0].path", "plot_embedding") //
|
||||
.containsEntry("latestDefinition.fields.[0].numDimensions", 1536) //
|
||||
.containsEntry("latestDefinition.fields.[0].similarity", "cosine"); //
|
||||
});
|
||||
|
||||
VectorIndex updatedIdx = new VectorIndex(indexName).addVector("plotEmbedding",
|
||||
builder -> builder.dimensions(1536).similarity(SimilarityFunction.DOT_PRODUCT));
|
||||
assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> indexOps.updateIndex(idx));
|
||||
}
|
||||
|
||||
@Test // GH-4706
|
||||
void createsVectorIndexWithFilters() {
|
||||
|
||||
VectorIndex idx = new VectorIndex("vector_index")
|
||||
.addVector("plotEmbedding", builder -> builder.dimensions(1536).cosine()).addFilter("description")
|
||||
.addFilter("year");
|
||||
|
||||
indexOps.ensureIndex(idx);
|
||||
|
||||
await().untilAsserted(() -> {
|
||||
Document raw = readRawIndexInfo(idx.getName());
|
||||
assertThat(raw).containsEntry("name", idx.getName()) //
|
||||
.containsEntry("type", "vectorSearch") //
|
||||
.containsEntry("latestDefinition.fields.[0].type", "vector") //
|
||||
.containsEntry("latestDefinition.fields.[1].type", "filter") //
|
||||
.containsEntry("latestDefinition.fields.[1].path", "plot") //
|
||||
.containsEntry("latestDefinition.fields.[2].type", "filter") //
|
||||
.containsEntry("latestDefinition.fields.[2].path", "year"); //
|
||||
});
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private Document readRawIndexInfo(String name) {
|
||||
|
||||
AggregateIterable<Document> indexes = template.execute(Movie.class, collection -> {
|
||||
return collection.aggregate(List.of(new Document("$listSearchIndexes", new Document("name", name))));
|
||||
});
|
||||
|
||||
return indexes.first();
|
||||
}
|
||||
|
||||
static class Movie {
|
||||
|
||||
@Id String id;
|
||||
String title;
|
||||
|
||||
@Field("plot") String description;
|
||||
int year;
|
||||
|
||||
@Field("plot_embedding") Double[] plotEmbedding;
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user