diff --git a/.github/workflows/build-against-langchain4j.yml b/.github/workflows/build-against-langchain4j.yml index 595637b50..3238920b2 100644 --- a/.github/workflows/build-against-langchain4j.yml +++ b/.github/workflows/build-against-langchain4j.yml @@ -13,6 +13,8 @@ jobs: build: name: Build on ${{ matrix.os }} - ${{ matrix.java }} strategy: + # PineconeEmbeddingStoreTest uses a single shared index, we can't run multiple CI runs on it at once + max-parallel: 1 fail-fast: false matrix: os: [ubuntu-latest] @@ -41,6 +43,16 @@ jobs: - name: Build with Maven run: mvn -B clean install -Dno-format + env: + PINECONE_API_KEY: ${{ secrets.PINECONE_API_KEY }} + PINECONE_ENVIRONMENT: ${{ secrets.PINECONE_ENVIRONMENT }} + PINECONE_INDEX_NAME: ${{ secrets.PINECONE_INDEX_NAME }} + PINECONE_PROJECT_ID: ${{ secrets.PINECONE_PROJECT_ID }} - name: Build with Maven (Native) run: mvn -B install -Dnative -Dquarkus.native.container-build -Dnative.surefire.skip + env: + PINECONE_API_KEY: ${{ secrets.PINECONE_API_KEY }} + PINECONE_ENVIRONMENT: ${{ secrets.PINECONE_ENVIRONMENT }} + PINECONE_INDEX_NAME: ${{ secrets.PINECONE_INDEX_NAME }} + PINECONE_PROJECT_ID: ${{ secrets.PINECONE_PROJECT_ID }} diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d42ab99ec..8098db1c5 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -34,6 +34,8 @@ jobs: build: name: Build on ${{ matrix.os }} - ${{ matrix.java }} strategy: + # PineconeEmbeddingStoreTest uses a single shared index, we can't run multiple CI runs on it at once + max-parallel: 1 fail-fast: false matrix: os: [ubuntu-latest] @@ -54,6 +56,16 @@ jobs: - name: Build with Maven run: mvn -B clean install -Dno-format + env: # note that secrets are not available when triggered by PR from a fork + PINECONE_API_KEY: ${{ secrets.PINECONE_API_KEY }} + PINECONE_ENVIRONMENT: ${{ secrets.PINECONE_ENVIRONMENT }} + PINECONE_INDEX_NAME: ${{ secrets.PINECONE_INDEX_NAME }} + PINECONE_PROJECT_ID: ${{ secrets.PINECONE_PROJECT_ID }} - name: Build with Maven (Native) run: mvn -B install -Dnative -Dquarkus.native.container-build -Dnative.surefire.skip + env: # note that secrets are not available when triggered by PR from a fork + PINECONE_API_KEY: ${{ secrets.PINECONE_API_KEY }} + PINECONE_ENVIRONMENT: ${{ secrets.PINECONE_ENVIRONMENT }} + PINECONE_INDEX_NAME: ${{ secrets.PINECONE_INDEX_NAME }} + PINECONE_PROJECT_ID: ${{ secrets.PINECONE_PROJECT_ID }} diff --git a/docs/modules/ROOT/nav.adoc b/docs/modules/ROOT/nav.adoc index 44e342554..396a4345a 100644 --- a/docs/modules/ROOT/nav.adoc +++ b/docs/modules/ROOT/nav.adoc @@ -14,7 +14,8 @@ * Document Stores ** xref:redis-store.adoc[Redis Store] ** xref:chroma-store.adoc[Chroma Store] +** xref:pinecone-store.adoc[Pinecone Store] ** xref:in-process-embedding.adoc[In-Process Embeddings] * Advanced topics -** xref:fault-tolerance.adoc[Fault Tolerance] \ No newline at end of file +** xref:fault-tolerance.adoc[Fault Tolerance] diff --git a/docs/modules/ROOT/pages/includes/quarkus-langchain4j-pinecone.adoc b/docs/modules/ROOT/pages/includes/quarkus-langchain4j-pinecone.adoc new file mode 100644 index 000000000..04197c7ed --- /dev/null +++ b/docs/modules/ROOT/pages/includes/quarkus-langchain4j-pinecone.adoc @@ -0,0 +1,168 @@ + +:summaryTableId: quarkus-langchain4j-pinecone +[.configuration-legend] +icon:lock[title=Fixed at build time] Configuration property fixed at build time - All other configuration properties are overridable at runtime +[.configuration-reference.searchable, cols="80,.^10,.^10"] +|=== + +h|[[quarkus-langchain4j-pinecone_configuration]]link:#quarkus-langchain4j-pinecone_configuration[Configuration property] + +h|Type +h|Default + +a|icon:lock[title=Fixed at build time] [[quarkus-langchain4j-pinecone_quarkus.langchain4j.pinecone.api-key]]`link:#quarkus-langchain4j-pinecone_quarkus.langchain4j.pinecone.api-key[quarkus.langchain4j.pinecone.api-key]` + + +[.description] +-- +The API key to Pinecone. + +ifdef::add-copy-button-to-env-var[] +Environment variable: env_var_with_copy_button:+++QUARKUS_LANGCHAIN4J_PINECONE_API_KEY+++[] +endif::add-copy-button-to-env-var[] +ifndef::add-copy-button-to-env-var[] +Environment variable: `+++QUARKUS_LANGCHAIN4J_PINECONE_API_KEY+++` +endif::add-copy-button-to-env-var[] +--|string +|required icon:exclamation-circle[title=Configuration property is required] + + +a|icon:lock[title=Fixed at build time] [[quarkus-langchain4j-pinecone_quarkus.langchain4j.pinecone.environment]]`link:#quarkus-langchain4j-pinecone_quarkus.langchain4j.pinecone.environment[quarkus.langchain4j.pinecone.environment]` + + +[.description] +-- +Environment name, e.g. gcp-starter or northamerica-northeast1-gcp. + +ifdef::add-copy-button-to-env-var[] +Environment variable: env_var_with_copy_button:+++QUARKUS_LANGCHAIN4J_PINECONE_ENVIRONMENT+++[] +endif::add-copy-button-to-env-var[] +ifndef::add-copy-button-to-env-var[] +Environment variable: `+++QUARKUS_LANGCHAIN4J_PINECONE_ENVIRONMENT+++` +endif::add-copy-button-to-env-var[] +--|string +|required icon:exclamation-circle[title=Configuration property is required] + + +a|icon:lock[title=Fixed at build time] [[quarkus-langchain4j-pinecone_quarkus.langchain4j.pinecone.project-id]]`link:#quarkus-langchain4j-pinecone_quarkus.langchain4j.pinecone.project-id[quarkus.langchain4j.pinecone.project-id]` + + +[.description] +-- +ID of the project. + +ifdef::add-copy-button-to-env-var[] +Environment variable: env_var_with_copy_button:+++QUARKUS_LANGCHAIN4J_PINECONE_PROJECT_ID+++[] +endif::add-copy-button-to-env-var[] +ifndef::add-copy-button-to-env-var[] +Environment variable: `+++QUARKUS_LANGCHAIN4J_PINECONE_PROJECT_ID+++` +endif::add-copy-button-to-env-var[] +--|string +|required icon:exclamation-circle[title=Configuration property is required] + + +a|icon:lock[title=Fixed at build time] [[quarkus-langchain4j-pinecone_quarkus.langchain4j.pinecone.index-name]]`link:#quarkus-langchain4j-pinecone_quarkus.langchain4j.pinecone.index-name[quarkus.langchain4j.pinecone.index-name]` + + +[.description] +-- +Name of the index within the project. If the index doesn't exist, it will be created. + +ifdef::add-copy-button-to-env-var[] +Environment variable: env_var_with_copy_button:+++QUARKUS_LANGCHAIN4J_PINECONE_INDEX_NAME+++[] +endif::add-copy-button-to-env-var[] +ifndef::add-copy-button-to-env-var[] +Environment variable: `+++QUARKUS_LANGCHAIN4J_PINECONE_INDEX_NAME+++` +endif::add-copy-button-to-env-var[] +--|string +|required icon:exclamation-circle[title=Configuration property is required] + + +a|icon:lock[title=Fixed at build time] [[quarkus-langchain4j-pinecone_quarkus.langchain4j.pinecone.dimension]]`link:#quarkus-langchain4j-pinecone_quarkus.langchain4j.pinecone.dimension[quarkus.langchain4j.pinecone.dimension]` + + +[.description] +-- +Dimension of the embeddings in the index. This is required only in case that the index doesn't exist yet and needs to be created. + +ifdef::add-copy-button-to-env-var[] +Environment variable: env_var_with_copy_button:+++QUARKUS_LANGCHAIN4J_PINECONE_DIMENSION+++[] +endif::add-copy-button-to-env-var[] +ifndef::add-copy-button-to-env-var[] +Environment variable: `+++QUARKUS_LANGCHAIN4J_PINECONE_DIMENSION+++` +endif::add-copy-button-to-env-var[] +--|int +| + + +a|icon:lock[title=Fixed at build time] [[quarkus-langchain4j-pinecone_quarkus.langchain4j.pinecone.namespace]]`link:#quarkus-langchain4j-pinecone_quarkus.langchain4j.pinecone.namespace[quarkus.langchain4j.pinecone.namespace]` + + +[.description] +-- +The namespace. + +ifdef::add-copy-button-to-env-var[] +Environment variable: env_var_with_copy_button:+++QUARKUS_LANGCHAIN4J_PINECONE_NAMESPACE+++[] +endif::add-copy-button-to-env-var[] +ifndef::add-copy-button-to-env-var[] +Environment variable: `+++QUARKUS_LANGCHAIN4J_PINECONE_NAMESPACE+++` +endif::add-copy-button-to-env-var[] +--|string +| + + +a|icon:lock[title=Fixed at build time] [[quarkus-langchain4j-pinecone_quarkus.langchain4j.pinecone.text-field-name]]`link:#quarkus-langchain4j-pinecone_quarkus.langchain4j.pinecone.text-field-name[quarkus.langchain4j.pinecone.text-field-name]` + + +[.description] +-- +The name of the field that contains the text segment. + +ifdef::add-copy-button-to-env-var[] +Environment variable: env_var_with_copy_button:+++QUARKUS_LANGCHAIN4J_PINECONE_TEXT_FIELD_NAME+++[] +endif::add-copy-button-to-env-var[] +ifndef::add-copy-button-to-env-var[] +Environment variable: `+++QUARKUS_LANGCHAIN4J_PINECONE_TEXT_FIELD_NAME+++` +endif::add-copy-button-to-env-var[] +--|string +|`text` + + +a|icon:lock[title=Fixed at build time] [[quarkus-langchain4j-pinecone_quarkus.langchain4j.pinecone.timeout]]`link:#quarkus-langchain4j-pinecone_quarkus.langchain4j.pinecone.timeout[quarkus.langchain4j.pinecone.timeout]` + + +[.description] +-- +The timeout duration for the Pinecone client. If not specified, 5 seconds will be used. + +ifdef::add-copy-button-to-env-var[] +Environment variable: env_var_with_copy_button:+++QUARKUS_LANGCHAIN4J_PINECONE_TIMEOUT+++[] +endif::add-copy-button-to-env-var[] +ifndef::add-copy-button-to-env-var[] +Environment variable: `+++QUARKUS_LANGCHAIN4J_PINECONE_TIMEOUT+++` +endif::add-copy-button-to-env-var[] +--|link:https://docs.oracle.com/javase/8/docs/api/java/time/Duration.html[Duration] + link:#duration-note-anchor-{summaryTableId}[icon:question-circle[], title=More information about the Duration format] +| + +|=== +ifndef::no-duration-note[] +[NOTE] +[id='duration-note-anchor-{summaryTableId}'] +.About the Duration format +==== +To write duration values, use the standard `java.time.Duration` format. +See the link:https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/time/Duration.html#parse(java.lang.CharSequence)[Duration#parse() javadoc] for more information. + +You can also use a simplified format, starting with a number: + +* If the value is only a number, it represents time in seconds. +* If the value is a number followed by `ms`, it represents time in milliseconds. + +In other cases, the simplified format is translated to the `java.time.Duration` format for parsing: + +* If the value is a number followed by `h`, `m`, or `s`, it is prefixed with `PT`. +* If the value is a number followed by `d`, it is prefixed with `P`. +==== +endif::no-duration-note[] diff --git a/docs/modules/ROOT/pages/pinecone-store.adoc b/docs/modules/ROOT/pages/pinecone-store.adoc new file mode 100644 index 000000000..33aa24759 --- /dev/null +++ b/docs/modules/ROOT/pages/pinecone-store.adoc @@ -0,0 +1,23 @@ += Pinecone Store for Retrieval Augmented Generation (RAG) + +include::./includes/attributes.adoc[] + +When implementing Retrieval Augmented Generation (RAG), a robust document store is crucial. This guide demonstrates how to leverage a https://www.pinecone.io/[Pinecone] database as the document store. + +== Leveraging the Pinecone Document Store + +To make use of the Pinecone document store, you'll need to include the following dependency: + +[source,xml,subs=attributes+] +---- + + io.quarkiverse.langchain4j + quarkus-langchain4j-pinecone + +---- + +== Configuration Settings + +Customize the behavior of the extension by exploring various configuration options: + +include::includes/quarkus-langchain4j-pinecone.adoc[leveloffset=+1,opts=optional] diff --git a/docs/pom.xml b/docs/pom.xml index 048f3a5ef..d9dfc52fc 100644 --- a/docs/pom.xml +++ b/docs/pom.xml @@ -38,6 +38,11 @@ + + io.quarkiverse.langchain4j + quarkus-langchain4j-pinecone + ${project.version} + @@ -55,6 +60,11 @@ + + io.quarkiverse.langchain4j + quarkus-langchain4j-pinecone-deployment + ${project.version} + @@ -112,6 +122,7 @@ quarkus-langchain4j-huggingface.adoc quarkus-langchain4j-redis.adoc quarkus-langchain4j-chroma.adoc + quarkus-langchain4j-pinecone.adoc false diff --git a/docs/src/main/resources/application.properties b/docs/src/main/resources/application.properties index 74eea3b87..e2275a79f 100644 --- a/docs/src/main/resources/application.properties +++ b/docs/src/main/resources/application.properties @@ -1,2 +1,6 @@ # Just there to satisfy mandatory properties -quarkus.langchain4j.redis.dimension=180 \ No newline at end of file +quarkus.langchain4j.redis.dimension=180 +quarkus.langchain4j.pinecone.environment=abc +quarkus.langchain4j.pinecone.index-name=abc +quarkus.langchain4j.pinecone.project-id=abc +quarkus.langchain4j.pinecone.api-key=abc \ No newline at end of file diff --git a/pinecone/deployment/pom.xml b/pinecone/deployment/pom.xml new file mode 100644 index 000000000..73b6bb340 --- /dev/null +++ b/pinecone/deployment/pom.xml @@ -0,0 +1,73 @@ + + + 4.0.0 + + io.quarkiverse.langchain4j + quarkus-langchain4j-pinecone-parent + 999-SNAPSHOT + + quarkus-langchain4j-pinecone-deployment + Quarkus Langchain4j - Pinecone embedding store - Deployment + + + io.quarkus + quarkus-arc-deployment + + + io.quarkus + quarkus-rest-client-reactive-jackson-deployment + + + io.quarkiverse.langchain4j + quarkus-langchain4j-core-deployment + ${project.version} + + + io.quarkiverse.langchain4j + quarkus-langchain4j-pinecone + ${project.version} + + + io.quarkus + quarkus-junit5-internal + test + + + org.assertj + assertj-core + ${assertj.version} + test + + + org.wiremock + wiremock-standalone + ${wiremock.version} + test + + + dev.langchain4j + langchain4j-embeddings-all-minilm-l6-v2-q + ${langchain4j.version} + test + + + + + + maven-compiler-plugin + + + + io.quarkus + quarkus-extension-processor + ${quarkus.version} + + + + + + + + diff --git a/pinecone/deployment/src/main/java/io/quarkiverse/langchain4j/pinecone/PineconeProcessor.java b/pinecone/deployment/src/main/java/io/quarkiverse/langchain4j/pinecone/PineconeProcessor.java new file mode 100644 index 000000000..660eaa9ba --- /dev/null +++ b/pinecone/deployment/src/main/java/io/quarkiverse/langchain4j/pinecone/PineconeProcessor.java @@ -0,0 +1,44 @@ +package io.quarkiverse.langchain4j.pinecone; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.jboss.jandex.DotName; + +import dev.langchain4j.store.embedding.EmbeddingStore; +import io.quarkiverse.langchain4j.pinecone.runtime.PineconeConfig; +import io.quarkiverse.langchain4j.pinecone.runtime.PineconeRecorder; +import io.quarkus.arc.deployment.SyntheticBeanBuildItem; +import io.quarkus.deployment.annotations.BuildProducer; +import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.annotations.ExecutionTime; +import io.quarkus.deployment.annotations.Record; +import io.quarkus.deployment.builditem.FeatureBuildItem; + +public class PineconeProcessor { + + public static final DotName PINECONE_EMBEDDING_STORE = DotName.createSimple(PineconeEmbeddingStore.class); + private static final String FEATURE = "langchain4j-pinecone"; + + @BuildStep + FeatureBuildItem feature() { + return new FeatureBuildItem(FEATURE); + } + + @BuildStep + @Record(ExecutionTime.RUNTIME_INIT) + public void createBean( + BuildProducer beanProducer, + PineconeRecorder recorder, + PineconeConfig config) { + beanProducer.produce(SyntheticBeanBuildItem + .configure(PINECONE_EMBEDDING_STORE) + .types(EmbeddingStore.class) + .defaultBean() + .setRuntimeInit() + .defaultBean() + .scope(ApplicationScoped.class) + .supplier(recorder.pineconeStoreSupplier(config)) + .done()); + } + +} diff --git a/pinecone/deployment/src/test/java/io/quarkiverse/langchain4j/pinecone/deployment/PineconeEmbeddingStoreTest.java b/pinecone/deployment/src/test/java/io/quarkiverse/langchain4j/pinecone/deployment/PineconeEmbeddingStoreTest.java new file mode 100644 index 000000000..b6ef2d2f7 --- /dev/null +++ b/pinecone/deployment/src/test/java/io/quarkiverse/langchain4j/pinecone/deployment/PineconeEmbeddingStoreTest.java @@ -0,0 +1,298 @@ +package io.quarkiverse.langchain4j.pinecone.deployment; + +import static dev.langchain4j.internal.Utils.randomUUID; +import static java.util.Arrays.asList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.data.Percentage.withPercentage; + +import java.util.List; +import java.util.concurrent.TimeUnit; + +import jakarta.inject.Inject; + +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; +import org.junit.jupiter.api.extension.RegisterExtension; + +import dev.langchain4j.data.document.Metadata; +import dev.langchain4j.data.embedding.Embedding; +import dev.langchain4j.data.segment.TextSegment; +import dev.langchain4j.model.embedding.AllMiniLmL6V2QuantizedEmbeddingModel; +import dev.langchain4j.model.embedding.EmbeddingModel; +import dev.langchain4j.store.embedding.CosineSimilarity; +import dev.langchain4j.store.embedding.EmbeddingMatch; +import dev.langchain4j.store.embedding.RelevanceScore; +import io.quarkiverse.langchain4j.pinecone.PineconeEmbeddingStore; +import io.quarkiverse.langchain4j.pinecone.runtime.DeleteRequest; +import io.quarkiverse.langchain4j.pinecone.runtime.PineconeVectorOperationsApi; +import io.quarkiverse.langchain4j.pinecone.runtime.QueryRequest; +import io.quarkiverse.langchain4j.pinecone.runtime.VectorMatch; +import io.quarkus.logging.Log; +import io.quarkus.test.QuarkusUnitTest; + +/** + * Prerequisites for this test: A pinecone index must exist (can be in the starter region) + * and the following environment variables must be set accordingly: + * PINECONE_API_KEY, PINECONE_ENVIRONMENT, PINECONE_PROJECT_ID and PINECONE_INDEX_NAME + * + * These are set as GitHub secrets in the main repository. GitHub doesn't + * pass them to workflows triggered from forks though, so this test only + * runs with the nightly CI workflow, or for PRs submitted from the main + * quarkiverse repository (NOT from a fork). + * + *

