From c8e03f1a98b6e7e55cb101a8200f2b20202e70d6 Mon Sep 17 00:00:00 2001 From: Christian Tzolov Date: Sun, 11 Aug 2024 08:30:02 +0200 Subject: [PATCH 1/8] Add observability support to VectorStore - Introduce AbstractObservationVectorStore for instrumentation. Add observe instrumentation for the add, delete and the similaritySearch methods. - Add VectorStoreObservationContext for capturing operation details. - Implement DefaultVectorStoreObservationConvention for naming and tagging - Create filters for add and delete request content observation Implement VectorStoreQueryResponseContentObservationFilter. - Update VectorStore interface with getName() method, returning the class name by default. - Add VectorStoreObservationDocumentation for defining observation keys. - Create VectorStoreObservationAutoConfiguration for auto-configuring observations. - Add VectorStoreObservationProperties for configuring observation behavior. They are used to control the optional, additional, observationcontent filters. - Update PgVectorStoreAutoConfiguration to support observations. - Modify PgVectorStore to extend AbstractObservationVectorStore - Add observation support to PgVectorStore's Builder. Resolves #1204 --- .../ai/vectorstore/VectorStore.java | 4 + .../AbstractObservationVectorStore.java | 109 ++++++++ ...faultVectorStoreObservationConvention.java | 164 ++++++++++++ ...oreAddRequestContentObservationFilter.java | 54 ++++ ...DeleteRequestContentObservationFilter.java | 55 ++++ .../VectorStoreObservationContext.java | 244 ++++++++++++++++++ .../VectorStoreObservationConvention.java | 33 +++ .../VectorStoreObservationDocumentation.java | 205 +++++++++++++++ ...QueryResponseContentObservationFilter.java | 55 ++++ ...ctorStoreObservationAutoConfiguration.java | 73 ++++++ .../VectorStoreObservationProperties.java | 70 +++++ .../vectorstore/observation/package-info.java | 22 ++ .../PgVectorStoreAutoConfiguration.java | 10 +- ...ot.autoconfigure.AutoConfiguration.imports | 1 + .../ai/vectorstore/PgVectorStore.java | 56 +++- 15 files changed, 1149 insertions(+), 6 deletions(-) create mode 100644 spring-ai-core/src/main/java/org/springframework/ai/vectorstore/observation/AbstractObservationVectorStore.java create mode 100644 spring-ai-core/src/main/java/org/springframework/ai/vectorstore/observation/DefaultVectorStoreObservationConvention.java create mode 100644 spring-ai-core/src/main/java/org/springframework/ai/vectorstore/observation/VectorStoreAddRequestContentObservationFilter.java create mode 100644 spring-ai-core/src/main/java/org/springframework/ai/vectorstore/observation/VectorStoreDeleteRequestContentObservationFilter.java create mode 100644 spring-ai-core/src/main/java/org/springframework/ai/vectorstore/observation/VectorStoreObservationContext.java create mode 100644 spring-ai-core/src/main/java/org/springframework/ai/vectorstore/observation/VectorStoreObservationConvention.java create mode 100644 spring-ai-core/src/main/java/org/springframework/ai/vectorstore/observation/VectorStoreObservationDocumentation.java create mode 100644 spring-ai-core/src/main/java/org/springframework/ai/vectorstore/observation/VectorStoreQueryResponseContentObservationFilter.java create mode 100644 spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/observation/VectorStoreObservationAutoConfiguration.java create mode 100644 spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/observation/VectorStoreObservationProperties.java create mode 100644 spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/observation/package-info.java diff --git a/spring-ai-core/src/main/java/org/springframework/ai/vectorstore/VectorStore.java b/spring-ai-core/src/main/java/org/springframework/ai/vectorstore/VectorStore.java index 8c4ff0c2a9c..dadcdda3b18 100644 --- a/spring-ai-core/src/main/java/org/springframework/ai/vectorstore/VectorStore.java +++ b/spring-ai-core/src/main/java/org/springframework/ai/vectorstore/VectorStore.java @@ -31,6 +31,10 @@ */ public interface VectorStore extends DocumentWriter { + default String getName() { + return this.getClass().getSimpleName(); + } + /** * Adds list of {@link Document}s to the vector store. * @param documents the list of documents to store. Throws an exception if the diff --git a/spring-ai-core/src/main/java/org/springframework/ai/vectorstore/observation/AbstractObservationVectorStore.java b/spring-ai-core/src/main/java/org/springframework/ai/vectorstore/observation/AbstractObservationVectorStore.java new file mode 100644 index 00000000000..3668d81c2ce --- /dev/null +++ b/spring-ai-core/src/main/java/org/springframework/ai/vectorstore/observation/AbstractObservationVectorStore.java @@ -0,0 +1,109 @@ +/* +* Copyright 2024 - 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.observation; + +import java.util.List; +import java.util.Optional; + +import org.springframework.ai.document.Document; +import org.springframework.ai.vectorstore.SearchRequest; +import org.springframework.ai.vectorstore.VectorStore; +import org.springframework.lang.Nullable; + +import io.micrometer.observation.ObservationRegistry; + +/** + * @author Christian Tzolov + * @since 1.0.0 + */ +public abstract class AbstractObservationVectorStore implements VectorStore { + + private static final VectorStoreObservationConvention DEFAULT_OBSERVATION_CONVENTION = new DefaultVectorStoreObservationConvention(); + + private final ObservationRegistry observationRegistry; + + @Nullable + private final VectorStoreObservationConvention customObservationConvention; + + public AbstractObservationVectorStore() { + this(ObservationRegistry.NOOP, null); + } + + public AbstractObservationVectorStore(ObservationRegistry observationRegistry) { + this(observationRegistry, null); + } + + public AbstractObservationVectorStore(ObservationRegistry observationRegistry, + VectorStoreObservationConvention customSearchObservationConvention) { + this.observationRegistry = observationRegistry; + this.customObservationConvention = customSearchObservationConvention; + } + + @Override + public void add(List documents) { + + VectorStoreObservationContext observationContext = this.createObservationContextBuilder() + .withAddRequest(documents) + .withOperationName("add") + .build(); + + VectorStoreObservationDocumentation.AI_VECTOR_STORE + .observation(this.customObservationConvention, DEFAULT_OBSERVATION_CONVENTION, () -> observationContext, + observationRegistry) + .observe(() -> this.doAdd(documents)); + } + + @Override + public Optional delete(List deleteDocIds) { + + VectorStoreObservationContext observationContext = this.createObservationContextBuilder() + .withDeleteRequest(deleteDocIds) + .withOperationName("delete") + .build(); + + return VectorStoreObservationDocumentation.AI_VECTOR_STORE + .observation(this.customObservationConvention, DEFAULT_OBSERVATION_CONVENTION, () -> observationContext, + this.observationRegistry) + .observe(() -> this.doDelete(deleteDocIds)); + } + + @Override + public List similaritySearch(SearchRequest request) { + + VectorStoreObservationContext searchObservationContext = this.createObservationContextBuilder() + .withSearchRequest(request) + .withOperationName("search") + .build(); + + return VectorStoreObservationDocumentation.AI_VECTOR_STORE + .observation(this.customObservationConvention, DEFAULT_OBSERVATION_CONVENTION, + () -> searchObservationContext, this.observationRegistry) + .observe(() -> { + var documents = this.doSimilaritySearch(request); + searchObservationContext.setSearchResponse(documents); + return documents; + }); + } + + abstract public void doAdd(List documents); + + abstract public Optional doDelete(List idList); + + abstract public List doSimilaritySearch(SearchRequest request); + + abstract public VectorStoreObservationContext.Builder createObservationContextBuilder(); + +} \ No newline at end of file diff --git a/spring-ai-core/src/main/java/org/springframework/ai/vectorstore/observation/DefaultVectorStoreObservationConvention.java b/spring-ai-core/src/main/java/org/springframework/ai/vectorstore/observation/DefaultVectorStoreObservationConvention.java new file mode 100644 index 00000000000..b78f0f81c5f --- /dev/null +++ b/spring-ai-core/src/main/java/org/springframework/ai/vectorstore/observation/DefaultVectorStoreObservationConvention.java @@ -0,0 +1,164 @@ +/* +* Copyright 2024 - 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.observation; + +import org.springframework.ai.vectorstore.observation.VectorStoreObservationDocumentation.HighCardinalityKeyNames; +import org.springframework.ai.vectorstore.observation.VectorStoreObservationDocumentation.LowCardinalityKeyNames; +import org.springframework.lang.Nullable; +import org.springframework.util.StringUtils; + +import io.micrometer.common.KeyValue; +import io.micrometer.common.KeyValues; + +/** + * @author Christian Tzolov + * @since 1.0.0 + */ + +public class DefaultVectorStoreObservationConvention implements VectorStoreObservationConvention { + + private static final String DEFAULT_NAME = "spring.ai.vector.store"; + + private static final KeyValue DIMENSIONS_NONE = KeyValue.of(HighCardinalityKeyNames.DIMENSIONS, + KeyValue.NONE_VALUE); + + private static final KeyValue QUERY_NONE = KeyValue.of(HighCardinalityKeyNames.QUERY, KeyValue.NONE_VALUE); + + private static final KeyValue METADATA_FILTER_NONE = KeyValue.of(HighCardinalityKeyNames.METADATA_FILTER, + KeyValue.NONE_VALUE); + + private static final KeyValue TOP_K_NONE = KeyValue.of(HighCardinalityKeyNames.TOP_K, KeyValue.NONE_VALUE); + + private static final KeyValue SIMILARITY_METRIC_NONE = KeyValue.of(HighCardinalityKeyNames.SIMILARITY_METRIC, + KeyValue.NONE_VALUE); + + private static final KeyValue COLLECTION_NAME_NONE = KeyValue.of(HighCardinalityKeyNames.COLLECTION_NAME, + KeyValue.NONE_VALUE); + + private static final KeyValue NAMESPACE_NONE = KeyValue.of(HighCardinalityKeyNames.NAMESPACE, KeyValue.NONE_VALUE); + + private static final KeyValue FIELD_NAME_NONE = KeyValue.of(HighCardinalityKeyNames.FIELD_NAME, + KeyValue.NONE_VALUE); + + private static final KeyValue INDEX_NAME_NONE = KeyValue.of(HighCardinalityKeyNames.INDEX_NAME, + KeyValue.NONE_VALUE); + + private final String name; + + public DefaultVectorStoreObservationConvention() { + this(DEFAULT_NAME); + } + + public DefaultVectorStoreObservationConvention(String name) { + this.name = name; + } + + @Override + public String getName() { + return this.name; + } + + @Override + @Nullable + public String getContextualName(VectorStoreObservationContext context) { + return "vector_db " + context.getDatabaseSystem() + " " + context.getOperationName(); + } + + @Override + public KeyValues getLowCardinalityKeyValues(VectorStoreObservationContext context) { + return KeyValues.of(dbSystem(context), dbOperationName(context)); + } + + @Override + public KeyValues getHighCardinalityKeyValues(VectorStoreObservationContext context) { + return KeyValues.of(query(context), metadataFilter(context), topK(context), dimensions(context), + similarityMetric(context), collectionName(context), namespace(context), fieldName(context), + indexName(context)); + } + + protected KeyValue dbSystem(VectorStoreObservationContext context) { + return KeyValue.of(LowCardinalityKeyNames.DB_SYSTEM, context.getDatabaseSystem()); + } + + protected KeyValue dbOperationName(VectorStoreObservationContext context) { + return KeyValue.of(LowCardinalityKeyNames.DB_OPERATION_NAME, context.getOperationName()); + } + + protected KeyValue dimensions(VectorStoreObservationContext context) { + if (context.getDimensions() > 0) { + return KeyValue.of(HighCardinalityKeyNames.DIMENSIONS, "" + context.getDimensions()); + } + return DIMENSIONS_NONE; + } + + protected KeyValue query(VectorStoreObservationContext context) { + if (context.getSearchRequest() != null && StringUtils.hasText(context.getSearchRequest().getQuery())) { + return KeyValue.of(HighCardinalityKeyNames.QUERY, "" + context.getSearchRequest().getQuery()); + } + return QUERY_NONE; + } + + protected KeyValue metadataFilter(VectorStoreObservationContext context) { + if (context.getSearchRequest() != null && context.getSearchRequest().getFilterExpression() != null) { + return KeyValue.of(HighCardinalityKeyNames.METADATA_FILTER, + "" + context.getSearchRequest().getFilterExpression().toString()); + } + return METADATA_FILTER_NONE; + } + + protected KeyValue topK(VectorStoreObservationContext context) { + if (context.getSearchRequest() != null && context.getSearchRequest().getTopK() > 0) { + return KeyValue.of(HighCardinalityKeyNames.TOP_K, "" + context.getSearchRequest().getTopK()); + } + return TOP_K_NONE; + } + + protected KeyValue similarityMetric(VectorStoreObservationContext context) { + if (StringUtils.hasText(context.getSimilarityMetric())) { + return KeyValue.of(HighCardinalityKeyNames.SIMILARITY_METRIC, context.getSimilarityMetric()); + } + return SIMILARITY_METRIC_NONE; + } + + protected KeyValue collectionName(VectorStoreObservationContext context) { + if (StringUtils.hasText(context.getCollectionName())) { + return KeyValue.of(HighCardinalityKeyNames.COLLECTION_NAME, context.getCollectionName()); + } + return COLLECTION_NAME_NONE; + } + + protected KeyValue namespace(VectorStoreObservationContext context) { + if (StringUtils.hasText(context.getNamespace())) { + return KeyValue.of(HighCardinalityKeyNames.NAMESPACE, context.getNamespace()); + } + return NAMESPACE_NONE; + } + + protected KeyValue fieldName(VectorStoreObservationContext context) { + if (StringUtils.hasText(context.getFieldName())) { + return KeyValue.of(HighCardinalityKeyNames.FIELD_NAME, context.getFieldName()); + } + return FIELD_NAME_NONE; + } + + protected KeyValue indexName(VectorStoreObservationContext context) { + if (StringUtils.hasText(context.getIndexName())) { + return KeyValue.of(HighCardinalityKeyNames.INDEX_NAME, context.getIndexName()); + } + return INDEX_NAME_NONE; + } + +} \ No newline at end of file diff --git a/spring-ai-core/src/main/java/org/springframework/ai/vectorstore/observation/VectorStoreAddRequestContentObservationFilter.java b/spring-ai-core/src/main/java/org/springframework/ai/vectorstore/observation/VectorStoreAddRequestContentObservationFilter.java new file mode 100644 index 00000000000..f7a740744fd --- /dev/null +++ b/spring-ai-core/src/main/java/org/springframework/ai/vectorstore/observation/VectorStoreAddRequestContentObservationFilter.java @@ -0,0 +1,54 @@ +/* + * 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.observation; + +import java.util.StringJoiner; + +import org.springframework.util.CollectionUtils; + +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationFilter; + +/** + * An {@link ObservationFilter} to include the Vector Store add request content in the + * observation. + * + * @author Christian Tzolov + * @since 1.0.0 + */ +public class VectorStoreAddRequestContentObservationFilter implements ObservationFilter { + + @Override + public Observation.Context map(Observation.Context context) { + + if (!(context instanceof VectorStoreObservationContext observationContext)) { + return context; + } + + if (CollectionUtils.isEmpty(observationContext.getAddRequest())) { + return observationContext; + } + + StringJoiner joiner = new StringJoiner(", ", "[", "]"); + observationContext.getAddRequest().forEach(document -> joiner.add("\"" + document.getContent() + "\"")); + + observationContext.addHighCardinalityKeyValue( + VectorStoreObservationDocumentation.HighCardinalityKeyNames.ADD_REQUEST.withValue(joiner.toString())); + + return observationContext; + } + +} diff --git a/spring-ai-core/src/main/java/org/springframework/ai/vectorstore/observation/VectorStoreDeleteRequestContentObservationFilter.java b/spring-ai-core/src/main/java/org/springframework/ai/vectorstore/observation/VectorStoreDeleteRequestContentObservationFilter.java new file mode 100644 index 00000000000..55b6ccd17b1 --- /dev/null +++ b/spring-ai-core/src/main/java/org/springframework/ai/vectorstore/observation/VectorStoreDeleteRequestContentObservationFilter.java @@ -0,0 +1,55 @@ +/* + * 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.observation; + +import java.util.StringJoiner; + +import org.springframework.util.CollectionUtils; + +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationFilter; + +/** + * An {@link ObservationFilter} to include the Vector Store delete request content in the + * observation. + * + * @author Christian Tzolov + * @since 1.0.0 + */ +public class VectorStoreDeleteRequestContentObservationFilter implements ObservationFilter { + + @Override + public Observation.Context map(Observation.Context context) { + + if (!(context instanceof VectorStoreObservationContext observationContext)) { + return context; + } + + if (CollectionUtils.isEmpty(observationContext.getDeleteRequest())) { + return observationContext; + } + + StringJoiner joiner = new StringJoiner(", ", "[", "]"); + observationContext.getDeleteRequest().forEach(documentId -> joiner.add("\"" + documentId + "\"")); + + observationContext + .addHighCardinalityKeyValue(VectorStoreObservationDocumentation.HighCardinalityKeyNames.DELETE_REQUEST + .withValue(joiner.toString())); + + return observationContext; + } + +} diff --git a/spring-ai-core/src/main/java/org/springframework/ai/vectorstore/observation/VectorStoreObservationContext.java b/spring-ai-core/src/main/java/org/springframework/ai/vectorstore/observation/VectorStoreObservationContext.java new file mode 100644 index 00000000000..2ea0b812dfc --- /dev/null +++ b/spring-ai-core/src/main/java/org/springframework/ai/vectorstore/observation/VectorStoreObservationContext.java @@ -0,0 +1,244 @@ +/* +* Copyright 2024 - 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.observation; + +import java.util.List; + +import org.springframework.ai.document.Document; +import org.springframework.ai.vectorstore.SearchRequest; + +import io.micrometer.observation.Observation; + +/** + * @author Christian Tzolov + * @since 1.0.0 + */ + +public class VectorStoreObservationContext extends Observation.Context { + + // DELETE + private List deleteRequest; + + // ADD + private List addRequest; + + // SEARCH + private SearchRequest searchRequest; + + private List searchResponse; + + // COMMON + private final String databaseSystem; + + private int dimensions = -1; + + private String similarityMetric = ""; + + private String collectionName = ""; + + private String namespace = ""; + + private String model = ""; + + private String fieldName = ""; + + private String indexName = ""; + + private String operationName; + + public VectorStoreObservationContext(String databaseSystem) { + this.databaseSystem = databaseSystem; + } + + public List getDeleteRequest() { + return this.deleteRequest; + } + + public void setDeleteRequest(List deleteRequest) { + this.deleteRequest = deleteRequest; + } + + public List getAddRequest() { + return this.addRequest; + } + + public void setAddRequest(List documents) { + this.addRequest = documents; + } + + public SearchRequest getSearchRequest() { + return this.searchRequest; + } + + public void setSearchRequest(SearchRequest request) { + this.searchRequest = request; + } + + public List getSearchResponse() { + return this.searchResponse; + } + + public void setSearchResponse(List documents) { + this.searchResponse = documents; + } + + public String getDatabaseSystem() { + return this.databaseSystem; + } + + public int getDimensions() { + return this.dimensions; + } + + public void setDimensions(int dimensions) { + this.dimensions = dimensions; + } + + public String getSimilarityMetric() { + return this.similarityMetric; + } + + public void setSimilarityMetric(String similarityMetric) { + this.similarityMetric = similarityMetric; + } + + public String getCollectionName() { + return this.collectionName; + } + + public void setCollectionName(String collectionName) { + this.collectionName = collectionName; + } + + public String getNamespace() { + return this.namespace; + } + + public void setNamespace(String namespace) { + this.namespace = namespace; + } + + public String getModel() { + return this.model; + } + + public void setModel(String model) { + this.model = model; + } + + public String getFieldName() { + return this.fieldName; + } + + public void setFieldName(String fieldName) { + this.fieldName = fieldName; + } + + public String getIndexName() { + return this.indexName; + } + + public void setIndexName(String indexName) { + this.indexName = indexName; + } + + public String getOperationName() { + return this.operationName; + } + + public void setOperationName(String operationName) { + this.operationName = operationName; + } + + public static Builder builder(String databaseSystem) { + return new Builder(databaseSystem); + } + + public static class Builder { + + private VectorStoreObservationContext context; + + public Builder(String databaseSystem) { + this.context = new VectorStoreObservationContext(databaseSystem); + } + + public Builder withDeleteRequest(List deleteRequest) { + this.context.setDeleteRequest(deleteRequest); + return this; + } + + public Builder withAddRequest(List documents) { + this.context.setAddRequest(documents); + return this; + } + + public Builder withSearchRequest(SearchRequest request) { + this.context.setSearchRequest(request); + return this; + } + + public Builder withSearchResponse(List documents) { + this.context.setSearchResponse(documents); + return this; + } + + public Builder withDimensions(int dimensions) { + this.context.setDimensions(dimensions); + return this; + } + + public Builder withSimilarityMetric(String similarityMetric) { + this.context.setSimilarityMetric(similarityMetric); + return this; + } + + public Builder withCollectionName(String collectionName) { + this.context.setCollectionName(collectionName); + return this; + } + + public Builder withNamespace(String namespace) { + this.context.setNamespace(namespace); + return this; + } + + public Builder withModel(String model) { + this.context.setModel(model); + return this; + } + + public Builder withFieldName(String fieldName) { + this.context.setFieldName(fieldName); + return this; + } + + public Builder withIndexName(String indexName) { + this.context.setIndexName(indexName); + return this; + } + + public Builder withOperationName(String operationName) { + this.context.setOperationName(operationName); + return this; + } + + public VectorStoreObservationContext build() { + return this.context; + } + + } + +} \ No newline at end of file diff --git a/spring-ai-core/src/main/java/org/springframework/ai/vectorstore/observation/VectorStoreObservationConvention.java b/spring-ai-core/src/main/java/org/springframework/ai/vectorstore/observation/VectorStoreObservationConvention.java new file mode 100644 index 00000000000..9bf80d838d2 --- /dev/null +++ b/spring-ai-core/src/main/java/org/springframework/ai/vectorstore/observation/VectorStoreObservationConvention.java @@ -0,0 +1,33 @@ +/* +* Copyright 2024 - 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.observation; + +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationConvention; + +/** + * @author Christian Tzolov + * @since 1.0.0 + */ + +public interface VectorStoreObservationConvention extends ObservationConvention { + + @Override + default boolean supportsContext(Observation.Context context) { + return context instanceof VectorStoreObservationContext; + } + +} \ No newline at end of file diff --git a/spring-ai-core/src/main/java/org/springframework/ai/vectorstore/observation/VectorStoreObservationDocumentation.java b/spring-ai-core/src/main/java/org/springframework/ai/vectorstore/observation/VectorStoreObservationDocumentation.java new file mode 100644 index 00000000000..7d8c0fc8da2 --- /dev/null +++ b/spring-ai-core/src/main/java/org/springframework/ai/vectorstore/observation/VectorStoreObservationDocumentation.java @@ -0,0 +1,205 @@ +/* +* Copyright 2024 - 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.observation; + +import io.micrometer.common.docs.KeyName; +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationConvention; +import io.micrometer.observation.docs.ObservationDocumentation; + +/** + * @author Christian Tzolov + * @since 1.0.0 + */ +public enum VectorStoreObservationDocumentation implements ObservationDocumentation { + + /** + * AI Chat Model observations for clients. + */ + AI_VECTOR_STORE { + @Override + public Class> getDefaultConvention() { + return DefaultVectorStoreObservationConvention.class; + } + + @Override + public KeyName[] getLowCardinalityKeyNames() { + return LowCardinalityKeyNames.values(); + } + + @Override + public KeyName[] getHighCardinalityKeyNames() { + return HighCardinalityKeyNames.values(); + } + }; + + public enum LowCardinalityKeyNames implements KeyName { + + /** + * The name of the operation or command being executed. + */ + DB_OPERATION_NAME { + @Override + public String asString() { + return "db.operation.name"; + } + }, + /** + * The database management system (DBMS) product as identified by the client + * instrumentation. + */ + DB_SYSTEM { + @Override + public String asString() { + return "db.system"; + } + }; + + } + + public enum HighCardinalityKeyNames implements KeyName { + + /** + * Add documents request content. + */ + DELETE_REQUEST { + @Override + public String asString() { + return "db.vector.delete.request"; + } + }, + /** + * Add documents request content. + */ + ADD_REQUEST { + @Override + public String asString() { + return "db.vector.add.request"; + } + }, + /** + * Similarity search response content. + */ + QUERY_RESPONSE { + @Override + public String asString() { + return "db.vector.query.response"; + } + }, + /** + * The database query being executed. + */ + QUERY { + @Override + public String asString() { + return "db.query.text"; + } + }, + /** + * The metadata filters used in the query. + */ + METADATA_FILTER { + @Override + public String asString() { + return "db.query.metadata.filter"; + } + }, + /** + * The metric used in similarity search. + */ + SIMILARITY_METRIC { + @Override + public String asString() { + return "db.vector.similarity_metric"; + } + }, + /** + * The top-k most similar vectors returned by a query. + */ + TOP_K { + @Override + public String asString() { + return "db.vector.query.top_k"; + } + }, + /** + * The dimension of the vector. + */ + DIMENSIONS { + @Override + public String asString() { + return "db.vector.dimension_count"; + } + }, + /** + * The name field as of the vector (e.g. a field name). + */ + FIELD_NAME { + @Override + public String asString() { + return "db.vector.name"; + } + }, + /** + * The model used for the embedding. + */ + MODEL { + @Override + public String asString() { + return "db.vector.model"; + } + }, + /** + * The name of a collection (table, container) within the database. + */ + COLLECTION_NAME { + @Override + public String asString() { + return "db.collection.name"; + } + }, + /** + * The namespace of the database. + */ + NAMESPACE { + @Override + public String asString() { + return "db.namespace"; + } + }, + /** + * The index name used in the query. + */ + INDEX_NAME { + @Override + public String asString() { + return "db.index.name"; + } + } + // , + // /** + // * The number of queries included in a batch operation. + // */ + // BATCH_SIZE { + // @Override + // public String asString() { + // return "db.operation.batch.size"; + // } + // } + + } + +} \ No newline at end of file diff --git a/spring-ai-core/src/main/java/org/springframework/ai/vectorstore/observation/VectorStoreQueryResponseContentObservationFilter.java b/spring-ai-core/src/main/java/org/springframework/ai/vectorstore/observation/VectorStoreQueryResponseContentObservationFilter.java new file mode 100644 index 00000000000..647b250d557 --- /dev/null +++ b/spring-ai-core/src/main/java/org/springframework/ai/vectorstore/observation/VectorStoreQueryResponseContentObservationFilter.java @@ -0,0 +1,55 @@ +/* + * 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.observation; + +import java.util.StringJoiner; + +import org.springframework.util.CollectionUtils; + +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationFilter; + +/** + * An {@link ObservationFilter} to include the Vector Store search response content in the + * observation. + * + * @author Christian Tzolov + * @since 1.0.0 + */ +public class VectorStoreQueryResponseContentObservationFilter implements ObservationFilter { + + @Override + public Observation.Context map(Observation.Context context) { + + if (!(context instanceof VectorStoreObservationContext observationContext)) { + return context; + } + + if (CollectionUtils.isEmpty(observationContext.getSearchResponse())) { + return observationContext; + } + + StringJoiner joiner = new StringJoiner(", ", "[", "]"); + observationContext.getSearchResponse().forEach(document -> joiner.add("\"" + document.getContent() + "\"")); + + observationContext + .addHighCardinalityKeyValue(VectorStoreObservationDocumentation.HighCardinalityKeyNames.QUERY_RESPONSE + .withValue(joiner.toString())); + + return observationContext; + } + +} diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/observation/VectorStoreObservationAutoConfiguration.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/observation/VectorStoreObservationAutoConfiguration.java new file mode 100644 index 00000000000..b26ad26bd51 --- /dev/null +++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/observation/VectorStoreObservationAutoConfiguration.java @@ -0,0 +1,73 @@ +/* + * 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.autoconfigure.vectorstore.observation; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.ai.vectorstore.VectorStore; +import org.springframework.ai.vectorstore.observation.VectorStoreAddRequestContentObservationFilter; +import org.springframework.ai.vectorstore.observation.VectorStoreDeleteRequestContentObservationFilter; +import org.springframework.ai.vectorstore.observation.VectorStoreQueryResponseContentObservationFilter; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; + +/** + * Auto-configuration for Spring AI vector store observations. + * + * @author Christian Tzolov + * @since 1.0.0 + */ +@AutoConfiguration( + afterName = { "org.springframework.boot.actuate.autoconfigure.observation.ObservationAutoConfiguration" }) +@ConditionalOnClass(VectorStore.class) +@EnableConfigurationProperties({ VectorStoreObservationProperties.class }) +public class VectorStoreObservationAutoConfiguration { + + private static final Logger logger = LoggerFactory.getLogger(VectorStoreObservationAutoConfiguration.class); + + @Bean + @ConditionalOnMissingBean + @ConditionalOnProperty(prefix = VectorStoreObservationProperties.CONFIG_PREFIX, name = "include-query-response", + havingValue = "true") + VectorStoreQueryResponseContentObservationFilter vectorStoreQueryResponseContentObservationFilter() { + logger.warn( + "You have enabled the inclusion of the query response content in the observations, with the risk of exposing sensitive or private information. Please, be careful!"); + return new VectorStoreQueryResponseContentObservationFilter(); + } + + @Bean + @ConditionalOnMissingBean + @ConditionalOnProperty(prefix = VectorStoreObservationProperties.CONFIG_PREFIX, name = "include-add-request", + havingValue = "true") + VectorStoreAddRequestContentObservationFilter vectorStoreAddRequestContentObservationFilter() { + logger.warn( + "You have enabled the inclusion of the add request content in the observations, with the risk of exposing sensitive or private information. Please, be careful!"); + return new VectorStoreAddRequestContentObservationFilter(); + } + + @Bean + @ConditionalOnMissingBean + @ConditionalOnProperty(prefix = VectorStoreObservationProperties.CONFIG_PREFIX, name = "include-delete-request", + havingValue = "true") + VectorStoreDeleteRequestContentObservationFilter vectorStoreDeleteRequestContentObservationFilter() { + return new VectorStoreDeleteRequestContentObservationFilter(); + } + +} diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/observation/VectorStoreObservationProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/observation/VectorStoreObservationProperties.java new file mode 100644 index 00000000000..1177bc3a2ac --- /dev/null +++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/observation/VectorStoreObservationProperties.java @@ -0,0 +1,70 @@ +/* + * 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.autoconfigure.vectorstore.observation; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * Configuration properties for vector store observations. + * + * @author Christian Tzolov + * @since 1.0.0 + */ +@ConfigurationProperties(VectorStoreObservationProperties.CONFIG_PREFIX) +public class VectorStoreObservationProperties { + + public static final String CONFIG_PREFIX = "spring.ai.vector.store.observations"; + + /** + * Whether to include the search response content in the observations. + */ + private boolean includeQueryResponse = false; + + /** + * Whether to include the add request content in the observations. + */ + private boolean includeAddRequest = false; + + /** + * Whether to include the delete request content in the observations. + */ + private boolean includeDeleteRequest = false; + + public boolean isIncludeQueryResponse() { + return this.includeQueryResponse; + } + + public void setIncludeQueryResponse(boolean includeQueryResponse) { + this.includeQueryResponse = includeQueryResponse; + } + + public boolean isIncludeAddRequest() { + return this.includeAddRequest; + } + + public void setIncludeAddRequest(boolean includeAddRequest) { + this.includeAddRequest = includeAddRequest; + } + + public boolean isIncludeDeleteRequest() { + return this.includeDeleteRequest; + } + + public void setIncludeDeleteRequest(boolean includeDeleteRequest) { + this.includeDeleteRequest = includeDeleteRequest; + } + +} diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/observation/package-info.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/observation/package-info.java new file mode 100644 index 00000000000..347dd6a3ef9 --- /dev/null +++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/observation/package-info.java @@ -0,0 +1,22 @@ +/* + * 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. + */ + +@NonNullApi +@NonNullFields +package org.springframework.ai.autoconfigure.vectorstore.observation; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/pgvector/PgVectorStoreAutoConfiguration.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/pgvector/PgVectorStoreAutoConfiguration.java index 7a81d9a5456..4c8746527b0 100644 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/pgvector/PgVectorStoreAutoConfiguration.java +++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/pgvector/PgVectorStoreAutoConfiguration.java @@ -19,6 +19,8 @@ import org.springframework.ai.embedding.EmbeddingModel; import org.springframework.ai.vectorstore.PgVectorStore; +import org.springframework.ai.vectorstore.observation.VectorStoreObservationConvention; +import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; @@ -27,6 +29,8 @@ import org.springframework.context.annotation.Bean; import org.springframework.jdbc.core.JdbcTemplate; +import io.micrometer.observation.ObservationRegistry; + /** * @author Christian Tzolov * @author Josh Long @@ -39,7 +43,9 @@ public class PgVectorStoreAutoConfiguration { @Bean @ConditionalOnMissingBean public PgVectorStore vectorStore(JdbcTemplate jdbcTemplate, EmbeddingModel embeddingModel, - PgVectorStoreProperties properties) { + PgVectorStoreProperties properties, ObjectProvider observationRegistry, + ObjectProvider customSearchObservationConvention) { + var initializeSchema = properties.isInitializeSchema(); return new PgVectorStore.Builder(jdbcTemplate, embeddingModel).withSchemaName(properties.getSchemaName()) @@ -50,6 +56,8 @@ public PgVectorStore vectorStore(JdbcTemplate jdbcTemplate, EmbeddingModel embed .withRemoveExistingVectorStoreTable(properties.isRemoveExistingVectorStoreTable()) .withIndexType(properties.getIndexType()) .withInitializeSchema(initializeSchema) + .withObservationRegistry(observationRegistry.getIfUnique(() -> ObservationRegistry.NOOP)) + .withSearchObservationConvention(customSearchObservationConvention.getIfAvailable(() -> null)) .build(); } diff --git a/spring-ai-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/spring-ai-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports index 1c74d1a0a7f..9fe46cc2609 100644 --- a/spring-ai-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ b/spring-ai-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -46,3 +46,4 @@ org.springframework.ai.autoconfigure.qianfan.QianFanAutoConfiguration org.springframework.ai.autoconfigure.minimax.MiniMaxAutoConfiguration org.springframework.ai.autoconfigure.vertexai.embedding.VertexAiEmbeddingAutoConfiguration org.springframework.ai.autoconfigure.chat.memory.cassandra.CassandraChatMemoryAutoConfiguration +org.springframework.ai.autoconfigure.vectorstore.observation.VectorStoreObservationAutoConfiguration 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 aa7966e767f..2f9f9fe243d 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 @@ -29,6 +29,9 @@ import org.springframework.ai.document.Document; import org.springframework.ai.embedding.EmbeddingModel; 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.jdbc.core.BatchPreparedStatementSetter; import org.springframework.jdbc.core.JdbcTemplate; @@ -42,6 +45,8 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.pgvector.PGvector; +import io.micrometer.observation.ObservationRegistry; + /** * Uses the "vector_store" table to store the Spring AI vector data. The table and the * vector index will be auto-created if not available. @@ -50,7 +55,7 @@ * @author Josh Long * @author Muthukumaran Navaneethakrishnan */ -public class PgVectorStore implements VectorStore, InitializingBean { +public class PgVectorStore extends AbstractObservationVectorStore implements InitializingBean { private static final Logger logger = LoggerFactory.getLogger(PgVectorStore.class); @@ -124,6 +129,19 @@ private PgVectorStore(String schemaName, String vectorTableName, boolean vectorT JdbcTemplate jdbcTemplate, EmbeddingModel embeddingModel, int dimensions, PgDistanceType distanceType, boolean removeExistingVectorStoreTable, PgIndexType createIndexMethod, boolean initializeSchema) { + this(schemaName, vectorTableName, vectorTableValidationsEnabled, jdbcTemplate, embeddingModel, dimensions, + distanceType, removeExistingVectorStoreTable, createIndexMethod, initializeSchema, + ObservationRegistry.NOOP, null); + } + + 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) { + + super(observationRegistry, customSearchObservationConvention); + this.vectorTableName = (null == vectorTableName || vectorTableName.isEmpty()) ? DEFAULT_TABLE_NAME : vectorTableName.trim(); logger.info("Using the vector table name: {}", @@ -150,7 +168,7 @@ public PgDistanceType getDistanceType() { } @Override - public void add(List documents) { + public void doAdd(List documents) { int size = documents.size(); @@ -205,7 +223,7 @@ private float[] toFloatArray(List embedding) { } @Override - public Optional delete(List idList) { + public Optional doDelete(List idList) { int updateCount = 0; for (String id : idList) { int count = jdbcTemplate.update("DELETE FROM " + getFullyQualifiedTableName() + " WHERE id = ?", @@ -217,7 +235,7 @@ public Optional delete(List idList) { } @Override - public List similaritySearch(SearchRequest request) { + public List doSimilaritySearch(SearchRequest request) { String nativeFilterExpression = (request.getFilterExpression() != null) ? this.filterExpressionConverter.convertExpression(request.getFilterExpression()) : ""; @@ -484,6 +502,11 @@ public static class Builder { private boolean initializeSchema; + private ObservationRegistry observationRegistry = ObservationRegistry.NOOP; + + @Nullable + private VectorStoreObservationConvention searchObservationConvention; + // Builder constructor with mandatory parameters public Builder(JdbcTemplate jdbcTemplate, EmbeddingModel embeddingModel) { if (jdbcTemplate == null || embeddingModel == null) { @@ -533,12 +556,35 @@ public Builder withInitializeSchema(boolean initializeSchema) { return this; } + public Builder withObservationRegistry(ObservationRegistry observationRegistry) { + this.observationRegistry = observationRegistry; + return this; + } + + public Builder withSearchObservationConvention( + VectorStoreObservationConvention customSearchObservationConvention) { + this.searchObservationConvention = customSearchObservationConvention; + return this; + } + public PgVectorStore build() { return new PgVectorStore(schemaName, vectorTableName, vectorTableValidationsEnabled, jdbcTemplate, embeddingModel, dimensions, distanceType, removeExistingVectorStoreTable, indexType, - initializeSchema); + initializeSchema, observationRegistry, searchObservationConvention); } } + @Override + public VectorStoreObservationContext.Builder createObservationContextBuilder() { + + return VectorStoreObservationContext.builder("pg_vector") + .withDimensions(this.embeddingDimensions()) + .withCollectionName(this.vectorTableName) + .withNamespace(this.schemaName) + .withSimilarityMetric(this.getDistanceType().name()) + .withIndexName(this.createIndexMethod.name()) + .withModel(this.embeddingModel.getClass().getSimpleName()); + } + } \ No newline at end of file From 629e0809f13be614fd47db5ddc2545b87589ce14 Mon Sep 17 00:00:00 2001 From: Christian Tzolov Date: Sun, 11 Aug 2024 10:20:43 +0200 Subject: [PATCH 2/8] Add VectorStoreObservationContext.Operation enum with ADD, DELETE and QUERY values. Add tests for the VectorStore contex, convention and filters. --- .../AbstractObservationVectorStore.java | 15 +- ...faultVectorStoreObservationConvention.java | 18 +-- .../VectorStoreObservationContext.java | 76 ++++++---- .../VectorStoreObservationDocumentation.java | 2 +- ...QueryResponseContentObservationFilter.java | 4 +- ...VectorStoreObservationConventionTests.java | 136 ++++++++++++++++++ ...StoreAddRequestObservationFilterTests.java | 71 +++++++++ ...reDeleteRequestObservationFilterTests.java | 70 +++++++++ .../VectorStoreObservationContextTests.java | 52 +++++++ ...reQueryResponseObservationFilterTests.java | 71 +++++++++ .../ai/vectorstore/PgVectorStore.java | 4 +- 11 files changed, 469 insertions(+), 50 deletions(-) create mode 100644 spring-ai-core/src/test/java/org/springframework/ai/vectorstore/observation/DefaultVectorStoreObservationConventionTests.java create mode 100644 spring-ai-core/src/test/java/org/springframework/ai/vectorstore/observation/VectorStoreAddRequestObservationFilterTests.java create mode 100644 spring-ai-core/src/test/java/org/springframework/ai/vectorstore/observation/VectorStoreDeleteRequestObservationFilterTests.java create mode 100644 spring-ai-core/src/test/java/org/springframework/ai/vectorstore/observation/VectorStoreObservationContextTests.java create mode 100644 spring-ai-core/src/test/java/org/springframework/ai/vectorstore/observation/VectorStoreQueryResponseObservationFilterTests.java diff --git a/spring-ai-core/src/main/java/org/springframework/ai/vectorstore/observation/AbstractObservationVectorStore.java b/spring-ai-core/src/main/java/org/springframework/ai/vectorstore/observation/AbstractObservationVectorStore.java index 3668d81c2ce..7b175bb62f7 100644 --- a/spring-ai-core/src/main/java/org/springframework/ai/vectorstore/observation/AbstractObservationVectorStore.java +++ b/spring-ai-core/src/main/java/org/springframework/ai/vectorstore/observation/AbstractObservationVectorStore.java @@ -55,9 +55,8 @@ public AbstractObservationVectorStore(ObservationRegistry observationRegistry, @Override public void add(List documents) { - VectorStoreObservationContext observationContext = this.createObservationContextBuilder() + VectorStoreObservationContext observationContext = this.createObservationContextBuilder("add") .withAddRequest(documents) - .withOperationName("add") .build(); VectorStoreObservationDocumentation.AI_VECTOR_STORE @@ -69,9 +68,8 @@ public void add(List documents) { @Override public Optional delete(List deleteDocIds) { - VectorStoreObservationContext observationContext = this.createObservationContextBuilder() + VectorStoreObservationContext observationContext = this.createObservationContextBuilder("delete") .withDeleteRequest(deleteDocIds) - .withOperationName("delete") .build(); return VectorStoreObservationDocumentation.AI_VECTOR_STORE @@ -83,9 +81,8 @@ public Optional delete(List deleteDocIds) { @Override public List similaritySearch(SearchRequest request) { - VectorStoreObservationContext searchObservationContext = this.createObservationContextBuilder() - .withSearchRequest(request) - .withOperationName("search") + VectorStoreObservationContext searchObservationContext = this.createObservationContextBuilder("search") + .withQueryRequest(request) .build(); return VectorStoreObservationDocumentation.AI_VECTOR_STORE @@ -93,7 +90,7 @@ public List similaritySearch(SearchRequest request) { () -> searchObservationContext, this.observationRegistry) .observe(() -> { var documents = this.doSimilaritySearch(request); - searchObservationContext.setSearchResponse(documents); + searchObservationContext.setQueryResponse(documents); return documents; }); } @@ -104,6 +101,6 @@ public List similaritySearch(SearchRequest request) { abstract public List doSimilaritySearch(SearchRequest request); - abstract public VectorStoreObservationContext.Builder createObservationContextBuilder(); + abstract public VectorStoreObservationContext.Builder createObservationContextBuilder(String operationName); } \ No newline at end of file diff --git a/spring-ai-core/src/main/java/org/springframework/ai/vectorstore/observation/DefaultVectorStoreObservationConvention.java b/spring-ai-core/src/main/java/org/springframework/ai/vectorstore/observation/DefaultVectorStoreObservationConvention.java index b78f0f81c5f..731a25b4d39 100644 --- a/spring-ai-core/src/main/java/org/springframework/ai/vectorstore/observation/DefaultVectorStoreObservationConvention.java +++ b/spring-ai-core/src/main/java/org/springframework/ai/vectorstore/observation/DefaultVectorStoreObservationConvention.java @@ -30,14 +30,14 @@ public class DefaultVectorStoreObservationConvention implements VectorStoreObservationConvention { - private static final String DEFAULT_NAME = "spring.ai.vector.store"; + public static final String DEFAULT_NAME = "spring.ai.vector.store"; private static final KeyValue DIMENSIONS_NONE = KeyValue.of(HighCardinalityKeyNames.DIMENSIONS, KeyValue.NONE_VALUE); private static final KeyValue QUERY_NONE = KeyValue.of(HighCardinalityKeyNames.QUERY, KeyValue.NONE_VALUE); - private static final KeyValue METADATA_FILTER_NONE = KeyValue.of(HighCardinalityKeyNames.METADATA_FILTER, + private static final KeyValue METADATA_FILTER_NONE = KeyValue.of(HighCardinalityKeyNames.QUERY_METADATA_FILTER, KeyValue.NONE_VALUE); private static final KeyValue TOP_K_NONE = KeyValue.of(HighCardinalityKeyNames.TOP_K, KeyValue.NONE_VALUE); @@ -105,23 +105,23 @@ protected KeyValue dimensions(VectorStoreObservationContext context) { } protected KeyValue query(VectorStoreObservationContext context) { - if (context.getSearchRequest() != null && StringUtils.hasText(context.getSearchRequest().getQuery())) { - return KeyValue.of(HighCardinalityKeyNames.QUERY, "" + context.getSearchRequest().getQuery()); + if (context.getQueryRequest() != null && StringUtils.hasText(context.getQueryRequest().getQuery())) { + return KeyValue.of(HighCardinalityKeyNames.QUERY, "" + context.getQueryRequest().getQuery()); } return QUERY_NONE; } protected KeyValue metadataFilter(VectorStoreObservationContext context) { - if (context.getSearchRequest() != null && context.getSearchRequest().getFilterExpression() != null) { - return KeyValue.of(HighCardinalityKeyNames.METADATA_FILTER, - "" + context.getSearchRequest().getFilterExpression().toString()); + if (context.getQueryRequest() != null && context.getQueryRequest().getFilterExpression() != null) { + return KeyValue.of(HighCardinalityKeyNames.QUERY_METADATA_FILTER, + "" + context.getQueryRequest().getFilterExpression().toString()); } return METADATA_FILTER_NONE; } protected KeyValue topK(VectorStoreObservationContext context) { - if (context.getSearchRequest() != null && context.getSearchRequest().getTopK() > 0) { - return KeyValue.of(HighCardinalityKeyNames.TOP_K, "" + context.getSearchRequest().getTopK()); + if (context.getQueryRequest() != null && context.getQueryRequest().getTopK() > 0) { + return KeyValue.of(HighCardinalityKeyNames.TOP_K, "" + context.getQueryRequest().getTopK()); } return TOP_K_NONE; } diff --git a/spring-ai-core/src/main/java/org/springframework/ai/vectorstore/observation/VectorStoreObservationContext.java b/spring-ai-core/src/main/java/org/springframework/ai/vectorstore/observation/VectorStoreObservationContext.java index 2ea0b812dfc..62e57a2e695 100644 --- a/spring-ai-core/src/main/java/org/springframework/ai/vectorstore/observation/VectorStoreObservationContext.java +++ b/spring-ai-core/src/main/java/org/springframework/ai/vectorstore/observation/VectorStoreObservationContext.java @@ -19,6 +19,7 @@ import org.springframework.ai.document.Document; import org.springframework.ai.vectorstore.SearchRequest; +import org.springframework.util.Assert; import io.micrometer.observation.Observation; @@ -29,6 +30,29 @@ public class VectorStoreObservationContext extends Observation.Context { + public enum Operation { + + /** + * VectorStore delete operation. + */ + ADD("add"), + /** + * VectorStore add operation. + */ + DELETE("delete"), + /** + * VectorStore similarity search operation. + */ + QUERY("query"); + + public final String value; + + Operation(String value) { + this.value = value; + } + + } + // DELETE private List deleteRequest; @@ -36,9 +60,9 @@ public class VectorStoreObservationContext extends Observation.Context { private List addRequest; // SEARCH - private SearchRequest searchRequest; + private SearchRequest queryRequest; - private List searchResponse; + private List queryResponse; // COMMON private final String databaseSystem; @@ -57,10 +81,13 @@ public class VectorStoreObservationContext extends Observation.Context { private String indexName = ""; - private String operationName; + private final String operationName; - public VectorStoreObservationContext(String databaseSystem) { + public VectorStoreObservationContext(String databaseSystem, String operationName) { + Assert.hasText(databaseSystem, "databaseSystem cannot be null or empty"); + Assert.hasText(operationName, "operationName cannot be null or empty"); this.databaseSystem = databaseSystem; + this.operationName = operationName; } public List getDeleteRequest() { @@ -79,20 +106,20 @@ public void setAddRequest(List documents) { this.addRequest = documents; } - public SearchRequest getSearchRequest() { - return this.searchRequest; + public SearchRequest getQueryRequest() { + return this.queryRequest; } - public void setSearchRequest(SearchRequest request) { - this.searchRequest = request; + public void setQueryRequest(SearchRequest request) { + this.queryRequest = request; } - public List getSearchResponse() { - return this.searchResponse; + public List getQueryResponse() { + return this.queryResponse; } - public void setSearchResponse(List documents) { - this.searchResponse = documents; + public void setQueryResponse(List documents) { + this.queryResponse = documents; } public String getDatabaseSystem() { @@ -159,20 +186,20 @@ public String getOperationName() { return this.operationName; } - public void setOperationName(String operationName) { - this.operationName = operationName; + public static Builder builder(String databaseSystem, String operationName) { + return new Builder(databaseSystem, operationName); } - public static Builder builder(String databaseSystem) { - return new Builder(databaseSystem); + public static Builder builder(String databaseSystem, Operation operation) { + return builder(databaseSystem, operation.value); } public static class Builder { private VectorStoreObservationContext context; - public Builder(String databaseSystem) { - this.context = new VectorStoreObservationContext(databaseSystem); + public Builder(String databaseSystem, String operationName) { + this.context = new VectorStoreObservationContext(databaseSystem, operationName); } public Builder withDeleteRequest(List deleteRequest) { @@ -185,13 +212,13 @@ public Builder withAddRequest(List documents) { return this; } - public Builder withSearchRequest(SearchRequest request) { - this.context.setSearchRequest(request); + public Builder withQueryRequest(SearchRequest request) { + this.context.setQueryRequest(request); return this; } - public Builder withSearchResponse(List documents) { - this.context.setSearchResponse(documents); + public Builder withQueryResponse(List documents) { + this.context.setQueryResponse(documents); return this; } @@ -230,11 +257,6 @@ public Builder withIndexName(String indexName) { return this; } - public Builder withOperationName(String operationName) { - this.context.setOperationName(operationName); - return this; - } - public VectorStoreObservationContext build() { return this.context; } diff --git a/spring-ai-core/src/main/java/org/springframework/ai/vectorstore/observation/VectorStoreObservationDocumentation.java b/spring-ai-core/src/main/java/org/springframework/ai/vectorstore/observation/VectorStoreObservationDocumentation.java index 7d8c0fc8da2..0acce21480a 100644 --- a/spring-ai-core/src/main/java/org/springframework/ai/vectorstore/observation/VectorStoreObservationDocumentation.java +++ b/spring-ai-core/src/main/java/org/springframework/ai/vectorstore/observation/VectorStoreObservationDocumentation.java @@ -111,7 +111,7 @@ public String asString() { /** * The metadata filters used in the query. */ - METADATA_FILTER { + QUERY_METADATA_FILTER { @Override public String asString() { return "db.query.metadata.filter"; diff --git a/spring-ai-core/src/main/java/org/springframework/ai/vectorstore/observation/VectorStoreQueryResponseContentObservationFilter.java b/spring-ai-core/src/main/java/org/springframework/ai/vectorstore/observation/VectorStoreQueryResponseContentObservationFilter.java index 647b250d557..ed38dd25112 100644 --- a/spring-ai-core/src/main/java/org/springframework/ai/vectorstore/observation/VectorStoreQueryResponseContentObservationFilter.java +++ b/spring-ai-core/src/main/java/org/springframework/ai/vectorstore/observation/VectorStoreQueryResponseContentObservationFilter.java @@ -38,12 +38,12 @@ public Observation.Context map(Observation.Context context) { return context; } - if (CollectionUtils.isEmpty(observationContext.getSearchResponse())) { + if (CollectionUtils.isEmpty(observationContext.getQueryResponse())) { return observationContext; } StringJoiner joiner = new StringJoiner(", ", "[", "]"); - observationContext.getSearchResponse().forEach(document -> joiner.add("\"" + document.getContent() + "\"")); + observationContext.getQueryResponse().forEach(document -> joiner.add("\"" + document.getContent() + "\"")); observationContext .addHighCardinalityKeyValue(VectorStoreObservationDocumentation.HighCardinalityKeyNames.QUERY_RESPONSE diff --git a/spring-ai-core/src/test/java/org/springframework/ai/vectorstore/observation/DefaultVectorStoreObservationConventionTests.java b/spring-ai-core/src/test/java/org/springframework/ai/vectorstore/observation/DefaultVectorStoreObservationConventionTests.java new file mode 100644 index 00000000000..79bd4f2aaaf --- /dev/null +++ b/spring-ai-core/src/test/java/org/springframework/ai/vectorstore/observation/DefaultVectorStoreObservationConventionTests.java @@ -0,0 +1,136 @@ +/* + * 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.observation; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.springframework.ai.document.Document; +import org.springframework.ai.vectorstore.SearchRequest; +import org.springframework.ai.vectorstore.observation.VectorStoreObservationDocumentation.HighCardinalityKeyNames; +import org.springframework.ai.vectorstore.observation.VectorStoreObservationDocumentation.LowCardinalityKeyNames; + +import io.micrometer.common.KeyValue; +import io.micrometer.observation.Observation; + +/** + * Unit tests for {@link DefaultVectorStoreObservationConvention}. + * + * @author Christian Tzolov + */ +class DefaultVectorStoreObservationConventionTests { + + private final DefaultVectorStoreObservationConvention observationConvention = new DefaultVectorStoreObservationConvention(); + + @Test + void shouldHaveName() { + assertThat(this.observationConvention.getName()) + .isEqualTo(DefaultVectorStoreObservationConvention.DEFAULT_NAME); + } + + @Test + void shouldHaveContextualName() { + VectorStoreObservationContext observationContext = VectorStoreObservationContext + .builder("my-database", VectorStoreObservationContext.Operation.QUERY) + .build(); + assertThat(this.observationConvention.getContextualName(observationContext)) + .isEqualTo("vector_db my-database query"); + } + + @Test + void supportsOnlyVectorStoreObservationContext() { + VectorStoreObservationContext observationContext = VectorStoreObservationContext + .builder("my-database", VectorStoreObservationContext.Operation.QUERY) + .build(); + assertThat(this.observationConvention.supportsContext(observationContext)).isTrue(); + assertThat(this.observationConvention.supportsContext(new Observation.Context())).isFalse(); + } + + @Test + void shouldHaveRequiredKeyValues() { + VectorStoreObservationContext observationContext = VectorStoreObservationContext + .builder("my_database", VectorStoreObservationContext.Operation.QUERY) + .build(); + assertThat(this.observationConvention.getLowCardinalityKeyValues(observationContext)).contains( + KeyValue.of(LowCardinalityKeyNames.DB_OPERATION_NAME.asString(), "query"), + KeyValue.of(LowCardinalityKeyNames.DB_SYSTEM.asString(), "my_database")); + } + + @Test + void shouldHaveOptionalKeyValues() { + + List addDocs = List.of(new Document("addDoc1"), new Document("addDoc2")); + List deleteIds = List.of("id1", "id2"); + + VectorStoreObservationContext observationContext = VectorStoreObservationContext + .builder("my-database", VectorStoreObservationContext.Operation.QUERY) + .withCollectionName("COLLECTION_NAME") + .withDimensions(696) + .withFieldName("FIELD_NAME") + .withIndexName("INDEX_NAME") + .withNamespace("NAMESPACE") + .withSimilarityMetric("SIMILARITY_METRIC") + .withAddRequest(addDocs) + .withDeleteRequest(deleteIds) + .withQueryRequest(SearchRequest.query("VDB QUERY").withFilterExpression("country == 'UK' && year >= 2020")) + .build(); + + List queryResponseDocs = List.of(new Document("doc1"), new Document("doc2")); + + observationContext.setQueryResponse(queryResponseDocs); + + assertThat(this.observationConvention.getLowCardinalityKeyValues(observationContext)) + .contains(KeyValue.of(LowCardinalityKeyNames.DB_OPERATION_NAME.asString(), + VectorStoreObservationContext.Operation.QUERY.value)); + + // Optional, filter only added content + assertThat(this.observationConvention.getHighCardinalityKeyValues(observationContext)).doesNotContain( + KeyValue.of(HighCardinalityKeyNames.ADD_REQUEST, "[addDoc1,addDoc2]"), + KeyValue.of(HighCardinalityKeyNames.DELETE_REQUEST, "[id1,id2]"), + KeyValue.of(HighCardinalityKeyNames.QUERY_RESPONSE, "[doc1,doc2]")); + + assertThat(this.observationConvention.getHighCardinalityKeyValues(observationContext)).contains( + KeyValue.of(HighCardinalityKeyNames.COLLECTION_NAME.asString(), "COLLECTION_NAME"), + KeyValue.of(HighCardinalityKeyNames.DIMENSIONS.asString(), "696"), + KeyValue.of(HighCardinalityKeyNames.FIELD_NAME.asString(), "FIELD_NAME"), + KeyValue.of(HighCardinalityKeyNames.INDEX_NAME.asString(), "INDEX_NAME"), + KeyValue.of(HighCardinalityKeyNames.NAMESPACE.asString(), "NAMESPACE"), + KeyValue.of(HighCardinalityKeyNames.SIMILARITY_METRIC.asString(), "SIMILARITY_METRIC"), + KeyValue.of(HighCardinalityKeyNames.QUERY.asString(), "VDB QUERY"), + KeyValue.of(HighCardinalityKeyNames.QUERY_METADATA_FILTER.asString(), + "Expression[type=AND, left=Expression[type=EQ, left=Key[key=country], right=Value[value=UK]], right=Expression[type=GTE, left=Key[key=year], right=Value[value=2020]]]")); + } + + @Test + void shouldHaveMissingKeyValues() { + VectorStoreObservationContext observationContext = VectorStoreObservationContext + .builder("my-database", VectorStoreObservationContext.Operation.QUERY) + .build(); + + assertThat(this.observationConvention.getHighCardinalityKeyValues(observationContext)).contains( + KeyValue.of(HighCardinalityKeyNames.COLLECTION_NAME.asString(), KeyValue.NONE_VALUE), + KeyValue.of(HighCardinalityKeyNames.DIMENSIONS.asString(), KeyValue.NONE_VALUE), + KeyValue.of(HighCardinalityKeyNames.FIELD_NAME.asString(), KeyValue.NONE_VALUE), + KeyValue.of(HighCardinalityKeyNames.INDEX_NAME.asString(), KeyValue.NONE_VALUE), + KeyValue.of(HighCardinalityKeyNames.NAMESPACE.asString(), KeyValue.NONE_VALUE), + KeyValue.of(HighCardinalityKeyNames.SIMILARITY_METRIC.asString(), KeyValue.NONE_VALUE), + KeyValue.of(HighCardinalityKeyNames.QUERY.asString(), KeyValue.NONE_VALUE), + KeyValue.of(HighCardinalityKeyNames.QUERY_METADATA_FILTER.asString(), KeyValue.NONE_VALUE)); + } + +} diff --git a/spring-ai-core/src/test/java/org/springframework/ai/vectorstore/observation/VectorStoreAddRequestObservationFilterTests.java b/spring-ai-core/src/test/java/org/springframework/ai/vectorstore/observation/VectorStoreAddRequestObservationFilterTests.java new file mode 100644 index 00000000000..9e748f4c2bc --- /dev/null +++ b/spring-ai-core/src/test/java/org/springframework/ai/vectorstore/observation/VectorStoreAddRequestObservationFilterTests.java @@ -0,0 +1,71 @@ +/* + * 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.observation; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.springframework.ai.document.Document; +import org.springframework.ai.vectorstore.observation.VectorStoreObservationDocumentation.HighCardinalityKeyNames; + +import io.micrometer.common.KeyValue; +import io.micrometer.observation.Observation; + +/** + * Unit tests for {@link VectorStoreAddRequestContentObservationFilter}. + * + * @author Christian Tzolov + */ +class VectorStoreAddRequestObservationFilterTests { + + private final VectorStoreAddRequestContentObservationFilter observationFilter = new VectorStoreAddRequestContentObservationFilter(); + + @Test + void whenNotSupportedObservationContextThenReturnOriginalContext() { + var expectedContext = new Observation.Context(); + var actualContext = observationFilter.map(expectedContext); + + assertThat(actualContext).isEqualTo(expectedContext); + } + + @Test + void whenEmptyQueryResponseThenReturnOriginalContext() { + var expectedContext = VectorStoreObservationContext.builder("db", VectorStoreObservationContext.Operation.ADD) + .build(); + + var actualContext = observationFilter.map(expectedContext); + + assertThat(actualContext).isEqualTo(expectedContext); + } + + @Test + void whenNonEmptyQueryResponseThenAugmentContext() { + var expectedContext = VectorStoreObservationContext.builder("db", VectorStoreObservationContext.Operation.ADD) + .build(); + + List addRequestContent = List.of(new Document("doc1"), new Document("doc2")); + + expectedContext.setAddRequest(addRequestContent); + + var augmentedContext = observationFilter.map(expectedContext); + + assertThat(augmentedContext.getHighCardinalityKeyValues()) + .contains(KeyValue.of(HighCardinalityKeyNames.ADD_REQUEST.asString(), "[\"doc1\", \"doc2\"]")); + } + +} diff --git a/spring-ai-core/src/test/java/org/springframework/ai/vectorstore/observation/VectorStoreDeleteRequestObservationFilterTests.java b/spring-ai-core/src/test/java/org/springframework/ai/vectorstore/observation/VectorStoreDeleteRequestObservationFilterTests.java new file mode 100644 index 00000000000..7d96d7d601d --- /dev/null +++ b/spring-ai-core/src/test/java/org/springframework/ai/vectorstore/observation/VectorStoreDeleteRequestObservationFilterTests.java @@ -0,0 +1,70 @@ +/* + * 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.observation; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.springframework.ai.vectorstore.observation.VectorStoreObservationDocumentation.HighCardinalityKeyNames; + +import io.micrometer.common.KeyValue; +import io.micrometer.observation.Observation; + +/** + * Unit tests for {@link VectorStoreDeleteRequestContentObservationFilter}. + * + * @author Christian Tzolov + */ +class VectorStoreDeleteRequestObservationFilterTests { + + private final VectorStoreDeleteRequestContentObservationFilter observationFilter = new VectorStoreDeleteRequestContentObservationFilter(); + + @Test + void whenNotSupportedObservationContextThenReturnOriginalContext() { + var expectedContext = new Observation.Context(); + var actualContext = observationFilter.map(expectedContext); + + assertThat(actualContext).isEqualTo(expectedContext); + } + + @Test + void whenEmptyQueryResponseThenReturnOriginalContext() { + var expectedContext = VectorStoreObservationContext.builder("db", VectorStoreObservationContext.Operation.ADD) + .build(); + + var actualContext = observationFilter.map(expectedContext); + + assertThat(actualContext).isEqualTo(expectedContext); + } + + @Test + void whenNonEmptyQueryResponseThenAugmentContext() { + var expectedContext = VectorStoreObservationContext.builder("db", VectorStoreObservationContext.Operation.ADD) + .build(); + + List deleteRequestContent = List.of("id1", "id2"); + + expectedContext.setDeleteRequest(deleteRequestContent); + + var augmentedContext = observationFilter.map(expectedContext); + + assertThat(augmentedContext.getHighCardinalityKeyValues()) + .contains(KeyValue.of(HighCardinalityKeyNames.DELETE_REQUEST.asString(), "[\"id1\", \"id2\"]")); + } + +} diff --git a/spring-ai-core/src/test/java/org/springframework/ai/vectorstore/observation/VectorStoreObservationContextTests.java b/spring-ai-core/src/test/java/org/springframework/ai/vectorstore/observation/VectorStoreObservationContextTests.java new file mode 100644 index 00000000000..06d54315135 --- /dev/null +++ b/spring-ai-core/src/test/java/org/springframework/ai/vectorstore/observation/VectorStoreObservationContextTests.java @@ -0,0 +1,52 @@ +/* + * 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.observation; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import org.junit.jupiter.api.Test; + +/** + * Unit tests for {@link VectorStoreObservationContext}. + * + * @author Christian Tzolov + */ +class VectorStoreObservationContextTests { + + @Test + void whenMandatoryFieldsThenReturn() { + var observationContext = VectorStoreObservationContext + .builder("db", VectorStoreObservationContext.Operation.ADD) + .build(); + assertThat(observationContext).isNotNull(); + } + + @Test + void whenDbSystemIsNullThenThrow() { + assertThatThrownBy(() -> VectorStoreObservationContext.builder(null, "delete").build()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("databaseSystem cannot be null or empty"); + } + + @Test + void whenOperationNameIsNullThenThrow() { + assertThatThrownBy(() -> VectorStoreObservationContext.builder("Db", "").build()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("operationName cannot be null or empty"); + } + +} diff --git a/spring-ai-core/src/test/java/org/springframework/ai/vectorstore/observation/VectorStoreQueryResponseObservationFilterTests.java b/spring-ai-core/src/test/java/org/springframework/ai/vectorstore/observation/VectorStoreQueryResponseObservationFilterTests.java new file mode 100644 index 00000000000..17441d81020 --- /dev/null +++ b/spring-ai-core/src/test/java/org/springframework/ai/vectorstore/observation/VectorStoreQueryResponseObservationFilterTests.java @@ -0,0 +1,71 @@ +/* + * 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.observation; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.springframework.ai.document.Document; +import org.springframework.ai.vectorstore.observation.VectorStoreObservationDocumentation.HighCardinalityKeyNames; + +import io.micrometer.common.KeyValue; +import io.micrometer.observation.Observation; + +/** + * Unit tests for {@link VectorStoreQueryResponseContentObservationFilter}. + * + * @author Christian Tzolov + */ +class VectorStoreQueryResponseObservationFilterTests { + + private final VectorStoreQueryResponseContentObservationFilter observationFilter = new VectorStoreQueryResponseContentObservationFilter(); + + @Test + void whenNotSupportedObservationContextThenReturnOriginalContext() { + var expectedContext = new Observation.Context(); + var actualContext = observationFilter.map(expectedContext); + + assertThat(actualContext).isEqualTo(expectedContext); + } + + @Test + void whenEmptyQueryResponseThenReturnOriginalContext() { + var expectedContext = VectorStoreObservationContext.builder("db", VectorStoreObservationContext.Operation.ADD) + .build(); + + var actualContext = observationFilter.map(expectedContext); + + assertThat(actualContext).isEqualTo(expectedContext); + } + + @Test + void whenNonEmptyQueryResponseThenAugmentContext() { + var expectedContext = VectorStoreObservationContext.builder("db", VectorStoreObservationContext.Operation.ADD) + .build(); + + List queryResponseDocs = List.of(new Document("doc1"), new Document("doc2")); + + expectedContext.setQueryResponse(queryResponseDocs); + + var augmentedContext = observationFilter.map(expectedContext); + + assertThat(augmentedContext.getHighCardinalityKeyValues()) + .contains(KeyValue.of(HighCardinalityKeyNames.QUERY_RESPONSE.asString(), "[\"doc1\", \"doc2\"]")); + } + +} 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 2f9f9fe243d..510b0b05caa 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 @@ -576,9 +576,9 @@ public PgVectorStore build() { } @Override - public VectorStoreObservationContext.Builder createObservationContextBuilder() { + public VectorStoreObservationContext.Builder createObservationContextBuilder(String operationName) { - return VectorStoreObservationContext.builder("pg_vector") + return VectorStoreObservationContext.builder("pg_vector", operationName) .withDimensions(this.embeddingDimensions()) .withCollectionName(this.vectorTableName) .withNamespace(this.schemaName) From 5dfbfb8b41f74ca2a52146bedcb030bf61008385 Mon Sep 17 00:00:00 2001 From: Christian Tzolov Date: Sun, 11 Aug 2024 10:28:29 +0200 Subject: [PATCH 3/8] Add VectorStoreObservationAutoConfiguration tests --- ...rStoreQueryResponseObservationFilter.java} | 2 +- ...reQueryResponseObservationFilterTests.java | 4 +- ...ctorStoreObservationAutoConfiguration.java | 6 +- ...toreObservationAutoConfigurationTests.java | 82 +++++++++++++++++++ 4 files changed, 88 insertions(+), 6 deletions(-) rename spring-ai-core/src/main/java/org/springframework/ai/vectorstore/observation/{VectorStoreQueryResponseContentObservationFilter.java => VectorStoreQueryResponseObservationFilter.java} (94%) create mode 100644 spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/vectorstore/observation/VectorStoreObservationAutoConfigurationTests.java diff --git a/spring-ai-core/src/main/java/org/springframework/ai/vectorstore/observation/VectorStoreQueryResponseContentObservationFilter.java b/spring-ai-core/src/main/java/org/springframework/ai/vectorstore/observation/VectorStoreQueryResponseObservationFilter.java similarity index 94% rename from spring-ai-core/src/main/java/org/springframework/ai/vectorstore/observation/VectorStoreQueryResponseContentObservationFilter.java rename to spring-ai-core/src/main/java/org/springframework/ai/vectorstore/observation/VectorStoreQueryResponseObservationFilter.java index ed38dd25112..0855d278a21 100644 --- a/spring-ai-core/src/main/java/org/springframework/ai/vectorstore/observation/VectorStoreQueryResponseContentObservationFilter.java +++ b/spring-ai-core/src/main/java/org/springframework/ai/vectorstore/observation/VectorStoreQueryResponseObservationFilter.java @@ -29,7 +29,7 @@ * @author Christian Tzolov * @since 1.0.0 */ -public class VectorStoreQueryResponseContentObservationFilter implements ObservationFilter { +public class VectorStoreQueryResponseObservationFilter implements ObservationFilter { @Override public Observation.Context map(Observation.Context context) { diff --git a/spring-ai-core/src/test/java/org/springframework/ai/vectorstore/observation/VectorStoreQueryResponseObservationFilterTests.java b/spring-ai-core/src/test/java/org/springframework/ai/vectorstore/observation/VectorStoreQueryResponseObservationFilterTests.java index 17441d81020..08ab42835c8 100644 --- a/spring-ai-core/src/test/java/org/springframework/ai/vectorstore/observation/VectorStoreQueryResponseObservationFilterTests.java +++ b/spring-ai-core/src/test/java/org/springframework/ai/vectorstore/observation/VectorStoreQueryResponseObservationFilterTests.java @@ -27,13 +27,13 @@ import io.micrometer.observation.Observation; /** - * Unit tests for {@link VectorStoreQueryResponseContentObservationFilter}. + * Unit tests for {@link VectorStoreQueryResponseObservationFilter}. * * @author Christian Tzolov */ class VectorStoreQueryResponseObservationFilterTests { - private final VectorStoreQueryResponseContentObservationFilter observationFilter = new VectorStoreQueryResponseContentObservationFilter(); + private final VectorStoreQueryResponseObservationFilter observationFilter = new VectorStoreQueryResponseObservationFilter(); @Test void whenNotSupportedObservationContextThenReturnOriginalContext() { diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/observation/VectorStoreObservationAutoConfiguration.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/observation/VectorStoreObservationAutoConfiguration.java index b26ad26bd51..fa02b93c6e9 100644 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/observation/VectorStoreObservationAutoConfiguration.java +++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/observation/VectorStoreObservationAutoConfiguration.java @@ -20,7 +20,7 @@ import org.springframework.ai.vectorstore.VectorStore; import org.springframework.ai.vectorstore.observation.VectorStoreAddRequestContentObservationFilter; import org.springframework.ai.vectorstore.observation.VectorStoreDeleteRequestContentObservationFilter; -import org.springframework.ai.vectorstore.observation.VectorStoreQueryResponseContentObservationFilter; +import org.springframework.ai.vectorstore.observation.VectorStoreQueryResponseObservationFilter; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; @@ -46,10 +46,10 @@ public class VectorStoreObservationAutoConfiguration { @ConditionalOnMissingBean @ConditionalOnProperty(prefix = VectorStoreObservationProperties.CONFIG_PREFIX, name = "include-query-response", havingValue = "true") - VectorStoreQueryResponseContentObservationFilter vectorStoreQueryResponseContentObservationFilter() { + VectorStoreQueryResponseObservationFilter vectorStoreQueryResponseContentObservationFilter() { logger.warn( "You have enabled the inclusion of the query response content in the observations, with the risk of exposing sensitive or private information. Please, be careful!"); - return new VectorStoreQueryResponseContentObservationFilter(); + return new VectorStoreQueryResponseObservationFilter(); } @Bean diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/vectorstore/observation/VectorStoreObservationAutoConfigurationTests.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/vectorstore/observation/VectorStoreObservationAutoConfigurationTests.java new file mode 100644 index 00000000000..39bd15a7735 --- /dev/null +++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/vectorstore/observation/VectorStoreObservationAutoConfigurationTests.java @@ -0,0 +1,82 @@ +/* + * 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.autoconfigure.vectorstore.observation; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; +import org.springframework.ai.vectorstore.observation.VectorStoreAddRequestContentObservationFilter; +import org.springframework.ai.vectorstore.observation.VectorStoreDeleteRequestContentObservationFilter; +import org.springframework.ai.vectorstore.observation.VectorStoreQueryResponseObservationFilter; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; + +/** + * Unit tests for {@link VectorStoreObservationAutoConfiguration}. + * + * @author Christian Tzolov + */ +class VectorStoreObservationAutoConfigurationTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(VectorStoreObservationAutoConfiguration.class)); + + @Test + void queryResponseFilterDefault() { + contextRunner.run(context -> { + assertThat(context).doesNotHaveBean(VectorStoreQueryResponseObservationFilter.class); + }); + } + + @Test + void queryResponseFilterFilterEnabled() { + contextRunner.withPropertyValues("spring.ai.vector.store.observations.include-query-response=true") + .run(context -> { + assertThat(context).hasSingleBean(VectorStoreQueryResponseObservationFilter.class); + }); + } + + @Test + void deleteRequestFilterDefault() { + contextRunner.run(context -> { + assertThat(context).doesNotHaveBean(VectorStoreDeleteRequestContentObservationFilter.class); + }); + } + + @Test + void deleteRequestFilterEnabled() { + contextRunner.withPropertyValues("spring.ai.vector.store.observations.include-delete-request=true") + .run(context -> { + assertThat(context).hasSingleBean(VectorStoreDeleteRequestContentObservationFilter.class); + }); + } + + @Test + void addRequestFilterDefault() { + contextRunner.run(context -> { + assertThat(context).doesNotHaveBean(VectorStoreAddRequestContentObservationFilter.class); + }); + } + + @Test + void addRequestFilterEnabled() { + contextRunner.withPropertyValues("spring.ai.vector.store.observations.include-add-request=true") + .run(context -> { + assertThat(context).hasSingleBean(VectorStoreAddRequestContentObservationFilter.class); + }); + } + +} From b005d5b68b571c0044272864ff39bfd1e959ed0e Mon Sep 17 00:00:00 2001 From: Christian Tzolov Date: Sun, 11 Aug 2024 11:36:24 +0200 Subject: [PATCH 4/8] code cleaning --- .../VectorStoreObservationDocumentation.java | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/spring-ai-core/src/main/java/org/springframework/ai/vectorstore/observation/VectorStoreObservationDocumentation.java b/spring-ai-core/src/main/java/org/springframework/ai/vectorstore/observation/VectorStoreObservationDocumentation.java index 0acce21480a..5f0b48847b6 100644 --- a/spring-ai-core/src/main/java/org/springframework/ai/vectorstore/observation/VectorStoreObservationDocumentation.java +++ b/spring-ai-core/src/main/java/org/springframework/ai/vectorstore/observation/VectorStoreObservationDocumentation.java @@ -27,7 +27,7 @@ public enum VectorStoreObservationDocumentation implements ObservationDocumentation { /** - * AI Chat Model observations for clients. + * Vector Store observations for clients. */ AI_VECTOR_STORE { @Override @@ -189,16 +189,6 @@ public String asString() { return "db.index.name"; } } - // , - // /** - // * The number of queries included in a batch operation. - // */ - // BATCH_SIZE { - // @Override - // public String asString() { - // return "db.operation.batch.size"; - // } - // } } From 9196feaba38151805c504a88ade90da9b2c27166 Mon Sep 17 00:00:00 2001 From: Christian Tzolov Date: Sun, 11 Aug 2024 19:02:27 +0200 Subject: [PATCH 5/8] Add vector_store Spring AI kind --- .../DefaultVectorStoreObservationConvention.java | 11 +++++++++-- .../VectorStoreObservationDocumentation.java | 9 +++++++++ .../DefaultVectorStoreObservationConventionTests.java | 2 +- 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/spring-ai-core/src/main/java/org/springframework/ai/vectorstore/observation/DefaultVectorStoreObservationConvention.java b/spring-ai-core/src/main/java/org/springframework/ai/vectorstore/observation/DefaultVectorStoreObservationConvention.java index 731a25b4d39..69c4192739c 100644 --- a/spring-ai-core/src/main/java/org/springframework/ai/vectorstore/observation/DefaultVectorStoreObservationConvention.java +++ b/spring-ai-core/src/main/java/org/springframework/ai/vectorstore/observation/DefaultVectorStoreObservationConvention.java @@ -32,6 +32,8 @@ public class DefaultVectorStoreObservationConvention implements VectorStoreObser public static final String DEFAULT_NAME = "spring.ai.vector.store"; + private static final String VECTOR_STORE_SPRING_AI_KIND = "vector_store"; + private static final KeyValue DIMENSIONS_NONE = KeyValue.of(HighCardinalityKeyNames.DIMENSIONS, KeyValue.NONE_VALUE); @@ -74,12 +76,13 @@ public String getName() { @Override @Nullable public String getContextualName(VectorStoreObservationContext context) { - return "vector_db " + context.getDatabaseSystem() + " " + context.getOperationName(); + return "%s %s %s".formatted(VECTOR_STORE_SPRING_AI_KIND, context.getDatabaseSystem(), + context.getOperationName()); } @Override public KeyValues getLowCardinalityKeyValues(VectorStoreObservationContext context) { - return KeyValues.of(dbSystem(context), dbOperationName(context)); + return KeyValues.of(springAiKind(context), dbSystem(context), dbOperationName(context)); } @Override @@ -89,6 +92,10 @@ public KeyValues getHighCardinalityKeyValues(VectorStoreObservationContext conte indexName(context)); } + protected KeyValue springAiKind(VectorStoreObservationContext context) { + return KeyValue.of(LowCardinalityKeyNames.SPRING_AI_KIND, VECTOR_STORE_SPRING_AI_KIND); + } + protected KeyValue dbSystem(VectorStoreObservationContext context) { return KeyValue.of(LowCardinalityKeyNames.DB_SYSTEM, context.getDatabaseSystem()); } diff --git a/spring-ai-core/src/main/java/org/springframework/ai/vectorstore/observation/VectorStoreObservationDocumentation.java b/spring-ai-core/src/main/java/org/springframework/ai/vectorstore/observation/VectorStoreObservationDocumentation.java index 5f0b48847b6..8a7c41eebe5 100644 --- a/spring-ai-core/src/main/java/org/springframework/ai/vectorstore/observation/VectorStoreObservationDocumentation.java +++ b/spring-ai-core/src/main/java/org/springframework/ai/vectorstore/observation/VectorStoreObservationDocumentation.java @@ -48,6 +48,15 @@ public KeyName[] getHighCardinalityKeyNames() { public enum LowCardinalityKeyNames implements KeyName { + /** + * Spring AI kind. + */ + SPRING_AI_KIND { + @Override + public String asString() { + return "spring.ai.kind"; + } + }, /** * The name of the operation or command being executed. */ diff --git a/spring-ai-core/src/test/java/org/springframework/ai/vectorstore/observation/DefaultVectorStoreObservationConventionTests.java b/spring-ai-core/src/test/java/org/springframework/ai/vectorstore/observation/DefaultVectorStoreObservationConventionTests.java index 79bd4f2aaaf..eaa6e4a9e80 100644 --- a/spring-ai-core/src/test/java/org/springframework/ai/vectorstore/observation/DefaultVectorStoreObservationConventionTests.java +++ b/spring-ai-core/src/test/java/org/springframework/ai/vectorstore/observation/DefaultVectorStoreObservationConventionTests.java @@ -49,7 +49,7 @@ void shouldHaveContextualName() { .builder("my-database", VectorStoreObservationContext.Operation.QUERY) .build(); assertThat(this.observationConvention.getContextualName(observationContext)) - .isEqualTo("vector_db my-database query"); + .isEqualTo("vector_store my-database query"); } @Test From 5c71946d4304b054ebb9aea0ca988fe04ae64aa8 Mon Sep 17 00:00:00 2001 From: Christian Tzolov Date: Mon, 12 Aug 2024 01:19:43 +0200 Subject: [PATCH 6/8] Add observability support to the SimpleVectorStore. Add PgVectorObservationIT --- .../ai/vectorstore/SimpleVectorStore.java | 41 ++-- .../spring-ai-pgvector-store/pom.xml | 6 + .../ai/vectorstore/PgVectorObservationIT.java | 199 ++++++++++++++++++ 3 files changed, 232 insertions(+), 14 deletions(-) create mode 100644 vector-stores/spring-ai-pgvector-store/src/test/java/org/springframework/ai/vectorstore/PgVectorObservationIT.java diff --git a/spring-ai-core/src/main/java/org/springframework/ai/vectorstore/SimpleVectorStore.java b/spring-ai-core/src/main/java/org/springframework/ai/vectorstore/SimpleVectorStore.java index 7026fa8675b..1963fc1a5bf 100644 --- a/spring-ai-core/src/main/java/org/springframework/ai/vectorstore/SimpleVectorStore.java +++ b/spring-ai-core/src/main/java/org/springframework/ai/vectorstore/SimpleVectorStore.java @@ -15,16 +15,6 @@ */ package org.springframework.ai.vectorstore; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.ObjectWriter; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.ai.document.Document; -import org.springframework.ai.embedding.EmbeddingModel; -import org.springframework.core.io.Resource; - import java.io.File; import java.io.FileOutputStream; import java.io.IOException; @@ -40,6 +30,19 @@ import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; +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.observation.AbstractObservationVectorStore; +import org.springframework.ai.vectorstore.observation.VectorStoreObservationContext; +import org.springframework.core.io.Resource; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.ObjectWriter; + /** * SimpleVectorStore is a simple implementation of the VectorStore interface. * @@ -55,7 +58,7 @@ * @author Mark Pollack * @author Christian Tzolov */ -public class SimpleVectorStore implements VectorStore { +public class SimpleVectorStore extends AbstractObservationVectorStore { private static final Logger logger = LoggerFactory.getLogger(SimpleVectorStore.class); @@ -69,7 +72,7 @@ public SimpleVectorStore(EmbeddingModel embeddingModel) { } @Override - public void add(List documents) { + public void doAdd(List documents) { for (Document document : documents) { logger.info("Calling EmbeddingModel for document id = {}", document.getId()); float[] embedding = this.embeddingModel.embed(document); @@ -79,7 +82,7 @@ public void add(List documents) { } @Override - public Optional delete(List idList) { + public Optional doDelete(List idList) { for (String id : idList) { this.store.remove(id); } @@ -87,7 +90,7 @@ public Optional delete(List idList) { } @Override - public List similaritySearch(SearchRequest request) { + public List doSimilaritySearch(SearchRequest request) { if (request.getFilterExpression() != null) { throw new UnsupportedOperationException( "The [" + this.getClass() + "] doesn't support metadata filtering!"); @@ -247,4 +250,14 @@ public static float norm(float[] vector) { } + @Override + public VectorStoreObservationContext.Builder createObservationContextBuilder(String operationName) { + + return VectorStoreObservationContext.builder("simple_memory_store", operationName) + .withDimensions(this.embeddingModel.dimensions()) + .withCollectionName("in-memory-map") + .withSimilarityMetric("cosine") + .withModel(this.embeddingModel.getClass().getSimpleName()); + } + } diff --git a/vector-stores/spring-ai-pgvector-store/pom.xml b/vector-stores/spring-ai-pgvector-store/pom.xml index 0da1ef57527..2cd142626d2 100644 --- a/vector-stores/spring-ai-pgvector-store/pom.xml +++ b/vector-stores/spring-ai-pgvector-store/pom.xml @@ -88,6 +88,12 @@ test + + io.micrometer + micrometer-observation-test + test + + 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 new file mode 100644 index 00000000000..8e6fb12242a --- /dev/null +++ b/vector-stores/spring-ai-pgvector-store/src/test/java/org/springframework/ai/vectorstore/PgVectorObservationIT.java @@ -0,0 +1,199 @@ +/* + * 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.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; +import org.springframework.ai.document.Document; +import org.springframework.ai.embedding.EmbeddingModel; +import org.springframework.ai.openai.OpenAiChatModel; +import org.springframework.ai.openai.OpenAiEmbeddingModel; +import org.springframework.ai.openai.api.OpenAiApi; +import org.springframework.ai.vectorstore.PgVectorStore.PgIndexType; +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.annotation.Bean; +import org.springframework.context.annotation.Primary; +import org.springframework.core.io.DefaultResourceLoader; +import org.springframework.jdbc.core.JdbcTemplate; +import org.testcontainers.containers.PostgreSQLContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +import com.zaxxer.hikari.HikariDataSource; + +import io.micrometer.observation.ObservationRegistry; +import io.micrometer.observation.tck.TestObservationRegistry; +import io.micrometer.observation.tck.TestObservationRegistryAssert; + +/** + * Integration tests for observation instrumentation in {@link OpenAiChatModel}. + * + * @author Christian Tzolov + */ +@EnabledIfEnvironmentVariable(named = "OPENAI_API_KEY", matches = ".+") +@Testcontainers +public class PgVectorObservationIT { + + @Container + @SuppressWarnings("resource") + static PostgreSQLContainer postgresContainer = new PostgreSQLContainer<>("pgvector/pgvector:pg16") + .withUsername("postgres") + .withPassword("postgres"); + + 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) + .withPropertyValues("test.spring.ai.vectorstore.pgvector.distanceType=COSINE_DISTANCE", + + // JdbcTemplate configuration + String.format("app.datasource.url=jdbc:postgresql://%s:%d/%s", postgresContainer.getHost(), + postgresContainer.getMappedPort(5432), "postgres"), + "app.datasource.username=postgres", "app.datasource.password=postgres", + "app.datasource.type=com.zaxxer.hikari.HikariDataSource"); + + @Test + void observationForChatOperation() { + + 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 pg_vector add") + .hasLowCardinalityKeyValue(LowCardinalityKeyNames.DB_OPERATION_NAME.asString(), "add") + .hasLowCardinalityKeyValue(LowCardinalityKeyNames.DB_SYSTEM.asString(), "pg_vector") + .hasLowCardinalityKeyValue(LowCardinalityKeyNames.SPRING_AI_KIND.asString(), "vector_store") + + .hasHighCardinalityKeyValue(HighCardinalityKeyNames.QUERY.asString(), "none") + .hasHighCardinalityKeyValue(HighCardinalityKeyNames.DIMENSIONS.asString(), "1536") + .hasHighCardinalityKeyValue(HighCardinalityKeyNames.COLLECTION_NAME.asString(), "vector_store") + .hasHighCardinalityKeyValue(HighCardinalityKeyNames.NAMESPACE.asString(), "public") + .hasHighCardinalityKeyValue(HighCardinalityKeyNames.FIELD_NAME.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 pg_vector search") + .hasLowCardinalityKeyValue(LowCardinalityKeyNames.DB_OPERATION_NAME.asString(), "search") + .hasLowCardinalityKeyValue(LowCardinalityKeyNames.DB_SYSTEM.asString(), "pg_vector") + .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(), "vector_store") + .hasHighCardinalityKeyValue(HighCardinalityKeyNames.NAMESPACE.asString(), "public") + .hasHighCardinalityKeyValue(HighCardinalityKeyNames.FIELD_NAME.asString(), "none") + + .hasBeenStarted() + .hasBeenStopped(); + + }); + } + + @SpringBootConfiguration + @EnableAutoConfiguration(exclude = { DataSourceAutoConfiguration.class }) + static class Config { + + @Bean + public TestObservationRegistry observationRegistry() { + return TestObservationRegistry.create(); + } + + @Bean + public VectorStore vectorStore(JdbcTemplate jdbcTemplate, EmbeddingModel embeddingModel, + ObservationRegistry observationRegistry) { + return new PgVectorStore.Builder(jdbcTemplate, embeddingModel) + .withDistanceType(PgVectorStore.PgDistanceType.COSINE_DISTANCE) + .withIndexType(PgIndexType.HNSW) + .withObservationRegistry(observationRegistry) + .withInitializeSchema(true) + .build(); + } + + @Bean + public JdbcTemplate myJdbcTemplate(DataSource dataSource) { + return new JdbcTemplate(dataSource); + } + + @Bean + @Primary + @ConfigurationProperties("app.datasource") + public DataSourceProperties dataSourceProperties() { + return new DataSourceProperties(); + } + + @Bean + public HikariDataSource dataSource(DataSourceProperties dataSourceProperties) { + return dataSourceProperties.initializeDataSourceBuilder().type(HikariDataSource.class).build(); + } + + @Bean + public EmbeddingModel embeddingModel() { + return new OpenAiEmbeddingModel(new OpenAiApi(System.getenv("OPENAI_API_KEY"))); + } + + } + +} From 070f3a910e556d27c877fd22ba75a3e959950480 Mon Sep 17 00:00:00 2001 From: Christian Tzolov Date: Thu, 15 Aug 2024 15:37:33 +0200 Subject: [PATCH 7/8] Address most of the review comments --- .../conventions/AiObservationAttributes.java | 6 ++- .../conventions/VectorStoreProvider.java | 39 ++++++++++++++ .../VectorStoreSimilarityMetric.java | 54 +++++++++++++++++++ .../ai/vectorstore/SimpleVectorStore.java | 7 +-- .../VectorStoreObservationContext.java | 15 ------ .../VectorStoreObservationDocumentation.java | 19 +++---- .../VectorStoreObservationProperties.java | 2 +- ...toreObservationAutoConfigurationTests.java | 11 ++-- .../ai/vectorstore/PgVectorStore.java | 21 ++++++-- .../ai/vectorstore/PgVectorObservationIT.java | 7 ++- 10 files changed, 136 insertions(+), 45 deletions(-) create mode 100644 spring-ai-core/src/main/java/org/springframework/ai/observation/conventions/VectorStoreProvider.java create mode 100644 spring-ai-core/src/main/java/org/springframework/ai/observation/conventions/VectorStoreSimilarityMetric.java diff --git a/spring-ai-core/src/main/java/org/springframework/ai/observation/conventions/AiObservationAttributes.java b/spring-ai-core/src/main/java/org/springframework/ai/observation/conventions/AiObservationAttributes.java index 29566449380..9e7aec175a9 100644 --- a/spring-ai-core/src/main/java/org/springframework/ai/observation/conventions/AiObservationAttributes.java +++ b/spring-ai-core/src/main/java/org/springframework/ai/observation/conventions/AiObservationAttributes.java @@ -132,7 +132,11 @@ public enum AiObservationAttributes { /** * The full response received from the model. */ - COMPLETION("gen_ai.completion"); + COMPLETION("gen_ai.completion"), + /** + * The name of the operation or command being executed. + */ + DB_OPERATION_NAME("db.operation.name"),; private final String value; diff --git a/spring-ai-core/src/main/java/org/springframework/ai/observation/conventions/VectorStoreProvider.java b/spring-ai-core/src/main/java/org/springframework/ai/observation/conventions/VectorStoreProvider.java new file mode 100644 index 00000000000..1ec2ae534df --- /dev/null +++ b/spring-ai-core/src/main/java/org/springframework/ai/observation/conventions/VectorStoreProvider.java @@ -0,0 +1,39 @@ +/* +* Copyright 2024 - 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.observation.conventions; + +/** + * @author Christian Tzolov + * @since 1.0.0 + */ +public enum VectorStoreProvider { + + // @formatter:off + PG_VECTOR("pg_vector"), + SIMPLE_VECTOR_STORE("simple_vector_store"); + + // @formatter:on + private final String value; + + VectorStoreProvider(String value) { + this.value = value; + } + + public String value() { + return this.value; + } + +} diff --git a/spring-ai-core/src/main/java/org/springframework/ai/observation/conventions/VectorStoreSimilarityMetric.java b/spring-ai-core/src/main/java/org/springframework/ai/observation/conventions/VectorStoreSimilarityMetric.java new file mode 100644 index 00000000000..6b95fa941d9 --- /dev/null +++ b/spring-ai-core/src/main/java/org/springframework/ai/observation/conventions/VectorStoreSimilarityMetric.java @@ -0,0 +1,54 @@ +/* +* Copyright 2024 - 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.observation.conventions; + +/** + * @author Christian Tzolov + * @since 1.0.0 + */ +public enum VectorStoreSimilarityMetric { + + // @formatter:off + + /** + * The cosine metric. + */ + COSINE("cosine"), + /** + * The euclidean distance metric. + */ + EUCLIDEAN("euclidean"), + /** + * The manhattan distance metric. + */ + MANHATTAN("manhattan"), + /** + * The dot product metric. + */ + DOT("dot"); + + // @formatter:on + private final String value; + + VectorStoreSimilarityMetric(String value) { + this.value = value; + } + + public String value() { + return this.value; + } + +} diff --git a/spring-ai-core/src/main/java/org/springframework/ai/vectorstore/SimpleVectorStore.java b/spring-ai-core/src/main/java/org/springframework/ai/vectorstore/SimpleVectorStore.java index 1963fc1a5bf..b68e2bb8fd6 100644 --- a/spring-ai-core/src/main/java/org/springframework/ai/vectorstore/SimpleVectorStore.java +++ b/spring-ai-core/src/main/java/org/springframework/ai/vectorstore/SimpleVectorStore.java @@ -34,6 +34,8 @@ 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.observation.AbstractObservationVectorStore; import org.springframework.ai.vectorstore.observation.VectorStoreObservationContext; import org.springframework.core.io.Resource; @@ -253,11 +255,10 @@ public static float norm(float[] vector) { @Override public VectorStoreObservationContext.Builder createObservationContextBuilder(String operationName) { - return VectorStoreObservationContext.builder("simple_memory_store", operationName) + return VectorStoreObservationContext.builder(VectorStoreProvider.SIMPLE_VECTOR_STORE.value(), operationName) .withDimensions(this.embeddingModel.dimensions()) .withCollectionName("in-memory-map") - .withSimilarityMetric("cosine") - .withModel(this.embeddingModel.getClass().getSimpleName()); + .withSimilarityMetric(VectorStoreSimilarityMetric.COSINE.value()); } } diff --git a/spring-ai-core/src/main/java/org/springframework/ai/vectorstore/observation/VectorStoreObservationContext.java b/spring-ai-core/src/main/java/org/springframework/ai/vectorstore/observation/VectorStoreObservationContext.java index 62e57a2e695..d35ae17e449 100644 --- a/spring-ai-core/src/main/java/org/springframework/ai/vectorstore/observation/VectorStoreObservationContext.java +++ b/spring-ai-core/src/main/java/org/springframework/ai/vectorstore/observation/VectorStoreObservationContext.java @@ -75,8 +75,6 @@ public enum Operation { private String namespace = ""; - private String model = ""; - private String fieldName = ""; private String indexName = ""; @@ -158,14 +156,6 @@ public void setNamespace(String namespace) { this.namespace = namespace; } - public String getModel() { - return this.model; - } - - public void setModel(String model) { - this.model = model; - } - public String getFieldName() { return this.fieldName; } @@ -242,11 +232,6 @@ public Builder withNamespace(String namespace) { return this; } - public Builder withModel(String model) { - this.context.setModel(model); - return this; - } - public Builder withFieldName(String fieldName) { this.context.setFieldName(fieldName); return this; diff --git a/spring-ai-core/src/main/java/org/springframework/ai/vectorstore/observation/VectorStoreObservationDocumentation.java b/spring-ai-core/src/main/java/org/springframework/ai/vectorstore/observation/VectorStoreObservationDocumentation.java index 8a7c41eebe5..602327b5e59 100644 --- a/spring-ai-core/src/main/java/org/springframework/ai/vectorstore/observation/VectorStoreObservationDocumentation.java +++ b/spring-ai-core/src/main/java/org/springframework/ai/vectorstore/observation/VectorStoreObservationDocumentation.java @@ -15,6 +15,8 @@ */ package org.springframework.ai.vectorstore.observation; +import org.springframework.ai.observation.conventions.AiObservationAttributes; + import io.micrometer.common.docs.KeyName; import io.micrometer.observation.Observation; import io.micrometer.observation.ObservationConvention; @@ -63,7 +65,7 @@ public String asString() { DB_OPERATION_NAME { @Override public String asString() { - return "db.operation.name"; + return AiObservationAttributes.DB_OPERATION_NAME.value(); } }, /** @@ -105,7 +107,7 @@ public String asString() { QUERY_RESPONSE { @Override public String asString() { - return "db.vector.query.response"; + return "db.vector.query.response.documents"; } }, /** @@ -114,7 +116,7 @@ public String asString() { QUERY { @Override public String asString() { - return "db.query.text"; + return "db.vector.query.content"; } }, /** @@ -123,7 +125,7 @@ public String asString() { QUERY_METADATA_FILTER { @Override public String asString() { - return "db.query.metadata.filter"; + return "db.vector.query.filter"; } }, /** @@ -162,15 +164,6 @@ public String asString() { return "db.vector.name"; } }, - /** - * The model used for the embedding. - */ - MODEL { - @Override - public String asString() { - return "db.vector.model"; - } - }, /** * The name of a collection (table, container) within the database. */ diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/observation/VectorStoreObservationProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/observation/VectorStoreObservationProperties.java index 1177bc3a2ac..9212a2ceaf7 100644 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/observation/VectorStoreObservationProperties.java +++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/observation/VectorStoreObservationProperties.java @@ -26,7 +26,7 @@ @ConfigurationProperties(VectorStoreObservationProperties.CONFIG_PREFIX) public class VectorStoreObservationProperties { - public static final String CONFIG_PREFIX = "spring.ai.vector.store.observations"; + public static final String CONFIG_PREFIX = "spring.ai.vectorstore.observations"; /** * Whether to include the search response content in the observations. diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/vectorstore/observation/VectorStoreObservationAutoConfigurationTests.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/vectorstore/observation/VectorStoreObservationAutoConfigurationTests.java index 39bd15a7735..7751cb4b9e3 100644 --- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/vectorstore/observation/VectorStoreObservationAutoConfigurationTests.java +++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/vectorstore/observation/VectorStoreObservationAutoConfigurationTests.java @@ -43,7 +43,7 @@ void queryResponseFilterDefault() { @Test void queryResponseFilterFilterEnabled() { - contextRunner.withPropertyValues("spring.ai.vector.store.observations.include-query-response=true") + contextRunner.withPropertyValues("spring.ai.vectorstore.observations.include-query-response=true") .run(context -> { assertThat(context).hasSingleBean(VectorStoreQueryResponseObservationFilter.class); }); @@ -58,7 +58,7 @@ void deleteRequestFilterDefault() { @Test void deleteRequestFilterEnabled() { - contextRunner.withPropertyValues("spring.ai.vector.store.observations.include-delete-request=true") + contextRunner.withPropertyValues("spring.ai.vectorstore.observations.include-delete-request=true") .run(context -> { assertThat(context).hasSingleBean(VectorStoreDeleteRequestContentObservationFilter.class); }); @@ -73,10 +73,9 @@ void addRequestFilterDefault() { @Test void addRequestFilterEnabled() { - contextRunner.withPropertyValues("spring.ai.vector.store.observations.include-add-request=true") - .run(context -> { - assertThat(context).hasSingleBean(VectorStoreAddRequestContentObservationFilter.class); - }); + contextRunner.withPropertyValues("spring.ai.vectorstore.observations.include-add-request=true").run(context -> { + assertThat(context).hasSingleBean(VectorStoreAddRequestContentObservationFilter.class); + }); } } 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 510b0b05caa..c1d67e16410 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 @@ -28,6 +28,8 @@ 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; @@ -578,13 +580,24 @@ public PgVectorStore build() { @Override public VectorStoreObservationContext.Builder createObservationContextBuilder(String operationName) { - return VectorStoreObservationContext.builder("pg_vector", operationName) + return VectorStoreObservationContext.builder(VectorStoreProvider.PG_VECTOR.value(), operationName) .withDimensions(this.embeddingDimensions()) .withCollectionName(this.vectorTableName) .withNamespace(this.schemaName) - .withSimilarityMetric(this.getDistanceType().name()) - .withIndexName(this.createIndexMethod.name()) - .withModel(this.embeddingModel.getClass().getSimpleName()); + .withSimilarityMetric(getSimilarityMetric()) + .withIndexName(this.createIndexMethod.name()); + } + + private static Map SIMILARITY_TYPE_MAPPING = Map.of( + PgDistanceType.COSINE_DISTANCE, VectorStoreSimilarityMetric.COSINE, PgDistanceType.EUCLIDEAN_DISTANCE, + VectorStoreSimilarityMetric.EUCLIDEAN, PgDistanceType.NEGATIVE_INNER_PRODUCT, + VectorStoreSimilarityMetric.DOT); + + private String getSimilarityMetric() { + if (!SIMILARITY_TYPE_MAPPING.containsKey(this.getDistanceType())) { + return this.getDistanceType().name(); + } + return SIMILARITY_TYPE_MAPPING.get(this.distanceType).value(); } } \ No newline at end of file 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 8e6fb12242a..124fe6ac582 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 @@ -96,7 +96,7 @@ public static String getText(String uri) { "app.datasource.type=com.zaxxer.hikari.HikariDataSource"); @Test - void observationForChatOperation() { + void observationVectorStoreAddAndQueryOperations() { contextRunner.run(context -> { @@ -114,12 +114,13 @@ void observationForChatOperation() { .hasLowCardinalityKeyValue(LowCardinalityKeyNames.DB_OPERATION_NAME.asString(), "add") .hasLowCardinalityKeyValue(LowCardinalityKeyNames.DB_SYSTEM.asString(), "pg_vector") .hasLowCardinalityKeyValue(LowCardinalityKeyNames.SPRING_AI_KIND.asString(), "vector_store") - .hasHighCardinalityKeyValue(HighCardinalityKeyNames.QUERY.asString(), "none") .hasHighCardinalityKeyValue(HighCardinalityKeyNames.DIMENSIONS.asString(), "1536") .hasHighCardinalityKeyValue(HighCardinalityKeyNames.COLLECTION_NAME.asString(), "vector_store") .hasHighCardinalityKeyValue(HighCardinalityKeyNames.NAMESPACE.asString(), "public") .hasHighCardinalityKeyValue(HighCardinalityKeyNames.FIELD_NAME.asString(), "none") + .hasHighCardinalityKeyValue(HighCardinalityKeyNames.SIMILARITY_METRIC.asString(), "cosine") + .hasHighCardinalityKeyValue(HighCardinalityKeyNames.TOP_K.asString(), "none") .hasBeenStarted() .hasBeenStopped(); @@ -145,6 +146,8 @@ void observationForChatOperation() { .hasHighCardinalityKeyValue(HighCardinalityKeyNames.COLLECTION_NAME.asString(), "vector_store") .hasHighCardinalityKeyValue(HighCardinalityKeyNames.NAMESPACE.asString(), "public") .hasHighCardinalityKeyValue(HighCardinalityKeyNames.FIELD_NAME.asString(), "none") + .hasHighCardinalityKeyValue(HighCardinalityKeyNames.SIMILARITY_METRIC.asString(), "cosine") + .hasHighCardinalityKeyValue(HighCardinalityKeyNames.TOP_K.asString(), "1") .hasBeenStarted() .hasBeenStopped(); From a0a71d2556e88cff3e4a392e6d5cbf1a56ab88ba Mon Sep 17 00:00:00 2001 From: Christian Tzolov Date: Fri, 16 Aug 2024 08:38:12 +0200 Subject: [PATCH 8/8] Remove the Add/Delete content observation support. --- .../AbstractObservationVectorStore.java | 11 +-- ...oreAddRequestContentObservationFilter.java | 54 -------------- ...DeleteRequestContentObservationFilter.java | 55 -------------- .../VectorStoreObservationContext.java | 36 ++-------- .../VectorStoreObservationDocumentation.java | 18 ----- ...VectorStoreObservationConventionTests.java | 11 +-- ...StoreAddRequestObservationFilterTests.java | 71 ------------------- ...reDeleteRequestObservationFilterTests.java | 70 ------------------ ...ctorStoreObservationAutoConfiguration.java | 20 ------ .../VectorStoreObservationProperties.java | 26 ------- ...toreObservationAutoConfigurationTests.java | 31 -------- .../ai/vectorstore/PgVectorObservationIT.java | 4 +- 12 files changed, 14 insertions(+), 393 deletions(-) delete mode 100644 spring-ai-core/src/main/java/org/springframework/ai/vectorstore/observation/VectorStoreAddRequestContentObservationFilter.java delete mode 100644 spring-ai-core/src/main/java/org/springframework/ai/vectorstore/observation/VectorStoreDeleteRequestContentObservationFilter.java delete mode 100644 spring-ai-core/src/test/java/org/springframework/ai/vectorstore/observation/VectorStoreAddRequestObservationFilterTests.java delete mode 100644 spring-ai-core/src/test/java/org/springframework/ai/vectorstore/observation/VectorStoreDeleteRequestObservationFilterTests.java diff --git a/spring-ai-core/src/main/java/org/springframework/ai/vectorstore/observation/AbstractObservationVectorStore.java b/spring-ai-core/src/main/java/org/springframework/ai/vectorstore/observation/AbstractObservationVectorStore.java index 7b175bb62f7..c0e09886734 100644 --- a/spring-ai-core/src/main/java/org/springframework/ai/vectorstore/observation/AbstractObservationVectorStore.java +++ b/spring-ai-core/src/main/java/org/springframework/ai/vectorstore/observation/AbstractObservationVectorStore.java @@ -55,8 +55,8 @@ public AbstractObservationVectorStore(ObservationRegistry observationRegistry, @Override public void add(List documents) { - VectorStoreObservationContext observationContext = this.createObservationContextBuilder("add") - .withAddRequest(documents) + VectorStoreObservationContext observationContext = this + .createObservationContextBuilder(VectorStoreObservationContext.Operation.ADD.value()) .build(); VectorStoreObservationDocumentation.AI_VECTOR_STORE @@ -68,8 +68,8 @@ public void add(List documents) { @Override public Optional delete(List deleteDocIds) { - VectorStoreObservationContext observationContext = this.createObservationContextBuilder("delete") - .withDeleteRequest(deleteDocIds) + VectorStoreObservationContext observationContext = this + .createObservationContextBuilder(VectorStoreObservationContext.Operation.DELETE.value()) .build(); return VectorStoreObservationDocumentation.AI_VECTOR_STORE @@ -81,7 +81,8 @@ public Optional delete(List deleteDocIds) { @Override public List similaritySearch(SearchRequest request) { - VectorStoreObservationContext searchObservationContext = this.createObservationContextBuilder("search") + VectorStoreObservationContext searchObservationContext = this + .createObservationContextBuilder(VectorStoreObservationContext.Operation.QUERY.value()) .withQueryRequest(request) .build(); diff --git a/spring-ai-core/src/main/java/org/springframework/ai/vectorstore/observation/VectorStoreAddRequestContentObservationFilter.java b/spring-ai-core/src/main/java/org/springframework/ai/vectorstore/observation/VectorStoreAddRequestContentObservationFilter.java deleted file mode 100644 index f7a740744fd..00000000000 --- a/spring-ai-core/src/main/java/org/springframework/ai/vectorstore/observation/VectorStoreAddRequestContentObservationFilter.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright 2024 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * 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.observation; - -import java.util.StringJoiner; - -import org.springframework.util.CollectionUtils; - -import io.micrometer.observation.Observation; -import io.micrometer.observation.ObservationFilter; - -/** - * An {@link ObservationFilter} to include the Vector Store add request content in the - * observation. - * - * @author Christian Tzolov - * @since 1.0.0 - */ -public class VectorStoreAddRequestContentObservationFilter implements ObservationFilter { - - @Override - public Observation.Context map(Observation.Context context) { - - if (!(context instanceof VectorStoreObservationContext observationContext)) { - return context; - } - - if (CollectionUtils.isEmpty(observationContext.getAddRequest())) { - return observationContext; - } - - StringJoiner joiner = new StringJoiner(", ", "[", "]"); - observationContext.getAddRequest().forEach(document -> joiner.add("\"" + document.getContent() + "\"")); - - observationContext.addHighCardinalityKeyValue( - VectorStoreObservationDocumentation.HighCardinalityKeyNames.ADD_REQUEST.withValue(joiner.toString())); - - return observationContext; - } - -} diff --git a/spring-ai-core/src/main/java/org/springframework/ai/vectorstore/observation/VectorStoreDeleteRequestContentObservationFilter.java b/spring-ai-core/src/main/java/org/springframework/ai/vectorstore/observation/VectorStoreDeleteRequestContentObservationFilter.java deleted file mode 100644 index 55b6ccd17b1..00000000000 --- a/spring-ai-core/src/main/java/org/springframework/ai/vectorstore/observation/VectorStoreDeleteRequestContentObservationFilter.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright 2024 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * 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.observation; - -import java.util.StringJoiner; - -import org.springframework.util.CollectionUtils; - -import io.micrometer.observation.Observation; -import io.micrometer.observation.ObservationFilter; - -/** - * An {@link ObservationFilter} to include the Vector Store delete request content in the - * observation. - * - * @author Christian Tzolov - * @since 1.0.0 - */ -public class VectorStoreDeleteRequestContentObservationFilter implements ObservationFilter { - - @Override - public Observation.Context map(Observation.Context context) { - - if (!(context instanceof VectorStoreObservationContext observationContext)) { - return context; - } - - if (CollectionUtils.isEmpty(observationContext.getDeleteRequest())) { - return observationContext; - } - - StringJoiner joiner = new StringJoiner(", ", "[", "]"); - observationContext.getDeleteRequest().forEach(documentId -> joiner.add("\"" + documentId + "\"")); - - observationContext - .addHighCardinalityKeyValue(VectorStoreObservationDocumentation.HighCardinalityKeyNames.DELETE_REQUEST - .withValue(joiner.toString())); - - return observationContext; - } - -} diff --git a/spring-ai-core/src/main/java/org/springframework/ai/vectorstore/observation/VectorStoreObservationContext.java b/spring-ai-core/src/main/java/org/springframework/ai/vectorstore/observation/VectorStoreObservationContext.java index d35ae17e449..4017cc9bc7c 100644 --- a/spring-ai-core/src/main/java/org/springframework/ai/vectorstore/observation/VectorStoreObservationContext.java +++ b/spring-ai-core/src/main/java/org/springframework/ai/vectorstore/observation/VectorStoreObservationContext.java @@ -51,13 +51,11 @@ public enum Operation { this.value = value; } - } - - // DELETE - private List deleteRequest; + public String value() { + return this.value; + } - // ADD - private List addRequest; + } // SEARCH private SearchRequest queryRequest; @@ -88,22 +86,6 @@ public VectorStoreObservationContext(String databaseSystem, String operationName this.operationName = operationName; } - public List getDeleteRequest() { - return this.deleteRequest; - } - - public void setDeleteRequest(List deleteRequest) { - this.deleteRequest = deleteRequest; - } - - public List getAddRequest() { - return this.addRequest; - } - - public void setAddRequest(List documents) { - this.addRequest = documents; - } - public SearchRequest getQueryRequest() { return this.queryRequest; } @@ -192,16 +174,6 @@ public Builder(String databaseSystem, String operationName) { this.context = new VectorStoreObservationContext(databaseSystem, operationName); } - public Builder withDeleteRequest(List deleteRequest) { - this.context.setDeleteRequest(deleteRequest); - return this; - } - - public Builder withAddRequest(List documents) { - this.context.setAddRequest(documents); - return this; - } - public Builder withQueryRequest(SearchRequest request) { this.context.setQueryRequest(request); return this; diff --git a/spring-ai-core/src/main/java/org/springframework/ai/vectorstore/observation/VectorStoreObservationDocumentation.java b/spring-ai-core/src/main/java/org/springframework/ai/vectorstore/observation/VectorStoreObservationDocumentation.java index 602327b5e59..6ae8a15174e 100644 --- a/spring-ai-core/src/main/java/org/springframework/ai/vectorstore/observation/VectorStoreObservationDocumentation.java +++ b/spring-ai-core/src/main/java/org/springframework/ai/vectorstore/observation/VectorStoreObservationDocumentation.java @@ -83,24 +83,6 @@ public String asString() { public enum HighCardinalityKeyNames implements KeyName { - /** - * Add documents request content. - */ - DELETE_REQUEST { - @Override - public String asString() { - return "db.vector.delete.request"; - } - }, - /** - * Add documents request content. - */ - ADD_REQUEST { - @Override - public String asString() { - return "db.vector.add.request"; - } - }, /** * Similarity search response content. */ diff --git a/spring-ai-core/src/test/java/org/springframework/ai/vectorstore/observation/DefaultVectorStoreObservationConventionTests.java b/spring-ai-core/src/test/java/org/springframework/ai/vectorstore/observation/DefaultVectorStoreObservationConventionTests.java index eaa6e4a9e80..d92366849c3 100644 --- a/spring-ai-core/src/test/java/org/springframework/ai/vectorstore/observation/DefaultVectorStoreObservationConventionTests.java +++ b/spring-ai-core/src/test/java/org/springframework/ai/vectorstore/observation/DefaultVectorStoreObservationConventionTests.java @@ -74,9 +74,6 @@ void shouldHaveRequiredKeyValues() { @Test void shouldHaveOptionalKeyValues() { - List addDocs = List.of(new Document("addDoc1"), new Document("addDoc2")); - List deleteIds = List.of("id1", "id2"); - VectorStoreObservationContext observationContext = VectorStoreObservationContext .builder("my-database", VectorStoreObservationContext.Operation.QUERY) .withCollectionName("COLLECTION_NAME") @@ -85,8 +82,6 @@ void shouldHaveOptionalKeyValues() { .withIndexName("INDEX_NAME") .withNamespace("NAMESPACE") .withSimilarityMetric("SIMILARITY_METRIC") - .withAddRequest(addDocs) - .withDeleteRequest(deleteIds) .withQueryRequest(SearchRequest.query("VDB QUERY").withFilterExpression("country == 'UK' && year >= 2020")) .build(); @@ -99,10 +94,8 @@ void shouldHaveOptionalKeyValues() { VectorStoreObservationContext.Operation.QUERY.value)); // Optional, filter only added content - assertThat(this.observationConvention.getHighCardinalityKeyValues(observationContext)).doesNotContain( - KeyValue.of(HighCardinalityKeyNames.ADD_REQUEST, "[addDoc1,addDoc2]"), - KeyValue.of(HighCardinalityKeyNames.DELETE_REQUEST, "[id1,id2]"), - KeyValue.of(HighCardinalityKeyNames.QUERY_RESPONSE, "[doc1,doc2]")); + assertThat(this.observationConvention.getHighCardinalityKeyValues(observationContext)) + .doesNotContain(KeyValue.of(HighCardinalityKeyNames.QUERY_RESPONSE, "[doc1,doc2]")); assertThat(this.observationConvention.getHighCardinalityKeyValues(observationContext)).contains( KeyValue.of(HighCardinalityKeyNames.COLLECTION_NAME.asString(), "COLLECTION_NAME"), diff --git a/spring-ai-core/src/test/java/org/springframework/ai/vectorstore/observation/VectorStoreAddRequestObservationFilterTests.java b/spring-ai-core/src/test/java/org/springframework/ai/vectorstore/observation/VectorStoreAddRequestObservationFilterTests.java deleted file mode 100644 index 9e748f4c2bc..00000000000 --- a/spring-ai-core/src/test/java/org/springframework/ai/vectorstore/observation/VectorStoreAddRequestObservationFilterTests.java +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright 2024 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * 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.observation; - -import static org.assertj.core.api.Assertions.assertThat; - -import java.util.List; - -import org.junit.jupiter.api.Test; -import org.springframework.ai.document.Document; -import org.springframework.ai.vectorstore.observation.VectorStoreObservationDocumentation.HighCardinalityKeyNames; - -import io.micrometer.common.KeyValue; -import io.micrometer.observation.Observation; - -/** - * Unit tests for {@link VectorStoreAddRequestContentObservationFilter}. - * - * @author Christian Tzolov - */ -class VectorStoreAddRequestObservationFilterTests { - - private final VectorStoreAddRequestContentObservationFilter observationFilter = new VectorStoreAddRequestContentObservationFilter(); - - @Test - void whenNotSupportedObservationContextThenReturnOriginalContext() { - var expectedContext = new Observation.Context(); - var actualContext = observationFilter.map(expectedContext); - - assertThat(actualContext).isEqualTo(expectedContext); - } - - @Test - void whenEmptyQueryResponseThenReturnOriginalContext() { - var expectedContext = VectorStoreObservationContext.builder("db", VectorStoreObservationContext.Operation.ADD) - .build(); - - var actualContext = observationFilter.map(expectedContext); - - assertThat(actualContext).isEqualTo(expectedContext); - } - - @Test - void whenNonEmptyQueryResponseThenAugmentContext() { - var expectedContext = VectorStoreObservationContext.builder("db", VectorStoreObservationContext.Operation.ADD) - .build(); - - List addRequestContent = List.of(new Document("doc1"), new Document("doc2")); - - expectedContext.setAddRequest(addRequestContent); - - var augmentedContext = observationFilter.map(expectedContext); - - assertThat(augmentedContext.getHighCardinalityKeyValues()) - .contains(KeyValue.of(HighCardinalityKeyNames.ADD_REQUEST.asString(), "[\"doc1\", \"doc2\"]")); - } - -} diff --git a/spring-ai-core/src/test/java/org/springframework/ai/vectorstore/observation/VectorStoreDeleteRequestObservationFilterTests.java b/spring-ai-core/src/test/java/org/springframework/ai/vectorstore/observation/VectorStoreDeleteRequestObservationFilterTests.java deleted file mode 100644 index 7d96d7d601d..00000000000 --- a/spring-ai-core/src/test/java/org/springframework/ai/vectorstore/observation/VectorStoreDeleteRequestObservationFilterTests.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright 2024 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * 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.observation; - -import static org.assertj.core.api.Assertions.assertThat; - -import java.util.List; - -import org.junit.jupiter.api.Test; -import org.springframework.ai.vectorstore.observation.VectorStoreObservationDocumentation.HighCardinalityKeyNames; - -import io.micrometer.common.KeyValue; -import io.micrometer.observation.Observation; - -/** - * Unit tests for {@link VectorStoreDeleteRequestContentObservationFilter}. - * - * @author Christian Tzolov - */ -class VectorStoreDeleteRequestObservationFilterTests { - - private final VectorStoreDeleteRequestContentObservationFilter observationFilter = new VectorStoreDeleteRequestContentObservationFilter(); - - @Test - void whenNotSupportedObservationContextThenReturnOriginalContext() { - var expectedContext = new Observation.Context(); - var actualContext = observationFilter.map(expectedContext); - - assertThat(actualContext).isEqualTo(expectedContext); - } - - @Test - void whenEmptyQueryResponseThenReturnOriginalContext() { - var expectedContext = VectorStoreObservationContext.builder("db", VectorStoreObservationContext.Operation.ADD) - .build(); - - var actualContext = observationFilter.map(expectedContext); - - assertThat(actualContext).isEqualTo(expectedContext); - } - - @Test - void whenNonEmptyQueryResponseThenAugmentContext() { - var expectedContext = VectorStoreObservationContext.builder("db", VectorStoreObservationContext.Operation.ADD) - .build(); - - List deleteRequestContent = List.of("id1", "id2"); - - expectedContext.setDeleteRequest(deleteRequestContent); - - var augmentedContext = observationFilter.map(expectedContext); - - assertThat(augmentedContext.getHighCardinalityKeyValues()) - .contains(KeyValue.of(HighCardinalityKeyNames.DELETE_REQUEST.asString(), "[\"id1\", \"id2\"]")); - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/observation/VectorStoreObservationAutoConfiguration.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/observation/VectorStoreObservationAutoConfiguration.java index fa02b93c6e9..46feafc9448 100644 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/observation/VectorStoreObservationAutoConfiguration.java +++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/observation/VectorStoreObservationAutoConfiguration.java @@ -18,8 +18,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.ai.vectorstore.VectorStore; -import org.springframework.ai.vectorstore.observation.VectorStoreAddRequestContentObservationFilter; -import org.springframework.ai.vectorstore.observation.VectorStoreDeleteRequestContentObservationFilter; import org.springframework.ai.vectorstore.observation.VectorStoreQueryResponseObservationFilter; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; @@ -52,22 +50,4 @@ VectorStoreQueryResponseObservationFilter vectorStoreQueryResponseContentObserva return new VectorStoreQueryResponseObservationFilter(); } - @Bean - @ConditionalOnMissingBean - @ConditionalOnProperty(prefix = VectorStoreObservationProperties.CONFIG_PREFIX, name = "include-add-request", - havingValue = "true") - VectorStoreAddRequestContentObservationFilter vectorStoreAddRequestContentObservationFilter() { - logger.warn( - "You have enabled the inclusion of the add request content in the observations, with the risk of exposing sensitive or private information. Please, be careful!"); - return new VectorStoreAddRequestContentObservationFilter(); - } - - @Bean - @ConditionalOnMissingBean - @ConditionalOnProperty(prefix = VectorStoreObservationProperties.CONFIG_PREFIX, name = "include-delete-request", - havingValue = "true") - VectorStoreDeleteRequestContentObservationFilter vectorStoreDeleteRequestContentObservationFilter() { - return new VectorStoreDeleteRequestContentObservationFilter(); - } - } diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/observation/VectorStoreObservationProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/observation/VectorStoreObservationProperties.java index 9212a2ceaf7..33589abad26 100644 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/observation/VectorStoreObservationProperties.java +++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/observation/VectorStoreObservationProperties.java @@ -33,16 +33,6 @@ public class VectorStoreObservationProperties { */ private boolean includeQueryResponse = false; - /** - * Whether to include the add request content in the observations. - */ - private boolean includeAddRequest = false; - - /** - * Whether to include the delete request content in the observations. - */ - private boolean includeDeleteRequest = false; - public boolean isIncludeQueryResponse() { return this.includeQueryResponse; } @@ -51,20 +41,4 @@ public void setIncludeQueryResponse(boolean includeQueryResponse) { this.includeQueryResponse = includeQueryResponse; } - public boolean isIncludeAddRequest() { - return this.includeAddRequest; - } - - public void setIncludeAddRequest(boolean includeAddRequest) { - this.includeAddRequest = includeAddRequest; - } - - public boolean isIncludeDeleteRequest() { - return this.includeDeleteRequest; - } - - public void setIncludeDeleteRequest(boolean includeDeleteRequest) { - this.includeDeleteRequest = includeDeleteRequest; - } - } diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/vectorstore/observation/VectorStoreObservationAutoConfigurationTests.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/vectorstore/observation/VectorStoreObservationAutoConfigurationTests.java index 7751cb4b9e3..24386fbc0e6 100644 --- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/vectorstore/observation/VectorStoreObservationAutoConfigurationTests.java +++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/vectorstore/observation/VectorStoreObservationAutoConfigurationTests.java @@ -18,8 +18,6 @@ import static org.assertj.core.api.Assertions.assertThat; import org.junit.jupiter.api.Test; -import org.springframework.ai.vectorstore.observation.VectorStoreAddRequestContentObservationFilter; -import org.springframework.ai.vectorstore.observation.VectorStoreDeleteRequestContentObservationFilter; import org.springframework.ai.vectorstore.observation.VectorStoreQueryResponseObservationFilter; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.test.context.runner.ApplicationContextRunner; @@ -49,33 +47,4 @@ void queryResponseFilterFilterEnabled() { }); } - @Test - void deleteRequestFilterDefault() { - contextRunner.run(context -> { - assertThat(context).doesNotHaveBean(VectorStoreDeleteRequestContentObservationFilter.class); - }); - } - - @Test - void deleteRequestFilterEnabled() { - contextRunner.withPropertyValues("spring.ai.vectorstore.observations.include-delete-request=true") - .run(context -> { - assertThat(context).hasSingleBean(VectorStoreDeleteRequestContentObservationFilter.class); - }); - } - - @Test - void addRequestFilterDefault() { - contextRunner.run(context -> { - assertThat(context).doesNotHaveBean(VectorStoreAddRequestContentObservationFilter.class); - }); - } - - @Test - void addRequestFilterEnabled() { - contextRunner.withPropertyValues("spring.ai.vectorstore.observations.include-add-request=true").run(context -> { - assertThat(context).hasSingleBean(VectorStoreAddRequestContentObservationFilter.class); - }); - } - } 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 124fe6ac582..59590735fd1 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 @@ -136,8 +136,8 @@ void observationVectorStoreAddAndQueryOperations() { .doesNotHaveAnyRemainingCurrentObservation() .hasObservationWithNameEqualTo(DefaultVectorStoreObservationConvention.DEFAULT_NAME) .that() - .hasContextualNameEqualTo("vector_store pg_vector search") - .hasLowCardinalityKeyValue(LowCardinalityKeyNames.DB_OPERATION_NAME.asString(), "search") + .hasContextualNameEqualTo("vector_store pg_vector query") + .hasLowCardinalityKeyValue(LowCardinalityKeyNames.DB_OPERATION_NAME.asString(), "query") .hasLowCardinalityKeyValue(LowCardinalityKeyNames.DB_SYSTEM.asString(), "pg_vector") .hasLowCardinalityKeyValue(LowCardinalityKeyNames.SPRING_AI_KIND.asString(), "vector_store")