Converted vector operations to search operations.

See #4706
Original pull request: #4882
This commit is contained in:
Marcin Grzejszczak
2024-12-04 16:51:48 +01:00
committed by Mark Paluch
parent 2b93bf385c
commit 1645d6c7e4
11 changed files with 146 additions and 78 deletions

View File

@@ -24,11 +24,9 @@ import org.springframework.dao.DataAccessException;
import org.springframework.data.mongodb.MongoDatabaseFactory;
import org.springframework.data.mongodb.UncategorizedMongoDbException;
import org.springframework.data.mongodb.core.convert.QueryMapper;
import org.springframework.data.mongodb.core.index.DefaultVectorIndexOperations;
import org.springframework.data.mongodb.core.index.IndexDefinition;
import org.springframework.data.mongodb.core.index.IndexInfo;
import org.springframework.data.mongodb.core.index.IndexOperations;
import org.springframework.data.mongodb.core.index.VectorIndexOperations;
import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
@@ -210,11 +208,6 @@ public class DefaultIndexOperations implements IndexOperations {
});
}
@Override
public VectorIndexOperations vectorIndex() {
return new DefaultVectorIndexOperations(mongoOperations, collectionName, type);
}
@Nullable
public <T> T execute(CollectionCallback<T> callback) {

View File

@@ -85,10 +85,13 @@ import org.springframework.data.mongodb.core.convert.MongoJsonSchemaMapper;
import org.springframework.data.mongodb.core.convert.MongoWriter;
import org.springframework.data.mongodb.core.convert.QueryMapper;
import org.springframework.data.mongodb.core.convert.UpdateMapper;
import org.springframework.data.mongodb.core.index.DefaultSearchIndexOperations;
import org.springframework.data.mongodb.core.index.IndexOperations;
import org.springframework.data.mongodb.core.index.IndexOperationsProvider;
import org.springframework.data.mongodb.core.index.MongoMappingEventPublisher;
import org.springframework.data.mongodb.core.index.MongoPersistentEntityIndexCreator;
import org.springframework.data.mongodb.core.index.SearchIndexOperations;
import org.springframework.data.mongodb.core.index.SearchIndexOperationsProvider;
import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
@@ -183,7 +186,7 @@ import com.mongodb.client.result.UpdateResult;
* @author Jakub Zurawa
*/
public class MongoTemplate
implements MongoOperations, ApplicationContextAware, IndexOperationsProvider, ReadPreferenceAware {
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;
@@ -3010,6 +3013,21 @@ 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
/**

View File

@@ -26,7 +26,7 @@ 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.VectorIndex.Filter;
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.lang.Nullable;
@@ -34,15 +34,15 @@ import org.springframework.lang.Nullable;
/**
* @author Christoph Strobl
*/
public class DefaultVectorIndexOperations extends DefaultIndexOperations implements VectorIndexOperations {
public class DefaultSearchIndexOperations extends DefaultIndexOperations implements SearchIndexOperations {
private static final Log LOGGER = LogFactory.getLog(VectorIndexOperations.class);
private static final Log LOGGER = LogFactory.getLog(SearchIndexOperations.class);
public DefaultVectorIndexOperations(MongoOperations mongoOperations, Class<?> type) {
public DefaultSearchIndexOperations(MongoOperations mongoOperations, Class<?> type) {
this(mongoOperations, mongoOperations.getCollectionName(type), type);
}
public DefaultVectorIndexOperations(MongoOperations mongoOperations, String collectionName, @Nullable Class<?> type) {
public DefaultSearchIndexOperations(MongoOperations mongoOperations, String collectionName, @Nullable Class<?> type) {
super(mongoOperations, collectionName, type);
}
@@ -62,7 +62,7 @@ public class DefaultVectorIndexOperations extends DefaultIndexOperations impleme
}
@Override
public void updateIndex(VectorIndex index) {
public void updateIndex(SearchIndex index) {
MongoPersistentEntity<?> entity = lookupPersistentEntity(type, collectionName);
@@ -106,10 +106,10 @@ public class DefaultVectorIndexOperations extends DefaultIndexOperations impleme
}
@Override
public String ensureIndex(IndexDefinition indexDefinition) {
public String ensureIndex(SearchIndexDefinition indexDefinition) {
if (!(indexDefinition instanceof VectorIndex vsi)) {
return super.ensureIndex(indexDefinition);
if (!(indexDefinition instanceof SearchIndex vsi)) {
throw new IllegalStateException("Index definitions must be of type VectorIndex");
}
MongoPersistentEntity<?> entity = lookupPersistentEntity(type, collectionName);
@@ -129,7 +129,7 @@ public class DefaultVectorIndexOperations extends DefaultIndexOperations impleme
}
@NonNull
private Document createIndexDocument(VectorIndex vsi, MongoPersistentEntity<?> entity) {
private Document createIndexDocument(SearchIndex vsi, MongoPersistentEntity<?> entity) {
Document index = new Document(vsi.getIndexOptions());
Document definition = new Document();

View File

@@ -25,7 +25,7 @@ import java.util.List;
* @author Christoph Strobl
* @author Jens Schauder
*/
public interface IndexOperations extends VectorIndexOperationsProvider {
public interface IndexOperations {
/**
* Ensure that an index for the provided {@link IndexDefinition} exists for the collection indicated by the entity

View File

@@ -40,11 +40,6 @@ public interface IndexOperationsAdapter extends IndexOperations {
return new IndexOperationsAdapter() {
@Override
public VectorIndexOperations vectorIndex() {
throw new IllegalStateException("currently not supported");
}
@Override
public String ensureIndex(IndexDefinition indexDefinition) {
return reactiveIndexOperations.ensureIndex(indexDefinition).block();

View File

@@ -44,57 +44,58 @@ import org.bson.Document;
*
* @author Christoph Strobl
*/
public class VectorIndex implements IndexDefinition {
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 VectorIndex} instance.
* Create a new {@link SearchIndex} instance.
*
* @param name The name of the index.
*/
public VectorIndex(String name) {
public SearchIndex(String name) {
this.name = name;
}
/**
* Create a new {@link VectorIndex} instance using similarity based on the angle between vectors.
* 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 VectorIndex}.
* @return new instance of {@link SearchIndex}.
*/
public static VectorIndex cosine(String name) {
public static SearchIndex cosine(String name) {
VectorIndex idx = new VectorIndex(name);
SearchIndex idx = new SearchIndex(name);
return idx.similarity(SimilarityFunction.COSINE);
}
/**
* Create a new {@link VectorIndex} instance using similarity based the distance between vector ends.
* 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 VectorIndex}.
* @return new instance of {@link SearchIndex}.
*/
public static VectorIndex euclidean(String name) {
public static SearchIndex euclidean(String name) {
VectorIndex idx = new VectorIndex(name);
SearchIndex idx = new SearchIndex(name);
return idx.similarity(SimilarityFunction.EUCLIDEAN);
}
/**
* Create a new {@link VectorIndex} instance using similarity based on based on both angle and magnitude of the
* 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 VectorIndex}.
* @return new instance of {@link SearchIndex}.
*/
public static VectorIndex dotProduct(String name) {
public static SearchIndex dotProduct(String name) {
VectorIndex idx = new VectorIndex(name);
SearchIndex idx = new SearchIndex(name);
return idx.similarity(SimilarityFunction.DOT_PRODUCT);
}
@@ -104,7 +105,7 @@ public class VectorIndex implements IndexDefinition {
* @param path The path using dot notation.
* @return this.
*/
public VectorIndex path(String path) {
public SearchIndex path(String path) {
this.path = path;
return this;
@@ -116,7 +117,7 @@ public class VectorIndex implements IndexDefinition {
* @param dimensions value between {@code 0} and {@code 4096}.
* @return this.
*/
public VectorIndex dimensions(int dimensions) {
public SearchIndex dimensions(int dimensions) {
this.dimensions = dimensions;
return this;
}
@@ -129,7 +130,7 @@ public class VectorIndex implements IndexDefinition {
* @see SimilarityFunction
* @see #similarity(SimilarityFunction)
*/
public VectorIndex similarity(String similarity) {
public SearchIndex similarity(String similarity) {
this.similarity = similarity;
return this;
}
@@ -140,17 +141,41 @@ public class VectorIndex implements IndexDefinition {
* @param similarity must not be {@literal null}.
* @return this.
*/
public VectorIndex similarity(SimilarityFunction similarity) {
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 VectorIndex filter(Filter filter) {
public SearchIndex filter(Filter filter) {
if (this.filters == null) {
this.filters = new ArrayList<>(3);
@@ -167,21 +192,10 @@ public class VectorIndex implements IndexDefinition {
* @return this.
* @see #filter(Filter)
*/
public VectorIndex filter(String path) {
public SearchIndex filter(String path) {
return filter(new Filter(path));
}
@Override
public Document getIndexKeys() {
// List<Document> fields = new ArrayList<>(filters.size()+1);
// fields.
// needs to be wrapped in new Document("definition", before sending to server
// return new Document("fields", fields);
return new Document();
}
@Override
public Document getIndexOptions() {
return new Document("name", name).append("type", "vectorSearch");
@@ -224,4 +238,18 @@ public class VectorIndex implements IndexDefinition {
return functionName;
}
}
public enum Quantization {
NONE("none"), SCALAR("scalar"), BINARY("binary");
String quantizationName;
Quantization(String quantizationName) {
this.quantizationName = quantizationName;
}
public String getQuantizationName() {
return quantizationName;
}
}
}

View File

@@ -0,0 +1,31 @@
/*
* Copyright 2011-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.mongodb.core.index;
import org.bson.Document;
/**
* @author Marcin Grzejszczak
*/
public interface SearchIndexDefinition {
/**
* Get the index properties such as {@literal unique},...
*
* @return never {@literal null}.
*/
Document getIndexOptions();
}

View File

@@ -20,11 +20,11 @@ import java.util.List;
/**
* @author Christoph Strobl
*/
public interface VectorIndexOperations {
public interface SearchIndexOperations {
String ensureIndex(IndexDefinition indexDefinition);
String ensureIndex(SearchIndexDefinition indexDefinition);
void updateIndex(VectorIndex index);
void updateIndex(SearchIndex index);
boolean exists(String indexName);

View File

@@ -18,8 +18,11 @@ package org.springframework.data.mongodb.core.index;
/**
* @author Christoph Strobl
*/
public interface VectorIndexOperationsProvider {
public interface SearchIndexOperationsProvider {
VectorIndexOperations vectorIndex();
SearchIndexOperations searchIndexOps(String collectionName);
SearchIndexOperations searchIndexOps(Class<?> type);
SearchIndexOperations searchIndexOps(Class<?> type, String collectionName);
}

View File

@@ -43,10 +43,10 @@ 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.DefaultVectorIndexOperations;
import org.springframework.data.mongodb.core.index.VectorIndex;
import org.springframework.data.mongodb.core.index.VectorIndex.SimilarityFunction;
import org.springframework.data.mongodb.core.index.VectorIndexOperations;
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;
@@ -59,7 +59,7 @@ import com.mongodb.client.AggregateIterable;
* @author Christoph Strobl
*/
@EnableIfVectorSearchAvailable
class DefaultVectorIndexOperationsTests {
class DefaultSearchIndexOperationsTests {
MongoTestTemplate template = new MongoTestTemplate(cfg -> {
cfg.configureMappingContext(ctx -> {
@@ -67,7 +67,7 @@ class DefaultVectorIndexOperationsTests {
});
});
VectorIndexOperations indexOps;
SearchIndexOperations indexOps;
@BeforeEach
void init() throws InterruptedException {
@@ -276,13 +276,13 @@ class DefaultVectorIndexOperationsTests {
Thread.sleep(5000);
indexOps = new DefaultVectorIndexOperations(template, Movie.class);
indexOps = new DefaultSearchIndexOperations(template, Movie.class);
}
@AfterEach
void cleanup() {
template.indexOps(Movie.class).vectorIndex().dropIndex("vector_index");
template.searchIndexOps(Movie.class).dropIndex("vector_index");
template.dropCollection(Movie.class);
}
@@ -290,7 +290,7 @@ class DefaultVectorIndexOperationsTests {
@ValueSource(strings = { "euclidean", "cosine", "dotProduct" })
void createsSimpleVectorIndex(String similarityFunction) throws InterruptedException {
VectorIndex idx = new VectorIndex("vector_index").dimensions(1536).path("plotEmbedding")
SearchIndex idx = new SearchIndex("vector_index").dimensions(1536).path("plotEmbedding")
.similarity(similarityFunction);
indexOps.ensureIndex(idx);
@@ -313,7 +313,7 @@ class DefaultVectorIndexOperationsTests {
""")
void updatesVectorIndex() throws InterruptedException {
VectorIndex idx = new VectorIndex("vector_index").dimensions(1536).path("plotEmbedding").similarity("cosine");
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
@@ -342,7 +342,7 @@ class DefaultVectorIndexOperationsTests {
@Test
void createsVectorIndexWithFilters() throws InterruptedException {
VectorIndex idx = VectorIndex.cosine("vector_index").dimensions(1536).path("plotEmbedding") //
SearchIndex idx = SearchIndex.cosine("vector_index").dimensions(1536).path("plotEmbedding") //
.filter("description") //
.filter("year");

View File

@@ -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.VectorIndex;
import org.springframework.data.mongodb.core.index.SearchIndex;
import org.springframework.data.mongodb.test.util.EnableIfVectorSearchAvailable;
import org.springframework.data.mongodb.test.util.MongoTemplateExtension;
import org.springframework.data.mongodb.test.util.MongoTestTemplate;
@@ -53,19 +53,19 @@ public class VectorSearchTests {
// return false;
// });
boolean hasIndex = template.indexOps(COLLECTION_NAME).vectorIndex().exists("vector_index");
boolean hasIndex = template.searchIndexOps(COLLECTION_NAME).exists("vector_index");
if(hasIndex) {
System.out.println("found the index: vector_index");
System.out.println(template.indexOps(COLLECTION_NAME).vectorIndex().getIndexInfo());
template.indexOps(COLLECTION_NAME).vectorIndex().updateIndex(new VectorIndex("vector_index").path("plot_embedding").dimensions(1536).similarity("euclidean"));
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");
}
else {
System.out.print("Creating index: ");
String s = template.indexOps(COLLECTION_NAME).ensureIndex(
new VectorIndex("vector_index").path("plot_embedding").dimensions(1536).similarity("cosine"));
String s = template.searchIndexOps(COLLECTION_NAME).ensureIndex(
new SearchIndex("vector_index").path("plot_embedding").dimensions(1536).similarity("cosine"));
System.out.println(s);
}