BREAKING CHANGE - Change vector store initialize-schema to false

* Change default schema initialization of vector stores from `true` to `false.`
  Users need to explicitly opt-in for schema initialization by setting the
  `initialize-schema` property on the corresponding vector store.
* Update integration tests
* Update docs

Fixes #907
This commit is contained in:
Soby Chacko
2024-06-21 12:37:36 -04:00
committed by Mark Pollack
parent edf943ec97
commit 50d34b8a48
57 changed files with 329 additions and 167 deletions

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2023 - 2024 the original author or authors.
* Copyright 2023-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.
@@ -21,7 +21,6 @@ import co.elastic.clients.elasticsearch.core.BulkResponse;
import co.elastic.clients.elasticsearch.core.SearchResponse;
import co.elastic.clients.elasticsearch.core.bulk.BulkResponseItem;
import co.elastic.clients.elasticsearch.core.search.Hit;
import co.elastic.clients.elasticsearch.indices.CreateIndexResponse;
import co.elastic.clients.json.jackson.JacksonJsonpMapper;
import co.elastic.clients.transport.rest_client.RestClientTransport;
import com.fasterxml.jackson.databind.DeserializationFeature;
@@ -57,6 +56,7 @@ import static org.springframework.ai.vectorstore.SimilarityFunction.l2_norm;
* @author Jemin Huh
* @author Wei Jiang
* @author Laura Trotta
* @author Soby Chacko
* @since 1.0.0
*/
public class ElasticsearchVectorStore implements VectorStore, InitializingBean {
@@ -98,12 +98,15 @@ public class ElasticsearchVectorStore implements VectorStore, InitializingBean {
logger.debug("Calling EmbeddingModel for document id = " + document.getId());
document.setEmbedding(this.embeddingModel.embed(document));
}
bulkRequestBuilder.operations(op -> op
.index(idx -> idx.index(this.options.getIndexName()).id(document.getId()).document(document)));
// We call operations on BulkRequest.Builder only if the index exists.
// For the index to be present, either it must be pre-created or set the
// initializeSchema to true.
if (indexExists()) {
bulkRequestBuilder.operations(op -> op
.index(idx -> idx.index(this.options.getIndexName()).id(document.getId()).document(document)));
}
}
BulkResponse bulkRequest = bulkRequest(bulkRequestBuilder.build());
if (bulkRequest.errors()) {
List<BulkResponseItem> bulkResponseItems = bulkRequest.items();
for (BulkResponseItem bulkResponseItem : bulkResponseItems) {
@@ -117,8 +120,14 @@ public class ElasticsearchVectorStore implements VectorStore, InitializingBean {
@Override
public Optional<Boolean> delete(List<String> idList) {
BulkRequest.Builder bulkRequestBuilder = new BulkRequest.Builder();
for (String id : idList)
bulkRequestBuilder.operations(op -> op.delete(idx -> idx.index(this.options.getIndexName()).id(id)));
// We call operations on BulkRequest.Builder only if the index exists.
// For the index to be present, either it must be pre-created or set the
// initializeSchema to true.
if (indexExists()) {
for (String id : idList) {
bulkRequestBuilder.operations(op -> op.delete(idx -> idx.index(this.options.getIndexName()).id(id)));
}
}
return Optional.of(bulkRequest(bulkRequestBuilder.build()).errors());
}
@@ -201,9 +210,9 @@ public class ElasticsearchVectorStore implements VectorStore, InitializingBean {
}
}
private CreateIndexResponse createIndexMapping() {
private void createIndexMapping() {
try {
return this.elasticsearchClient.indices()
this.elasticsearchClient.indices()
.create(cr -> cr.index(options.getIndexName())
.mappings(map -> map.properties("embedding", p -> p.denseVector(
dv -> dv.similarity(options.getSimilarity().toString()).dims(options.getDimensions())))));
@@ -215,11 +224,9 @@ public class ElasticsearchVectorStore implements VectorStore, InitializingBean {
@Override
public void afterPropertiesSet() {
if (!this.initializeSchema) {
return;
}
if (!indexExists()) {
createIndexMapping();
}

View File

@@ -63,6 +63,34 @@ public class GemFireVectorStore implements VectorStore, InitializingBean {
private static final String DOCUMENT_FIELD = "document";
private final boolean initializeSchema;
/**
* Configures and initializes a GemFireVectorStore instance based on the provided
* configuration.
* @param config the configuration for the GemFireVectorStore
* @param embeddingModel the embedding client used for generating embeddings
*/
public GemFireVectorStore(GemFireVectorStoreConfig config, EmbeddingModel embeddingModel,
boolean initializeSchema) {
Assert.notNull(config, "GemFireVectorStoreConfig must not be null");
Assert.notNull(embeddingModel, "EmbeddingModel must not be null");
this.initializeSchema = initializeSchema;
this.indexName = config.indexName;
this.embeddingModel = embeddingModel;
this.beamWidth = config.beamWidth;
this.maxConnections = config.maxConnections;
this.buckets = config.buckets;
this.vectorSimilarityFunction = config.vectorSimilarityFunction;
this.fields = config.fields;
String base = UriComponentsBuilder.fromUriString(DEFAULT_URI)
.build(config.sslEnabled ? "s" : "", config.host, config.port)
.toString();
this.client = WebClient.create(base);
}
// Create Index Parameters
private String indexName;
@@ -113,11 +141,12 @@ public class GemFireVectorStore implements VectorStore, InitializingBean {
*/
@Override
public void afterPropertiesSet() throws Exception {
if (indexExists()) {
deleteIndex();
if (!this.initializeSchema) {
return;
}
if (!indexExists()) {
createIndex();
}
createIndex();
}
/**
@@ -133,30 +162,6 @@ public class GemFireVectorStore implements VectorStore, InitializingBean {
return client.get().uri("/" + indexName).retrieve().bodyToMono(String.class).onErrorReturn("").block();
}
/**
* Configures and initializes a GemFireVectorStore instance based on the provided
* configuration.
* @param config the configuration for the GemFireVectorStore
* @param embeddingModel the embedding client used for generating embeddings
*/
public GemFireVectorStore(GemFireVectorStoreConfig config, EmbeddingModel embeddingModel) {
Assert.notNull(config, "GemFireVectorStoreConfig must not be null");
Assert.notNull(embeddingModel, "EmbeddingModel must not be null");
this.indexName = config.indexName;
this.embeddingModel = embeddingModel;
this.beamWidth = config.beamWidth;
this.maxConnections = config.maxConnections;
this.buckets = config.buckets;
this.vectorSimilarityFunction = config.vectorSimilarityFunction;
this.fields = config.fields;
String base = UriComponentsBuilder.fromUriString(DEFAULT_URI)
.build(config.sslEnabled ? "s" : "", config.host, config.port)
.toString();
this.client = WebClient.create(base);
}
public static class CreateRequest {
@JsonProperty("name")

View File

@@ -216,7 +216,7 @@ public class GemFireVectorStoreIT {
@Bean
public GemFireVectorStore vectorStore(GemFireVectorStoreConfig config, EmbeddingModel embeddingModel) {
return new GemFireVectorStore(config, embeddingModel);
return new GemFireVectorStore(config, embeddingModel, true);
}
@Bean

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2023 - 2024 the original author or authors.
* Copyright 2023-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.
@@ -45,6 +45,7 @@ import java.util.stream.Collectors;
/**
* @author Jemin Huh
* @author Soby Chacko
* @since 1.0.0
*/
public class OpenSearchVectorStore implements VectorStore, InitializingBean {
@@ -78,16 +79,21 @@ public class OpenSearchVectorStore implements VectorStore, InitializingBean {
private String similarityFunction;
public OpenSearchVectorStore(OpenSearchClient openSearchClient, EmbeddingModel embeddingModel) {
this(openSearchClient, embeddingModel, DEFAULT_MAPPING_EMBEDDING_TYPE_KNN_VECTOR_DIMENSION_1536);
private final boolean initializeSchema;
public OpenSearchVectorStore(OpenSearchClient openSearchClient, EmbeddingModel embeddingModel,
boolean initializeSchema) {
this(openSearchClient, embeddingModel, DEFAULT_MAPPING_EMBEDDING_TYPE_KNN_VECTOR_DIMENSION_1536,
initializeSchema);
}
public OpenSearchVectorStore(OpenSearchClient openSearchClient, EmbeddingModel embeddingModel, String mappingJson) {
this(DEFAULT_INDEX_NAME, openSearchClient, embeddingModel, mappingJson);
public OpenSearchVectorStore(OpenSearchClient openSearchClient, EmbeddingModel embeddingModel, String mappingJson,
boolean initializeSchema) {
this(DEFAULT_INDEX_NAME, openSearchClient, embeddingModel, mappingJson, initializeSchema);
}
public OpenSearchVectorStore(String index, OpenSearchClient openSearchClient, EmbeddingModel embeddingModel,
String mappingJson) {
String mappingJson, boolean initializeSchema) {
Objects.requireNonNull(embeddingModel, "RestClient must not be null");
Objects.requireNonNull(embeddingModel, "EmbeddingModel must not be null");
this.openSearchClient = openSearchClient;
@@ -98,6 +104,7 @@ public class OpenSearchVectorStore implements VectorStore, InitializingBean {
// the potential functions for vector fields at
// https://opensearch.org/docs/latest/search-plugins/knn/approximate-knn/#spaces
this.similarityFunction = COSINE_SIMILARITY_FUNCTION;
this.initializeSchema = initializeSchema;
}
public OpenSearchVectorStore withSimilarityFunction(String similarityFunction) {
@@ -228,8 +235,8 @@ public class OpenSearchVectorStore implements VectorStore, InitializingBean {
@Override
public void afterPropertiesSet() {
if (!exists(this.index)) {
createIndexMapping(this.index, mappingJson);
if (this.initializeSchema && !exists(this.index)) {
createIndexMapping(this.index, this.mappingJson);
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2023 - 2024 the original author or authors.
* Copyright 2023-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.
@@ -54,6 +54,11 @@ import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.hasSize;
/**
* @author Jemin Huh
* @author Soby Chacko
* @since 1.0.0
*/
@Testcontainers
@EnabledIfEnvironmentVariable(named = "OPENAI_API_KEY", matches = ".+")
class OpenSearchVectorStoreIT {
@@ -346,7 +351,7 @@ class OpenSearchVectorStoreIT {
try {
return new OpenSearchVectorStore(new OpenSearchClient(ApacheHttpClient5TransportBuilder
.builder(HttpHost.create(opensearchContainer.getHttpHostAddress()))
.build()), embeddingModel);
.build()), embeddingModel, true);
}
catch (URISyntaxException e) {
throw new RuntimeException(e);

View File

@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.ai.vectorstore;
import java.time.Duration;

View File

@@ -1,3 +1,19 @@
/*
* Copyright 2023-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.ai.vectorstore;
import java.util.HashMap;
@@ -25,6 +41,7 @@ import org.typesense.model.MultiSearchSearchesParameter;
/**
* @author Pablo Sanchidrian Herrera
* @author Soby Chacko
*/
public class TypesenseVectorStore implements VectorStore, InitializingBean {
@@ -56,6 +73,8 @@ public class TypesenseVectorStore implements VectorStore, InitializingBean {
public final FilterExpressionConverter filterExpressionConverter = new TypesenseFilterExpressionConverter();
private final boolean initializeSchema;
public static class TypesenseVectorStoreConfig {
private final String collectionName;
@@ -127,16 +146,18 @@ public class TypesenseVectorStore implements VectorStore, InitializingBean {
}
public TypesenseVectorStore(Client client, EmbeddingModel embeddingModel) {
this(client, embeddingModel, TypesenseVectorStoreConfig.defaultConfig());
this(client, embeddingModel, TypesenseVectorStoreConfig.defaultConfig(), false);
}
public TypesenseVectorStore(Client client, EmbeddingModel embeddingModel, TypesenseVectorStoreConfig config) {
public TypesenseVectorStore(Client client, EmbeddingModel embeddingModel, TypesenseVectorStoreConfig config,
boolean initializeSchema) {
Assert.notNull(client, "Typesense must not be null");
Assert.notNull(embeddingModel, "EmbeddingModel must not be null");
this.client = client;
this.embeddingModel = embeddingModel;
this.config = config;
this.initializeSchema = initializeSchema;
}
@Override
@@ -265,8 +286,10 @@ public class TypesenseVectorStore implements VectorStore, InitializingBean {
// Initialization
// ---------------------------------------------------------------------------------
@Override
public void afterPropertiesSet() throws Exception {
this.createCollection();
public void afterPropertiesSet() {
if (this.initializeSchema) {
this.createCollection();
}
}
private boolean hasCollection() {

View File

@@ -1,3 +1,19 @@
/*
* Copyright 2023-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.ai.vectorstore;
import org.junit.jupiter.api.Test;
@@ -32,6 +48,7 @@ import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Pablo Sanchidrian Herrera
* @author Eddú Meléndez
* @author Soby Chacko
*/
@Testcontainers
public class TypesenseVectorStoreIT {
@@ -59,22 +76,12 @@ public class TypesenseVectorStoreIT {
}
}
private void resetCollection(VectorStore vectorStore) {
((TypesenseVectorStore) vectorStore).dropCollection();
((TypesenseVectorStore) vectorStore).createCollection();
}
@Test
void documentUpdate() {
contextRunner.run(context -> {
VectorStore vectorStore = context.getBean(VectorStore.class);
resetCollection(vectorStore);
Document document = new Document(UUID.randomUUID().toString(), "Spring AI rocks!!",
Collections.singletonMap("meta1", "meta1"));
vectorStore.add(List.of(document));
Map<String, Object> info = ((TypesenseVectorStore) vectorStore).getCollectionInfo();
@@ -112,17 +119,16 @@ public class TypesenseVectorStoreIT {
info = ((TypesenseVectorStore) vectorStore).getCollectionInfo();
assertThat(info.get("num_documents")).isEqualTo(0L);
((TypesenseVectorStore) vectorStore).dropCollection();
});
}
@Test
void addAndSearch() {
contextRunner.run(context -> {
VectorStore vectorStore = context.getBean(VectorStore.class);
resetCollection(vectorStore);
vectorStore.add(documents);
Map<String, Object> info = ((TypesenseVectorStore) vectorStore).getCollectionInfo();
@@ -132,17 +138,16 @@ public class TypesenseVectorStoreIT {
List<Document> results = vectorStore.similaritySearch(SearchRequest.query("Spring"));
assertThat(results).hasSize(3);
((TypesenseVectorStore) vectorStore).dropCollection();
});
}
@Test
void searchWithFilters() {
contextRunner.run(context -> {
VectorStore vectorStore = context.getBean(VectorStore.class);
resetCollection(vectorStore);
var bgDocument = new Document("The World is Big and Salvation Lurks Around the Corner",
Map.of("country", "BG", "year", 2020));
var nlDocument = new Document("The World is Big and Salvation Lurks Around the Corner",
@@ -188,18 +193,17 @@ public class TypesenseVectorStoreIT {
assertThat(results.get(0).getId()).isIn(nlDocument.getId(), bgDocument2.getId());
assertThat(results.get(1).getId()).isIn(nlDocument.getId(), bgDocument2.getId());
((TypesenseVectorStore) vectorStore).dropCollection();
});
}
@Test
void searchWithThreshold() {
contextRunner.run(context -> {
VectorStore vectorStore = context.getBean(VectorStore.class);
resetCollection(vectorStore);
vectorStore.add(documents);
List<Document> fullResult = vectorStore
@@ -221,6 +225,8 @@ public class TypesenseVectorStoreIT {
"Spring AI provides abstractions that serve as the foundation for developing AI applications.");
assertThat(resultDoc.getMetadata()).containsKeys("meta1", "distance");
((TypesenseVectorStore) vectorStore).dropCollection();
});
}
@@ -236,7 +242,7 @@ public class TypesenseVectorStoreIT {
.withEmbeddingDimension(embeddingModel.dimensions())
.build();
return new TypesenseVectorStore(client, embeddingModel, config);
return new TypesenseVectorStore(client, embeddingModel, config, true);
}
@Bean

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2023 - 2024 the original author or authors.
* Copyright 2023-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.
@@ -62,8 +62,9 @@ import org.springframework.util.StringUtils;
* @author Christian Tzolov
* @author Eddú Meléndez
* @author Josh Long
* @author Soby Chacko
*/
public class WeaviateVectorStore implements VectorStore, InitializingBean {
public class WeaviateVectorStore implements VectorStore {
public static final String DOCUMENT_METADATA_DISTANCE_KEY_NAME = "distance";
@@ -281,11 +282,10 @@ public class WeaviateVectorStore implements VectorStore, InitializingBean {
* @param embeddingModel The client for embedding operations.
*/
public WeaviateVectorStore(WeaviateVectorStoreConfig vectorStoreConfig, EmbeddingModel embeddingModel,
WeaviateClient weaviateClient, boolean initializeSchema) {
WeaviateClient weaviateClient) {
Assert.notNull(vectorStoreConfig, "WeaviateVectorStoreConfig must not be null");
Assert.notNull(embeddingModel, "EmbeddingModel must not be null");
this.initializeSchema = initializeSchema;
this.embeddingModel = embeddingModel;
this.consistencyLevel = vectorStoreConfig.consistencyLevel;
this.weaviateObjectClass = vectorStoreConfig.weaviateObjectClass;
@@ -526,37 +526,4 @@ public class WeaviateVectorStore implements VectorStore, InitializingBean {
return doubleList.stream().map(Number::floatValue).toList().toArray(new Float[0]);
}
private final boolean initializeSchema;
@Override
public void afterPropertiesSet() throws Exception {
if (!this.initializeSchema) {
return;
}
Map<String, Object> metadata = new HashMap<>();
if (!CollectionUtils.isEmpty(this.filterMetadataFields)) {
for (MetadataField mf : this.filterMetadataFields) {
switch (mf.type()) {
case TEXT:
metadata.put(mf.name(), "Hello");
break;
case NUMBER:
metadata.put(mf.name(), 3.14);
break;
case BOOLEAN:
metadata.put(mf.name(), true);
break;
default:
break;
}
}
}
var document = new Document("Hello world", metadata);
this.add(List.of(document));
this.delete(List.of(document.getId()));
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2023 - 2024 the original author or authors.
* Copyright 2023-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.
@@ -46,6 +46,7 @@ import io.weaviate.client.WeaviateClient;
/**
* @author Christian Tzolov
* @author Eddú Meléndez
* @author Soby Chacko
*/
@Testcontainers
public class WeaviateVectorStoreIT {
@@ -256,7 +257,7 @@ public class WeaviateVectorStoreIT {
.withConsistencyLevel(WeaviateVectorStoreConfig.ConsistentLevel.ONE)
.build();
return new WeaviateVectorStore(config, embeddingModel, weaviateClient, true);
return new WeaviateVectorStore(config, embeddingModel, weaviateClient);
}