Pinecone: Add configurable content and distance metadata fields (#882)

- Add `withContentFieldName` and `withDistanceMetadataFieldName` to Pinecone Config Builder
 - Add `spring.ai.vectorstore.pinecone.contentFieldName` and `spring.ai.vectorstore.pinecone.distanceMetadataFieldName`
   properties to the auto-config
 - Default content field name: "document_content", distance field: "distance"
 - Update tests and docs

 Resolves #882
This commit is contained in:
Christian Tzolov
2024-06-18 07:52:46 +02:00
parent aca3720f5b
commit 2c7dc3d6f1
8 changed files with 96 additions and 19 deletions

View File

@@ -63,7 +63,7 @@
*** xref:api/vectordbs/mongodb.adoc[]
*** xref:api/vectordbs/neo4j.adoc[]
*** xref:api/vectordbs/opensearch.adoc[]
*** xref:api/vectordbs/oracle.adoc[Oracle DB AI Vector Search]
*** xref:api/vectordbs/oracle.adoc[Oracle]
*** xref:api/vectordbs/pgvector.adoc[]
*** xref:api/vectordbs/pinecone.adoc[]
*** xref:api/vectordbs/qdrant.adoc[]

View File

@@ -111,6 +111,9 @@ You can use the following properties in your Spring Boot configuration to custom
|`spring.ai.vectorstore.pinecone.project-id`| Pinecone project ID | -
|`spring.ai.vectorstore.pinecone.index-name`| Pinecone index name | -
|`spring.ai.vectorstore.pinecone.namespace`| Pinecone namespace | -
|`spring.ai.vectorstore.pinecone.namespace`| Pinecone namespace | -
|`spring.ai.vectorstore.pinecone.content-field-name`| Pinecone metadata field name used to store the origina text content. | `document_content`
|`spring.ai.vectorstore.pinecone.distance-metadata-field-name`| Pinecone metadata field name used to store the computed distance. | `distance`
|`spring.ai.vectorstore.pinecone.server-side-timeout`| | 20 sec.
|===
@@ -193,6 +196,7 @@ public PineconeVectorStoreConfig pineconeVectorStoreConfig() {
.withProjectId("89309e6")
.withIndexName("spring-ai-test-index")
.withNamespace("") // the free tier doesn't support namespaces.
.withContentFieldName("my_content") // optional field to store the original content. Defaults to `document_content`
.build();
}
----

View File

@@ -42,6 +42,8 @@ public class PineconeVectorStoreAutoConfiguration {
.withProjectId(properties.getProjectId())
.withIndexName(properties.getIndexName())
.withNamespace(properties.getNamespace())
.withContentFieldName(properties.getContentFieldName())
.withDistanceMetadataFieldName(properties.getDistanceMetadataFieldName())
.withServerSideTimeout(properties.getServerSideTimeout())
.build();

View File

@@ -17,6 +17,7 @@ package org.springframework.ai.autoconfigure.vectorstore.pinecone;
import java.time.Duration;
import org.springframework.ai.vectorstore.PineconeVectorStore;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
@@ -37,6 +38,10 @@ public class PineconeVectorStoreProperties {
private String namespace = "";
private String contentFieldName = PineconeVectorStore.CONTENT_FIELD_NAME;
private String distanceMetadataFieldName = PineconeVectorStore.DISTANCE_METADATA_FIELD_NAME;
private Duration serverSideTimeout = Duration.ofSeconds(20);
public String getApiKey() {
@@ -87,4 +92,20 @@ public class PineconeVectorStoreProperties {
this.serverSideTimeout = serverSideTimeout;
}
public String getContentFieldName() {
return this.contentFieldName;
}
public void setContentFieldName(String contentFieldName) {
this.contentFieldName = contentFieldName;
}
public String getDistanceMetadataFieldName() {
return this.distanceMetadataFieldName;
}
public void setDistanceMetadataFieldName(String distanceMetadataFieldName) {
this.distanceMetadataFieldName = distanceMetadataFieldName;
}
}

View File

@@ -15,32 +15,31 @@
*/
package org.springframework.ai.autoconfigure.vectorstore.pinecone;
import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.Matchers.hasSize;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import org.awaitility.Awaitility;
import java.time.Duration;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
import org.springframework.ai.document.Document;
import org.springframework.ai.embedding.EmbeddingModel;
import org.springframework.ai.transformers.TransformersEmbeddingModel;
import org.springframework.ai.vectorstore.PineconeVectorStore;
import org.springframework.ai.vectorstore.SearchRequest;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.DefaultResourceLoader;
import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.Matchers.hasSize;
/**
* @author Christian Tzolov
*/
@@ -68,7 +67,9 @@ public class PineconeVectorStoreAutoConfigurationIT {
.withPropertyValues("spring.ai.vectorstore.pinecone.apiKey=" + System.getenv("PINECONE_API_KEY"),
"spring.ai.vectorstore.pinecone.environment=gcp-starter",
"spring.ai.vectorstore.pinecone.projectId=814621f",
"spring.ai.vectorstore.pinecone.indexName=spring-ai-test-index");
"spring.ai.vectorstore.pinecone.indexName=spring-ai-test-index",
"spring.ai.vectorstore.pinecone.contentFieldName=customContentField",
"spring.ai.vectorstore.pinecone.distanceMetadataFieldName=customDistanceField");
@BeforeAll
public static void beforeAll() {
@@ -82,7 +83,7 @@ public class PineconeVectorStoreAutoConfigurationIT {
contextRunner.run(context -> {
VectorStore vectorStore = context.getBean(VectorStore.class);
PineconeVectorStore vectorStore = context.getBean(PineconeVectorStore.class);
vectorStore.add(documents);
@@ -98,7 +99,7 @@ public class PineconeVectorStoreAutoConfigurationIT {
assertThat(resultDoc.getContent()).contains(
"Spring AI provides abstractions that serve as the foundation for developing AI applications.");
assertThat(resultDoc.getMetadata()).hasSize(2);
assertThat(resultDoc.getMetadata()).containsKeys("spring", "distance");
assertThat(resultDoc.getMetadata()).containsKeys("spring", "customDistanceField");
// Remove all documents from the store
vectorStore.delete(documents.stream().map(doc -> doc.getId()).toList());

View File

@@ -15,11 +15,12 @@
*/
package org.springframework.ai.autoconfigure.vectorstore.pinecone;
import static org.assertj.core.api.Assertions.assertThat;
import java.time.Duration;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import org.springframework.ai.vectorstore.PineconeVectorStore;
/**
* @author Christian Tzolov
@@ -35,6 +36,8 @@ public class PineconeVectorStorePropertiesTests {
assertThat(props.getProjectId()).isNull();
assertThat(props.getIndexName()).isNull();
assertThat(props.getServerSideTimeout()).isEqualTo(Duration.ofSeconds(20));
assertThat(props.getContentFieldName()).isEqualTo(PineconeVectorStore.CONTENT_FIELD_NAME);
assertThat(props.getDistanceMetadataFieldName()).isEqualTo(PineconeVectorStore.DISTANCE_METADATA_FIELD_NAME);
}
@Test
@@ -46,6 +49,8 @@ public class PineconeVectorStorePropertiesTests {
props.setNamespace("namespace");
props.setProjectId("project");
props.setServerSideTimeout(Duration.ofSeconds(60));
props.setContentFieldName("article");
props.setDistanceMetadataFieldName("distance2");
assertThat(props.getEnvironment()).isEqualTo("env");
assertThat(props.getNamespace()).isEqualTo("namespace");
@@ -53,6 +58,8 @@ public class PineconeVectorStorePropertiesTests {
assertThat(props.getProjectId()).isEqualTo("project");
assertThat(props.getIndexName()).isEqualTo("index");
assertThat(props.getServerSideTimeout()).isEqualTo(Duration.ofSeconds(60));
assertThat(props.getContentFieldName()).isEqualTo("article");
assertThat(props.getDistanceMetadataFieldName()).isEqualTo("distance2");
}
}

View File

@@ -20,6 +20,7 @@ import java.util.List;
import java.util.Map;
import java.util.Optional;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.protobuf.Struct;
import com.google.protobuf.Value;
@@ -51,9 +52,9 @@ import org.springframework.util.StringUtils;
*/
public class PineconeVectorStore implements VectorStore {
private static final String CONTENT_FIELD_NAME = "document_content";
public static final String CONTENT_FIELD_NAME = "document_content";
private static final String DISTANCE_METADATA_FIELD_NAME = "distance";
public static final String DISTANCE_METADATA_FIELD_NAME = "distance";
public final FilterExpressionConverter filterExpressionConverter = new PineconeFilterExpressionConverter();
@@ -63,6 +64,10 @@ public class PineconeVectorStore implements VectorStore {
private final String pineconeNamespace;
private final String pineconeContentFieldName;
private final String pineconeDistanceMetadataFieldName;
private final ObjectMapper objectMapper;
/**
@@ -74,6 +79,10 @@ public class PineconeVectorStore implements VectorStore {
// Leave the namespace empty (e.g. "") for the free tier.
private final String namespace;
private final String contentFieldName;
private final String distanceMetadataFieldName;
private final PineconeConnectionConfig connectionConfig;
private final PineconeClientConfig clientConfig;
@@ -90,6 +99,9 @@ public class PineconeVectorStore implements VectorStore {
*/
public PineconeVectorStoreConfig(Builder builder) {
this.namespace = builder.namespace;
this.contentFieldName = builder.contentFieldName;
this.distanceMetadataFieldName = builder.distanceMetadataFieldName;
// this.defaultSimilarityTopK = builder.defaultSimilarityTopK;
this.connectionConfig = new PineconeConnectionConfig().withIndexName(builder.indexName);
this.clientConfig = new PineconeClientConfig().withApiKey(builder.apiKey)
@@ -127,6 +139,10 @@ public class PineconeVectorStore implements VectorStore {
// The free-tier (gcp-starter) doesn't support Namespaces!
private String namespace = "";
private String contentFieldName = CONTENT_FIELD_NAME;
private String distanceMetadataFieldName = DISTANCE_METADATA_FIELD_NAME;
/**
* Optional server-side timeout in seconds for all operations. Default: 20
* seconds.
@@ -187,6 +203,26 @@ public class PineconeVectorStore implements VectorStore {
return this;
}
/**
* Content field name.
* @param contentFieldName content field name to use.
* @return this builder.
*/
public Builder withContentFieldName(String contentFieldName) {
this.contentFieldName = contentFieldName;
return this;
}
/**
* Distance metadata field name.
* @param distanceMetadataFieldName distance metadata field name to use.
* @return this builder.
*/
public Builder withDistanceMetadataFieldName(String distanceMetadataFieldName) {
this.distanceMetadataFieldName = distanceMetadataFieldName;
return this;
}
/**
* Pinecone server side timeout.
* @param serverSideTimeout server timeout to use.
@@ -219,6 +255,8 @@ public class PineconeVectorStore implements VectorStore {
this.embeddingModel = embeddingModel;
this.pineconeNamespace = config.namespace;
this.pineconeContentFieldName = config.contentFieldName;
this.pineconeDistanceMetadataFieldName = config.distanceMetadataFieldName;
this.pineconeConnection = new PineconeClient(config.clientConfig).connect(config.connectionConfig);
this.objectMapper = new ObjectMapper();
}
@@ -269,7 +307,7 @@ public class PineconeVectorStore implements VectorStore {
JsonFormat.parser()
.ignoringUnknownFields()
.merge(this.objectMapper.writeValueAsString(document.getMetadata()), structBuilder);
structBuilder.putFields(CONTENT_FIELD_NAME, contentValue(document));
structBuilder.putFields(this.pineconeContentFieldName, contentValue(document));
return structBuilder.build();
}
catch (Exception e) {
@@ -341,9 +379,9 @@ public class PineconeVectorStore implements VectorStore {
.map(scoredVector -> {
var id = scoredVector.getId();
Struct metadataStruct = scoredVector.getMetadata();
var content = metadataStruct.getFieldsOrThrow(CONTENT_FIELD_NAME).getStringValue();
var content = metadataStruct.getFieldsOrThrow(this.pineconeContentFieldName).getStringValue();
Map<String, Object> metadata = extractMetadata(metadataStruct);
metadata.put(DISTANCE_METADATA_FIELD_NAME, 1 - scoredVector.getScore());
metadata.put(this.pineconeDistanceMetadataFieldName, 1 - scoredVector.getScore());
return new Document(id, content, metadata);
})
.toList();
@@ -374,8 +412,9 @@ public class PineconeVectorStore implements VectorStore {
private Map<String, Object> extractMetadata(Struct metadataStruct) {
try {
String json = JsonFormat.printer().print(metadataStruct);
Map<String, Object> metadata = this.objectMapper.readValue(json, Map.class);
metadata.remove(CONTENT_FIELD_NAME);
Map<String, Object> metadata = this.objectMapper.readValue(json, new TypeReference<Map<String, Object>>() {
});
metadata.remove(this.pineconeContentFieldName);
return metadata;
}
catch (Exception e) {

View File

@@ -60,6 +60,8 @@ public class PineconeVectorStoreIT {
// NOTE: Leave it empty as for free tier as later doesn't support namespaces.
private static final String PINECONE_NAMESPACE = "";
private static final String CUSTOM_CONTENT_FIELD_NAME = "article";
List<Document> documents = List.of(
new Document("1", getText("classpath:/test/data/spring.ai.txt"), Map.of("meta1", "meta1")),
new Document("2", getText("classpath:/test/data/time.shelter.txt"), Map.of()),
@@ -283,6 +285,7 @@ public class PineconeVectorStoreIT {
.withProjectId(PINECONE_PROJECT_ID)
.withIndexName(PINECONE_INDEX_NAME)
.withNamespace(PINECONE_NAMESPACE)
.withContentFieldName(CUSTOM_CONTENT_FIELD_NAME)
.build();
}