Skip to content

Commit 64f88a3

Browse files
committed
Improve optional values handling in vector store observations
Vector store observations support several key-value pairs, coming from the Spring AI abstractions. Currently, whenever a value is not available (either because not configured by the user or not supported by the vector store provider), span/metrics attributes are generated anyway with value none. That causes several issues, including an unneeded increase in time series, challenges in alerting/monitoring (especially for integer/double attributes that suddenly are populated with a string), and non-compliance with the OpenTelemetry Semantic Conventions (according to which, attributes should be excluded altogether if there's no value). This pull request changes the conventions for vector store observations to exclude the generation of span/metrics attributes for optional values which don't have any value. Signed-off-by: Thomas Vitale <[email protected]>
1 parent 3b1c68a commit 64f88a3

File tree

35 files changed

+695
-322
lines changed

35 files changed

+695
-322
lines changed

spring-ai-core/src/main/java/org/springframework/ai/observation/conventions/VectorStoreObservationAttributes.java

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,13 @@ public enum VectorStoreObservationAttributes {
5656
*/
5757
DB_SYSTEM("db.system"),
5858

59+
// DB Search
60+
61+
/**
62+
* The metric used in similarity search.
63+
*/
64+
DB_SEARCH_SIMILARITY_METRIC("db.search.similarity_metric"),
65+
5966
// DB Vector
6067

6168
/**
@@ -68,11 +75,6 @@ public enum VectorStoreObservationAttributes {
6875
*/
6976
DB_VECTOR_FIELD_NAME("db.vector.field_name"),
7077

71-
/**
72-
* The model used for the embedding.
73-
*/
74-
DB_VECTOR_MODEL("db.vector.model"),
75-
7678
/**
7779
* The content of the search query being executed.
7880
*/
@@ -98,12 +100,7 @@ public enum VectorStoreObservationAttributes {
98100
/**
99101
* The top-k most similar vectors returned by a query.
100102
*/
101-
DB_VECTOR_QUERY_TOP_K("db.vector.query.top_k"),
102-
103-
/**
104-
* The metric used in similarity search.
105-
*/
106-
DB_VECTOR_SIMILARITY_METRIC("db.vector.similarity_metric");
103+
DB_VECTOR_QUERY_TOP_K("db.vector.query.top_k");
107104

108105
private final String value;
109106

spring-ai-core/src/main/java/org/springframework/ai/observation/conventions/VectorStoreProvider.java

Lines changed: 30 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -16,32 +16,42 @@
1616
package org.springframework.ai.observation.conventions;
1717

1818
/**
19+
* Collection of systems providing vector store functionality. Based on the OpenTelemetry
20+
* Semantic Conventions for Vector Databases.
21+
*
1922
* @author Christian Tzolov
23+
* @author Thomas Vitale
2024
* @since 1.0.0
25+
* @see <a href=
26+
* "https://github.com/open-telemetry/semantic-conventions/tree/main/docs/database">DB
27+
* Semantic Conventions</a>.
2128
*/
2229
public enum VectorStoreProvider {
2330

2431
// @formatter:off
25-
PG_VECTOR("pg_vector"),
26-
AZURE("azure"),
27-
CASSANDRA("cassandra"),
28-
CHROMA("chroma"),
29-
ELASTICSEARCH("elasticsearch"),
30-
MILVUS("milvus"),
31-
NEO4J("neo4j"),
32-
OPENSEARCH("opensearch"),
33-
QDRANT("qdrant"),
34-
REDIS("redis"),
35-
TYPESENSE("typesense"),
36-
WEAVIATE("weaviate"),
37-
PINECONE("pinecone"),
38-
ORACLE("oracle"),
39-
MONGODB("mongodb"),
40-
GEMFIRE("gemfire"),
41-
HANA("hana"),
42-
SIMPLE("simple");
43-
44-
// @formatter:on
32+
33+
// Please, keep the alphabetical sorting.
34+
AZURE("azure"),
35+
CASSANDRA("cassandra"),
36+
CHROMA("chroma"),
37+
ELASTICSEARCH("elasticsearch"),
38+
GEMFIRE("gemfire"),
39+
HANA("hana"),
40+
MILVUS("milvus"),
41+
MONGODB("mongodb"),
42+
NEO4J("neo4j"),
43+
OPENSEARCH("opensearch"),
44+
ORACLE("oracle"),
45+
PG_VECTOR("pg_vector"),
46+
PINECONE("pinecone"),
47+
QDRANT("qdrant"),
48+
REDIS("redis"),
49+
SIMPLE("simple"),
50+
TYPESENSE("typesense"),
51+
WEAVIATE("weaviate");
52+
53+
// @formatter:on
54+
4555
private final String value;
4656

4757
VectorStoreProvider(String value) {

spring-ai-core/src/main/java/org/springframework/ai/vectorstore/observation/DefaultVectorStoreObservationConvention.java

Lines changed: 44 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -35,33 +35,6 @@ public class DefaultVectorStoreObservationConvention implements VectorStoreObser
3535

3636
public static final String DEFAULT_NAME = "db.vector.client.operation";
3737

38-
private static final KeyValue COLLECTION_NAME_NONE = KeyValue.of(HighCardinalityKeyNames.DB_COLLECTION_NAME,
39-
KeyValue.NONE_VALUE);
40-
41-
private static final KeyValue DIMENSIONS_NONE = KeyValue.of(HighCardinalityKeyNames.DB_VECTOR_DIMENSION_COUNT,
42-
KeyValue.NONE_VALUE);
43-
44-
private static final KeyValue METADATA_FILTER_NONE = KeyValue.of(HighCardinalityKeyNames.DB_VECTOR_QUERY_FILTER,
45-
KeyValue.NONE_VALUE);
46-
47-
private static final KeyValue FIELD_NAME_NONE = KeyValue.of(HighCardinalityKeyNames.DB_VECTOR_FIELD_NAME,
48-
KeyValue.NONE_VALUE);
49-
50-
private static final KeyValue NAMESPACE_NONE = KeyValue.of(HighCardinalityKeyNames.DB_NAMESPACE,
51-
KeyValue.NONE_VALUE);
52-
53-
private static final KeyValue QUERY_CONTENT_NONE = KeyValue.of(HighCardinalityKeyNames.DB_VECTOR_QUERY_CONTENT,
54-
KeyValue.NONE_VALUE);
55-
56-
private static final KeyValue SIMILARITY_METRIC_NONE = KeyValue
57-
.of(HighCardinalityKeyNames.DB_VECTOR_SIMILARITY_METRIC, KeyValue.NONE_VALUE);
58-
59-
private static final KeyValue SIMILARITY_THRESHOLD_NONE = KeyValue
60-
.of(HighCardinalityKeyNames.DB_VECTOR_QUERY_SIMILARITY_THRESHOLD, KeyValue.NONE_VALUE);
61-
62-
private static final KeyValue TOP_K_NONE = KeyValue.of(HighCardinalityKeyNames.DB_VECTOR_QUERY_TOP_K,
63-
KeyValue.NONE_VALUE);
64-
6538
private final String name;
6639

6740
public DefaultVectorStoreObservationConvention() {
@@ -102,74 +75,86 @@ protected KeyValue dbOperationName(VectorStoreObservationContext context) {
10275

10376
@Override
10477
public KeyValues getHighCardinalityKeyValues(VectorStoreObservationContext context) {
105-
return KeyValues.of(collectionName(context), dimensions(context), fieldName(context), metadataFilter(context),
106-
namespace(context), queryContent(context), similarityMetric(context), similarityThreshold(context),
107-
topK(context));
108-
}
109-
110-
protected KeyValue collectionName(VectorStoreObservationContext context) {
78+
var keyValues = KeyValues.empty();
79+
keyValues = collectionName(keyValues, context);
80+
keyValues = dimensions(keyValues, context);
81+
keyValues = fieldName(keyValues, context);
82+
keyValues = metadataFilter(keyValues, context);
83+
keyValues = namespace(keyValues, context);
84+
keyValues = queryContent(keyValues, context);
85+
keyValues = similarityMetric(keyValues, context);
86+
keyValues = similarityThreshold(keyValues, context);
87+
keyValues = topK(keyValues, context);
88+
return keyValues;
89+
}
90+
91+
protected KeyValues collectionName(KeyValues keyValues, VectorStoreObservationContext context) {
11192
if (StringUtils.hasText(context.getCollectionName())) {
112-
return KeyValue.of(HighCardinalityKeyNames.DB_COLLECTION_NAME, context.getCollectionName());
93+
return keyValues.and(HighCardinalityKeyNames.DB_COLLECTION_NAME.asString(), context.getCollectionName());
11394
}
114-
return COLLECTION_NAME_NONE;
95+
return keyValues;
11596
}
11697

117-
protected KeyValue dimensions(VectorStoreObservationContext context) {
98+
protected KeyValues dimensions(KeyValues keyValues, VectorStoreObservationContext context) {
11899
if (context.getDimensions() != null && context.getDimensions() > 0) {
119-
return KeyValue.of(HighCardinalityKeyNames.DB_VECTOR_DIMENSION_COUNT, "" + context.getDimensions());
100+
return keyValues.and(HighCardinalityKeyNames.DB_VECTOR_DIMENSION_COUNT.asString(),
101+
"" + context.getDimensions());
120102
}
121-
return DIMENSIONS_NONE;
103+
return keyValues;
122104
}
123105

124-
protected KeyValue fieldName(VectorStoreObservationContext context) {
106+
protected KeyValues fieldName(KeyValues keyValues, VectorStoreObservationContext context) {
125107
if (StringUtils.hasText(context.getFieldName())) {
126-
return KeyValue.of(HighCardinalityKeyNames.DB_VECTOR_FIELD_NAME, context.getFieldName());
108+
return keyValues.and(HighCardinalityKeyNames.DB_VECTOR_FIELD_NAME.asString(), context.getFieldName());
127109
}
128-
return FIELD_NAME_NONE;
110+
return keyValues;
129111
}
130112

131-
protected KeyValue metadataFilter(VectorStoreObservationContext context) {
113+
protected KeyValues metadataFilter(KeyValues keyValues, VectorStoreObservationContext context) {
132114
if (context.getQueryRequest() != null && context.getQueryRequest().getFilterExpression() != null) {
133-
return KeyValue.of(HighCardinalityKeyNames.DB_VECTOR_QUERY_FILTER,
115+
return keyValues.and(HighCardinalityKeyNames.DB_VECTOR_QUERY_FILTER.asString(),
134116
context.getQueryRequest().getFilterExpression().toString());
135117
}
136-
return METADATA_FILTER_NONE;
118+
return keyValues;
137119
}
138120

139-
protected KeyValue namespace(VectorStoreObservationContext context) {
121+
protected KeyValues namespace(KeyValues keyValues, VectorStoreObservationContext context) {
140122
if (StringUtils.hasText(context.getNamespace())) {
141-
return KeyValue.of(HighCardinalityKeyNames.DB_NAMESPACE, context.getNamespace());
123+
return keyValues.and(HighCardinalityKeyNames.DB_NAMESPACE.asString(), context.getNamespace());
142124
}
143-
return NAMESPACE_NONE;
125+
return keyValues;
144126
}
145127

146-
protected KeyValue queryContent(VectorStoreObservationContext context) {
128+
protected KeyValues queryContent(KeyValues keyValues, VectorStoreObservationContext context) {
147129
if (context.getQueryRequest() != null && StringUtils.hasText(context.getQueryRequest().getQuery())) {
148-
return KeyValue.of(HighCardinalityKeyNames.DB_VECTOR_QUERY_CONTENT, context.getQueryRequest().getQuery());
130+
return keyValues.and(HighCardinalityKeyNames.DB_VECTOR_QUERY_CONTENT.asString(),
131+
context.getQueryRequest().getQuery());
149132
}
150-
return QUERY_CONTENT_NONE;
133+
return keyValues;
151134
}
152135

153-
protected KeyValue similarityMetric(VectorStoreObservationContext context) {
136+
protected KeyValues similarityMetric(KeyValues keyValues, VectorStoreObservationContext context) {
154137
if (StringUtils.hasText(context.getSimilarityMetric())) {
155-
return KeyValue.of(HighCardinalityKeyNames.DB_VECTOR_SIMILARITY_METRIC, context.getSimilarityMetric());
138+
return keyValues.and(HighCardinalityKeyNames.DB_SEARCH_SIMILARITY_METRIC.asString(),
139+
context.getSimilarityMetric());
156140
}
157-
return SIMILARITY_METRIC_NONE;
141+
return keyValues;
158142
}
159143

160-
protected KeyValue similarityThreshold(VectorStoreObservationContext context) {
144+
protected KeyValues similarityThreshold(KeyValues keyValues, VectorStoreObservationContext context) {
161145
if (context.getQueryRequest() != null && context.getQueryRequest().getSimilarityThreshold() >= 0) {
162-
return KeyValue.of(HighCardinalityKeyNames.DB_VECTOR_QUERY_SIMILARITY_THRESHOLD,
146+
return keyValues.and(HighCardinalityKeyNames.DB_VECTOR_QUERY_SIMILARITY_THRESHOLD.asString(),
163147
String.valueOf(context.getQueryRequest().getSimilarityThreshold()));
164148
}
165-
return SIMILARITY_THRESHOLD_NONE;
149+
return keyValues;
166150
}
167151

168-
protected KeyValue topK(VectorStoreObservationContext context) {
152+
protected KeyValues topK(KeyValues keyValues, VectorStoreObservationContext context) {
169153
if (context.getQueryRequest() != null && context.getQueryRequest().getTopK() > 0) {
170-
return KeyValue.of(HighCardinalityKeyNames.DB_VECTOR_QUERY_TOP_K, "" + context.getQueryRequest().getTopK());
154+
return keyValues.and(HighCardinalityKeyNames.DB_VECTOR_QUERY_TOP_K.asString(),
155+
"" + context.getQueryRequest().getTopK());
171156
}
172-
return TOP_K_NONE;
157+
return keyValues;
173158
}
174159

175160
}

spring-ai-core/src/main/java/org/springframework/ai/vectorstore/observation/VectorStoreObservationDocumentation.java

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,18 @@ public String asString() {
116116
}
117117
},
118118

119+
// DB Search
120+
121+
/**
122+
* The metric used in similarity search.
123+
*/
124+
DB_SEARCH_SIMILARITY_METRIC {
125+
@Override
126+
public String asString() {
127+
return VectorStoreObservationAttributes.DB_SEARCH_SIMILARITY_METRIC.value();
128+
}
129+
},
130+
119131
// DB Vector
120132

121133
/**
@@ -188,16 +200,6 @@ public String asString() {
188200
public String asString() {
189201
return VectorStoreObservationAttributes.DB_VECTOR_QUERY_TOP_K.value();
190202
}
191-
},
192-
193-
/**
194-
* The metric used in similarity search.
195-
*/
196-
DB_VECTOR_SIMILARITY_METRIC {
197-
@Override
198-
public String asString() {
199-
return VectorStoreObservationAttributes.DB_VECTOR_SIMILARITY_METRIC.value();
200-
}
201203
};
202204

203205
}

spring-ai-core/src/test/java/org/springframework/ai/vectorstore/observation/DefaultVectorStoreObservationConventionTests.java

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -68,13 +68,13 @@ void shouldHaveRequiredKeyValues() {
6868
.builder("my_database", VectorStoreObservationContext.Operation.QUERY)
6969
.build();
7070
assertThat(this.observationConvention.getLowCardinalityKeyValues(observationContext)).contains(
71+
KeyValue.of(LowCardinalityKeyNames.SPRING_AI_KIND.asString(), SpringAiKind.VECTOR_STORE.value()),
7172
KeyValue.of(LowCardinalityKeyNames.DB_OPERATION_NAME.asString(), "query"),
7273
KeyValue.of(LowCardinalityKeyNames.DB_SYSTEM.asString(), "my_database"));
7374
}
7475

7576
@Test
7677
void shouldHaveOptionalKeyValues() {
77-
7878
VectorStoreObservationContext observationContext = VectorStoreObservationContext
7979
.builder("my-database", VectorStoreObservationContext.Operation.QUERY)
8080
.withCollectionName("COLLECTION_NAME")
@@ -102,26 +102,28 @@ void shouldHaveOptionalKeyValues() {
102102
KeyValue.of(HighCardinalityKeyNames.DB_VECTOR_DIMENSION_COUNT.asString(), "696"),
103103
KeyValue.of(HighCardinalityKeyNames.DB_VECTOR_FIELD_NAME.asString(), "FIELD_NAME"),
104104
KeyValue.of(HighCardinalityKeyNames.DB_NAMESPACE.asString(), "NAMESPACE"),
105-
KeyValue.of(HighCardinalityKeyNames.DB_VECTOR_SIMILARITY_METRIC.asString(), "SIMILARITY_METRIC"),
105+
KeyValue.of(HighCardinalityKeyNames.DB_SEARCH_SIMILARITY_METRIC.asString(), "SIMILARITY_METRIC"),
106106
KeyValue.of(HighCardinalityKeyNames.DB_VECTOR_QUERY_CONTENT.asString(), "VDB QUERY"),
107107
KeyValue.of(HighCardinalityKeyNames.DB_VECTOR_QUERY_FILTER.asString(),
108108
"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]]]"));
109109
}
110110

111111
@Test
112-
void shouldHaveMissingKeyValues() {
112+
void shouldNotHaveKeyValuesWhenMissing() {
113113
VectorStoreObservationContext observationContext = VectorStoreObservationContext
114114
.builder("my-database", VectorStoreObservationContext.Operation.QUERY)
115115
.build();
116116

117-
assertThat(this.observationConvention.getHighCardinalityKeyValues(observationContext)).contains(
118-
KeyValue.of(HighCardinalityKeyNames.DB_COLLECTION_NAME.asString(), KeyValue.NONE_VALUE),
119-
KeyValue.of(HighCardinalityKeyNames.DB_VECTOR_DIMENSION_COUNT.asString(), KeyValue.NONE_VALUE),
120-
KeyValue.of(HighCardinalityKeyNames.DB_VECTOR_FIELD_NAME.asString(), KeyValue.NONE_VALUE),
121-
KeyValue.of(HighCardinalityKeyNames.DB_NAMESPACE.asString(), KeyValue.NONE_VALUE),
122-
KeyValue.of(HighCardinalityKeyNames.DB_VECTOR_SIMILARITY_METRIC.asString(), KeyValue.NONE_VALUE),
123-
KeyValue.of(HighCardinalityKeyNames.DB_VECTOR_QUERY_CONTENT.asString(), KeyValue.NONE_VALUE),
124-
KeyValue.of(HighCardinalityKeyNames.DB_VECTOR_QUERY_FILTER.asString(), KeyValue.NONE_VALUE));
117+
assertThat(this.observationConvention.getHighCardinalityKeyValues(observationContext)
118+
.stream()
119+
.map(KeyValue::getKey)
120+
.toList()).doesNotContain(HighCardinalityKeyNames.DB_COLLECTION_NAME.asString(),
121+
HighCardinalityKeyNames.DB_VECTOR_DIMENSION_COUNT.asString(),
122+
HighCardinalityKeyNames.DB_VECTOR_FIELD_NAME.asString(),
123+
HighCardinalityKeyNames.DB_NAMESPACE.asString(),
124+
HighCardinalityKeyNames.DB_SEARCH_SIMILARITY_METRIC.asString(),
125+
HighCardinalityKeyNames.DB_VECTOR_QUERY_CONTENT.asString(),
126+
HighCardinalityKeyNames.DB_VECTOR_QUERY_FILTER.asString());
125127
}
126128

127129
}

0 commit comments

Comments
 (0)