+ * Original data in the index will be lost during the test. + *

+ * Because of delays in Pinecone when deleting vectors, the test adds + * artificial delays (the `delay` method) to make sure we see the correct + * data, and thus the test takes a relatively long time to run. If you see + * intermittent failures, it may mean that the delay isn't long enough... + * + */ +@EnabledIfEnvironmentVariable(named = "PINECONE_API_KEY", matches = ".+") +public class PineconeEmbeddingStoreTest { + + @RegisterExtension + static final QuarkusUnitTest unitTest = new QuarkusUnitTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) + .addAsResource(new StringAsset( + // to enable rest client logging: + "quarkus.rest-client.logging.scope=request-response\n" + + "quarkus.rest-client.logging.body-limit=10000\n" + + "quarkus.log.category.\"org.jboss.resteasy.reactive.client.logging\".level=DEBUG\n" + + "quarkus.langchain4j.pinecone.api-key=${pinecone.api.key}\n" + + "quarkus.langchain4j.pinecone.environment=${pinecone.environment}\n" + + "quarkus.langchain4j.pinecone.project-id=${pinecone.project-id}\n" + + "quarkus.langchain4j.pinecone.index-name=${pinecone.index-name}\n"), + "application.properties")); + + @Inject + PineconeEmbeddingStore embeddingStore; + + private final EmbeddingModel embeddingModel = new AllMiniLmL6V2QuantizedEmbeddingModel(); + + @BeforeEach + public void cleanup() { + // Normally we would use deleteAll=true for deleting all vectors, + // but that doesn't work in the gcp-starter environment, + // so make it a two-step process instead by querying for all vectors, and then removing them. + PineconeVectorOperationsApi client = embeddingStore.getUnderlyingClient(); + float[] vector = new float[384]; + QueryRequest allRequest = new QueryRequest(null, 10000L, false, false, vector); + List existingEntries = client.query(allRequest).getMatches().stream().map(VectorMatch::getId).toList(); + if (!existingEntries.isEmpty()) { + Log.info("Deleting " + existingEntries.size() + " embeddings"); + client.delete(new DeleteRequest(existingEntries, false, null, null)); + } + + } + + /** + * Seems we have to add some delay after each insert operation before Pinecone + * processes the vector and makes it available for querying. + */ + private static void delay() { + try { + TimeUnit.SECONDS.sleep(30); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + + @Test + void should_add_embedding() { + Embedding embedding = embeddingModel.embed(randomUUID()).content(); + + String id = embeddingStore.add(embedding); + assertThat(id).isNotNull(); + + delay(); + + List> relevant = embeddingStore.findRelevant(embedding, 10); + assertThat(relevant).hasSize(1); + + EmbeddingMatch match = relevant.get(0); + assertThat(match.score()).isCloseTo(1, withPercentage(1)); + assertThat(match.embeddingId()).isEqualTo(id); + assertThat(match.embedding()).isEqualTo(embedding); + assertThat(match.embedded()).isNull(); + } + + @Test + void should_add_embedding_with_id() { + String id = randomUUID(); + Embedding embedding = embeddingModel.embed(randomUUID()).content(); + + embeddingStore.add(id, embedding); + delay(); + + List> relevant = embeddingStore.findRelevant(embedding, 10); + assertThat(relevant).hasSize(1); + + EmbeddingMatch match = relevant.get(0); + assertThat(match.score()).isCloseTo(1, withPercentage(1)); + assertThat(match.embeddingId()).isEqualTo(id); + assertThat(match.embedding()).isEqualTo(embedding); + assertThat(match.embedded()).isNull(); + } + + @Test + void should_add_embedding_with_segment() { + TextSegment segment = TextSegment.from(randomUUID()); + Embedding embedding = embeddingModel.embed(segment.text()).content(); + + String id = embeddingStore.add(embedding, segment); + delay(); + assertThat(id).isNotNull(); + + List> relevant = embeddingStore.findRelevant(embedding, 10); + assertThat(relevant).hasSize(1); + + EmbeddingMatch match = relevant.get(0); + assertThat(match.score()).isCloseTo(1, withPercentage(1)); + assertThat(match.embeddingId()).isEqualTo(id); + assertThat(match.embedding()).isEqualTo(embedding); + assertThat(match.embedded()).isEqualTo(segment); + } + + @Test + void should_add_embedding_with_segment_with_metadata() { + TextSegment segment = TextSegment.from(randomUUID(), Metadata.from("test-key", "test-value")); + Embedding embedding = embeddingModel.embed(segment.text()).content(); + + String id = embeddingStore.add(embedding, segment); + + assertThat(id).isNotNull(); + + delay(); + List> relevant = embeddingStore.findRelevant(embedding, 10); + assertThat(relevant).hasSize(1); + + EmbeddingMatch match = relevant.get(0); + assertThat(match.score()).isCloseTo(1, withPercentage(1)); + assertThat(match.embeddingId()).isEqualTo(id); + assertThat(match.embedding()).isEqualTo(embedding); + assertThat(match.embedded()).isEqualTo(segment); + } + + @Test + void should_add_multiple_embeddings() { + Embedding firstEmbedding = embeddingModel.embed(randomUUID()).content(); + Embedding secondEmbedding = embeddingModel.embed(randomUUID()).content(); + + List ids = embeddingStore.addAll(asList(firstEmbedding, secondEmbedding)); + assertThat(ids).hasSize(2); + delay(); + + List> relevant = embeddingStore.findRelevant(firstEmbedding, 10); + assertThat(relevant).hasSize(2); + + EmbeddingMatch firstMatch = relevant.get(0); + assertThat(firstMatch.score()).isCloseTo(1, withPercentage(1)); + assertThat(firstMatch.embeddingId()).isEqualTo(ids.get(0)); + assertThat(firstMatch.embedding()).isEqualTo(firstEmbedding); + assertThat(firstMatch.embedded()).isNull(); + + EmbeddingMatch secondMatch = relevant.get(1); + assertThat(secondMatch.score()).isBetween(0d, 1d); + assertThat(secondMatch.embeddingId()).isEqualTo(ids.get(1)); + assertThat(secondMatch.embedding()).isEqualTo(secondEmbedding); + assertThat(secondMatch.embedded()).isNull(); + } + + @Test + void should_add_multiple_embeddings_with_segments() { + TextSegment firstSegment = TextSegment.from(randomUUID()); + Embedding firstEmbedding = embeddingModel.embed(firstSegment.text()).content(); + TextSegment secondSegment = TextSegment.from(randomUUID()); + Embedding secondEmbedding = embeddingModel.embed(secondSegment.text()).content(); + + List ids = embeddingStore.addAll( + asList(firstEmbedding, secondEmbedding), + asList(firstSegment, secondSegment)); + assertThat(ids).hasSize(2); + delay(); + + List> relevant = embeddingStore.findRelevant(firstEmbedding, 10); + assertThat(relevant).hasSize(2); + + EmbeddingMatch firstMatch = relevant.get(0); + assertThat(firstMatch.score()).isCloseTo(1, withPercentage(1)); + assertThat(firstMatch.embeddingId()).isEqualTo(ids.get(0)); + assertThat(firstMatch.embedding()).isEqualTo(firstEmbedding); + assertThat(firstMatch.embedded()).isEqualTo(firstSegment); + + EmbeddingMatch secondMatch = relevant.get(1); + assertThat(secondMatch.score()).isBetween(0d, 1d); + assertThat(secondMatch.embeddingId()).isEqualTo(ids.get(1)); + assertThat(secondMatch.embedding()).isEqualTo(secondEmbedding); + assertThat(secondMatch.embedded()).isEqualTo(secondSegment); + } + + @Test + void should_find_with_min_score() { + String firstId = randomUUID(); + Embedding firstEmbedding = embeddingModel.embed(randomUUID()).content(); + embeddingStore.add(firstId, firstEmbedding); + + String secondId = randomUUID(); + Embedding secondEmbedding = embeddingModel.embed(randomUUID()).content(); + embeddingStore.add(secondId, secondEmbedding); + + delay(); + List> relevant = embeddingStore.findRelevant(firstEmbedding, 10); + assertThat(relevant).hasSize(2); + EmbeddingMatch firstMatch = relevant.get(0); + assertThat(firstMatch.score()).isCloseTo(1, withPercentage(1)); + assertThat(firstMatch.embeddingId()).isEqualTo(firstId); + EmbeddingMatch secondMatch = relevant.get(1); + assertThat(secondMatch.score()).isBetween(0d, 1d); + assertThat(secondMatch.embeddingId()).isEqualTo(secondId); + + List> relevant2 = embeddingStore.findRelevant( + firstEmbedding, + 10, + secondMatch.score() - 0.01); + assertThat(relevant2).hasSize(2); + assertThat(relevant2.get(0).embeddingId()).isEqualTo(firstId); + assertThat(relevant2.get(1).embeddingId()).isEqualTo(secondId); + + List> relevant3 = embeddingStore.findRelevant( + firstEmbedding, + 10, + secondMatch.score()); + assertThat(relevant3).hasSize(2); + assertThat(relevant3.get(0).embeddingId()).isEqualTo(firstId); + assertThat(relevant3.get(1).embeddingId()).isEqualTo(secondId); + + List> relevant4 = embeddingStore.findRelevant( + firstEmbedding, + 10, + secondMatch.score() + 0.01); + assertThat(relevant4).hasSize(1); + assertThat(relevant4.get(0).embeddingId()).isEqualTo(firstId); + } + + @Test + void should_return_correct_score() { + Embedding embedding = embeddingModel.embed("hello").content(); + + String id = embeddingStore.add(embedding); + assertThat(id).isNotNull(); + + Embedding referenceEmbedding = embeddingModel.embed("hi").content(); + + delay(); + List> relevant = embeddingStore.findRelevant(referenceEmbedding, 1); + assertThat(relevant).hasSize(1); + + EmbeddingMatch match = relevant.get(0); + assertThat(match.score()).isCloseTo( + RelevanceScore.fromCosineSimilarity(CosineSimilarity.between(embedding, referenceEmbedding)), + withPercentage(1)); + } +} diff --git a/pinecone/pom.xml b/pinecone/pom.xml new file mode 100644 index 000000000..54c9a5560 --- /dev/null +++ b/pinecone/pom.xml @@ -0,0 +1,20 @@ + + + 4.0.0 + + io.quarkiverse.langchain4j + quarkus-langchain4j-parent + 999-SNAPSHOT + + quarkus-langchain4j-pinecone-parent + Quarkus Langchain4j - Pinecone embedding store - Parent + pom + + + deployment + runtime + + + diff --git a/pinecone/runtime/pom.xml b/pinecone/runtime/pom.xml new file mode 100644 index 000000000..a453c9325 --- /dev/null +++ b/pinecone/runtime/pom.xml @@ -0,0 +1,82 @@ + + + 4.0.0 + + io.quarkiverse.langchain4j + quarkus-langchain4j-pinecone-parent + 999-SNAPSHOT + + quarkus-langchain4j-pinecone + Quarkus Langchain4j - Pinecone embedding store - Runtime + + + io.quarkus + quarkus-arc + + + io.quarkus + quarkus-rest-client-reactive-jackson + + + io.quarkiverse.langchain4j + quarkus-langchain4j-core + ${project.version} + + + + + + io.quarkus + quarkus-extension-maven-plugin + ${quarkus.version} + + + compile + + extension-descriptor + + + ${project.groupId}:${project.artifactId}-deployment:${project.version} + + + + + + + maven-compiler-plugin + + + + io.quarkus + quarkus-extension-processor + ${quarkus.version} + + + + + + maven-jar-plugin + + + generate-codestart-jar + generate-resources + + jar + + + ${project.basedir}/src/main + + codestarts/** + + codestarts + true + + + + + + + + diff --git a/pinecone/runtime/src/main/java/io/quarkiverse/langchain4j/pinecone/PineconeEmbeddingStore.java b/pinecone/runtime/src/main/java/io/quarkiverse/langchain4j/pinecone/PineconeEmbeddingStore.java new file mode 100644 index 000000000..092f6d11b --- /dev/null +++ b/pinecone/runtime/src/main/java/io/quarkiverse/langchain4j/pinecone/PineconeEmbeddingStore.java @@ -0,0 +1,196 @@ +package io.quarkiverse.langchain4j.pinecone; + +import static dev.langchain4j.internal.Utils.randomUUID; +import static java.util.Collections.singletonList; +import static java.util.stream.Collectors.toList; + +import java.net.URI; +import java.net.URISyntaxException; +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +import jakarta.ws.rs.core.MultivaluedHashMap; +import jakarta.ws.rs.core.MultivaluedMap; + +import org.eclipse.microprofile.rest.client.ext.ClientHeadersFactory; + +import dev.langchain4j.data.document.Metadata; +import dev.langchain4j.data.embedding.Embedding; +import dev.langchain4j.data.segment.TextSegment; +import dev.langchain4j.store.embedding.EmbeddingMatch; +import dev.langchain4j.store.embedding.EmbeddingStore; +import dev.langchain4j.store.embedding.RelevanceScore; +import io.quarkiverse.langchain4j.pinecone.runtime.CreateIndexRequest; +import io.quarkiverse.langchain4j.pinecone.runtime.DistanceMetric; +import io.quarkiverse.langchain4j.pinecone.runtime.PineconeIndexOperationsApi; +import io.quarkiverse.langchain4j.pinecone.runtime.PineconeVectorOperationsApi; +import io.quarkiverse.langchain4j.pinecone.runtime.QueryRequest; +import io.quarkiverse.langchain4j.pinecone.runtime.QueryResponse; +import io.quarkiverse.langchain4j.pinecone.runtime.UpsertRequest; +import io.quarkiverse.langchain4j.pinecone.runtime.UpsertResponse; +import io.quarkiverse.langchain4j.pinecone.runtime.UpsertVector; +import io.quarkus.arc.impl.LazyValue; +import io.quarkus.logging.Log; +import io.quarkus.rest.client.reactive.QuarkusRestClientBuilder; + +public class PineconeEmbeddingStore implements EmbeddingStore { + + private final PineconeVectorOperationsApi vectorOperations; + private final PineconeIndexOperationsApi indexOperations; + private final String namespace; + private final String textFieldName; + private final String indexName; + private final Integer dimension; + private final LazyValue indexExists; + + public PineconeEmbeddingStore(String apiKey, + String indexName, + String projectId, + String environment, + String namespace, + String textFieldName, + Duration timeout, + Integer dimension) { + this.indexName = indexName; + this.dimension = dimension; + String baseUrl = "https://" + indexName + "-" + projectId + ".svc." + environment + ".pinecone.io"; + String baseUrlIndexOperations = "https://controller." + environment + ".pinecone.io"; + try { + ClientHeadersFactory clientHeadersFactory = new ClientHeadersFactory() { + @Override + public MultivaluedMap update(MultivaluedMap incoming, + MultivaluedMap outgoing) { + MultivaluedMap headers = new MultivaluedHashMap<>(); + headers.put("Api-Key", singletonList(apiKey)); + return headers; + } + }; + vectorOperations = QuarkusRestClientBuilder.newBuilder() + .baseUri(new URI(baseUrl)) + .connectTimeout(timeout.toSeconds(), TimeUnit.SECONDS) + .readTimeout(timeout.toSeconds(), TimeUnit.SECONDS) + .clientHeadersFactory(clientHeadersFactory) + .build(PineconeVectorOperationsApi.class); + indexOperations = QuarkusRestClientBuilder.newBuilder() + .baseUri(new URI(baseUrlIndexOperations)) + .connectTimeout(timeout.toSeconds(), TimeUnit.SECONDS) + .readTimeout(timeout.toSeconds(), TimeUnit.SECONDS) + .clientHeadersFactory(clientHeadersFactory) + .build(PineconeIndexOperationsApi.class); + } catch (URISyntaxException e) { + throw new RuntimeException(e); + } + this.namespace = namespace; + this.textFieldName = textFieldName; + Log.info("PineconeEmbeddingStore using base URL: " + baseUrl); + this.indexExists = new LazyValue<>(new Supplier() { + @Override + public Object get() { + if (indexOperations.listIndexes().contains(indexName)) { + Log.info("Pinecone index " + indexName + " already exists"); + } else { + if (dimension == null) { + throw new IllegalArgumentException( + "quarkus.langchain4j.pinecone.dimension must be specified when creating a new index"); + } + indexOperations.createIndex(new CreateIndexRequest(indexName, dimension, DistanceMetric.COSINE)); + Log.info("Created Pinecone index " + indexName + " with dimension = " + dimension); + } + return new Object(); + } + }); + } + + @Override + public String add(Embedding embedding) { + String id = randomUUID(); + add(id, embedding); + return id; + } + + @Override + public void add(String id, Embedding embedding) { + addInternal(id, embedding, null); + } + + @Override + public String add(Embedding embedding, TextSegment textSegment) { + String id = randomUUID(); + addInternal(id, embedding, textSegment); + return id; + } + + @Override + public List addAll(List embeddings) { + List ids = embeddings.stream() + .map(ignored -> randomUUID()) + .collect(toList()); + addAllInternal(ids, embeddings, null); + return ids; + } + + @Override + public List addAll(List embeddings, List embedded) { + List ids = embeddings.stream() + .map(ignored -> randomUUID()) + .collect(toList()); + addAllInternal(ids, embeddings, embedded); + return ids; + } + + @Override + public List> findRelevant(Embedding embedding, int maxResults, double minScore) { + indexExists.get(); + QueryRequest request = new QueryRequest(namespace, (long) maxResults, true, true, embedding.vector()); + QueryResponse response = vectorOperations.query(request); + return response + .getMatches().stream().map(match -> new EmbeddingMatch<>( + RelevanceScore.fromCosineSimilarity(match.getScore()), + match.getId(), + new Embedding(match.getValues()), + match.getMetadata().get(textFieldName) != null ? new TextSegment( + match.getMetadata().get(textFieldName), + new Metadata(mapWithoutKey(match.getMetadata(), textFieldName))) : null)) + .filter(match -> match.score() >= minScore) + .collect(toList()); + } + + public PineconeVectorOperationsApi getUnderlyingClient() { + return vectorOperations; + } + + public Map mapWithoutKey(Map input, String key) { + return input.entrySet().stream() + .filter(entry -> !entry.getKey().equals(key)) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + } + + private void addInternal(String id, Embedding embedding, TextSegment textSegment) { + addAllInternal(singletonList(id), singletonList(embedding), textSegment == null ? null : singletonList(textSegment)); + } + + private void addAllInternal(List ids, List embeddings, List textSegments) { + indexExists.get(); + Log.debug("Adding embeddings: " + embeddings); + int count = ids.size(); + List vectorList = new ArrayList<>(); + for (int i = 0; i < count; i++) { + UpsertVector vector = new UpsertVector.Builder() + .id(ids.get(i)) + .value(embeddings.get(i).vector()) + .metadata(textFieldName, textSegments == null ? null : textSegments.get(i).text()) + .metadata(textSegments != null ? textSegments.get(i).metadata().asMap() : null) + .build(); + vectorList.add(vector); + } + UpsertRequest request = new UpsertRequest(vectorList, namespace); + UpsertResponse response = vectorOperations.upsert(request); + Log.debug("Added embeddings: " + response.getUpsertedCount()); + } + +} diff --git a/pinecone/runtime/src/main/java/io/quarkiverse/langchain4j/pinecone/runtime/CreateIndexRequest.java b/pinecone/runtime/src/main/java/io/quarkiverse/langchain4j/pinecone/runtime/CreateIndexRequest.java new file mode 100644 index 000000000..e0e45cb36 --- /dev/null +++ b/pinecone/runtime/src/main/java/io/quarkiverse/langchain4j/pinecone/runtime/CreateIndexRequest.java @@ -0,0 +1,35 @@ +package io.quarkiverse.langchain4j.pinecone.runtime; + +import io.quarkus.runtime.annotations.RegisterForReflection; + +/** + * Represents a Create index operation against Pinecone. + * See the API documentation. + * Note that after the successful request, Pinecone takes some time (usually up tens of seconds) for the index to start being + * usable. + */ +@RegisterForReflection +public class CreateIndexRequest { + + private final String name; + private final Integer dimension; + private final DistanceMetric metric; + + public CreateIndexRequest(String name, Integer dimension, DistanceMetric metric) { + this.name = name; + this.dimension = dimension; + this.metric = metric; + } + + public String getName() { + return name; + } + + public Integer getDimension() { + return dimension; + } + + public DistanceMetric getMetric() { + return metric; + } +} diff --git a/pinecone/runtime/src/main/java/io/quarkiverse/langchain4j/pinecone/runtime/DeleteRequest.java b/pinecone/runtime/src/main/java/io/quarkiverse/langchain4j/pinecone/runtime/DeleteRequest.java new file mode 100644 index 000000000..090379950 --- /dev/null +++ b/pinecone/runtime/src/main/java/io/quarkiverse/langchain4j/pinecone/runtime/DeleteRequest.java @@ -0,0 +1,45 @@ +package io.quarkiverse.langchain4j.pinecone.runtime; + +import java.util.List; +import java.util.Map; + +import io.quarkus.runtime.annotations.RegisterForReflection; + +/** + * Represents a Delete vector operation against Pinecone. + * See the API documentation. + */ +@RegisterForReflection +public class DeleteRequest { + + private final List ids; + + private final Boolean deleteAll; + + private final String namespace; + + private final Map filter; + + public DeleteRequest(List ids, Boolean deleteAll, String namespace, Map filter) { + this.ids = ids; + this.deleteAll = deleteAll; + this.namespace = namespace; + this.filter = filter; + } + + public List getIds() { + return ids; + } + + public boolean isDeleteAll() { + return deleteAll; + } + + public String getNamespace() { + return namespace; + } + + public Map getFilter() { + return filter; + } +} diff --git a/pinecone/runtime/src/main/java/io/quarkiverse/langchain4j/pinecone/runtime/DistanceMetric.java b/pinecone/runtime/src/main/java/io/quarkiverse/langchain4j/pinecone/runtime/DistanceMetric.java new file mode 100644 index 000000000..2aa74f84e --- /dev/null +++ b/pinecone/runtime/src/main/java/io/quarkiverse/langchain4j/pinecone/runtime/DistanceMetric.java @@ -0,0 +1,9 @@ +package io.quarkiverse.langchain4j.pinecone.runtime; + +public enum DistanceMetric { + + EUCLIDEAN, + COSINE, + DOTPRODUCT + +} diff --git a/pinecone/runtime/src/main/java/io/quarkiverse/langchain4j/pinecone/runtime/PineconeConfig.java b/pinecone/runtime/src/main/java/io/quarkiverse/langchain4j/pinecone/runtime/PineconeConfig.java new file mode 100644 index 000000000..ececc116b --- /dev/null +++ b/pinecone/runtime/src/main/java/io/quarkiverse/langchain4j/pinecone/runtime/PineconeConfig.java @@ -0,0 +1,58 @@ +package io.quarkiverse.langchain4j.pinecone.runtime; + +import static io.quarkus.runtime.annotations.ConfigPhase.BUILD_AND_RUN_TIME_FIXED; + +import java.time.Duration; +import java.util.Optional; + +import io.quarkus.runtime.annotations.ConfigRoot; +import io.smallrye.config.ConfigMapping; +import io.smallrye.config.WithDefault; + +@ConfigRoot(phase = BUILD_AND_RUN_TIME_FIXED) +@ConfigMapping(prefix = "quarkus.langchain4j.pinecone") +public interface PineconeConfig { + + /** + * The API key to Pinecone. + */ + String apiKey(); + + /** + * Environment name, e.g. gcp-starter or northamerica-northeast1-gcp. + */ + String environment(); + + /** + * ID of the project. + */ + String projectId(); + + /** + * Name of the index within the project. If the index doesn't exist, it will be created. + */ + String indexName(); + + /** + * Dimension of the embeddings in the index. This is required only in case that the index doesn't exist yet + * and needs to be created. + */ + Optional dimension(); + + /** + * The namespace. + */ + Optional namespace(); + + /** + * The name of the field that contains the text segment. + */ + @WithDefault("text") + String textFieldName(); + + /** + * The timeout duration for the Pinecone client. If not specified, 5 seconds will be used. + */ + Optional timeout(); + +} diff --git a/pinecone/runtime/src/main/java/io/quarkiverse/langchain4j/pinecone/runtime/PineconeIndexOperationsApi.java b/pinecone/runtime/src/main/java/io/quarkiverse/langchain4j/pinecone/runtime/PineconeIndexOperationsApi.java new file mode 100644 index 000000000..fe5c51b17 --- /dev/null +++ b/pinecone/runtime/src/main/java/io/quarkiverse/langchain4j/pinecone/runtime/PineconeIndexOperationsApi.java @@ -0,0 +1,25 @@ +package io.quarkiverse.langchain4j.pinecone.runtime; + +import java.util.List; + +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +@Consumes(MediaType.APPLICATION_JSON) +@Produces(MediaType.APPLICATION_JSON) +@Path("/") +public interface PineconeIndexOperationsApi { + + @POST + @Path("/databases") + void createIndex(CreateIndexRequest request); + + @GET + @Path("/databases") + List listIndexes(); + +} diff --git a/pinecone/runtime/src/main/java/io/quarkiverse/langchain4j/pinecone/runtime/PineconeRecorder.java b/pinecone/runtime/src/main/java/io/quarkiverse/langchain4j/pinecone/runtime/PineconeRecorder.java new file mode 100644 index 000000000..21d9debc3 --- /dev/null +++ b/pinecone/runtime/src/main/java/io/quarkiverse/langchain4j/pinecone/runtime/PineconeRecorder.java @@ -0,0 +1,27 @@ +package io.quarkiverse.langchain4j.pinecone.runtime; + +import java.time.Duration; +import java.util.function.Supplier; + +import io.quarkiverse.langchain4j.pinecone.PineconeEmbeddingStore; +import io.quarkus.runtime.annotations.Recorder; + +@Recorder +public class PineconeRecorder { + + public Supplier pineconeStoreSupplier(PineconeConfig config) { + return new Supplier<>() { + @Override + public PineconeEmbeddingStore get() { + return new PineconeEmbeddingStore(config.apiKey(), + config.indexName(), + config.projectId(), + config.environment(), + config.namespace().orElse(null), + config.textFieldName(), + config.timeout().orElse(Duration.ofSeconds(5)), + config.dimension().orElse(null)); + } + }; + } +} diff --git a/pinecone/runtime/src/main/java/io/quarkiverse/langchain4j/pinecone/runtime/PineconeVectorOperationsApi.java b/pinecone/runtime/src/main/java/io/quarkiverse/langchain4j/pinecone/runtime/PineconeVectorOperationsApi.java new file mode 100644 index 000000000..0985e9b34 --- /dev/null +++ b/pinecone/runtime/src/main/java/io/quarkiverse/langchain4j/pinecone/runtime/PineconeVectorOperationsApi.java @@ -0,0 +1,32 @@ +package io.quarkiverse.langchain4j.pinecone.runtime; + +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.DELETE; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +@Consumes(MediaType.APPLICATION_JSON) +@Produces(MediaType.APPLICATION_JSON) +@Path("/") +public interface PineconeVectorOperationsApi { + + @POST + @Path("/vectors/upsert") + UpsertResponse upsert(UpsertRequest vector); + + @POST + @Path("/query") + QueryResponse query(QueryRequest request); + + @POST + @Path("/vectors/delete") + void delete(DeleteRequest request); + + @DELETE + @Path("/databases/{indexName}") + void deleteIndex(@PathParam("indexName") String indexName); + +} diff --git a/pinecone/runtime/src/main/java/io/quarkiverse/langchain4j/pinecone/runtime/QueryRequest.java b/pinecone/runtime/src/main/java/io/quarkiverse/langchain4j/pinecone/runtime/QueryRequest.java new file mode 100644 index 000000000..a1a7f8d8b --- /dev/null +++ b/pinecone/runtime/src/main/java/io/quarkiverse/langchain4j/pinecone/runtime/QueryRequest.java @@ -0,0 +1,45 @@ +package io.quarkiverse.langchain4j.pinecone.runtime; + +import io.quarkus.runtime.annotations.RegisterForReflection; + +/** + * Represents a query against Pinecone. + * See the API documentation. + */ +@RegisterForReflection +public class QueryRequest { + + private final String namespace; + private final Long topK; + private final boolean includeMetadata; + private final boolean includeValues; + private final float[] vector; + + public QueryRequest(String namespace, Long topK, boolean includeMetadata, boolean includeValues, float[] vector) { + this.namespace = namespace; + this.topK = topK; + this.includeMetadata = includeMetadata; + this.includeValues = includeValues; + this.vector = vector; + } + + public String getNamespace() { + return namespace; + } + + public Long getTopK() { + return topK; + } + + public boolean isIncludeMetadata() { + return includeMetadata; + } + + public float[] getVector() { + return vector; + } + + public boolean isIncludeValues() { + return includeValues; + } +} diff --git a/pinecone/runtime/src/main/java/io/quarkiverse/langchain4j/pinecone/runtime/QueryResponse.java b/pinecone/runtime/src/main/java/io/quarkiverse/langchain4j/pinecone/runtime/QueryResponse.java new file mode 100644 index 000000000..fc2e6260a --- /dev/null +++ b/pinecone/runtime/src/main/java/io/quarkiverse/langchain4j/pinecone/runtime/QueryResponse.java @@ -0,0 +1,34 @@ +package io.quarkiverse.langchain4j.pinecone.runtime; + +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonCreator; + +import io.quarkus.runtime.annotations.RegisterForReflection; + +/** + * Represents a response to a Query operation. + * See the API documentation. + */ +@RegisterForReflection +public class QueryResponse { + + private final List matches; + + private String metadata; + + @JsonCreator + public QueryResponse(List matches, String metadata) { + this.matches = matches; + this.metadata = metadata; + } + + public List getMatches() { + return matches; + } + + public String getMetadata() { + return metadata; + } + +} diff --git a/pinecone/runtime/src/main/java/io/quarkiverse/langchain4j/pinecone/runtime/UpsertRequest.java b/pinecone/runtime/src/main/java/io/quarkiverse/langchain4j/pinecone/runtime/UpsertRequest.java new file mode 100644 index 000000000..a68b6800a --- /dev/null +++ b/pinecone/runtime/src/main/java/io/quarkiverse/langchain4j/pinecone/runtime/UpsertRequest.java @@ -0,0 +1,26 @@ +package io.quarkiverse.langchain4j.pinecone.runtime; + +import java.util.List; + +import io.quarkus.runtime.annotations.RegisterForReflection; + +/** + * Represents an upsert operation against Pinecone. + * See the API documentation. + */ +@RegisterForReflection +public class UpsertRequest { + + private final List vectors; + + private final String namespace; + + public UpsertRequest(List vectors, String namespace) { + this.vectors = vectors; + this.namespace = namespace; + } + + public List getVectors() { + return vectors; + } +} diff --git a/pinecone/runtime/src/main/java/io/quarkiverse/langchain4j/pinecone/runtime/UpsertResponse.java b/pinecone/runtime/src/main/java/io/quarkiverse/langchain4j/pinecone/runtime/UpsertResponse.java new file mode 100644 index 000000000..489e467f0 --- /dev/null +++ b/pinecone/runtime/src/main/java/io/quarkiverse/langchain4j/pinecone/runtime/UpsertResponse.java @@ -0,0 +1,24 @@ +package io.quarkiverse.langchain4j.pinecone.runtime; + +import com.fasterxml.jackson.annotation.JsonCreator; + +import io.quarkus.runtime.annotations.RegisterForReflection; + +/** + * Represents a response to an Upsert operation. + * See the API documentation. + */ +@RegisterForReflection +public class UpsertResponse { + + private final long upsertedCount; + + @JsonCreator + public UpsertResponse(long upsertedCount) { + this.upsertedCount = upsertedCount; + } + + public long getUpsertedCount() { + return upsertedCount; + } +} diff --git a/pinecone/runtime/src/main/java/io/quarkiverse/langchain4j/pinecone/runtime/UpsertVector.java b/pinecone/runtime/src/main/java/io/quarkiverse/langchain4j/pinecone/runtime/UpsertVector.java new file mode 100644 index 000000000..f8ef1c86b --- /dev/null +++ b/pinecone/runtime/src/main/java/io/quarkiverse/langchain4j/pinecone/runtime/UpsertVector.java @@ -0,0 +1,70 @@ +package io.quarkiverse.langchain4j.pinecone.runtime; + +import java.util.HashMap; +import java.util.Map; + +import io.quarkus.runtime.annotations.RegisterForReflection; + +/** + * Represents a vector passed to the UPSERT operation. + */ +@RegisterForReflection +public class UpsertVector { + + private final String id; + private final float[] values; + private final Map metadata; + + public UpsertVector(Builder builder) { + this.id = builder.id; + this.values = builder.value; + this.metadata = builder.metadata; + } + + public String getId() { + return id; + } + + public float[] getValues() { + return values; + } + + public Map getMetadata() { + return metadata; + } + + public static class Builder { + + private String id = null; + private float[] value = null; + private Map metadata = new HashMap<>(); + + public Builder id(String id) { + this.id = id; + return this; + } + + public Builder value(float[] value) { + this.value = value; + return this; + } + + public Builder metadata(String key, String value) { + if (key != null && value != null) { + this.metadata.put(key, value); + } + return this; + } + + public Builder metadata(Map map) { + if (map != null) { + this.metadata.putAll(map); + } + return this; + } + + public UpsertVector build() { + return new UpsertVector(this); + } + } +} diff --git a/pinecone/runtime/src/main/java/io/quarkiverse/langchain4j/pinecone/runtime/VectorMatch.java b/pinecone/runtime/src/main/java/io/quarkiverse/langchain4j/pinecone/runtime/VectorMatch.java new file mode 100644 index 000000000..d5c59fd56 --- /dev/null +++ b/pinecone/runtime/src/main/java/io/quarkiverse/langchain4j/pinecone/runtime/VectorMatch.java @@ -0,0 +1,43 @@ +package io.quarkiverse.langchain4j.pinecone.runtime; + +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonCreator; + +import io.quarkus.runtime.annotations.RegisterForReflection; + +@RegisterForReflection +public class VectorMatch { + + private final String id; + + private final float score; + + private final Map metadata; + + private final float[] values; + + @JsonCreator + public VectorMatch(String id, float score, Map metadata, float[] values) { + this.id = id; + this.score = score; + this.metadata = metadata; + this.values = values; + } + + public String getId() { + return id; + } + + public float getScore() { + return score; + } + + public Map getMetadata() { + return metadata; + } + + public float[] getValues() { + return values; + } +} diff --git a/pinecone/runtime/src/main/resources/META-INF/beans.xml b/pinecone/runtime/src/main/resources/META-INF/beans.xml new file mode 100644 index 000000000..e69de29bb diff --git a/pinecone/runtime/src/main/resources/META-INF/quarkus-extension.yaml b/pinecone/runtime/src/main/resources/META-INF/quarkus-extension.yaml new file mode 100644 index 000000000..fbfa676fa --- /dev/null +++ b/pinecone/runtime/src/main/resources/META-INF/quarkus-extension.yaml @@ -0,0 +1,12 @@ +name: Langchain4j Pinecone embedding store +artifact: ${project.groupId}:${project.artifactId}:${project.version} +description: Provides the Pinecone Embedding store for Langchain4j +metadata: + keywords: + - ai + - langchain4j + - openai + - pinecone + categories: + - "miscellaneous" + status: "experimental" \ No newline at end of file diff --git a/pom.xml b/pom.xml index aa23dd6c5..9648c61b0 100644 --- a/pom.xml +++ b/pom.xml @@ -21,6 +21,7 @@ openai/azure-openai openai/openai-common openai/openai-vanilla + pinecone redis