results = vectorStore
+ .similaritySearch(SearchRequest.query("What is Great Depression").withTopK(1));
+
+ assertThat(results).isNotEmpty();
+
+ TestObservationRegistryAssert.assertThat(observationRegistry)
+ .doesNotHaveAnyRemainingCurrentObservation()
+ .hasObservationWithNameEqualTo(DefaultVectorStoreObservationConvention.DEFAULT_NAME)
+ .that()
+ .hasContextualNameEqualTo("vector_store opensearch query")
+ .hasLowCardinalityKeyValue(LowCardinalityKeyNames.DB_OPERATION_NAME.asString(), "query")
+ .hasLowCardinalityKeyValue(LowCardinalityKeyNames.DB_SYSTEM.asString(),
+ VectorStoreProvider.OPENSEARCH.value())
+ .hasLowCardinalityKeyValue(LowCardinalityKeyNames.SPRING_AI_KIND.asString(), "vector_store")
+
+ .hasHighCardinalityKeyValue(HighCardinalityKeyNames.QUERY.asString(), "What is Great Depression")
+ .hasHighCardinalityKeyValue(HighCardinalityKeyNames.DIMENSIONS.asString(), "1536")
+ .hasHighCardinalityKeyValue(HighCardinalityKeyNames.COLLECTION_NAME.asString(), "none")
+ .hasHighCardinalityKeyValue(HighCardinalityKeyNames.NAMESPACE.asString(), "none")
+ .hasHighCardinalityKeyValue(HighCardinalityKeyNames.FIELD_NAME.asString(), "none")
+ .hasHighCardinalityKeyValue(HighCardinalityKeyNames.SIMILARITY_METRIC.asString(), "cosine")
+ .hasHighCardinalityKeyValue(HighCardinalityKeyNames.TOP_K.asString(), "1")
+ .hasHighCardinalityKeyValue(HighCardinalityKeyNames.SIMILARITY_THRESHOLD.asString(), "0.0")
+
+ .hasBeenStarted()
+ .hasBeenStopped();
+
+ observationRegistry.clear();
+
+ vectorStore.delete(documents.stream().map(Document::getId).toList());
+
+ Awaitility.await()
+ .until(() -> vectorStore
+ .similaritySearch(SearchRequest.query("Great Depression").withTopK(1).withSimilarityThreshold(0)),
+ hasSize(0));
+
+ });
+ }
+
+ @SpringBootConfiguration
+ @EnableAutoConfiguration(exclude = { DataSourceAutoConfiguration.class })
+ static class Config {
+
+ @Bean
+ public TestObservationRegistry observationRegistry() {
+ return TestObservationRegistry.create();
+ }
+
+ @Bean
+ public OpenSearchVectorStore vectorStore(EmbeddingModel embeddingModel,
+ ObservationRegistry observationRegistry) {
+ try {
+ return new OpenSearchVectorStore(OpenSearchVectorStore.DEFAULT_INDEX_NAME,
+ new OpenSearchClient(ApacheHttpClient5TransportBuilder
+ .builder(HttpHost.create(opensearchContainer.getHttpHostAddress()))
+ .build()),
+ embeddingModel, OpenSearchVectorStore.DEFAULT_MAPPING_EMBEDDING_TYPE_KNN_VECTOR_DIMENSION_1536,
+ true, observationRegistry, null);
+ }
+ catch (URISyntaxException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Bean
+ public EmbeddingModel embeddingModel() {
+ return new OpenAiEmbeddingModel(new OpenAiApi(System.getenv("OPENAI_API_KEY")));
+ }
+
+ }
+
+}
diff --git a/vector-stores/spring-ai-oracle-store/pom.xml b/vector-stores/spring-ai-oracle-store/pom.xml
index 816bcbfa86..b95d5ee012 100644
--- a/vector-stores/spring-ai-oracle-store/pom.xml
+++ b/vector-stores/spring-ai-oracle-store/pom.xml
@@ -95,7 +95,11 @@
junit-jupiter
test
-
+
+ io.micrometer
+ micrometer-observation-test
+ test
+
diff --git a/vector-stores/spring-ai-oracle-store/src/main/java/org/springframework/ai/vectorstore/OracleVectorStore.java b/vector-stores/spring-ai-oracle-store/src/main/java/org/springframework/ai/vectorstore/OracleVectorStore.java
index d3b8bfd469..118cbabea2 100644
--- a/vector-stores/spring-ai-oracle-store/src/main/java/org/springframework/ai/vectorstore/OracleVectorStore.java
+++ b/vector-stores/spring-ai-oracle-store/src/main/java/org/springframework/ai/vectorstore/OracleVectorStore.java
@@ -15,22 +15,8 @@
*/
package org.springframework.ai.vectorstore;
-import oracle.jdbc.OracleType;
-import oracle.sql.VECTOR;
-import oracle.sql.json.OracleJsonFactory;
-import oracle.sql.json.OracleJsonGenerator;
-import oracle.sql.json.OracleJsonObject;
-import oracle.sql.json.OracleJsonValue;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.springframework.ai.document.Document;
-import org.springframework.ai.embedding.EmbeddingModel;
-import org.springframework.ai.vectorstore.filter.FilterExpressionConverter;
-import org.springframework.beans.factory.InitializingBean;
-import org.springframework.jdbc.core.BatchPreparedStatementSetter;
-import org.springframework.jdbc.core.JdbcTemplate;
-import org.springframework.jdbc.core.RowMapper;
-import org.springframework.util.StringUtils;
+import static org.springframework.ai.vectorstore.OracleVectorStore.OracleVectorStoreDistanceType.DOT;
+import static org.springframework.jdbc.core.StatementCreatorUtils.setParameterValue;
import java.io.ByteArrayOutputStream;
import java.sql.PreparedStatement;
@@ -44,8 +30,30 @@
import java.util.Map;
import java.util.Optional;
-import static org.springframework.ai.vectorstore.OracleVectorStore.OracleVectorStoreDistanceType.DOT;
-import static org.springframework.jdbc.core.StatementCreatorUtils.setParameterValue;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.ai.document.Document;
+import org.springframework.ai.embedding.EmbeddingModel;
+import org.springframework.ai.observation.conventions.VectorStoreProvider;
+import org.springframework.ai.observation.conventions.VectorStoreSimilarityMetric;
+import org.springframework.ai.vectorstore.filter.FilterExpressionConverter;
+import org.springframework.ai.vectorstore.observation.AbstractObservationVectorStore;
+import org.springframework.ai.vectorstore.observation.VectorStoreObservationContext;
+import org.springframework.ai.vectorstore.observation.VectorStoreObservationContext.Builder;
+import org.springframework.ai.vectorstore.observation.VectorStoreObservationConvention;
+import org.springframework.beans.factory.InitializingBean;
+import org.springframework.jdbc.core.BatchPreparedStatementSetter;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.jdbc.core.RowMapper;
+import org.springframework.util.StringUtils;
+
+import io.micrometer.observation.ObservationRegistry;
+import oracle.jdbc.OracleType;
+import oracle.sql.VECTOR;
+import oracle.sql.json.OracleJsonFactory;
+import oracle.sql.json.OracleJsonGenerator;
+import oracle.sql.json.OracleJsonObject;
+import oracle.sql.json.OracleJsonValue;
/**
*
@@ -69,8 +77,9 @@
*
*
* @author Loïc Lefèvre
+ * @author Christian Tzolov
*/
-public class OracleVectorStore implements VectorStore, InitializingBean {
+public class OracleVectorStore extends AbstractObservationVectorStore implements InitializingBean {
private static final Logger logger = LoggerFactory.getLogger(OracleVectorStore.class);
@@ -126,7 +135,7 @@ public enum OracleVectorStoreIndexType {
public enum OracleVectorStoreDistanceType {
/**
- * Default metric. It calculates the cosine distane between two vectors.
+ * Default metric. It calculates the cosine distance between two vectors.
*/
COSINE,
@@ -220,6 +229,18 @@ public OracleVectorStore(JdbcTemplate jdbcTemplate, EmbeddingModel embeddingMode
OracleVectorStoreIndexType indexType, OracleVectorStoreDistanceType distanceType, int dimensions,
int searchAccuracy, boolean initializeSchema, boolean removeExistingVectorStoreTable,
boolean forcedNormalization) {
+ this(jdbcTemplate, embeddingModel, tableName, indexType, distanceType, dimensions, searchAccuracy,
+ initializeSchema, removeExistingVectorStoreTable, forcedNormalization, ObservationRegistry.NOOP, null);
+ }
+
+ public OracleVectorStore(JdbcTemplate jdbcTemplate, EmbeddingModel embeddingModel, String tableName,
+ OracleVectorStoreIndexType indexType, OracleVectorStoreDistanceType distanceType, int dimensions,
+ int searchAccuracy, boolean initializeSchema, boolean removeExistingVectorStoreTable,
+ boolean forcedNormalization, ObservationRegistry observationRegistry,
+ VectorStoreObservationConvention customObservationConvention) {
+
+ super(observationRegistry, customObservationConvention);
+
if (dimensions != DEFAULT_DIMENSIONS) {
if (dimensions <= 0) {
throw new RuntimeException("Number of dimensions must be strictly positive");
@@ -251,7 +272,7 @@ public OracleVectorStore(JdbcTemplate jdbcTemplate, EmbeddingModel embeddingMode
}
@Override
- public void add(final List documents) {
+ public void doAdd(final List documents) {
this.jdbcTemplate.batchUpdate(getIngestStatement(), new BatchPreparedStatementSetter() {
@Override
public void setValues(PreparedStatement ps, int i) throws SQLException {
@@ -366,7 +387,7 @@ private double[] normalize(final double[] v) {
}
@Override
- public Optional delete(final List idList) {
+ public Optional doDelete(final List idList) {
final String sql = String.format("delete from %s where id=?", tableName);
final int[] argTypes = { Types.VARCHAR };
@@ -429,7 +450,7 @@ private List toFloatList(final float[] embeddings) {
}
@Override
- public List similaritySearch(SearchRequest request) {
+ public List doSimilaritySearch(SearchRequest request) {
try {
// From the provided query, generate a vector using the embedding model
final VECTOR embeddingVector = toVECTOR(embeddingModel.embed(request.getQuery()));
@@ -599,4 +620,24 @@ public String getTableName() {
return tableName;
}
+ @Override
+ public Builder createObservationContextBuilder(String operationName) {
+ return VectorStoreObservationContext.builder(VectorStoreProvider.ORACLE.value(), operationName)
+ .withDimensions(this.embeddingModel.dimensions())
+ .withCollectionName(this.getTableName())
+ .withFieldName(getSimilarityMetric());
+ }
+
+ private static Map SIMILARITY_TYPE_MAPPING = Map.of(
+ OracleVectorStoreDistanceType.COSINE, VectorStoreSimilarityMetric.COSINE,
+ OracleVectorStoreDistanceType.EUCLIDEAN, VectorStoreSimilarityMetric.EUCLIDEAN,
+ OracleVectorStoreDistanceType.DOT, VectorStoreSimilarityMetric.DOT);
+
+ private String getSimilarityMetric() {
+ if (!SIMILARITY_TYPE_MAPPING.containsKey(this.distanceType)) {
+ return this.distanceType.name();
+ }
+ return SIMILARITY_TYPE_MAPPING.get(this.distanceType).value();
+ }
+
}
diff --git a/vector-stores/spring-ai-oracle-store/src/test/java/org/springframework/ai/vectorstore/OracleVectorStoreObservationIT.java b/vector-stores/spring-ai-oracle-store/src/test/java/org/springframework/ai/vectorstore/OracleVectorStoreObservationIT.java
new file mode 100644
index 0000000000..ab5397e0c5
--- /dev/null
+++ b/vector-stores/spring-ai-oracle-store/src/test/java/org/springframework/ai/vectorstore/OracleVectorStoreObservationIT.java
@@ -0,0 +1,206 @@
+/*
+ * Copyright 2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.ai.vectorstore;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+import java.util.Map;
+
+import javax.sql.DataSource;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.ai.document.Document;
+import org.springframework.ai.embedding.EmbeddingModel;
+import org.springframework.ai.observation.conventions.VectorStoreProvider;
+import org.springframework.ai.transformers.TransformersEmbeddingModel;
+import org.springframework.ai.vectorstore.OracleVectorStore.OracleVectorStoreDistanceType;
+import org.springframework.ai.vectorstore.observation.DefaultVectorStoreObservationConvention;
+import org.springframework.ai.vectorstore.observation.VectorStoreObservationDocumentation.HighCardinalityKeyNames;
+import org.springframework.ai.vectorstore.observation.VectorStoreObservationDocumentation.LowCardinalityKeyNames;
+import org.springframework.boot.SpringBootConfiguration;
+import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
+import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
+import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.boot.test.context.runner.ApplicationContextRunner;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Primary;
+import org.springframework.core.io.DefaultResourceLoader;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.testcontainers.junit.jupiter.Container;
+import org.testcontainers.junit.jupiter.Testcontainers;
+import org.testcontainers.oracle.OracleContainer;
+import org.testcontainers.utility.MountableFile;
+
+import io.micrometer.observation.ObservationRegistry;
+import io.micrometer.observation.tck.TestObservationRegistry;
+import io.micrometer.observation.tck.TestObservationRegistryAssert;
+import oracle.jdbc.pool.OracleDataSource;
+
+/**
+ * @author Christian Tzolov
+ */
+@Testcontainers
+public class OracleVectorStoreObservationIT {
+
+ @Container
+ static OracleContainer oracle23aiContainer = new OracleContainer("gvenzl/oracle-free:23-slim")
+ .withCopyFileToContainer(MountableFile.forClasspathResource("/initialize.sql"),
+ "/container-entrypoint-initdb.d/initialize.sql");
+
+ private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
+ .withUserConfiguration(Config.class)
+ .withPropertyValues("test.spring.ai.vectorstore.oracle.dimensions=384",
+ // JdbcTemplate configuration
+ String.format("app.datasource.url=%s", oracle23aiContainer.getJdbcUrl()),
+ String.format("app.datasource.username=%s", oracle23aiContainer.getUsername()),
+ String.format("app.datasource.password=%s", oracle23aiContainer.getPassword()),
+ "app.datasource.type=oracle.jdbc.pool.OracleDataSource");
+
+ List documents = List.of(
+ new Document(getText("classpath:/test/data/spring.ai.txt"), Map.of("meta1", "meta1")),
+ new Document(getText("classpath:/test/data/time.shelter.txt")),
+ new Document(getText("classpath:/test/data/great.depression.txt"), Map.of("meta2", "meta2")));
+
+ public static String getText(String uri) {
+ var resource = new DefaultResourceLoader().getResource(uri);
+ try {
+ return resource.getContentAsString(StandardCharsets.UTF_8);
+ }
+ catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private static void dropTable(ApplicationContext context, String tableName) {
+ JdbcTemplate jdbcTemplate = context.getBean(JdbcTemplate.class);
+ jdbcTemplate.execute("DROP TABLE IF EXISTS " + tableName + " PURGE");
+ }
+
+ @Test
+ void observationVectorStoreAddAndQueryOperations() {
+
+ contextRunner.run(context -> {
+
+ VectorStore vectorStore = context.getBean(VectorStore.class);
+
+ TestObservationRegistry observationRegistry = context.getBean(TestObservationRegistry.class);
+
+ vectorStore.add(documents);
+
+ TestObservationRegistryAssert.assertThat(observationRegistry)
+ .doesNotHaveAnyRemainingCurrentObservation()
+ .hasObservationWithNameEqualTo(DefaultVectorStoreObservationConvention.DEFAULT_NAME)
+ .that()
+ .hasContextualNameEqualTo("vector_store oracle add")
+ .hasLowCardinalityKeyValue(LowCardinalityKeyNames.DB_OPERATION_NAME.asString(), "add")
+ .hasLowCardinalityKeyValue(LowCardinalityKeyNames.DB_SYSTEM.asString(),
+ VectorStoreProvider.ORACLE.value())
+ .hasLowCardinalityKeyValue(LowCardinalityKeyNames.SPRING_AI_KIND.asString(), "vector_store")
+ .hasHighCardinalityKeyValue(HighCardinalityKeyNames.QUERY.asString(), "none")
+ .hasHighCardinalityKeyValue(HighCardinalityKeyNames.DIMENSIONS.asString(), "384")
+ .hasHighCardinalityKeyValue(HighCardinalityKeyNames.COLLECTION_NAME.asString(),
+ OracleVectorStore.DEFAULT_TABLE_NAME)
+ .hasHighCardinalityKeyValue(HighCardinalityKeyNames.NAMESPACE.asString(), "none")
+ .hasHighCardinalityKeyValue(HighCardinalityKeyNames.FIELD_NAME.asString(), "none")
+ .hasHighCardinalityKeyValue(HighCardinalityKeyNames.SIMILARITY_METRIC.asString(), "none")
+ .hasHighCardinalityKeyValue(HighCardinalityKeyNames.TOP_K.asString(), "none")
+ .hasHighCardinalityKeyValue(HighCardinalityKeyNames.SIMILARITY_THRESHOLD.asString(), "cosine")
+
+ .hasBeenStarted()
+ .hasBeenStopped();
+
+ observationRegistry.clear();
+
+ List results = vectorStore
+ .similaritySearch(SearchRequest.query("What is Great Depression").withTopK(1));
+
+ assertThat(results).isNotEmpty();
+
+ TestObservationRegistryAssert.assertThat(observationRegistry)
+ .doesNotHaveAnyRemainingCurrentObservation()
+ .hasObservationWithNameEqualTo(DefaultVectorStoreObservationConvention.DEFAULT_NAME)
+ .that()
+ .hasContextualNameEqualTo("vector_store oracle query")
+ .hasLowCardinalityKeyValue(LowCardinalityKeyNames.DB_OPERATION_NAME.asString(), "query")
+ .hasLowCardinalityKeyValue(LowCardinalityKeyNames.DB_SYSTEM.asString(),
+ VectorStoreProvider.ORACLE.value())
+ .hasLowCardinalityKeyValue(LowCardinalityKeyNames.SPRING_AI_KIND.asString(), "vector_store")
+
+ .hasHighCardinalityKeyValue(HighCardinalityKeyNames.QUERY.asString(), "What is Great Depression")
+ .hasHighCardinalityKeyValue(HighCardinalityKeyNames.DIMENSIONS.asString(), "384")
+ .hasHighCardinalityKeyValue(HighCardinalityKeyNames.COLLECTION_NAME.asString(),
+ OracleVectorStore.DEFAULT_TABLE_NAME)
+ .hasHighCardinalityKeyValue(HighCardinalityKeyNames.NAMESPACE.asString(), "none")
+ .hasHighCardinalityKeyValue(HighCardinalityKeyNames.FIELD_NAME.asString(), "none")
+ .hasHighCardinalityKeyValue(HighCardinalityKeyNames.SIMILARITY_METRIC.asString(), "cosine")
+ .hasHighCardinalityKeyValue(HighCardinalityKeyNames.TOP_K.asString(), "1")
+ .hasHighCardinalityKeyValue(HighCardinalityKeyNames.SIMILARITY_THRESHOLD.asString(), "0.0")
+
+ .hasBeenStarted()
+ .hasBeenStopped();
+
+ dropTable(context, ((OracleVectorStore) vectorStore).getTableName());
+
+ });
+ }
+
+ @SpringBootConfiguration
+ @EnableAutoConfiguration(exclude = { DataSourceAutoConfiguration.class })
+ public static class Config {
+
+ @Bean
+ public TestObservationRegistry observationRegistry() {
+ return TestObservationRegistry.create();
+ }
+
+ @Bean
+ public VectorStore vectorStore(JdbcTemplate jdbcTemplate, EmbeddingModel embeddingModel,
+ ObservationRegistry observationRegistry) {
+ return new OracleVectorStore(jdbcTemplate, embeddingModel, OracleVectorStore.DEFAULT_TABLE_NAME,
+ OracleVectorStore.OracleVectorStoreIndexType.IVF, OracleVectorStoreDistanceType.COSINE, 384,
+ OracleVectorStore.DEFAULT_SEARCH_ACCURACY, true, true, true, observationRegistry, null);
+ }
+
+ @Bean
+ public JdbcTemplate myJdbcTemplate(DataSource dataSource) {
+ return new JdbcTemplate(dataSource);
+ }
+
+ @Bean
+ @Primary
+ @ConfigurationProperties("app.datasource")
+ public DataSourceProperties dataSourceProperties() {
+ return new DataSourceProperties();
+ }
+
+ @Bean
+ public OracleDataSource dataSource(DataSourceProperties dataSourceProperties) {
+ return dataSourceProperties.initializeDataSourceBuilder().type(OracleDataSource.class).build();
+ }
+
+ @Bean
+ public EmbeddingModel embeddingModel() {
+ return new TransformersEmbeddingModel();
+ }
+
+ }
+
+}
diff --git a/vector-stores/spring-ai-pgvector-store/src/main/java/org/springframework/ai/vectorstore/PgVectorStore.java b/vector-stores/spring-ai-pgvector-store/src/main/java/org/springframework/ai/vectorstore/PgVectorStore.java
index 00f93e6679..450f20489f 100644
--- a/vector-stores/spring-ai-pgvector-store/src/main/java/org/springframework/ai/vectorstore/PgVectorStore.java
+++ b/vector-stores/spring-ai-pgvector-store/src/main/java/org/springframework/ai/vectorstore/PgVectorStore.java
@@ -139,10 +139,9 @@ private PgVectorStore(String schemaName, String vectorTableName, boolean vectorT
private PgVectorStore(String schemaName, String vectorTableName, boolean vectorTableValidationsEnabled,
JdbcTemplate jdbcTemplate, EmbeddingModel embeddingModel, int dimensions, PgDistanceType distanceType,
boolean removeExistingVectorStoreTable, PgIndexType createIndexMethod, boolean initializeSchema,
- ObservationRegistry observationRegistry,
- VectorStoreObservationConvention customSearchObservationConvention) {
+ ObservationRegistry observationRegistry, VectorStoreObservationConvention customObservationConvention) {
- super(observationRegistry, customSearchObservationConvention);
+ super(observationRegistry, customObservationConvention);
this.vectorTableName = (null == vectorTableName || vectorTableName.isEmpty()) ? DEFAULT_TABLE_NAME
: vectorTableName.trim();
@@ -554,9 +553,8 @@ public Builder withObservationRegistry(ObservationRegistry observationRegistry)
return this;
}
- public Builder withSearchObservationConvention(
- VectorStoreObservationConvention customSearchObservationConvention) {
- this.searchObservationConvention = customSearchObservationConvention;
+ public Builder withSearchObservationConvention(VectorStoreObservationConvention customObservationConvention) {
+ this.searchObservationConvention = customObservationConvention;
return this;
}
diff --git a/vector-stores/spring-ai-pgvector-store/src/test/java/org/springframework/ai/vectorstore/PgVectorObservationIT.java b/vector-stores/spring-ai-pgvector-store/src/test/java/org/springframework/ai/vectorstore/PgVectorObservationIT.java
index 3cc054b9d5..966df742a9 100644
--- a/vector-stores/spring-ai-pgvector-store/src/test/java/org/springframework/ai/vectorstore/PgVectorObservationIT.java
+++ b/vector-stores/spring-ai-pgvector-store/src/test/java/org/springframework/ai/vectorstore/PgVectorObservationIT.java
@@ -122,6 +122,7 @@ void observationVectorStoreAddAndQueryOperations() {
.hasHighCardinalityKeyValue(HighCardinalityKeyNames.FIELD_NAME.asString(), "none")
.hasHighCardinalityKeyValue(HighCardinalityKeyNames.SIMILARITY_METRIC.asString(), "cosine")
.hasHighCardinalityKeyValue(HighCardinalityKeyNames.TOP_K.asString(), "none")
+ .hasHighCardinalityKeyValue(HighCardinalityKeyNames.SIMILARITY_THRESHOLD.asString(), "none")
.hasBeenStarted()
.hasBeenStopped();
@@ -149,6 +150,7 @@ void observationVectorStoreAddAndQueryOperations() {
.hasHighCardinalityKeyValue(HighCardinalityKeyNames.FIELD_NAME.asString(), "none")
.hasHighCardinalityKeyValue(HighCardinalityKeyNames.SIMILARITY_METRIC.asString(), "cosine")
.hasHighCardinalityKeyValue(HighCardinalityKeyNames.TOP_K.asString(), "1")
+ .hasHighCardinalityKeyValue(HighCardinalityKeyNames.SIMILARITY_THRESHOLD.asString(), "0.0")
.hasBeenStarted()
.hasBeenStopped();
diff --git a/vector-stores/spring-ai-pinecone-store/pom.xml b/vector-stores/spring-ai-pinecone-store/pom.xml
index 3021617be7..b1c883453f 100644
--- a/vector-stores/spring-ai-pinecone-store/pom.xml
+++ b/vector-stores/spring-ai-pinecone-store/pom.xml
@@ -101,6 +101,11 @@
test
+
+ io.micrometer
+ micrometer-observation-test
+ test
+
diff --git a/vector-stores/spring-ai-pinecone-store/src/main/java/org/springframework/ai/vectorstore/PineconeVectorStore.java b/vector-stores/spring-ai-pinecone-store/src/main/java/org/springframework/ai/vectorstore/PineconeVectorStore.java
index 8515b7fb13..1d5796a646 100644
--- a/vector-stores/spring-ai-pinecone-store/src/main/java/org/springframework/ai/vectorstore/PineconeVectorStore.java
+++ b/vector-stores/spring-ai-pinecone-store/src/main/java/org/springframework/ai/vectorstore/PineconeVectorStore.java
@@ -21,11 +21,25 @@
import java.util.Map;
import java.util.Optional;
+import org.springframework.ai.document.Document;
+import org.springframework.ai.embedding.EmbeddingModel;
+import org.springframework.ai.model.EmbeddingUtils;
+import org.springframework.ai.observation.conventions.VectorStoreProvider;
+import org.springframework.ai.vectorstore.filter.FilterExpressionConverter;
+import org.springframework.ai.vectorstore.filter.converter.PineconeFilterExpressionConverter;
+import org.springframework.ai.vectorstore.observation.AbstractObservationVectorStore;
+import org.springframework.ai.vectorstore.observation.VectorStoreObservationContext;
+import org.springframework.ai.vectorstore.observation.VectorStoreObservationConvention;
+import org.springframework.util.Assert;
+import org.springframework.util.StringUtils;
+
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.protobuf.Struct;
import com.google.protobuf.Value;
import com.google.protobuf.util.JsonFormat;
+
+import io.micrometer.observation.ObservationRegistry;
import io.pinecone.PineconeClient;
import io.pinecone.PineconeClientConfig;
import io.pinecone.PineconeConnection;
@@ -36,14 +50,6 @@
import io.pinecone.proto.UpsertRequest;
import io.pinecone.proto.Vector;
-import org.springframework.ai.document.Document;
-import org.springframework.ai.embedding.EmbeddingModel;
-import org.springframework.ai.model.EmbeddingUtils;
-import org.springframework.ai.vectorstore.filter.FilterExpressionConverter;
-import org.springframework.ai.vectorstore.filter.converter.PineconeFilterExpressionConverter;
-import org.springframework.util.Assert;
-import org.springframework.util.StringUtils;
-
/**
* A VectorStore implementation backed by Pinecone, a cloud-based vector database. This
* store supports creating, updating, deleting, and similarity searching of documents in a
@@ -52,7 +58,7 @@
* @author Christian Tzolov
* @author Adam Bchouti
*/
-public class PineconeVectorStore implements VectorStore {
+public class PineconeVectorStore extends AbstractObservationVectorStore {
public static final String CONTENT_FIELD_NAME = "document_content";
@@ -252,6 +258,19 @@ public PineconeVectorStoreConfig build() {
* @param embeddingModel The client for embedding operations.
*/
public PineconeVectorStore(PineconeVectorStoreConfig config, EmbeddingModel embeddingModel) {
+ this(config, embeddingModel, ObservationRegistry.NOOP, null);
+ }
+
+ /**
+ * Constructs a new PineconeVectorStore.
+ * @param config The configuration for the store.
+ * @param embeddingModel The client for embedding operations.
+ * @param observationRegistry The registry for observations.
+ * @param customObservationConvention The custom observation convention.
+ */
+ public PineconeVectorStore(PineconeVectorStoreConfig config, EmbeddingModel embeddingModel,
+ ObservationRegistry observationRegistry, VectorStoreObservationConvention customObservationConvention) {
+ super(observationRegistry, customObservationConvention);
Assert.notNull(config, "PineconeVectorStoreConfig must not be null");
Assert.notNull(embeddingModel, "EmbeddingModel must not be null");
@@ -294,7 +313,7 @@ public void add(List documents, String namespace) {
* @param documents The list of documents to be added.
*/
@Override
- public void add(List documents) {
+ public void doAdd(List documents) {
add(documents, this.pineconeNamespace);
}
@@ -352,7 +371,7 @@ public Optional delete(List documentIds, String namespace) {
* @return An optional boolean indicating the deletion status.
*/
@Override
- public Optional delete(List documentIds) {
+ public Optional doDelete(List documentIds) {
return delete(documentIds, this.pineconeNamespace);
}
@@ -390,7 +409,7 @@ public List similaritySearch(SearchRequest request, String namespace)
}
@Override
- public List similaritySearch(SearchRequest request) {
+ public List doSimilaritySearch(SearchRequest request) {
return similaritySearch(request, this.pineconeNamespace);
}
@@ -424,4 +443,13 @@ private Map extractMetadata(Struct metadataStruct) {
}
}
+ @Override
+ public VectorStoreObservationContext.Builder createObservationContextBuilder(String operationName) {
+
+ return VectorStoreObservationContext.builder(VectorStoreProvider.PINECONE.value(), operationName)
+ .withDimensions(this.embeddingModel.dimensions())
+ .withNamespace(this.pineconeNamespace)
+ .withFieldName(this.pineconeContentFieldName);
+ }
+
}
diff --git a/vector-stores/spring-ai-pinecone-store/src/test/java/org/springframework/ai/vectorstore/PineconeVectorStoreObservationIT.java b/vector-stores/spring-ai-pinecone-store/src/test/java/org/springframework/ai/vectorstore/PineconeVectorStoreObservationIT.java
new file mode 100644
index 0000000000..06cc36cea9
--- /dev/null
+++ b/vector-stores/spring-ai-pinecone-store/src/test/java/org/springframework/ai/vectorstore/PineconeVectorStoreObservationIT.java
@@ -0,0 +1,202 @@
+/*
+ * Copyright 2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.ai.vectorstore;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.hamcrest.Matchers.hasSize;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+import org.awaitility.Awaitility;
+import org.awaitility.Duration;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
+import org.springframework.ai.document.Document;
+import org.springframework.ai.embedding.EmbeddingModel;
+import org.springframework.ai.observation.conventions.VectorStoreProvider;
+import org.springframework.ai.transformers.TransformersEmbeddingModel;
+import org.springframework.ai.vectorstore.PineconeVectorStore.PineconeVectorStoreConfig;
+import org.springframework.ai.vectorstore.observation.DefaultVectorStoreObservationConvention;
+import org.springframework.ai.vectorstore.observation.VectorStoreObservationDocumentation.HighCardinalityKeyNames;
+import org.springframework.ai.vectorstore.observation.VectorStoreObservationDocumentation.LowCardinalityKeyNames;
+import org.springframework.boot.SpringBootConfiguration;
+import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
+import org.springframework.boot.test.context.runner.ApplicationContextRunner;
+import org.springframework.context.annotation.Bean;
+import org.springframework.core.io.DefaultResourceLoader;
+
+import io.micrometer.observation.ObservationRegistry;
+import io.micrometer.observation.tck.TestObservationRegistry;
+import io.micrometer.observation.tck.TestObservationRegistryAssert;
+
+/**
+ * @author Christian Tzolov
+ */
+@EnabledIfEnvironmentVariable(named = "PINECONE_API_KEY", matches = ".+")
+public class PineconeVectorStoreObservationIT {
+
+ private static final String PINECONE_ENVIRONMENT = "gcp-starter";
+
+ private static final String PINECONE_PROJECT_ID = "814621f";
+
+ private static final String PINECONE_INDEX_NAME = "spring-ai-test-index";
+
+ // NOTE: Leave it empty as for free tier as later doesn't support namespaces.
+ private static final String PINECONE_NAMESPACE = "";
+
+ private static final String CUSTOM_CONTENT_FIELD_NAME = "article";
+
+ List documents = List.of(
+ new Document(getText("classpath:/test/data/spring.ai.txt"), Map.of("meta1", "meta1")),
+ new Document(getText("classpath:/test/data/time.shelter.txt")),
+ new Document(getText("classpath:/test/data/great.depression.txt"), Map.of("meta2", "meta2")));
+
+ public static String getText(String uri) {
+ var resource = new DefaultResourceLoader().getResource(uri);
+ try {
+ return resource.getContentAsString(StandardCharsets.UTF_8);
+ }
+ catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
+ .withUserConfiguration(Config.class);
+
+ @BeforeAll
+ public static void beforeAll() {
+ Awaitility.setDefaultPollInterval(2, TimeUnit.SECONDS);
+ Awaitility.setDefaultPollDelay(Duration.ZERO);
+ Awaitility.setDefaultTimeout(Duration.ONE_MINUTE);
+ }
+
+ @Test
+ void observationVectorStoreAddAndQueryOperations() {
+
+ contextRunner.run(context -> {
+
+ VectorStore vectorStore = context.getBean(VectorStore.class);
+
+ TestObservationRegistry observationRegistry = context.getBean(TestObservationRegistry.class);
+
+ vectorStore.add(documents);
+
+ TestObservationRegistryAssert.assertThat(observationRegistry)
+ .doesNotHaveAnyRemainingCurrentObservation()
+ .hasObservationWithNameEqualTo(DefaultVectorStoreObservationConvention.DEFAULT_NAME)
+ .that()
+ .hasContextualNameEqualTo("vector_store pinecone add")
+ .hasLowCardinalityKeyValue(LowCardinalityKeyNames.DB_OPERATION_NAME.asString(), "add")
+ .hasLowCardinalityKeyValue(LowCardinalityKeyNames.DB_SYSTEM.asString(),
+ VectorStoreProvider.PINECONE.value())
+ .hasLowCardinalityKeyValue(LowCardinalityKeyNames.SPRING_AI_KIND.asString(), "vector_store")
+ .hasHighCardinalityKeyValue(HighCardinalityKeyNames.QUERY.asString(), "none")
+ .hasHighCardinalityKeyValue(HighCardinalityKeyNames.DIMENSIONS.asString(), "384")
+ .hasHighCardinalityKeyValue(HighCardinalityKeyNames.COLLECTION_NAME.asString(), "none")
+ .hasHighCardinalityKeyValue(HighCardinalityKeyNames.NAMESPACE.asString(), "none")
+ .hasHighCardinalityKeyValue(HighCardinalityKeyNames.FIELD_NAME.asString(), "article")
+ .hasHighCardinalityKeyValue(HighCardinalityKeyNames.SIMILARITY_METRIC.asString(), "none")
+ .hasHighCardinalityKeyValue(HighCardinalityKeyNames.TOP_K.asString(), "none")
+ .hasHighCardinalityKeyValue(HighCardinalityKeyNames.SIMILARITY_THRESHOLD.asString(), "none")
+
+ .hasBeenStarted()
+ .hasBeenStopped();
+
+ Awaitility.await().until(() -> {
+ return vectorStore.similaritySearch(SearchRequest.query("What is Great Depression").withTopK(1));
+ }, hasSize(1));
+
+ observationRegistry.clear();
+
+ List results = vectorStore
+ .similaritySearch(SearchRequest.query("What is Great Depression").withTopK(1));
+
+ assertThat(results).isNotEmpty();
+
+ TestObservationRegistryAssert.assertThat(observationRegistry)
+ .doesNotHaveAnyRemainingCurrentObservation()
+ .hasObservationWithNameEqualTo(DefaultVectorStoreObservationConvention.DEFAULT_NAME)
+ .that()
+ .hasContextualNameEqualTo("vector_store pinecone query")
+ .hasLowCardinalityKeyValue(LowCardinalityKeyNames.DB_OPERATION_NAME.asString(), "query")
+ .hasLowCardinalityKeyValue(LowCardinalityKeyNames.DB_SYSTEM.asString(),
+ VectorStoreProvider.PINECONE.value())
+ .hasLowCardinalityKeyValue(LowCardinalityKeyNames.SPRING_AI_KIND.asString(), "vector_store")
+
+ .hasHighCardinalityKeyValue(HighCardinalityKeyNames.QUERY.asString(), "What is Great Depression")
+ .hasHighCardinalityKeyValue(HighCardinalityKeyNames.DIMENSIONS.asString(), "384")
+ .hasHighCardinalityKeyValue(HighCardinalityKeyNames.COLLECTION_NAME.asString(), "none")
+ .hasHighCardinalityKeyValue(HighCardinalityKeyNames.NAMESPACE.asString(), "none")
+ .hasHighCardinalityKeyValue(HighCardinalityKeyNames.FIELD_NAME.asString(), "article")
+ .hasHighCardinalityKeyValue(HighCardinalityKeyNames.SIMILARITY_METRIC.asString(), "none")
+ .hasHighCardinalityKeyValue(HighCardinalityKeyNames.TOP_K.asString(), "1")
+ .hasHighCardinalityKeyValue(HighCardinalityKeyNames.SIMILARITY_THRESHOLD.asString(), "0.0")
+
+ .hasBeenStarted()
+ .hasBeenStopped();
+
+ // Remove all documents from the store
+ vectorStore.delete(documents.stream().map(doc -> doc.getId()).toList());
+
+ Awaitility.await().until(() -> {
+ return vectorStore.similaritySearch(SearchRequest.query("Hello").withTopK(1));
+ }, hasSize(0));
+
+ });
+ }
+
+ @SpringBootConfiguration
+ @EnableAutoConfiguration
+ public static class Config {
+
+ @Bean
+ public TestObservationRegistry observationRegistry() {
+ return TestObservationRegistry.create();
+ }
+
+ @Bean
+ public PineconeVectorStoreConfig pineconeVectorStoreConfig() {
+
+ return PineconeVectorStoreConfig.builder()
+ .withApiKey(System.getenv("PINECONE_API_KEY"))
+ .withEnvironment(PINECONE_ENVIRONMENT)
+ .withProjectId(PINECONE_PROJECT_ID)
+ .withIndexName(PINECONE_INDEX_NAME)
+ .withNamespace(PINECONE_NAMESPACE)
+ .withContentFieldName(CUSTOM_CONTENT_FIELD_NAME)
+ .build();
+ }
+
+ @Bean
+ public VectorStore vectorStore(PineconeVectorStoreConfig config, EmbeddingModel embeddingModel,
+ ObservationRegistry observationRegistry) {
+ return new PineconeVectorStore(config, embeddingModel, observationRegistry, null);
+ }
+
+ @Bean
+ public EmbeddingModel embeddingModel() {
+ return new TransformersEmbeddingModel();
+ }
+
+ }
+
+}
diff --git a/vector-stores/spring-ai-qdrant-store/pom.xml b/vector-stores/spring-ai-qdrant-store/pom.xml
index df834e266f..f3b5ea9e9c 100644
--- a/vector-stores/spring-ai-qdrant-store/pom.xml
+++ b/vector-stores/spring-ai-qdrant-store/pom.xml
@@ -75,5 +75,18 @@
junit-jupiter
test
+
+
+ org.springframework.ai
+ spring-ai-test
+ ${parent.version}
+ test
+
+
+
+ io.micrometer
+ micrometer-observation-test
+ test
+
diff --git a/vector-stores/spring-ai-qdrant-store/src/main/java/org/springframework/ai/vectorstore/qdrant/QdrantVectorStore.java b/vector-stores/spring-ai-qdrant-store/src/main/java/org/springframework/ai/vectorstore/qdrant/QdrantVectorStore.java
index 184af9dd71..4f45ece4c1 100644
--- a/vector-stores/spring-ai-qdrant-store/src/main/java/org/springframework/ai/vectorstore/qdrant/QdrantVectorStore.java
+++ b/vector-stores/spring-ai-qdrant-store/src/main/java/org/springframework/ai/vectorstore/qdrant/QdrantVectorStore.java
@@ -29,11 +29,16 @@
import org.springframework.ai.document.Document;
import org.springframework.ai.embedding.EmbeddingModel;
import org.springframework.ai.model.EmbeddingUtils;
+import org.springframework.ai.observation.conventions.VectorStoreProvider;
+import org.springframework.ai.observation.conventions.VectorStoreSimilarityMetric;
import org.springframework.ai.vectorstore.SearchRequest;
-import org.springframework.ai.vectorstore.VectorStore;
+import org.springframework.ai.vectorstore.observation.AbstractObservationVectorStore;
+import org.springframework.ai.vectorstore.observation.VectorStoreObservationContext;
+import org.springframework.ai.vectorstore.observation.VectorStoreObservationConvention;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.util.Assert;
+import io.micrometer.observation.ObservationRegistry;
import io.qdrant.client.QdrantClient;
import io.qdrant.client.grpc.Collections.Distance;
import io.qdrant.client.grpc.Collections.VectorParams;
@@ -55,7 +60,7 @@
* @author Josh Long
* @since 0.8.1
*/
-public class QdrantVectorStore implements VectorStore, InitializingBean {
+public class QdrantVectorStore extends AbstractObservationVectorStore implements InitializingBean {
private static final String CONTENT_FIELD_NAME = "doc_content";
@@ -152,9 +157,28 @@ public QdrantVectorStore(QdrantClient qdrantClient, QdrantVectorStoreConfig conf
* @param qdrantClient A {@link QdrantClient} instance for interfacing with Qdrant.
* @param collectionName The name of the collection to use in Qdrant.
* @param embeddingModel The client for embedding operations.
+ * @param initializeSchema A boolean indicating whether to initialize the schema.
*/
public QdrantVectorStore(QdrantClient qdrantClient, String collectionName, EmbeddingModel embeddingModel,
boolean initializeSchema) {
+ this(qdrantClient, collectionName, embeddingModel, initializeSchema, ObservationRegistry.NOOP, null);
+ }
+
+ /**
+ * Constructs a new QdrantVectorStore.
+ * @param qdrantClient A {@link QdrantClient} instance for interfacing with Qdrant.
+ * @param collectionName The name of the collection to use in Qdrant.
+ * @param embeddingModel The client for embedding operations.
+ * @param initializeSchema A boolean indicating whether to initialize the schema.
+ * @param observationRegistry The observation registry to use.
+ * @param customObservationConvention The custom search observation convention to use.
+ */
+ public QdrantVectorStore(QdrantClient qdrantClient, String collectionName, EmbeddingModel embeddingModel,
+ boolean initializeSchema, ObservationRegistry observationRegistry,
+ VectorStoreObservationConvention customObservationConvention) {
+
+ super(observationRegistry, customObservationConvention);
+
Assert.notNull(qdrantClient, "QdrantClient must not be null");
Assert.notNull(collectionName, "collectionName must not be null");
Assert.notNull(embeddingModel, "EmbeddingModel must not be null");
@@ -170,7 +194,7 @@ public QdrantVectorStore(QdrantClient qdrantClient, String collectionName, Embed
* @param documents The list of documents to be added.
*/
@Override
- public void add(List documents) {
+ public void doAdd(List documents) {
try {
List points = documents.stream().map(document -> {
// Compute and assign an embedding to the document.
@@ -196,7 +220,7 @@ public void add(List documents) {
* @return An optional boolean indicating the deletion status.
*/
@Override
- public Optional delete(List documentIds) {
+ public Optional doDelete(List documentIds) {
try {
List ids = documentIds.stream().map(id -> id(UUID.fromString(id))).toList();
var result = this.qdrantClient.deleteAsync(this.collectionName, ids)
@@ -216,7 +240,7 @@ public Optional delete(List documentIds) {
* @return A list of documents that are similar to the query.
*/
@Override
- public List similaritySearch(SearchRequest request) {
+ public List doSimilaritySearch(SearchRequest request) {
try {
Filter filter = (request.getFilterExpression() != null)
? this.filterExpressionConverter.convertExpression(request.getFilterExpression())
@@ -307,4 +331,13 @@ private boolean isCollectionExists() {
}
}
+ @Override
+ public VectorStoreObservationContext.Builder createObservationContextBuilder(String operationName) {
+
+ return VectorStoreObservationContext.builder(VectorStoreProvider.QDRANT.value(), operationName)
+ .withDimensions(this.embeddingModel.dimensions())
+ .withCollectionName(this.collectionName);
+
+ }
+
}
\ No newline at end of file
diff --git a/vector-stores/spring-ai-qdrant-store/src/test/java/org/springframework/ai/vectorstore/qdrant/QdrantVectorStoreObservationIT.java b/vector-stores/spring-ai-qdrant-store/src/test/java/org/springframework/ai/vectorstore/qdrant/QdrantVectorStoreObservationIT.java
new file mode 100644
index 0000000000..4ea5910159
--- /dev/null
+++ b/vector-stores/spring-ai-qdrant-store/src/test/java/org/springframework/ai/vectorstore/qdrant/QdrantVectorStoreObservationIT.java
@@ -0,0 +1,209 @@
+/*
+ * Copyright 2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.ai.vectorstore.qdrant;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ExecutionException;
+
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
+import org.springframework.ai.azure.openai.AzureOpenAiEmbeddingModel;
+import org.springframework.ai.document.Document;
+import org.springframework.ai.embedding.EmbeddingModel;
+import org.springframework.ai.observation.conventions.VectorStoreProvider;
+import org.springframework.ai.vectorstore.SearchRequest;
+import org.springframework.ai.vectorstore.VectorStore;
+import org.springframework.ai.vectorstore.observation.DefaultVectorStoreObservationConvention;
+import org.springframework.ai.vectorstore.observation.VectorStoreObservationDocumentation.HighCardinalityKeyNames;
+import org.springframework.ai.vectorstore.observation.VectorStoreObservationDocumentation.LowCardinalityKeyNames;
+import org.springframework.boot.SpringBootConfiguration;
+import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
+import org.springframework.boot.test.context.runner.ApplicationContextRunner;
+import org.springframework.context.annotation.Bean;
+import org.springframework.core.io.DefaultResourceLoader;
+import org.testcontainers.junit.jupiter.Container;
+import org.testcontainers.junit.jupiter.Testcontainers;
+import org.testcontainers.qdrant.QdrantContainer;
+
+import com.azure.ai.openai.OpenAIClient;
+import com.azure.ai.openai.OpenAIClientBuilder;
+import com.azure.core.credential.AzureKeyCredential;
+
+import io.micrometer.observation.ObservationRegistry;
+import io.micrometer.observation.tck.TestObservationRegistry;
+import io.micrometer.observation.tck.TestObservationRegistryAssert;
+import io.qdrant.client.QdrantClient;
+import io.qdrant.client.QdrantGrpcClient;
+import io.qdrant.client.grpc.Collections.Distance;
+import io.qdrant.client.grpc.Collections.VectorParams;
+
+/**
+ * @author Christian Tzolov
+ */
+@Testcontainers
+@EnabledIfEnvironmentVariable(named = "AZURE_AI_SEARCH_API_KEY", matches = ".+")
+@EnabledIfEnvironmentVariable(named = "AZURE_AI_SEARCH_ENDPOINT", matches = ".+")
+public class QdrantVectorStoreObservationIT {
+
+ private static final String COLLECTION_NAME = "test_collection";
+
+ private static final int EMBEDDING_DIMENSION = 1536;
+
+ @Container
+ static QdrantContainer qdrantContainer = new QdrantContainer("qdrant/qdrant:v1.9.2");
+
+ List documents = List.of(
+ new Document(getText("classpath:/test/data/spring.ai.txt"), Map.of("meta1", "meta1")),
+ new Document(getText("classpath:/test/data/time.shelter.txt")),
+ new Document(getText("classpath:/test/data/great.depression.txt"), Map.of("meta2", "meta2")));
+
+ public static String getText(String uri) {
+ var resource = new DefaultResourceLoader().getResource(uri);
+ try {
+ return resource.getContentAsString(StandardCharsets.UTF_8);
+ }
+ catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
+ .withUserConfiguration(Config.class);
+
+ @BeforeAll
+ static void setup() throws InterruptedException, ExecutionException {
+
+ String host = qdrantContainer.getHost();
+ int port = qdrantContainer.getGrpcPort();
+ QdrantClient client = new QdrantClient(QdrantGrpcClient.newBuilder(host, port, false).build());
+
+ client
+ .createCollectionAsync(COLLECTION_NAME,
+ VectorParams.newBuilder().setDistance(Distance.Cosine).setSize(EMBEDDING_DIMENSION).build())
+ .get();
+
+ client.close();
+ }
+
+ @Test
+ void observationVectorStoreAddAndQueryOperations() {
+
+ contextRunner.run(context -> {
+
+ VectorStore vectorStore = context.getBean(VectorStore.class);
+
+ TestObservationRegistry observationRegistry = context.getBean(TestObservationRegistry.class);
+
+ vectorStore.add(documents);
+
+ TestObservationRegistryAssert.assertThat(observationRegistry)
+ .doesNotHaveAnyRemainingCurrentObservation()
+ .hasObservationWithNameEqualTo(DefaultVectorStoreObservationConvention.DEFAULT_NAME)
+ .that()
+ .hasContextualNameEqualTo("vector_store qdrant add")
+ .hasLowCardinalityKeyValue(LowCardinalityKeyNames.DB_OPERATION_NAME.asString(), "add")
+ .hasLowCardinalityKeyValue(LowCardinalityKeyNames.DB_SYSTEM.asString(),
+ VectorStoreProvider.QDRANT.value())
+ .hasLowCardinalityKeyValue(LowCardinalityKeyNames.SPRING_AI_KIND.asString(), "vector_store")
+ .hasHighCardinalityKeyValue(HighCardinalityKeyNames.QUERY.asString(), "none")
+ .hasHighCardinalityKeyValue(HighCardinalityKeyNames.DIMENSIONS.asString(), "1536")
+ .hasHighCardinalityKeyValue(HighCardinalityKeyNames.COLLECTION_NAME.asString(), "test_collection")
+ .hasHighCardinalityKeyValue(HighCardinalityKeyNames.NAMESPACE.asString(), "none")
+ .hasHighCardinalityKeyValue(HighCardinalityKeyNames.FIELD_NAME.asString(), "none")
+ .hasHighCardinalityKeyValue(HighCardinalityKeyNames.SIMILARITY_METRIC.asString(), "none")
+ .hasHighCardinalityKeyValue(HighCardinalityKeyNames.TOP_K.asString(), "none")
+ .hasHighCardinalityKeyValue(HighCardinalityKeyNames.SIMILARITY_THRESHOLD.asString(), "none")
+
+ .hasBeenStarted()
+ .hasBeenStopped();
+
+ observationRegistry.clear();
+
+ List results = vectorStore
+ .similaritySearch(SearchRequest.query("What is Great Depression").withTopK(1));
+
+ assertThat(results).isNotEmpty();
+
+ TestObservationRegistryAssert.assertThat(observationRegistry)
+ .doesNotHaveAnyRemainingCurrentObservation()
+ .hasObservationWithNameEqualTo(DefaultVectorStoreObservationConvention.DEFAULT_NAME)
+ .that()
+ .hasContextualNameEqualTo("vector_store qdrant query")
+ .hasLowCardinalityKeyValue(LowCardinalityKeyNames.DB_OPERATION_NAME.asString(), "query")
+ .hasLowCardinalityKeyValue(LowCardinalityKeyNames.DB_SYSTEM.asString(),
+ VectorStoreProvider.QDRANT.value())
+ .hasLowCardinalityKeyValue(LowCardinalityKeyNames.SPRING_AI_KIND.asString(), "vector_store")
+
+ .hasHighCardinalityKeyValue(HighCardinalityKeyNames.QUERY.asString(), "What is Great Depression")
+ .hasHighCardinalityKeyValue(HighCardinalityKeyNames.DIMENSIONS.asString(), "1536")
+ .hasHighCardinalityKeyValue(HighCardinalityKeyNames.COLLECTION_NAME.asString(), "test_collection")
+ .hasHighCardinalityKeyValue(HighCardinalityKeyNames.NAMESPACE.asString(), "none")
+ .hasHighCardinalityKeyValue(HighCardinalityKeyNames.FIELD_NAME.asString(), "none")
+ .hasHighCardinalityKeyValue(HighCardinalityKeyNames.SIMILARITY_METRIC.asString(), "none")
+ .hasHighCardinalityKeyValue(HighCardinalityKeyNames.TOP_K.asString(), "1")
+ .hasHighCardinalityKeyValue(HighCardinalityKeyNames.SIMILARITY_THRESHOLD.asString(), "0.0")
+
+ .hasBeenStarted()
+ .hasBeenStopped();
+
+ });
+ }
+
+ @SpringBootConfiguration
+ @EnableAutoConfiguration
+ public static class Config {
+
+ @Bean
+ public TestObservationRegistry observationRegistry() {
+ return TestObservationRegistry.create();
+ }
+
+ @Bean
+ public QdrantClient qdrantClient() {
+ String host = qdrantContainer.getHost();
+ int port = qdrantContainer.getGrpcPort();
+ QdrantClient qdrantClient = new QdrantClient(QdrantGrpcClient.newBuilder(host, port, false).build());
+ return qdrantClient;
+ }
+
+ @Bean
+ public VectorStore qdrantVectorStore(EmbeddingModel embeddingModel, QdrantClient qdrantClient,
+ ObservationRegistry observationRegistry) {
+ return new QdrantVectorStore(qdrantClient, COLLECTION_NAME, embeddingModel, true, observationRegistry,
+ null);
+ }
+
+ @Bean
+ public OpenAIClient openAIClient() {
+ return new OpenAIClientBuilder().credential(new AzureKeyCredential(System.getenv("AZURE_OPENAI_API_KEY")))
+ .endpoint(System.getenv("AZURE_OPENAI_ENDPOINT"))
+ .buildClient();
+ }
+
+ @Bean
+ public AzureOpenAiEmbeddingModel azureEmbeddingModel(OpenAIClient openAIClient) {
+ return new AzureOpenAiEmbeddingModel(openAIClient);
+ }
+
+ }
+
+}
diff --git a/vector-stores/spring-ai-redis-store/pom.xml b/vector-stores/spring-ai-redis-store/pom.xml
index aee0919a0f..6d8a0ca9a2 100644
--- a/vector-stores/spring-ai-redis-store/pom.xml
+++ b/vector-stores/spring-ai-redis-store/pom.xml
@@ -79,6 +79,11 @@
test
+
+ io.micrometer
+ micrometer-observation-test
+ test
+
diff --git a/vector-stores/spring-ai-redis-store/src/main/java/org/springframework/ai/vectorstore/RedisVectorStore.java b/vector-stores/spring-ai-redis-store/src/main/java/org/springframework/ai/vectorstore/RedisVectorStore.java
index c4498a82b4..dcf780309a 100644
--- a/vector-stores/spring-ai-redis-store/src/main/java/org/springframework/ai/vectorstore/RedisVectorStore.java
+++ b/vector-stores/spring-ai-redis-store/src/main/java/org/springframework/ai/vectorstore/RedisVectorStore.java
@@ -30,10 +30,16 @@
import org.slf4j.LoggerFactory;
import org.springframework.ai.document.Document;
import org.springframework.ai.embedding.EmbeddingModel;
+import org.springframework.ai.observation.conventions.VectorStoreProvider;
import org.springframework.ai.vectorstore.filter.FilterExpressionConverter;
+import org.springframework.ai.vectorstore.observation.AbstractObservationVectorStore;
+import org.springframework.ai.vectorstore.observation.VectorStoreObservationContext;
+import org.springframework.ai.vectorstore.observation.VectorStoreObservationConvention;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
+
+import io.micrometer.observation.ObservationRegistry;
import redis.clients.jedis.JedisPooled;
import redis.clients.jedis.Pipeline;
import redis.clients.jedis.json.Path2;
@@ -72,7 +78,7 @@
* @see RedisVectorStoreConfig
* @see EmbeddingModel
*/
-public class RedisVectorStore implements VectorStore, InitializingBean {
+public class RedisVectorStore extends AbstractObservationVectorStore implements InitializingBean {
public enum Algorithm {
@@ -274,6 +280,15 @@ public RedisVectorStoreConfig build() {
public RedisVectorStore(RedisVectorStoreConfig config, EmbeddingModel embeddingModel, JedisPooled jedis,
boolean initializeSchema) {
+ this(config, embeddingModel, jedis, initializeSchema, ObservationRegistry.NOOP, null);
+ }
+
+ public RedisVectorStore(RedisVectorStoreConfig config, EmbeddingModel embeddingModel, JedisPooled jedis,
+ boolean initializeSchema, ObservationRegistry observationRegistry,
+ VectorStoreObservationConvention customObservationConvention) {
+
+ super(observationRegistry, customObservationConvention);
+
Assert.notNull(config, "Config must not be null");
Assert.notNull(embeddingModel, "Embedding model must not be null");
this.initializeSchema = initializeSchema;
@@ -289,7 +304,7 @@ public JedisPooled getJedis() {
}
@Override
- public void add(List documents) {
+ public void doAdd(List documents) {
try (Pipeline pipeline = this.jedis.pipelined()) {
for (Document document : documents) {
var embedding = this.embeddingModel.embed(document);
@@ -318,7 +333,7 @@ private String key(String id) {
}
@Override
- public Optional delete(List idList) {
+ public Optional doDelete(List idList) {
try (Pipeline pipeline = this.jedis.pipelined()) {
for (String id : idList) {
pipeline.jsonDel(key(id));
@@ -336,7 +351,7 @@ public Optional delete(List idList) {
}
@Override
- public List similaritySearch(SearchRequest request) {
+ public List doSimilaritySearch(SearchRequest request) {
Assert.isTrue(request.getTopK() > 0, "The number of documents to returned must be greater than zero");
Assert.isTrue(request.getSimilarityThreshold() >= 0 && request.getSimilarityThreshold() <= 1,
@@ -457,13 +472,15 @@ private String jsonPath(String field) {
return JSON_PATH_PREFIX + field;
}
- private static float[] toFloatArray(List embedding) {
- float[] embeddingFloat = new float[embedding.size()];
- int i = 0;
- for (Float d : embedding) {
- embeddingFloat[i++] = d.floatValue();
- }
- return embeddingFloat;
+ @Override
+ public VectorStoreObservationContext.Builder createObservationContextBuilder(String operationName) {
+
+ return VectorStoreObservationContext.builder(VectorStoreProvider.REDIS.value(), operationName)
+ .withDimensions(this.embeddingModel.dimensions())
+ .withFieldName(this.config.embeddingFieldName)
+ .withSimilarityMetric(vectorAlgorithm().name())
+ .withIndexName(this.config.indexName);
+
}
}
\ No newline at end of file
diff --git a/vector-stores/spring-ai-redis-store/src/test/java/org/springframework/ai/vectorstore/RedisVectorStoreObservationIT.java b/vector-stores/spring-ai-redis-store/src/test/java/org/springframework/ai/vectorstore/RedisVectorStoreObservationIT.java
new file mode 100644
index 0000000000..80169d52c3
--- /dev/null
+++ b/vector-stores/spring-ai-redis-store/src/test/java/org/springframework/ai/vectorstore/RedisVectorStoreObservationIT.java
@@ -0,0 +1,184 @@
+/*
+ * Copyright 2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.ai.vectorstore;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+import java.util.Map;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.ai.document.Document;
+import org.springframework.ai.embedding.EmbeddingModel;
+import org.springframework.ai.observation.conventions.VectorStoreProvider;
+import org.springframework.ai.transformers.TransformersEmbeddingModel;
+import org.springframework.ai.vectorstore.RedisVectorStore.MetadataField;
+import org.springframework.ai.vectorstore.RedisVectorStore.RedisVectorStoreConfig;
+import org.springframework.ai.vectorstore.observation.DefaultVectorStoreObservationConvention;
+import org.springframework.ai.vectorstore.observation.VectorStoreObservationDocumentation.HighCardinalityKeyNames;
+import org.springframework.ai.vectorstore.observation.VectorStoreObservationDocumentation.LowCardinalityKeyNames;
+import org.springframework.boot.SpringBootConfiguration;
+import org.springframework.boot.autoconfigure.AutoConfigurations;
+import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
+import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
+import org.springframework.boot.test.context.runner.ApplicationContextRunner;
+import org.springframework.context.annotation.Bean;
+import org.springframework.core.io.DefaultResourceLoader;
+import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
+import org.testcontainers.junit.jupiter.Container;
+import org.testcontainers.junit.jupiter.Testcontainers;
+
+import com.redis.testcontainers.RedisStackContainer;
+
+import io.micrometer.observation.ObservationRegistry;
+import io.micrometer.observation.tck.TestObservationRegistry;
+import io.micrometer.observation.tck.TestObservationRegistryAssert;
+import redis.clients.jedis.JedisPooled;
+
+/**
+ * @author Christian Tzolov
+ */
+@Testcontainers
+public class RedisVectorStoreObservationIT {
+
+ @Container
+ static RedisStackContainer redisContainer = new RedisStackContainer(
+ RedisStackContainer.DEFAULT_IMAGE_NAME.withTag(RedisStackContainer.DEFAULT_TAG));
+
+ List documents = List.of(
+ new Document(getText("classpath:/test/data/spring.ai.txt"), Map.of("meta1", "meta1")),
+ new Document(getText("classpath:/test/data/time.shelter.txt")),
+ new Document(getText("classpath:/test/data/great.depression.txt"), Map.of("meta2", "meta2")));
+
+ public static String getText(String uri) {
+ var resource = new DefaultResourceLoader().getResource(uri);
+ try {
+ return resource.getContentAsString(StandardCharsets.UTF_8);
+ }
+ catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
+ .withConfiguration(AutoConfigurations.of(RedisAutoConfiguration.class))
+ .withUserConfiguration(Config.class)
+ .withPropertyValues("spring.data.redis.url=" + redisContainer.getRedisURI());
+
+ @BeforeEach
+ void cleanDatabase() {
+ this.contextRunner.run(context -> context.getBean(RedisVectorStore.class).getJedis().flushAll());
+ }
+
+ @Test
+ void observationVectorStoreAddAndQueryOperations() {
+
+ contextRunner.run(context -> {
+
+ VectorStore vectorStore = context.getBean(VectorStore.class);
+
+ TestObservationRegistry observationRegistry = context.getBean(TestObservationRegistry.class);
+
+ vectorStore.add(documents);
+
+ TestObservationRegistryAssert.assertThat(observationRegistry)
+ .doesNotHaveAnyRemainingCurrentObservation()
+ .hasObservationWithNameEqualTo(DefaultVectorStoreObservationConvention.DEFAULT_NAME)
+ .that()
+ .hasContextualNameEqualTo("vector_store redis add")
+ .hasLowCardinalityKeyValue(LowCardinalityKeyNames.DB_OPERATION_NAME.asString(), "add")
+ .hasLowCardinalityKeyValue(LowCardinalityKeyNames.DB_SYSTEM.asString(),
+ VectorStoreProvider.REDIS.value())
+ .hasLowCardinalityKeyValue(LowCardinalityKeyNames.SPRING_AI_KIND.asString(), "vector_store")
+ .hasHighCardinalityKeyValue(HighCardinalityKeyNames.QUERY.asString(), "none")
+ .hasHighCardinalityKeyValue(HighCardinalityKeyNames.DIMENSIONS.asString(), "384")
+ .hasHighCardinalityKeyValue(HighCardinalityKeyNames.COLLECTION_NAME.asString(), "none")
+ .hasHighCardinalityKeyValue(HighCardinalityKeyNames.NAMESPACE.asString(), "none")
+ .hasHighCardinalityKeyValue(HighCardinalityKeyNames.FIELD_NAME.asString(), "embedding")
+ .hasHighCardinalityKeyValue(HighCardinalityKeyNames.SIMILARITY_METRIC.asString(), "HNSW")
+ .hasHighCardinalityKeyValue(HighCardinalityKeyNames.INDEX_NAME.asString(), "spring-ai-index")
+ .hasHighCardinalityKeyValue(HighCardinalityKeyNames.TOP_K.asString(), "none")
+ .hasHighCardinalityKeyValue(HighCardinalityKeyNames.SIMILARITY_THRESHOLD.asString(), "none")
+
+ .hasBeenStarted()
+ .hasBeenStopped();
+
+ observationRegistry.clear();
+
+ List results = vectorStore
+ .similaritySearch(SearchRequest.query("What is Great Depression").withTopK(1));
+
+ assertThat(results).isNotEmpty();
+
+ TestObservationRegistryAssert.assertThat(observationRegistry)
+ .doesNotHaveAnyRemainingCurrentObservation()
+ .hasObservationWithNameEqualTo(DefaultVectorStoreObservationConvention.DEFAULT_NAME)
+ .that()
+ .hasContextualNameEqualTo("vector_store redis query")
+ .hasLowCardinalityKeyValue(LowCardinalityKeyNames.DB_OPERATION_NAME.asString(), "query")
+ .hasLowCardinalityKeyValue(LowCardinalityKeyNames.DB_SYSTEM.asString(),
+ VectorStoreProvider.REDIS.value())
+ .hasLowCardinalityKeyValue(LowCardinalityKeyNames.SPRING_AI_KIND.asString(), "vector_store")
+
+ .hasHighCardinalityKeyValue(HighCardinalityKeyNames.QUERY.asString(), "What is Great Depression")
+ .hasHighCardinalityKeyValue(HighCardinalityKeyNames.DIMENSIONS.asString(), "384")
+ .hasHighCardinalityKeyValue(HighCardinalityKeyNames.COLLECTION_NAME.asString(), "none")
+ .hasHighCardinalityKeyValue(HighCardinalityKeyNames.NAMESPACE.asString(), "none")
+ .hasHighCardinalityKeyValue(HighCardinalityKeyNames.FIELD_NAME.asString(), "embedding")
+ .hasHighCardinalityKeyValue(HighCardinalityKeyNames.SIMILARITY_METRIC.asString(), "HNSW")
+ .hasHighCardinalityKeyValue(HighCardinalityKeyNames.INDEX_NAME.asString(), "spring-ai-index")
+ .hasHighCardinalityKeyValue(HighCardinalityKeyNames.TOP_K.asString(), "1")
+ .hasHighCardinalityKeyValue(HighCardinalityKeyNames.SIMILARITY_THRESHOLD.asString(), "0.0")
+
+ .hasBeenStarted()
+ .hasBeenStopped();
+
+ });
+ }
+
+ @SpringBootConfiguration
+ @EnableAutoConfiguration
+ public static class Config {
+
+ @Bean
+ public TestObservationRegistry observationRegistry() {
+ return TestObservationRegistry.create();
+ }
+
+ @Bean
+ public RedisVectorStore vectorStore(EmbeddingModel embeddingModel,
+ JedisConnectionFactory jedisConnectionFactory, ObservationRegistry observationRegistry) {
+ return new RedisVectorStore(
+ RedisVectorStoreConfig.builder()
+ .withMetadataFields(MetadataField.tag("meta1"), MetadataField.tag("meta2"),
+ MetadataField.tag("country"), MetadataField.numeric("year"))
+ .build(),
+ embeddingModel,
+ new JedisPooled(jedisConnectionFactory.getHostName(), jedisConnectionFactory.getPort()), true,
+ observationRegistry, null);
+ }
+
+ @Bean
+ public EmbeddingModel embeddingModel() {
+ return new TransformersEmbeddingModel();
+ }
+
+ }
+
+}
diff --git a/vector-stores/spring-ai-typesense-store/pom.xml b/vector-stores/spring-ai-typesense-store/pom.xml
index df2484c19f..b43f7711d5 100644
--- a/vector-stores/spring-ai-typesense-store/pom.xml
+++ b/vector-stores/spring-ai-typesense-store/pom.xml
@@ -62,6 +62,13 @@
test
+
+
+ io.micrometer
+ micrometer-observation-test
+ test
+
+
\ No newline at end of file
diff --git a/vector-stores/spring-ai-typesense-store/src/main/java/org/springframework/ai/vectorstore/TypesenseVectorStore.java b/vector-stores/spring-ai-typesense-store/src/main/java/org/springframework/ai/vectorstore/TypesenseVectorStore.java
index 885cc7f5a7..8dda34eaf2 100644
--- a/vector-stores/spring-ai-typesense-store/src/main/java/org/springframework/ai/vectorstore/TypesenseVectorStore.java
+++ b/vector-stores/spring-ai-typesense-store/src/main/java/org/springframework/ai/vectorstore/TypesenseVectorStore.java
@@ -16,7 +16,6 @@
package org.springframework.ai.vectorstore;
-import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -28,7 +27,12 @@
import org.slf4j.LoggerFactory;
import org.springframework.ai.document.Document;
import org.springframework.ai.embedding.EmbeddingModel;
+import org.springframework.ai.observation.conventions.VectorStoreProvider;
+import org.springframework.ai.observation.conventions.VectorStoreSimilarityMetric;
import org.springframework.ai.vectorstore.filter.FilterExpressionConverter;
+import org.springframework.ai.vectorstore.observation.AbstractObservationVectorStore;
+import org.springframework.ai.vectorstore.observation.VectorStoreObservationContext;
+import org.springframework.ai.vectorstore.observation.VectorStoreObservationConvention;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.util.Assert;
import org.typesense.api.Client;
@@ -42,11 +46,14 @@
import org.typesense.model.MultiSearchResult;
import org.typesense.model.MultiSearchSearchesParameter;
+import io.micrometer.observation.ObservationRegistry;
+
/**
* @author Pablo Sanchidrian Herrera
* @author Soby Chacko
+ * @author Christian Tzolov
*/
-public class TypesenseVectorStore implements VectorStore, InitializingBean {
+public class TypesenseVectorStore extends AbstractObservationVectorStore implements InitializingBean {
private static final Logger logger = LoggerFactory.getLogger(TypesenseVectorStore.class);
@@ -154,6 +161,16 @@ public TypesenseVectorStore(Client client, EmbeddingModel embeddingModel) {
public TypesenseVectorStore(Client client, EmbeddingModel embeddingModel, TypesenseVectorStoreConfig config,
boolean initializeSchema) {
+
+ this(client, embeddingModel, config, initializeSchema, ObservationRegistry.NOOP, null);
+ }
+
+ public TypesenseVectorStore(Client client, EmbeddingModel embeddingModel, TypesenseVectorStoreConfig config,
+ boolean initializeSchema, ObservationRegistry observationRegistry,
+ VectorStoreObservationConvention customObservationConvention) {
+
+ super(observationRegistry, customObservationConvention);
+
Assert.notNull(client, "Typesense must not be null");
Assert.notNull(embeddingModel, "EmbeddingModel must not be null");
@@ -164,7 +181,7 @@ public TypesenseVectorStore(Client client, EmbeddingModel embeddingModel, Typese
}
@Override
- public void add(List documents) {
+ public void doAdd(List documents) {
Assert.notNull(documents, "Documents must not be null");
List> documentList = documents.stream().map(document -> {
@@ -194,7 +211,7 @@ public void add(List documents) {
}
@Override
- public Optional delete(List idList) {
+ public Optional doDelete(List idList) {
DeleteDocumentsParameters deleteDocumentsParameters = new DeleteDocumentsParameters();
deleteDocumentsParameters.filterBy(DOC_ID_FIELD_NAME + ":=[" + String.join(",", idList) + "]");
@@ -217,7 +234,7 @@ public Optional delete(List idList) {
}
@Override
- public List similaritySearch(SearchRequest request) {
+ public List doSimilaritySearch(SearchRequest request) {
Assert.notNull(request.getQuery(), "Query string must not be null");
String nativeFilterExpressions = (request.getFilterExpression() != null)
@@ -361,4 +378,14 @@ Map