Common vector store functionality for Spring AI providing a portable abstraction layer for integrating vector databases with comprehensive filtering, similarity search, and observability support.
Built-in observability support through Micrometer for monitoring vector store operations. Provides comprehensive metrics collection for add, delete, and query operations with configurable observation conventions. Integrates seamlessly with distributed tracing systems like Zipkin, Jaeger, and cloud-native observability platforms.
Abstract base class that adds Micrometer observation support to VectorStore implementations. All vector store implementations should extend this class to inherit observability features.
package org.springframework.ai.vectorstore.observation;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.ai.vectorstore.SearchRequest;
import org.springframework.ai.vectorstore.AbstractVectorStoreBuilder;
import org.springframework.ai.vectorstore.filter.Filter;
import org.springframework.ai.document.Document;
import org.springframework.ai.document.BatchingStrategy;
import org.springframework.ai.embedding.EmbeddingModel;
import io.micrometer.observation.Observation;
import java.util.List;
/**
* Abstract base implementation providing Micrometer observation support.
* Custom vector store implementations should extend this class and
* delegate to createObservation() for operation tracking.
*
* This class implements the Template Method pattern:
* - Public methods (add, delete, similaritySearch) create observations
* - Protected abstract methods (doAdd, doDelete, doSimilaritySearch) contain actual logic
* - Subclasses implement the protected methods without worrying about observability
*/
public abstract class AbstractObservationVectorStore implements VectorStore {
/**
* Constructor initializing observation support from builder configuration.
*
* @param builder AbstractVectorStoreBuilder with observation configuration
* @throws IllegalArgumentException if builder is null or missing required fields
*/
protected AbstractObservationVectorStore(AbstractVectorStoreBuilder<?> builder);
/**
* Protected fields accessible to subclasses for implementing operations.
* These are initialized from the builder in the constructor.
*/
protected final EmbeddingModel embeddingModel;
protected final BatchingStrategy batchingStrategy;
protected final ObservationRegistry observationRegistry;
protected final VectorStoreObservationConvention observationConvention;
/**
* Template method that implementations must override to provide the actual add operation.
* This method is called within an observation context created by the public add() method.
* Implementations should NOT create observations themselves.
*
* The observation context will automatically track:
* - Operation start and end time
* - Number of documents added
* - Any exceptions thrown
*
* @param documents Documents to add to the vector store
* @throws RuntimeException if add operation fails
*/
protected abstract void doAdd(List<Document> documents);
/**
* Template method that implementations must override to provide the actual delete by ID operation.
* This method is called within an observation context created by the public delete() method.
* Implementations should NOT create observations themselves.
*
* The observation context will automatically track:
* - Operation start and end time
* - Number of IDs to delete
* - Any exceptions thrown
*
* @param idList List of document IDs to delete
* @throws RuntimeException if delete operation fails
*/
protected abstract void doDelete(List<String> idList);
/**
* Template method that implementations must override to provide the actual similarity search operation.
* This method is called within an observation context created by the public similaritySearch() method.
* Implementations should NOT create observations themselves.
*
* The observation context will automatically track:
* - Operation start and end time
* - Query parameters (topK, threshold, filter)
* - Number of results returned
* - Any exceptions thrown
*
* @param request Search request with query, topK, threshold, and filter parameters
* @return List of similar documents ordered by similarity (highest first)
* @throws RuntimeException if search operation fails
*/
protected abstract List<Document> doSimilaritySearch(SearchRequest request);
/**
* Factory method that implementations must override to create observation context builders.
* The context builder should be populated with vector store-specific metadata
* like collection name, dimensions, similarity metric, etc.
*
* This method is called by the public add/delete/similaritySearch methods
* before invoking the corresponding do* methods.
*
* @param operationName Name of the operation (e.g., "add", "delete", "query")
* @return Builder for creating observation context with vector store metadata
*/
protected abstract VectorStoreObservationContext.Builder createObservationContextBuilder(String operationName);
/**
* Template method for concrete implementations to provide filter-based deletion logic.
* Default implementation throws UnsupportedOperationException.
* Override this method only if the vector store natively supports filter-based deletion.
*
* If overridden, this method is called within an observation context.
*
* @param filterExpression Filter expression to identify documents to delete
* @throws UnsupportedOperationException if not supported by implementation (default behavior)
* @throws RuntimeException if delete operation fails
*/
protected void doDelete(Filter.Expression filterExpression) {
throw new UnsupportedOperationException(
"Filter-based deletion is not supported by this vector store implementation"
);
}
/**
* Creates an observation for a vector store operation.
* This is a utility method that subclasses can use if they need to create
* custom observations for additional operations.
*
* @param context VectorStoreObservationContext with operation metadata
* @return Observation that can be started and stopped
*/
protected Observation createObservation(VectorStoreObservationContext context);
}Implementation Example:
import org.springframework.ai.vectorstore.observation.AbstractObservationVectorStore;
import org.springframework.ai.vectorstore.observation.VectorStoreObservationContext;
import org.springframework.ai.vectorstore.SpringAIVectorStoreTypes;
import org.springframework.ai.vectorstore.SearchRequest;
import org.springframework.ai.document.Document;
import java.util.List;
public class CustomVectorStore extends AbstractObservationVectorStore {
private final String collectionName;
private final int dimensions;
public CustomVectorStore(CustomVectorStoreBuilder builder) {
super(builder);
this.collectionName = builder.getCollectionName();
this.dimensions = builder.getDimensions();
}
@Override
protected void doAdd(List<Document> documents) {
// Actual implementation of add operation
// Observations are handled automatically by the parent class
for (Document doc : documents) {
// Embed if needed
if (doc.getEmbedding() == null || doc.getEmbedding().isEmpty()) {
float[] embedding = embeddingModel.embed(doc.getContent()).getOutput();
doc.setEmbedding(embedding);
}
// Store document
storeDocument(doc);
}
}
@Override
protected void doDelete(List<String> idList) {
// Actual implementation of delete operation
for (String id : idList) {
removeDocument(id);
}
}
@Override
protected List<Document> doSimilaritySearch(SearchRequest request) {
// Actual implementation of similarity search
float[] queryEmbedding = embeddingModel.embed(request.getQuery()).getOutput();
List<Document> results = findSimilarDocuments(queryEmbedding, request.getTopK());
// Apply filter if present
if (request.hasFilterExpression()) {
results = applyFilter(results, request.getFilterExpression());
}
// Apply similarity threshold
if (request.getSimilarityThreshold() > 0.0) {
results = results.stream()
.filter(doc -> {
Double score = (Double) doc.getMetadata().get("distance");
return score != null && score >= request.getSimilarityThreshold();
})
.toList();
}
return results;
}
@Override
protected VectorStoreObservationContext.Builder createObservationContextBuilder(String operationName) {
// Create observation context with vector store-specific metadata
return VectorStoreObservationContext
.builder(SpringAIVectorStoreTypes.SIMPLE, operationName)
.collectionName(collectionName)
.dimensions(dimensions)
.similarityMetric("cosine")
.fieldName("embedding");
}
// Helper methods
private void storeDocument(Document doc) {
// Implementation
}
private void removeDocument(String id) {
// Implementation
}
private List<Document> findSimilarDocuments(float[] embedding, int topK) {
// Implementation
return List.of();
}
private List<Document> applyFilter(List<Document> docs, Filter.Expression filter) {
// Implementation
return docs;
}
}Context object for storing metadata about vector store operations. Used to pass information to observation conventions and handlers.
package org.springframework.ai.vectorstore.observation;
import org.springframework.ai.vectorstore.SearchRequest;
import org.springframework.ai.document.Document;
import org.springframework.lang.Nullable;
import io.micrometer.observation.Observation;
import java.util.List;
/**
* Observation context for vector store operations.
* Contains metadata about the operation for metrics collection and tracing.
*
* This context is populated by vector store implementations and consumed
* by observation conventions and handlers to generate metrics and traces.
*/
public class VectorStoreObservationContext extends Observation.Context {
/**
* Vector store operation types.
* Defines the three main operations that can be observed.
*/
public enum Operation {
/** Adding documents to vector store */
ADD("add"),
/** Deleting documents from vector store */
DELETE("delete"),
/** Querying/searching vector store */
QUERY("query");
/** Operation name string value */
public final String value;
/**
* Constructor for Operation enum.
*
* @param value The string value for this operation
*/
Operation(String value) {
this.value = value;
}
/**
* Returns the operation name string.
*
* @return The operation name ("add", "delete", or "query")
*/
public String value() {
return this.value;
}
}
private final String databaseSystem;
private final String operationName;
private String collectionName;
private Integer dimensions;
private String fieldName;
private String namespace;
private String similarityMetric;
private SearchRequest queryRequest;
private List<Document> queryResponse;
/**
* Creates observation context for a vector store operation.
*
* @param databaseSystem Database system identifier (e.g., "pinecone", "chroma", "simple")
* @param operationName Operation name ("add", "delete", "query")
* @throws IllegalArgumentException if databaseSystem or operationName is null/empty
*/
public VectorStoreObservationContext(String databaseSystem, String operationName) {
if (databaseSystem == null || databaseSystem.isEmpty()) {
throw new IllegalArgumentException("Database system cannot be null or empty");
}
if (operationName == null || operationName.isEmpty()) {
throw new IllegalArgumentException("Operation name cannot be null or empty");
}
this.databaseSystem = databaseSystem;
this.operationName = operationName;
}
/**
* Creates a builder for VectorStoreObservationContext.
*
* @param databaseSystem Database system identifier
* @param operationName Operation name
* @return Builder instance
*/
public static Builder builder(String databaseSystem, String operationName) {
return new Builder(databaseSystem, operationName);
}
/**
* Creates a builder using an Operation enum.
*
* @param databaseSystem Database system identifier
* @param operation Operation enum value
* @return Builder instance
*/
public static Builder builder(String databaseSystem, Operation operation) {
return new Builder(databaseSystem, operation.value());
}
// Getters and setters
/**
* Returns the database system identifier.
* Used to identify the vector store provider in metrics.
*
* @return Database system (e.g., "pinecone", "chroma", "simple")
*/
public String getDatabaseSystem() {
return databaseSystem;
}
/**
* Returns the operation name.
*
* @return Operation name ("add", "delete", or "query")
*/
public String getOperationName() {
return operationName;
}
/**
* Returns the collection or index name.
*
* @return Collection name, or null if not set
*/
@Nullable
public String getCollectionName() {
return collectionName;
}
/**
* Sets the collection or index name.
*
* @param collectionName Name of the collection/index
*/
public void setCollectionName(@Nullable String collectionName) {
this.collectionName = collectionName;
}
/**
* Returns the embedding dimensions.
*
* @return Number of dimensions, or null if not set
*/
@Nullable
public Integer getDimensions() {
return dimensions;
}
/**
* Sets the embedding dimensions.
*
* @param dimensions Number of dimensions in embedding vectors
*/
public void setDimensions(@Nullable Integer dimensions) {
this.dimensions = dimensions;
}
/**
* Returns the vector field name.
*
* @return Field name, or null if not set
*/
@Nullable
public String getFieldName() {
return fieldName;
}
/**
* Sets the vector field name.
*
* @param fieldName Name of the field containing vectors
*/
public void setFieldName(@Nullable String fieldName) {
this.fieldName = fieldName;
}
/**
* Returns the namespace (for namespaced vector stores).
*
* @return Namespace, or null if not set
*/
@Nullable
public String getNamespace() {
return namespace;
}
/**
* Sets the namespace.
*
* @param namespace Namespace identifier
*/
public void setNamespace(@Nullable String namespace) {
this.namespace = namespace;
}
/**
* Returns the similarity metric (e.g., "cosine", "euclidean", "dot_product").
*
* @return Similarity metric, or null if not set
*/
@Nullable
public String getSimilarityMetric() {
return similarityMetric;
}
/**
* Sets the similarity metric.
*
* @param similarityMetric Metric name (e.g., "cosine", "euclidean", "dot_product")
*/
public void setSimilarityMetric(@Nullable String similarityMetric) {
this.similarityMetric = similarityMetric;
}
/**
* Returns the search request for query operations.
* Only populated for QUERY operations.
*
* @return SearchRequest, or null if not a query operation
*/
@Nullable
public SearchRequest getQueryRequest() {
return queryRequest;
}
/**
* Sets the search request.
* Should be called for QUERY operations to track query parameters.
*
* @param queryRequest SearchRequest with query parameters
*/
public void setQueryRequest(@Nullable SearchRequest queryRequest) {
this.queryRequest = queryRequest;
}
/**
* Returns the search response documents.
* Only populated for QUERY operations after execution.
*
* @return List of documents returned from search, or null if not set
*/
@Nullable
public List<Document> getQueryResponse() {
return queryResponse;
}
/**
* Sets the search response documents.
* Should be called for QUERY operations after execution to track results.
*
* @param queryResponse List of documents returned from search
*/
public void setQueryResponse(@Nullable List<Document> queryResponse) {
this.queryResponse = queryResponse;
}
/**
* Builder for VectorStoreObservationContext with fluent API.
*/
public static class Builder {
private final VectorStoreObservationContext context;
/**
* Creates a builder with required fields.
*
* @param databaseSystem Database system identifier
* @param operationName Operation name
*/
private Builder(String databaseSystem, String operationName) {
this.context = new VectorStoreObservationContext(databaseSystem, operationName);
}
/**
* Sets the collection or index name.
*
* @param collectionName Name of the collection/index
* @return This builder for chaining
*/
public Builder collectionName(@Nullable String collectionName) {
context.setCollectionName(collectionName);
return this;
}
/**
* Sets the embedding vector dimensions.
*
* @param dimensions Number of dimensions in embedding vectors
* @return This builder for chaining
*/
public Builder dimensions(@Nullable Integer dimensions) {
context.setDimensions(dimensions);
return this;
}
/**
* Sets the vector field name.
*
* @param fieldName Name of the field containing vectors
* @return This builder for chaining
*/
public Builder fieldName(@Nullable String fieldName) {
context.setFieldName(fieldName);
return this;
}
/**
* Sets the namespace for namespaced vector stores.
*
* @param namespace Namespace identifier
* @return This builder for chaining
*/
public Builder namespace(@Nullable String namespace) {
context.setNamespace(namespace);
return this;
}
/**
* Sets the similarity metric used for search.
*
* @param similarityMetric Metric name (e.g., "cosine", "euclidean", "dot_product")
* @return This builder for chaining
*/
public Builder similarityMetric(@Nullable String similarityMetric) {
context.setSimilarityMetric(similarityMetric);
return this;
}
/**
* Sets the search request for query operations.
*
* @param queryRequest SearchRequest with query parameters
* @return This builder for chaining
*/
public Builder queryRequest(@Nullable SearchRequest queryRequest) {
context.setQueryRequest(queryRequest);
return this;
}
/**
* Sets the search response documents.
*
* @param queryResponse List of documents returned from search
* @return This builder for chaining
*/
public Builder queryResponse(@Nullable List<Document> queryResponse) {
context.setQueryResponse(queryResponse);
return this;
}
/**
* Builds the VectorStoreObservationContext.
*
* @return Configured VectorStoreObservationContext instance
*/
public VectorStoreObservationContext build() {
return context;
}
}
}Usage Examples:
import org.springframework.ai.vectorstore.observation.VectorStoreObservationContext;
import org.springframework.ai.vectorstore.SearchRequest;
import java.util.List;
// Context for add operation
VectorStoreObservationContext addContext = VectorStoreObservationContext
.builder("pinecone", VectorStoreObservationContext.Operation.ADD)
.collectionName("documents")
.dimensions(1536)
.fieldName("embedding")
.namespace("production")
.similarityMetric("cosine")
.build();
// Context for query operation
SearchRequest request = SearchRequest.builder()
.query("machine learning")
.topK(10)
.similarityThreshold(0.75)
.build();
VectorStoreObservationContext queryContext = VectorStoreObservationContext
.builder("chroma", VectorStoreObservationContext.Operation.QUERY)
.collectionName("ml_docs")
.dimensions(768)
.similarityMetric("cosine")
.queryRequest(request)
.build();
// After query execution, set response
List<Document> results = vectorStore.similaritySearch(request);
queryContext.setQueryResponse(results);
// Context for delete operation
VectorStoreObservationContext deleteContext = VectorStoreObservationContext
.builder("simple", "delete")
.collectionName("temp_docs")
.build();Interface for defining how observations are named and what key-value pairs they contain. Allows customization of metrics structure.
package org.springframework.ai.vectorstore.observation;
import io.micrometer.observation.ObservationConvention;
import io.micrometer.observation.Observation;
import io.micrometer.common.KeyValues;
/**
* Convention for creating observations from VectorStoreObservationContext.
* Defines observation names and key-value pairs for metrics.
*
* Implementations control:
* - Observation names (used as metric names)
* - Contextual names (used in traces)
* - Low cardinality key-values (for efficient aggregation)
* - High cardinality key-values (for detailed analysis)
*/
public interface VectorStoreObservationConvention
extends ObservationConvention<VectorStoreObservationContext> {
/**
* Checks if this convention supports the given observation context.
* Default implementation checks if context is an instance of VectorStoreObservationContext.
*
* @param context The observation context to check
* @return true if context is supported (is VectorStoreObservationContext)
*/
@Override
default boolean supportsContext(Observation.Context context) {
return context instanceof VectorStoreObservationContext;
}
/**
* Returns the observation name.
* Used as the metric/span name in observability systems.
*
* Should be a constant value for consistent metric naming.
* Example: "db.vector.client.operation"
*
* @return Observation name (e.g., "db.vector.client.operation")
*/
@Override
String getName();
/**
* Returns contextual name for the observation.
* Typically includes operation name for human-readable traces.
*
* Examples:
* - "query documents"
* - "add documents"
* - "delete documents"
*
* @param context Observation context with operation metadata
* @return Contextual name
*/
@Override
String getContextualName(VectorStoreObservationContext context);
/**
* Returns low cardinality key-values for metrics.
* These are dimensions with limited distinct values (e.g., operation type, db system).
*
* Low cardinality keys are used for efficient metric aggregation and should have
* a bounded set of possible values (typically < 100 distinct values).
*
* Examples:
* - db.system: "pinecone", "chroma", "simple"
* - db.operation.name: "add", "delete", "query"
* - db.vector.similarity.metric: "cosine", "euclidean", "dot_product"
*
* @param context Observation context
* @return KeyValues for low cardinality dimensions
*/
@Override
KeyValues getLowCardinalityKeyValues(VectorStoreObservationContext context);
/**
* Returns high cardinality key-values for metrics.
* These are dimensions with many distinct values (e.g., query text, document IDs).
*
* High cardinality keys are used for detailed analysis but may not be suitable
* for aggregation due to the large number of distinct values.
*
* Examples:
* - db.collection.name: specific collection names
* - db.vector.query.top_k: specific K values
* - db.vector.query.filter: filter expressions
* - db.vector.query.response.documents.ids: document IDs
*
* @param context Observation context
* @return KeyValues for high cardinality dimensions
*/
@Override
KeyValues getHighCardinalityKeyValues(VectorStoreObservationContext context);
}Default implementation of VectorStoreObservationConvention with standard observation structure.
package org.springframework.ai.vectorstore.observation;
import io.micrometer.common.KeyValue;
import io.micrometer.common.KeyValues;
/**
* Default observation convention for vector store operations.
* Provides standard naming and key-value structure for metrics.
*
* This implementation follows OpenTelemetry semantic conventions for database operations
* with extensions for vector-specific attributes.
*/
public class DefaultVectorStoreObservationConvention
implements VectorStoreObservationConvention {
/**
* Default name for vector store observations.
* Follows the pattern: db.{database_type}.client.operation
*/
public static final String DEFAULT_NAME = "db.vector.client.operation";
private final String name;
/**
* Default constructor using the default observation name.
*/
public DefaultVectorStoreObservationConvention() {
this(DEFAULT_NAME);
}
/**
* Constructor with custom observation name.
* Allows overriding the default name for custom metric naming schemes.
*
* @param name Custom name for observations (overrides default)
*/
public DefaultVectorStoreObservationConvention(String name) {
this.name = name;
}
/**
* Returns "db.vector.client.operation" as the observation name.
*
* @return The observation name
*/
@Override
public String getName() {
return name;
}
/**
* Returns contextual name like "query documents" or "add documents".
*
* @param context Observation context
* @return Contextual name combining operation and "documents"
*/
@Override
public String getContextualName(VectorStoreObservationContext context) {
return context.getOperationName() + " documents";
}
/**
* Returns low cardinality key-values:
* - db.system: Database system identifier
* - db.operation.name: Operation name (add/delete/query)
* - db.vector.similarity.metric: Similarity metric if available
*
* @param context Observation context
* @return Low cardinality KeyValues
*/
@Override
public KeyValues getLowCardinalityKeyValues(VectorStoreObservationContext context) {
return KeyValues.of(
dbSystemKeyValue(context),
dbOperationNameKeyValue(context),
dbVectorSimilarityMetricKeyValue(context)
);
}
/**
* Returns high cardinality key-values:
* - db.collection.name: Collection/index name
* - db.namespace: Namespace if applicable
* - db.vector.dimension_count: Embedding dimensions
* - db.vector.field.name: Vector field name
* - db.vector.query.top_k: Top K results requested
* - db.vector.query.filter: Filter expression
* - db.vector.query.similarity_threshold: Similarity threshold
* - db.vector.query.response.documents.count: Number of results returned
* - db.vector.query.response.documents.ids: Document IDs (comma-separated)
* - db.vector.query.response.documents.contents: Document contents (truncated)
*
* @param context Observation context
* @return High cardinality KeyValues
*/
@Override
public KeyValues getHighCardinalityKeyValues(VectorStoreObservationContext context) {
return KeyValues.of(
dbCollectionNameKeyValue(context),
dbNamespaceKeyValue(context),
dbVectorDimensionCountKeyValue(context),
dbVectorFieldNameKeyValue(context),
dbVectorQueryTopKKeyValue(context),
dbVectorQueryFilterKeyValue(context),
dbVectorQuerySimilarityThresholdKeyValue(context),
dbVectorQueryResponseDocumentsCountKeyValue(context),
dbVectorQueryResponseDocumentsIdsKeyValue(context),
dbVectorQueryResponseDocumentsContentsKeyValue(context)
);
}
/**
* Protected helper methods for creating individual key-values.
* Subclasses can override these to customize specific key-values.
*/
protected KeyValue dbSystemKeyValue(VectorStoreObservationContext context) {
return KeyValue.of("db.system", context.getDatabaseSystem());
}
protected KeyValue dbOperationNameKeyValue(VectorStoreObservationContext context) {
return KeyValue.of("db.operation.name", context.getOperationName());
}
protected KeyValue dbCollectionNameKeyValue(VectorStoreObservationContext context) {
String collectionName = context.getCollectionName();
return KeyValue.of("db.collection.name", collectionName != null ? collectionName : "unknown");
}
protected KeyValue dbNamespaceKeyValue(VectorStoreObservationContext context) {
String namespace = context.getNamespace();
return namespace != null ? KeyValue.of("db.namespace", namespace) : KeyValue.of("db.namespace", "none");
}
protected KeyValue dbVectorDimensionCountKeyValue(VectorStoreObservationContext context) {
Integer dimensions = context.getDimensions();
return dimensions != null ? KeyValue.of("db.vector.dimension_count", String.valueOf(dimensions)) : KeyValue.of("db.vector.dimension_count", "unknown");
}
protected KeyValue dbVectorFieldNameKeyValue(VectorStoreObservationContext context) {
String fieldName = context.getFieldName();
return fieldName != null ? KeyValue.of("db.vector.field.name", fieldName) : KeyValue.of("db.vector.field.name", "unknown");
}
protected KeyValue dbVectorSimilarityMetricKeyValue(VectorStoreObservationContext context) {
String metric = context.getSimilarityMetric();
return metric != null ? KeyValue.of("db.vector.similarity.metric", metric) : KeyValue.of("db.vector.similarity.metric", "unknown");
}
protected KeyValue dbVectorQueryTopKKeyValue(VectorStoreObservationContext context) {
SearchRequest request = context.getQueryRequest();
if (request != null) {
return KeyValue.of("db.vector.query.top_k", String.valueOf(request.getTopK()));
}
return KeyValue.of("db.vector.query.top_k", "none");
}
protected KeyValue dbVectorQueryFilterKeyValue(VectorStoreObservationContext context) {
SearchRequest request = context.getQueryRequest();
if (request != null && request.hasFilterExpression()) {
return KeyValue.of("db.vector.query.filter", request.getFilterExpression().toString());
}
return KeyValue.of("db.vector.query.filter", "none");
}
protected KeyValue dbVectorQuerySimilarityThresholdKeyValue(VectorStoreObservationContext context) {
SearchRequest request = context.getQueryRequest();
if (request != null) {
return KeyValue.of("db.vector.query.similarity_threshold", String.valueOf(request.getSimilarityThreshold()));
}
return KeyValue.of("db.vector.query.similarity_threshold", "none");
}
protected KeyValue dbVectorQueryResponseDocumentsCountKeyValue(VectorStoreObservationContext context) {
List<Document> response = context.getQueryResponse();
if (response != null) {
return KeyValue.of("db.vector.query.response.documents.count", String.valueOf(response.size()));
}
return KeyValue.of("db.vector.query.response.documents.count", "0");
}
protected KeyValue dbVectorQueryResponseDocumentsIdsKeyValue(VectorStoreObservationContext context) {
List<Document> response = context.getQueryResponse();
if (response != null && !response.isEmpty()) {
String ids = response.stream()
.map(Document::getId)
.limit(10) // Limit to first 10 IDs
.collect(Collectors.joining(","));
return KeyValue.of("db.vector.query.response.documents.ids", ids);
}
return KeyValue.of("db.vector.query.response.documents.ids", "none");
}
protected KeyValue dbVectorQueryResponseDocumentsContentsKeyValue(VectorStoreObservationContext context) {
List<Document> response = context.getQueryResponse();
if (response != null && !response.isEmpty()) {
String contents = response.stream()
.map(doc -> doc.getContent().substring(0, Math.min(50, doc.getContent().length())))
.limit(3) // Limit to first 3 documents
.collect(Collectors.joining("; "));
return KeyValue.of("db.vector.query.response.documents.contents", contents);
}
return KeyValue.of("db.vector.query.response.documents.contents", "none");
}
}Custom Convention Example:
import org.springframework.ai.vectorstore.observation.VectorStoreObservationConvention;
import org.springframework.ai.vectorstore.observation.VectorStoreObservationContext;
import io.micrometer.common.KeyValue;
import io.micrometer.common.KeyValues;
public class CustomObservationConvention implements VectorStoreObservationConvention {
@Override
public String getName() {
return "custom.vector.operation";
}
@Override
public String getContextualName(VectorStoreObservationContext context) {
return context.getOperationName() + " on " + context.getCollectionName();
}
@Override
public KeyValues getLowCardinalityKeyValues(VectorStoreObservationContext context) {
return KeyValues.of(
KeyValue.of("db.type", context.getDatabaseSystem()),
KeyValue.of("operation", context.getOperationName()),
KeyValue.of("environment", System.getenv().getOrDefault("ENV", "dev"))
);
}
@Override
public KeyValues getHighCardinalityKeyValues(VectorStoreObservationContext context) {
return KeyValues.of(
KeyValue.of("collection", context.getCollectionName() != null ?
context.getCollectionName() : "unknown"),
KeyValue.of("timestamp", String.valueOf(System.currentTimeMillis()))
);
}
}
// Use custom convention in builder
SimpleVectorStore store = SimpleVectorStore.builder(embeddingModel)
.observationRegistry(observationRegistry)
.customObservationConvention(new CustomObservationConvention())
.build();import org.springframework.ai.vectorstore.SimpleVectorStore;
import org.springframework.ai.vectorstore.SearchRequest;
import org.springframework.ai.vectorstore.observation.DefaultVectorStoreObservationConvention;
import org.springframework.ai.embedding.EmbeddingModel;
import org.springframework.ai.document.Document;
import io.micrometer.observation.ObservationRegistry;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
import io.micrometer.tracing.Tracer;
import io.micrometer.tracing.brave.bridge.BraveBaggageManager;
import io.micrometer.tracing.brave.bridge.BraveTracer;
import brave.Tracing;
import java.util.List;
import java.util.Map;
// Setup meter registry for metrics
MeterRegistry meterRegistry = new SimpleMeterRegistry();
// Setup tracing (optional - for distributed tracing)
Tracing braveTracing = Tracing.newBuilder().build();
Tracer tracer = new BraveTracer(
braveTracing.tracer(),
braveTracing.currentTraceContext(),
new BraveBaggageManager()
);
// Create observation registry
ObservationRegistry observationRegistry = ObservationRegistry.create();
observationRegistry.observationConfig()
.observationHandler(new DefaultTracingObservationHandler(tracer))
.observationHandler(new DefaultMeterObservationHandler(meterRegistry));
// Create vector store with full observability
SimpleVectorStore vectorStore = SimpleVectorStore.builder(embeddingModel)
.observationRegistry(observationRegistry)
.customObservationConvention(new DefaultVectorStoreObservationConvention())
.build();
// Add documents - metrics collected automatically
List<Document> documents = List.of(
new Document("Spring Boot tutorial", Map.of("category", "framework", "year", 2024)),
new Document("Java programming guide", Map.of("category", "language", "year", 2023))
);
vectorStore.add(documents); // Observation: db.vector.client.operation (operation=add)
// Query documents - metrics collected automatically
SearchRequest request = SearchRequest.builder()
.query("Spring Boot")
.topK(10)
.similarityThreshold(0.7)
.filterExpression("year >= 2023")
.build();
List<Document> results = vectorStore.similaritySearch(request); // Observation: db.vector.client.operation (operation=query)
// Delete documents - metrics collected automatically
vectorStore.delete(List.of("doc-1", "doc-2")); // Observation: db.vector.client.operation (operation=delete)
// View collected metrics
meterRegistry.getMeters().forEach(meter -> {
System.out.println("Meter: " + meter.getId());
System.out.println("Measurements: " + meter.measure());
});
// Metrics available:
// - db.vector.client.operation (timer) - operation duration
// Tags: db.system=simple, db.operation.name=add/delete/query
// - db.vector.client.operation.active (gauge) - active operations
// - db.vector.client.operation.max (gauge) - max operation durationimport org.springframework.boot.actuate.autoconfigure.observation.ObservationAutoConfiguration;
import org.springframework.boot.actuate.autoconfigure.tracing.BraveAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
@Configuration
@Import({ObservationAutoConfiguration.class, BraveAutoConfiguration.class})
public class ObservabilityConfig {
@Bean
public SimpleVectorStore vectorStore(
EmbeddingModel embeddingModel,
ObservationRegistry observationRegistry) {
return SimpleVectorStore.builder(embeddingModel)
.observationRegistry(observationRegistry)
.customObservationConvention(new DefaultVectorStoreObservationConvention())
.build();
}
}application.yml:
management:
metrics:
export:
prometheus:
enabled: true
datadog:
enabled: false
tracing:
sampling:
probability: 1.0 # Sample 100% of requests (reduce in production)
observations:
key-values:
application: my-vector-app
environment: productionAfter running operations, the following metrics will be available at /actuator/prometheus:
# HELP db_vector_client_operation_seconds
# TYPE db_vector_client_operation_seconds summary
db_vector_client_operation_seconds_count{db_operation_name="add",db_system="simple",} 10.0
db_vector_client_operation_seconds_sum{db_operation_name="add",db_system="simple",} 0.523
db_vector_client_operation_seconds_count{db_operation_name="query",db_system="simple",} 50.0
db_vector_client_operation_seconds_sum{db_operation_name="query",db_system="simple",} 1.234
db_vector_client_operation_seconds_count{db_operation_name="delete",db_system="simple",} 5.0
db_vector_client_operation_seconds_sum{db_operation_name="delete",db_system="simple",} 0.089With Zipkin or Jaeger configured, traces will show:
Trace: user-request-12345
├─ HTTP GET /search
│ └─ db.vector.client.operation (query documents)
│ ├─ Span ID: abc123
│ ├─ Duration: 45ms
│ ├─ Tags:
│ │ ├─ db.system: simple
│ │ ├─ db.operation.name: query
│ │ ├─ db.collection.name: documents
│ │ ├─ db.vector.query.top_k: 10
│ │ ├─ db.vector.query.similarity_threshold: 0.7
│ │ └─ db.vector.query.response.documents.count: 8
│ └─ Events:
│ ├─ query.start
│ ├─ embedding.generated
│ ├─ similarity.computed
│ └─ query.completeInstall with Tessl CLI
npx tessl i tessl/maven-org-springframework-ai--spring-ai-vector-store@1.1.0