diff --git a/pom.xml b/pom.xml
index d48021dfa55..69f1e5d35fb 100644
--- a/pom.xml
+++ b/pom.xml
@@ -28,6 +28,8 @@
document-readers/pdf-reader
document-readers/tika-reader
embedding-clients/transformers-embedding
+ vector-stores/spring-ai-pinecone
+
@@ -82,6 +84,8 @@
0.1.3
42.6.0
2.3.0
+ 0.6.0
+ 3.24.4
1.19.0
diff --git a/vector-stores/spring-ai-pinecone/README.md b/vector-stores/spring-ai-pinecone/README.md
new file mode 100644
index 00000000000..5620694f11c
--- /dev/null
+++ b/vector-stores/spring-ai-pinecone/README.md
@@ -0,0 +1,113 @@
+# Pinecone VectorStore
+
+This readme will walk you through setting up the Pinecone VectorStore to store document embeddings and perform similarity searches.
+
+## What is Pinecone?
+
+[Pinecone](https://www.pinecone.io/) is a popular cloud-based vector database, which allows you to store and search vectors efficiently.
+
+## Prerequisites
+
+1. Pinecone Account: Before you start, ensure you sign up for a [Pinecone account](https://app.pinecone.io/).
+2. Pinecone Project: Once registered, create a new project, an index, and generate an API key. You'll need these details for configuration.
+3. OpenAI Account: Create an account at [OpenAI Signup](https://platform.openai.com/signup) and generate the token at [API Keys](https://platform.openai.com/account/api-keys)
+
+## Configuraiton
+
+To set up PineconeVectorStore, gather the following details from your Pinecone account:
+
+* Pinecond API Key
+* Pinecone Environment
+* Pinecone Project ID
+* Pinecone Index Name
+* Pinecone Namespace
+
+> **Note**
+> This information is available to you in the Pinecone UI portal.
+
+
+When setting up embeddings, select a vector dimension of 1526. This matches the dimensionality of OpenAI's model "text-embedding-ada-002", which we'll be using for this guide.
+
+Additionally, you'll need to provide your OpenAI API Key. Set it as an environment variable like so:
+
+```bash
+export SPRING_AI_OPENAI_API_KEY='Your_OpenAI_API_Key'
+```
+
+## Dependencies
+
+Add these dependencies to your project:
+
+1. OpenAI: Required for calculating embeddings.
+
+```xml
+
+ org.springframework.experimental.ai
+ spring-ai-openai-spring-boot-starter
+ 0.7.0-SNAPSHOT
+
+```
+
+2. Pinecone
+
+```xml
+
+ org.springframework.experimental.ai
+ spring-ai-pinecone
+ 0.7.0-SNAPSHOT
+
+```
+
+## Sample Code
+
+To configure Pinecone in your application, you can use the following setup:
+
+```java
+@Bean
+public PineconeVectorStoreConfig pineconeVectorStoreConfig() {
+
+ return PineconeVectorStoreConfig.builder()
+ .withApiKey(System.getenv( ))
+ .withEnvironment(gcp-starter)
+ .withProjectId(89309e6)
+ .withIndexName(spring-ai-test-index)
+ .withNamespace("") // Leave it empty as for free tier as later doesn't support namespaces.
+ .build();
+}
+```
+
+Integrate with OpenAI's embeddings by adding the Spring Boot OpenAI starter to your project.
+This provides you with an implementation of the Embeddings client:
+
+```java
+@Bean
+public VectorStore vectorStore(PineconeVectorStoreConfig config, EmbeddingClient embeddingClient) {
+ return new PineconeVectorStore(config, embeddingClient);
+}
+```
+
+In your main code, create some documents
+
+```java
+ List documents = List.of(
+ new Document("Spring AI rocks!! Spring AI rocks!! Spring AI rocks!! Spring AI rocks!! Spring AI rocks!!",
+ Collections.singletonMap("meta1", "meta1")),
+ new Document("Hello World Hello World Hello World Hello World Hello World Hello World Hello World"),
+ new Document(
+ "Great Depression Great Depression Great Depression Great Depression Great Depression Great Depression",
+ Collections.singletonMap("meta2", "meta2")));
+```
+
+Add the documents to your vector store:
+
+```java
+vectorStore.add(List.of(document));
+```
+
+And finally, retrieve documents similar to a query:
+
+```java
+List results = vectorStore.similaritySearch("Spring", 5);
+```
+
+If all goes well, you should retrieve the document containing the text "Spring AI rocks!!".
\ No newline at end of file
diff --git a/vector-stores/spring-ai-pinecone/pom.xml b/vector-stores/spring-ai-pinecone/pom.xml
new file mode 100644
index 00000000000..729d1b083b5
--- /dev/null
+++ b/vector-stores/spring-ai-pinecone/pom.xml
@@ -0,0 +1,70 @@
+
+
+ 4.0.0
+
+ org.springframework.experimental.ai
+ spring-ai
+ 0.7.0-SNAPSHOT
+ ../../pom.xml
+
+ spring-ai-pinecone
+ jar
+ spring-ai-pinecone
+ spring-ai-pinecone
+ https://github.com/spring-projects-experimental/spring-ai
+
+
+ https://github.com/spring-projects-experimental/spring-ai
+ git://github.com/spring-projects-experimental/spring-ai.git
+ git@github.com:spring-projects-experimental/spring-ai.git
+
+
+
+ 17
+ 17
+
+
+
+
+ org.springframework.experimental.ai
+ spring-ai-core
+ ${project.parent.version}
+
+
+
+ io.pinecone
+ pinecone-client
+ ${pinecone.version}
+
+
+
+ com.google.protobuf
+ protobuf-java-util
+ ${protobuf-java-util.version}
+
+
+
+
+ org.springframework.experimental.ai
+ spring-ai-openai-spring-boot-starter
+ ${parent.version}
+ test
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+ org.awaitility
+ awaitility
+ 3.0.0
+ test
+
+
+
+
+
diff --git a/vector-stores/spring-ai-pinecone/src/main/java/org/springframework/ai/vectorstore/PineconeVectorStore.java b/vector-stores/spring-ai-pinecone/src/main/java/org/springframework/ai/vectorstore/PineconeVectorStore.java
new file mode 100644
index 00000000000..3fc72228d18
--- /dev/null
+++ b/vector-stores/spring-ai-pinecone/src/main/java/org/springframework/ai/vectorstore/PineconeVectorStore.java
@@ -0,0 +1,393 @@
+/*
+ * Copyright 2023-2023 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 java.time.Duration;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.protobuf.Struct;
+import com.google.protobuf.Value;
+import com.google.protobuf.util.JsonFormat;
+import io.pinecone.PineconeClient;
+import io.pinecone.PineconeClientConfig;
+import io.pinecone.PineconeConnection;
+import io.pinecone.PineconeConnectionConfig;
+import io.pinecone.proto.DeleteRequest;
+import io.pinecone.proto.QueryRequest;
+import io.pinecone.proto.QueryResponse;
+import io.pinecone.proto.UpsertRequest;
+import io.pinecone.proto.Vector;
+
+import org.springframework.ai.document.Document;
+import org.springframework.ai.embedding.EmbeddingClient;
+import org.springframework.util.Assert;
+
+/**
+ * A VectorStore implementation backed by Pinecone, a cloud-based vector database. This
+ * store supports creating, updating, deleting, and similarity searching of documents in a
+ * Pinecone index.
+ *
+ * @author Christian Tzolov
+ */
+public class PineconeVectorStore implements VectorStore {
+
+ private static final String CONTENT_FIELD_NAME = "document_content";
+
+ private static final String DISTANCE_METADATA_FIELD_NAME = "distance";
+
+ private static final Double SIMILARITY_THRESHOLD_ALL = 0.0;
+
+ private final EmbeddingClient embeddingClient;
+
+ private final PineconeConnection pineconeConnection;
+
+ private final String pineconeNamespace;
+
+ private final int defaultSimilarityTopK;
+
+ private final ObjectMapper objectMapper;
+
+ /**
+ * Configuration class for the PineconeVectorStore.
+ */
+ public static final class PineconeVectorStoreConfig {
+
+ // The free tier (gcp-starter) doesn't support Namespaces.
+ // Leave the namespace empty (e.g. "") for the free tier.
+ private final String namespace;
+
+ private final PineconeConnectionConfig connectionConfig;
+
+ private final PineconeClientConfig clientConfig;
+
+ private final int defaultSimilarityTopK;
+
+ /**
+ * Constructor using the builder.
+ * @param builder The configuration builder.
+ */
+ /**
+ * Constructor using the builder.
+ * @param builder The configuration builder.
+ */
+ public PineconeVectorStoreConfig(Builder builder) {
+ this.namespace = builder.namespace;
+ this.defaultSimilarityTopK = builder.defaultSimilarityTopK;
+ this.connectionConfig = new PineconeConnectionConfig().withIndexName(builder.indexName);
+ this.clientConfig = new PineconeClientConfig().withApiKey(builder.apiKey)
+ .withEnvironment(builder.environment)
+ .withProjectName(builder.projectId)
+ .withApiKey(builder.apiKey)
+ .withServerSideTimeoutSec((int) builder.serverSideTimeout.toSeconds());
+ }
+
+ /**
+ * Start building a new configuration.
+ * @return The entry point for creating a new configuration.
+ */
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ /**
+ * {@return the default config}
+ */
+ public static PineconeVectorStoreConfig defaultConfig() {
+ return builder().build();
+ }
+
+ public static class Builder {
+
+ private String apiKey;
+
+ private String projectId;
+
+ private String environment;
+
+ private String indexName;
+
+ // The free-tier (gcp-starter) doesn't support Namespaces!
+ private String namespace = "";
+
+ private int defaultSimilarityTopK = 5;
+
+ /**
+ * Optional server-side timeout in seconds for all operations. Default: 20
+ * seconds.
+ */
+ private Duration serverSideTimeout = Duration.ofSeconds(20);
+
+ private Builder() {
+ }
+
+ /**
+ * Pinecone api key.
+ * @param apiKey key to use.
+ * @return this builder.
+ */
+ public Builder withApiKey(String apiKey) {
+ this.apiKey = apiKey;
+ return this;
+ }
+
+ /**
+ * Pinecone project id.
+ * @param projectId Project id to use.
+ * @return this builder.
+ */
+ public Builder withProjectId(String projectId) {
+ this.projectId = projectId;
+ return this;
+ }
+
+ /**
+ * Pinecone environment name.
+ * @param environment Environment name (e.g. gcp-starter).
+ * @return this builder.
+ */
+ public Builder withEnvironment(String environment) {
+ this.environment = environment;
+ return this;
+ }
+
+ /**
+ * Pinecone index name.
+ * @param indexName Pinecone index name to use.
+ * @return this builder.
+ */
+ public Builder withIndexName(String indexName) {
+ this.indexName = indexName;
+ return this;
+ }
+
+ /**
+ * Pinecone Namespace. The free-tier (gcp-starter) doesn't support Namespaces.
+ * For free-tier leave the namespace empty.
+ * @param namespace Pinecone namespace to use.
+ * @return this builder.
+ */
+ public Builder withNamespace(String namespace) {
+ this.namespace = namespace;
+ return this;
+ }
+
+ /**
+ * Pinecone server side timeout.
+ * @param serverSideTimeout server timeout to use.
+ * @return this builder.
+ */
+ public Builder withServerSideTimeout(Duration serverSideTimeout) {
+ this.serverSideTimeout = serverSideTimeout;
+ return this;
+ }
+
+ /**
+ * Pinecone default top K similarity search response size.
+ * @param defaultSimilarityTopK default top K to use.
+ * @return this builder.
+ */
+ public Builder withDefaultTopK(int defaultSimilarityTopK) {
+ this.defaultSimilarityTopK = defaultSimilarityTopK;
+ return this;
+ }
+
+ /**
+ * {@return the immutable configuration}
+ */
+ public PineconeVectorStoreConfig build() {
+ return new PineconeVectorStoreConfig(this);
+ }
+
+ }
+
+ }
+
+ /**
+ * Constructs a new PineconeVectorStore.
+ * @param config The configuration for the store.
+ * @param embeddingClient The client for embedding operations.
+ */
+ public PineconeVectorStore(PineconeVectorStoreConfig config, EmbeddingClient embeddingClient) {
+ Assert.notNull(config, "PineconeVectorStoreConfig must not be null");
+ Assert.notNull(embeddingClient, "EmbeddingClient must not be null");
+
+ this.embeddingClient = embeddingClient;
+ this.pineconeNamespace = config.namespace;
+ this.defaultSimilarityTopK = config.defaultSimilarityTopK;
+ this.pineconeConnection = new PineconeClient(config.clientConfig).connect(config.connectionConfig);
+ this.objectMapper = new ObjectMapper();
+ }
+
+ /**
+ * Adds a list of documents to the vector store.
+ * @param documents The list of documents to be added.
+ */
+ @Override
+ public void add(List documents) {
+
+ List upsertVectors = documents.stream().map(document -> {
+ // Compute and assign an embedding to the document.
+ document.setEmbedding(this.embeddingClient.embed(document));
+
+ return Vector.newBuilder()
+ .setId(document.getId())
+ .addAllValues(toFloatList(document.getEmbedding()))
+ .setMetadata(metadataToStruct(document))
+ .build();
+ }).toList();
+
+ UpsertRequest upsertRequest = UpsertRequest.newBuilder()
+ .addAllVectors(upsertVectors)
+ .setNamespace(this.pineconeNamespace)
+ .build();
+
+ this.pineconeConnection.getBlockingStub().upsert(upsertRequest);
+ }
+
+ /**
+ * Converts the document metadata to a Protobuf Struct.
+ * @param document The document containing metadata.
+ * @return The metadata as a Protobuf Struct.
+ */
+ private Struct metadataToStruct(Document document) {
+ try {
+ var structBuilder = Struct.newBuilder();
+ JsonFormat.parser()
+ .ignoringUnknownFields()
+ .merge(this.objectMapper.writeValueAsString(document.getMetadata()), structBuilder);
+ structBuilder.putFields(CONTENT_FIELD_NAME, contentValue(document));
+ return structBuilder.build();
+ }
+ catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * Retrieves the content value of a document.
+ * @param document The document.
+ * @return The content value.
+ */
+ private Value contentValue(Document document) {
+ return Value.newBuilder().setStringValue(document.getContent()).build();
+ }
+
+ /**
+ * Deletes a list of documents by their IDs.
+ * @param documentIds The list of document IDs to be deleted.
+ * @return An optional boolean indicating the deletion status.
+ */
+ @Override
+ public Optional delete(List documentIds) {
+
+ DeleteRequest deleteRequest = DeleteRequest.newBuilder()
+ .setNamespace(this.pineconeNamespace) // ignored for free tier.
+ .addAllIds(documentIds)
+ .setDeleteAll(false)
+ .build();
+
+ this.pineconeConnection.getBlockingStub().delete(deleteRequest);
+
+ // The Pinecone delete API does not provide deletion status info.
+ return Optional.of(true);
+ }
+
+ /**
+ * Searches for documents similar to the given query. Uses the default topK value.
+ * @param query The query string.
+ * @return A list of similar documents.
+ */
+ @Override
+ public List similaritySearch(String query) {
+ return similaritySearch(query, this.defaultSimilarityTopK);
+ }
+
+ /**
+ * Searches for documents similar to the given query.
+ * @param query The query string.
+ * @param topK The maximum number of results to return.
+ * @return A list of similar documents.
+ */
+ @Override
+ public List similaritySearch(String query, int topK) {
+ return similaritySearch(query, topK, SIMILARITY_THRESHOLD_ALL);
+ }
+
+ /**
+ * Searches for documents similar to the given query.
+ * @param query The query string.
+ * @param topK The maximum number of results to return.
+ * @param similarityThreshold The similarity threshold for results.
+ * @return A list of similar documents.
+ */
+ @Override
+ public List similaritySearch(String query, int topK, double similarityThreshold) {
+
+ List queryEmbedding = this.embeddingClient.embed(query);
+
+ QueryRequest queryRequest = QueryRequest.newBuilder()
+ .addAllVector(toFloatList(queryEmbedding))
+ .setTopK(topK)
+ .setIncludeMetadata(true)
+ .setNamespace(this.pineconeNamespace)
+ .build();
+
+ QueryResponse queryResponse = this.pineconeConnection.getBlockingStub().query(queryRequest);
+
+ return queryResponse.getMatchesList()
+ .stream()
+ .filter(scoredVector -> scoredVector.getScore() >= similarityThreshold)
+ .map(scoredVector -> {
+ var id = scoredVector.getId();
+ Struct metadataStruct = scoredVector.getMetadata();
+ var content = metadataStruct.getFieldsOrThrow(CONTENT_FIELD_NAME).getStringValue();
+ Map metadata = extractMetadata(metadataStruct);
+ metadata.put(DISTANCE_METADATA_FIELD_NAME, 1 - scoredVector.getScore());
+ return new Document(id, content, metadata);
+ })
+ .toList();
+ }
+
+ /**
+ * Extracts metadata from a Protobuf Struct.
+ * @param metadataStruct The Protobuf Struct containing metadata.
+ * @return The metadata as a map.
+ */
+ private Map extractMetadata(Struct metadataStruct) {
+ try {
+ String json = JsonFormat.printer().print(metadataStruct);
+ Map metadata = this.objectMapper.readValue(json, Map.class);
+ metadata.remove(CONTENT_FIELD_NAME);
+ return metadata;
+ }
+ catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * Converts a list of doubles to a list of floats.
+ * @param doubleList The list of doubles.
+ * @return The converted list of floats.
+ */
+ private List toFloatList(List doubleList) {
+ return doubleList.stream().map(d -> d.floatValue()).toList();
+ }
+
+}
diff --git a/vector-stores/spring-ai-pinecone/src/test/java/org/springframework/ai/vectorstore/PineconeVectorStoreIT.java b/vector-stores/spring-ai-pinecone/src/test/java/org/springframework/ai/vectorstore/PineconeVectorStoreIT.java
new file mode 100644
index 00000000000..ad4fd372ca0
--- /dev/null
+++ b/vector-stores/spring-ai-pinecone/src/test/java/org/springframework/ai/vectorstore/PineconeVectorStoreIT.java
@@ -0,0 +1,230 @@
+/*
+ * Copyright 2023-2023 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 java.util.Collections;
+import java.util.List;
+import java.util.UUID;
+import java.util.concurrent.TimeUnit;
+
+import org.awaitility.Awaitility;
+import org.awaitility.Duration;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
+
+import org.springframework.ai.autoconfigure.openai.OpenAiAutoConfiguration;
+import org.springframework.ai.document.Document;
+import org.springframework.ai.embedding.EmbeddingClient;
+import org.springframework.ai.vectorstore.PineconeVectorStore.PineconeVectorStoreConfig;
+import org.springframework.boot.SpringBootConfiguration;
+import org.springframework.boot.autoconfigure.AutoConfigurations;
+import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
+import org.springframework.boot.test.context.runner.ApplicationContextRunner;
+import org.springframework.context.annotation.Bean;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.hasSize;
+
+/**
+ * @author Christian Tzolov
+ */
+@EnabledIfEnvironmentVariable(named = "PINECONE_API_KEY", matches = ".+")
+@EnabledIfEnvironmentVariable(named = "OPENAI_API_KEY", matches = ".+")
+public class PineconeVectorStoreIT {
+
+ // Replace the PINECONE_ENVIRONMENT, PINECONE_PROJECT_ID, PINECONE_INDEX_NAME and
+ // PINECONE_API_KEY with your pinecone credentials.
+ private static final String PINECONE_ENVIRONMENT = "gcp-starter";
+
+ private static final String PINECONE_PROJECT_ID = "89309e6";
+
+ private static final String PINECONE_INDEX_NAME = "spring-ai-test-index";
+
+ // NOTE: Leave it empty as for free tier as later doesn't support namespaces.
+ private static final String PINECONE_NAMESPACE = "";
+
+ List documents = List.of(
+ new Document("Spring AI rocks!! Spring AI rocks!! Spring AI rocks!! Spring AI rocks!! Spring AI rocks!!",
+ Collections.singletonMap("meta1", "meta1")),
+ new Document("Hello World Hello World Hello World Hello World Hello World Hello World Hello World"),
+ new Document(
+ "Great Depression Great Depression Great Depression Great Depression Great Depression Great Depression",
+ Collections.singletonMap("meta2", "meta2")));
+
+ private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
+ .withUserConfiguration(TestApplication.class)
+ .withPropertyValues("spring.ai.openai.apiKey=" + System.getenv("OPENAI_API_KEY"));
+
+ @BeforeAll
+ public static void beforeAll() {
+ Awaitility.setDefaultPollInterval(10, TimeUnit.SECONDS);
+ Awaitility.setDefaultPollDelay(Duration.ZERO);
+ Awaitility.setDefaultTimeout(Duration.ONE_MINUTE);
+ }
+
+ @Test
+ public void addAndSearchTest() {
+
+ contextRunner.withConfiguration(AutoConfigurations.of(OpenAiAutoConfiguration.class)).run(context -> {
+
+ VectorStore vectorStore = context.getBean(VectorStore.class);
+
+ vectorStore.add(documents);
+
+ Awaitility.await().until(() -> {
+ return vectorStore.similaritySearch("Great", 1);
+ }, hasSize(1));
+
+ List results = vectorStore.similaritySearch("Great", 1);
+
+ assertThat(results).hasSize(1);
+ Document resultDoc = results.get(0);
+ assertThat(resultDoc.getId()).isEqualTo(documents.get(2).getId());
+ assertThat(resultDoc.getContent()).isEqualTo(
+ "Great Depression Great Depression Great Depression Great Depression Great Depression Great Depression");
+ assertThat(resultDoc.getMetadata()).hasSize(2);
+ assertThat(resultDoc.getMetadata()).containsKey("meta2");
+ assertThat(resultDoc.getMetadata()).containsKey("distance");
+
+ // Remove all documents from the store
+ vectorStore.delete(documents.stream().map(doc -> doc.getId()).toList());
+
+ Awaitility.await().until(() -> {
+ return vectorStore.similaritySearch("Hello", 1);
+ }, hasSize(0));
+ });
+ }
+
+ @Test
+ public void documentUpdateTest() {
+
+ // Note ,using OpenAI to calculate embeddings
+ contextRunner.withConfiguration(AutoConfigurations.of(OpenAiAutoConfiguration.class)).run(context -> {
+
+ VectorStore vectorStore = context.getBean(VectorStore.class);
+
+ Document document = new Document(UUID.randomUUID().toString(), "Spring AI rocks!!",
+ Collections.singletonMap("meta1", "meta1"));
+
+ vectorStore.add(List.of(document));
+
+ Awaitility.await().until(() -> {
+ return vectorStore.similaritySearch("Spring", 5);
+ }, hasSize(1));
+
+ List results = vectorStore.similaritySearch("Spring", 5);
+
+ assertThat(results).hasSize(1);
+ Document resultDoc = results.get(0);
+ assertThat(resultDoc.getId()).isEqualTo(document.getId());
+ assertThat(resultDoc.getContent()).isEqualTo("Spring AI rocks!!");
+ assertThat(resultDoc.getMetadata()).containsKey("meta1");
+ assertThat(resultDoc.getMetadata()).containsKey("distance");
+
+ Document sameIdDocument = new Document(document.getId(),
+ "The World is Big and Salvation Lurks Around the Corner",
+ Collections.singletonMap("meta2", "meta2"));
+
+ vectorStore.add(List.of(sameIdDocument));
+
+ Awaitility.await().until(() -> {
+ return vectorStore.similaritySearch("FooBar", 5).get(0).getContent();
+ }, equalTo("The World is Big and Salvation Lurks Around the Corner"));
+
+ results = vectorStore.similaritySearch("FooBar", 5);
+
+ assertThat(results).hasSize(1);
+ resultDoc = results.get(0);
+ assertThat(resultDoc.getId()).isEqualTo(document.getId());
+ assertThat(resultDoc.getContent()).isEqualTo("The World is Big and Salvation Lurks Around the Corner");
+ assertThat(resultDoc.getMetadata()).containsKey("meta2");
+ assertThat(resultDoc.getMetadata()).containsKey("distance");
+
+ // Remove all documents from the store
+ vectorStore.delete(List.of(document.getId()));
+ Awaitility.await().until(() -> {
+ return vectorStore.similaritySearch("FooBar", 1);
+ }, hasSize(0));
+
+ });
+ }
+
+ @Test
+ public void searchThresholdTest() {
+
+ contextRunner.withConfiguration(AutoConfigurations.of(OpenAiAutoConfiguration.class)).run(context -> {
+
+ VectorStore vectorStore = context.getBean(VectorStore.class);
+
+ vectorStore.add(documents);
+
+ Awaitility.await().until(() -> {
+ return vectorStore.similaritySearch("Great", 5);
+ }, hasSize(3));
+
+ List fullResult = vectorStore.similaritySearch("Great", 5, 0.0);
+
+ List distances = fullResult.stream().map(doc -> (Float) doc.getMetadata().get("distance")).toList();
+
+ assertThat(distances).hasSize(3);
+
+ float threshold = (distances.get(0) + distances.get(1)) / 2;
+
+ List results = vectorStore.similaritySearch("Great", 5, (1 - threshold));
+
+ assertThat(results).hasSize(1);
+ Document resultDoc = results.get(0);
+ assertThat(resultDoc.getId()).isEqualTo(documents.get(2).getId());
+ assertThat(resultDoc.getContent()).isEqualTo(
+ "Great Depression Great Depression Great Depression Great Depression Great Depression Great Depression");
+ assertThat(resultDoc.getMetadata()).containsKey("meta2");
+ assertThat(resultDoc.getMetadata()).containsKey("distance");
+
+ // Remove all documents from the store
+ vectorStore.delete(documents.stream().map(doc -> doc.getId()).toList());
+ Awaitility.await().until(() -> {
+ return vectorStore.similaritySearch("Hello", 1);
+ }, hasSize(0));
+ });
+ }
+
+ @SpringBootConfiguration
+ @EnableAutoConfiguration
+ public static class TestApplication {
+
+ @Bean
+ public PineconeVectorStoreConfig pineconeVectorStoreConfig() {
+
+ return PineconeVectorStoreConfig.builder()
+ .withApiKey(System.getenv("PINECONE_API_KEY"))
+ .withEnvironment(PINECONE_ENVIRONMENT)
+ .withProjectId(PINECONE_PROJECT_ID)
+ .withIndexName(PINECONE_INDEX_NAME)
+ .withNamespace(PINECONE_NAMESPACE)
+ .build();
+ }
+
+ @Bean
+ public VectorStore vectorStore(PineconeVectorStoreConfig config, EmbeddingClient embeddingClient) {
+ return new PineconeVectorStore(config, embeddingClient);
+ }
+
+ }
+
+}
\ No newline at end of file