This is an automated email from the ASF dual-hosted git repository.
davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/main by this push:
new 664692c4f6ed CAMEL-26300: camel-pinecone - pinecone:embeddings stores
the text pinecone:rag reads, and names a missing embedding header (#29213)
664692c4f6ed is described below
commit 664692c4f6edff3ab0f84c8465715664cf48c027
Author: Andrea Cosentino <[email protected]>
AuthorDate: Thu Nov 5 13:02:35 2026 +0100
CAMEL-26300: camel-pinecone - pinecone:embeddings stores the text
pinecone:rag reads, and names a missing embedding header (#29213)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
.../apache/camel/catalog/components/pinecone.json | 3 +-
.../camel/catalog/docs/pinecone-component.adoc | 4 ++
.../catalog/transformers/pinecone-embeddings.json | 2 +-
.../apache/camel/component/pinecone/pinecone.json | 3 +-
.../camel/transformer/pinecone-embeddings.json | 2 +-
.../src/main/docs/pinecone-component.adoc | 4 ++
.../pinecone/PineconeVectorDbHeaders.java | 6 ++
.../pinecone/PineconeVectorDbProducer.java | 8 ++-
.../PineconeEmbeddingsDataTypeTransformer.java | 28 +++++++-
.../pinecone/PineconeVectorDbProducerTest.java | 83 ++++++++++++++++++++--
.../PineconeEmbeddingsDataTypeTransformerTest.java | 82 +++++++++++++++++----
.../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc | 12 ++++
.../PineconeVectorDbEndpointBuilderFactory.java | 15 ++++
13 files changed, 225 insertions(+), 27 deletions(-)
diff --git
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/pinecone.json
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/pinecone.json
index fe55acd56013..d0ca3048a279 100644
---
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/pinecone.json
+++
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/pinecone.json
@@ -56,7 +56,8 @@
"CamelPineconeNamespace": { "index": 13, "kind": "header", "displayName":
"", "group": "producer", "label": "", "required": false, "javaType": "String",
"deprecated": false, "deprecationNote": "", "autowired": false, "secret":
false, "description": "Namespace for actions (query\/upsert\/etc)",
"constantName":
"org.apache.camel.component.pinecone.PineconeVectorDbHeaders#NAMESPACE" },
"CamelPineconeQueryFilter": { "index": 14, "kind": "header",
"displayName": "", "group": "producer", "label": "", "required": false,
"javaType": "com.google.protobuf.Struct", "deprecated": false,
"deprecationNote": "", "autowired": false, "secret": false, "description":
"Query Filter", "constantName":
"org.apache.camel.component.pinecone.PineconeVectorDbHeaders#QUERY_FILTER" },
"CamelPineconeQueryIncludeValues": { "index": 15, "kind": "header",
"displayName": "", "group": "producer", "label": "", "required": false,
"javaType": "boolean", "deprecated": false, "deprecationNote": "", "autowired":
false, "secret": false, "description": "Query Include Values", "constantName":
"org.apache.camel.component.pinecone.PineconeVectorDbHeaders#QUERY_INCLUDE_VALUES"
},
- "CamelPineconeQueryIncludeMetadata": { "index": 16, "kind": "header",
"displayName": "", "group": "producer", "label": "", "required": false,
"javaType": "boolean", "deprecated": false, "deprecationNote": "", "autowired":
false, "secret": false, "description": "Query Include Metadata",
"constantName":
"org.apache.camel.component.pinecone.PineconeVectorDbHeaders#QUERY_INCLUDE_METADATA"
}
+ "CamelPineconeQueryIncludeMetadata": { "index": 16, "kind": "header",
"displayName": "", "group": "producer", "label": "", "required": false,
"javaType": "boolean", "deprecated": false, "deprecationNote": "", "autowired":
false, "secret": false, "description": "Query Include Metadata",
"constantName":
"org.apache.camel.component.pinecone.PineconeVectorDbHeaders#QUERY_INCLUDE_METADATA"
},
+ "CamelPineconeMetadata": { "index": 17, "kind": "header", "displayName":
"", "group": "producer", "label": "", "required": false, "javaType":
"com.google.protobuf.Struct", "deprecated": false, "deprecationNote": "",
"autowired": false, "secret": false, "description": "The metadata UPSERT stores
with the vector, and the fields of the metadata UPDATE sets. The
pinecone:embeddings data type transformer puts the text of the embedding in its
text_segment field, which the pinecone:rag data [...]
},
"bodyTypes": {
"producerIn": { "carriers": [ "list", "null" ], "byOption": [ { "option":
"action", "value": "UPSERT", "carriers": [ "list" ] }, { "option": "action",
"value": "UPDATE", "carriers": [ "list" ] }, { "option": "action", "value":
"QUERY", "carriers": [ "list" ] }, { "option": "action", "value": "FETCH",
"carriers": [ "list" ] }, { "option": "action", "value": "DELETE_BY_ID",
"carriers": [ "list" ] }, { "option": "action", "value": "QUERY_BY_ID",
"carriers": [ "any" ] }, { "option": "act [...]
diff --git
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/pinecone-component.adoc
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/pinecone-component.adoc
index 35a185854b8a..ae76560e4878 100644
---
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/pinecone-component.adoc
+++
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/pinecone-component.adoc
@@ -76,4 +76,8 @@ The `action` option, or the `CamelPineconeAction` header,
selects the operation.
The vector of UPSERT, UPDATE and QUERY is the message body: a List of numbers,
such as the List of Double a JSON array unmarshals to, or a `float[]` or a
`double[]`; each number is converted to a float. Another body fails, and so
does a List with an element that is not a number. UPSERT stores the vector
under the id in the `CamelPineconeIndexId` header; without the header, it
generates a random UUID and sets the header to it.
+UPSERT stores the `CamelPineconeMetadata` header, a
`com.google.protobuf.Struct`, as the metadata of the vector, and UPDATE sets
the fields of that header in the metadata of the vector.
+
+The `pinecone:embeddings` data type transformer turns the embedding of the
`CamelLangChain4jEmbeddingsVector` header, which the langchain4j-embeddings
component sets, into the vector of the message body, and fails when the header
is missing. It puts the text of a LangChain4j `TextSegment` or `String` body in
the `text_segment` field of the `CamelPineconeMetadata` header, and keeps the
other fields of that header. Without the `CamelPineconeIndexName` and
`CamelPineconeIndexId` headers, it [...]
+
The `pinecone:rag` data type transformer turns the result of a QUERY or
QUERY_BY_ID into the List of the texts of its matches, read from their
`text_segment` metadata, which the query returns when the
`CamelPineconeQueryIncludeMetadata` header is `true`.
diff --git
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/transformers/pinecone-embeddings.json
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/transformers/pinecone-embeddings.json
index 614b93a419d1..69346d3e34de 100644
---
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/transformers/pinecone-embeddings.json
+++
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/transformers/pinecone-embeddings.json
@@ -15,7 +15,7 @@
"carriers": [
"any"
],
- "description": "The body is not read: the vector comes from the
CamelLangChain4jEmbeddingsVector header, which must be set, and the text is not
stored"
+ "description": "A LangChain4j TextSegment, or a String the converter of
camel-langchain4j-embeddings turns into one, whose text goes in the
text_segment field of the CamelPineconeMetadata header, which UPSERT and UPDATE
store with the vector; another body stores no text. The vector comes from the
CamelLangChain4jEmbeddingsVector header, which must be set"
},
"transformerOut": {
"carriers": [
diff --git
a/components/camel-ai/camel-pinecone/src/generated/resources/META-INF/org/apache/camel/component/pinecone/pinecone.json
b/components/camel-ai/camel-pinecone/src/generated/resources/META-INF/org/apache/camel/component/pinecone/pinecone.json
index fe55acd56013..d0ca3048a279 100644
---
a/components/camel-ai/camel-pinecone/src/generated/resources/META-INF/org/apache/camel/component/pinecone/pinecone.json
+++
b/components/camel-ai/camel-pinecone/src/generated/resources/META-INF/org/apache/camel/component/pinecone/pinecone.json
@@ -56,7 +56,8 @@
"CamelPineconeNamespace": { "index": 13, "kind": "header", "displayName":
"", "group": "producer", "label": "", "required": false, "javaType": "String",
"deprecated": false, "deprecationNote": "", "autowired": false, "secret":
false, "description": "Namespace for actions (query\/upsert\/etc)",
"constantName":
"org.apache.camel.component.pinecone.PineconeVectorDbHeaders#NAMESPACE" },
"CamelPineconeQueryFilter": { "index": 14, "kind": "header",
"displayName": "", "group": "producer", "label": "", "required": false,
"javaType": "com.google.protobuf.Struct", "deprecated": false,
"deprecationNote": "", "autowired": false, "secret": false, "description":
"Query Filter", "constantName":
"org.apache.camel.component.pinecone.PineconeVectorDbHeaders#QUERY_FILTER" },
"CamelPineconeQueryIncludeValues": { "index": 15, "kind": "header",
"displayName": "", "group": "producer", "label": "", "required": false,
"javaType": "boolean", "deprecated": false, "deprecationNote": "", "autowired":
false, "secret": false, "description": "Query Include Values", "constantName":
"org.apache.camel.component.pinecone.PineconeVectorDbHeaders#QUERY_INCLUDE_VALUES"
},
- "CamelPineconeQueryIncludeMetadata": { "index": 16, "kind": "header",
"displayName": "", "group": "producer", "label": "", "required": false,
"javaType": "boolean", "deprecated": false, "deprecationNote": "", "autowired":
false, "secret": false, "description": "Query Include Metadata",
"constantName":
"org.apache.camel.component.pinecone.PineconeVectorDbHeaders#QUERY_INCLUDE_METADATA"
}
+ "CamelPineconeQueryIncludeMetadata": { "index": 16, "kind": "header",
"displayName": "", "group": "producer", "label": "", "required": false,
"javaType": "boolean", "deprecated": false, "deprecationNote": "", "autowired":
false, "secret": false, "description": "Query Include Metadata",
"constantName":
"org.apache.camel.component.pinecone.PineconeVectorDbHeaders#QUERY_INCLUDE_METADATA"
},
+ "CamelPineconeMetadata": { "index": 17, "kind": "header", "displayName":
"", "group": "producer", "label": "", "required": false, "javaType":
"com.google.protobuf.Struct", "deprecated": false, "deprecationNote": "",
"autowired": false, "secret": false, "description": "The metadata UPSERT stores
with the vector, and the fields of the metadata UPDATE sets. The
pinecone:embeddings data type transformer puts the text of the embedding in its
text_segment field, which the pinecone:rag data [...]
},
"bodyTypes": {
"producerIn": { "carriers": [ "list", "null" ], "byOption": [ { "option":
"action", "value": "UPSERT", "carriers": [ "list" ] }, { "option": "action",
"value": "UPDATE", "carriers": [ "list" ] }, { "option": "action", "value":
"QUERY", "carriers": [ "list" ] }, { "option": "action", "value": "FETCH",
"carriers": [ "list" ] }, { "option": "action", "value": "DELETE_BY_ID",
"carriers": [ "list" ] }, { "option": "action", "value": "QUERY_BY_ID",
"carriers": [ "any" ] }, { "option": "act [...]
diff --git
a/components/camel-ai/camel-pinecone/src/generated/resources/META-INF/services/org/apache/camel/transformer/pinecone-embeddings.json
b/components/camel-ai/camel-pinecone/src/generated/resources/META-INF/services/org/apache/camel/transformer/pinecone-embeddings.json
index 614b93a419d1..69346d3e34de 100644
---
a/components/camel-ai/camel-pinecone/src/generated/resources/META-INF/services/org/apache/camel/transformer/pinecone-embeddings.json
+++
b/components/camel-ai/camel-pinecone/src/generated/resources/META-INF/services/org/apache/camel/transformer/pinecone-embeddings.json
@@ -15,7 +15,7 @@
"carriers": [
"any"
],
- "description": "The body is not read: the vector comes from the
CamelLangChain4jEmbeddingsVector header, which must be set, and the text is not
stored"
+ "description": "A LangChain4j TextSegment, or a String the converter of
camel-langchain4j-embeddings turns into one, whose text goes in the
text_segment field of the CamelPineconeMetadata header, which UPSERT and UPDATE
store with the vector; another body stores no text. The vector comes from the
CamelLangChain4jEmbeddingsVector header, which must be set"
},
"transformerOut": {
"carriers": [
diff --git
a/components/camel-ai/camel-pinecone/src/main/docs/pinecone-component.adoc
b/components/camel-ai/camel-pinecone/src/main/docs/pinecone-component.adoc
index 35a185854b8a..ae76560e4878 100644
--- a/components/camel-ai/camel-pinecone/src/main/docs/pinecone-component.adoc
+++ b/components/camel-ai/camel-pinecone/src/main/docs/pinecone-component.adoc
@@ -76,4 +76,8 @@ The `action` option, or the `CamelPineconeAction` header,
selects the operation.
The vector of UPSERT, UPDATE and QUERY is the message body: a List of numbers,
such as the List of Double a JSON array unmarshals to, or a `float[]` or a
`double[]`; each number is converted to a float. Another body fails, and so
does a List with an element that is not a number. UPSERT stores the vector
under the id in the `CamelPineconeIndexId` header; without the header, it
generates a random UUID and sets the header to it.
+UPSERT stores the `CamelPineconeMetadata` header, a
`com.google.protobuf.Struct`, as the metadata of the vector, and UPDATE sets
the fields of that header in the metadata of the vector.
+
+The `pinecone:embeddings` data type transformer turns the embedding of the
`CamelLangChain4jEmbeddingsVector` header, which the langchain4j-embeddings
component sets, into the vector of the message body, and fails when the header
is missing. It puts the text of a LangChain4j `TextSegment` or `String` body in
the `text_segment` field of the `CamelPineconeMetadata` header, and keeps the
other fields of that header. Without the `CamelPineconeIndexName` and
`CamelPineconeIndexId` headers, it [...]
+
The `pinecone:rag` data type transformer turns the result of a QUERY or
QUERY_BY_ID into the List of the texts of its matches, read from their
`text_segment` metadata, which the query returns when the
`CamelPineconeQueryIncludeMetadata` header is `true`.
diff --git
a/components/camel-ai/camel-pinecone/src/main/java/org/apache/camel/component/pinecone/PineconeVectorDbHeaders.java
b/components/camel-ai/camel-pinecone/src/main/java/org/apache/camel/component/pinecone/PineconeVectorDbHeaders.java
index 222d31fe84dc..d5d527ae52fe 100644
---
a/components/camel-ai/camel-pinecone/src/main/java/org/apache/camel/component/pinecone/PineconeVectorDbHeaders.java
+++
b/components/camel-ai/camel-pinecone/src/main/java/org/apache/camel/component/pinecone/PineconeVectorDbHeaders.java
@@ -73,4 +73,10 @@ public class PineconeVectorDbHeaders {
@Metadata(description = "Query Include Metadata", javaType = "boolean")
public static final String QUERY_INCLUDE_METADATA =
"CamelPineconeQueryIncludeMetadata";
+ @Metadata(description = "The metadata UPSERT stores with the vector, and
the fields of the metadata UPDATE sets. The"
+ + " pinecone:embeddings data type transformer puts
the text of the embedding in its text_segment"
+ + " field, which the pinecone:rag data type
transformer reads",
+ javaType = "com.google.protobuf.Struct")
+ public static final String METADATA = "CamelPineconeMetadata";
+
}
diff --git
a/components/camel-ai/camel-pinecone/src/main/java/org/apache/camel/component/pinecone/PineconeVectorDbProducer.java
b/components/camel-ai/camel-pinecone/src/main/java/org/apache/camel/component/pinecone/PineconeVectorDbProducer.java
index 4ea721b30af8..d5736e8f50ce 100644
---
a/components/camel-ai/camel-pinecone/src/main/java/org/apache/camel/component/pinecone/PineconeVectorDbProducer.java
+++
b/components/camel-ai/camel-pinecone/src/main/java/org/apache/camel/component/pinecone/PineconeVectorDbProducer.java
@@ -224,10 +224,12 @@ public class PineconeVectorDbProducer extends
DefaultProducer {
in.setHeader(PineconeVectorDbHeaders.INDEX_ID, indexId);
}
String namespace = in.getHeader(PineconeVectorDbHeaders.NAMESPACE,
String.class);
+ // such as the text_segment the pinecone:embeddings transformer sets,
which the pinecone:rag transformer reads
+ Struct metadata = in.getHeader(PineconeVectorDbHeaders.METADATA,
Struct.class);
Index index = this.client.getIndexConnection(indexName);
- UpsertResponse result = index.upsert(indexId, elements, null, null,
null, namespace);
+ UpsertResponse result = index.upsert(indexId, elements, null, null,
metadata, namespace);
populateUpsertResponse(result, exchange);
@@ -243,9 +245,11 @@ public class PineconeVectorDbProducer extends
DefaultProducer {
}
String indexId = in.getHeader(PineconeVectorDbHeaders.INDEX_ID,
String.class);
String namespace = in.getHeader(PineconeVectorDbHeaders.NAMESPACE,
String.class);
+ // the fields of the metadata to set, such as the text_segment the
pinecone:embeddings transformer sets
+ Struct metadata = in.getHeader(PineconeVectorDbHeaders.METADATA,
Struct.class);
Index index = this.client.getIndexConnection(indexName);
- UpdateResponse result = index.update(indexId, elements, namespace);
+ UpdateResponse result = index.update(indexId, elements, metadata,
namespace, null, null);
populateUpdateResponse(result, exchange);
diff --git
a/components/camel-ai/camel-pinecone/src/main/java/org/apache/camel/component/pinecone/transform/PineconeEmbeddingsDataTypeTransformer.java
b/components/camel-ai/camel-pinecone/src/main/java/org/apache/camel/component/pinecone/transform/PineconeEmbeddingsDataTypeTransformer.java
index 7c8dc627c3dc..4d0c6075a4e4 100644
---
a/components/camel-ai/camel-pinecone/src/main/java/org/apache/camel/component/pinecone/transform/PineconeEmbeddingsDataTypeTransformer.java
+++
b/components/camel-ai/camel-pinecone/src/main/java/org/apache/camel/component/pinecone/transform/PineconeEmbeddingsDataTypeTransformer.java
@@ -19,7 +19,10 @@ package org.apache.camel.component.pinecone.transform;
import java.util.UUID;
+import com.google.protobuf.Struct;
+import com.google.protobuf.Value;
import dev.langchain4j.data.embedding.Embedding;
+import dev.langchain4j.data.segment.TextSegment;
import org.apache.camel.Message;
import org.apache.camel.ai.CamelLangchain4jAttributes;
import org.apache.camel.component.pinecone.PineconeVectorDbHeaders;
@@ -37,21 +40,40 @@ import org.apache.camel.spi.Transformer;
@DataTypeTransformer(name = "pinecone:embeddings",
description = "Prepares the message to become an object
writable by Pinecone component")
@BodyTypes(transformerIn = @BodyType(value = Carrier.ANY,
- description = "The body is not read: the
vector comes from the"
- + "
CamelLangChain4jEmbeddingsVector header, which must be set, and the"
- + " text is not stored"),
+ description = "A LangChain4j TextSegment,
or a String the converter of"
+ + "
camel-langchain4j-embeddings turns into one, whose text goes in the"
+ + " text_segment field of
the CamelPineconeMetadata header, which UPSERT"
+ + " and UPDATE store with
the vector; another body stores no text. The"
+ + " vector comes from the
CamelLangChain4jEmbeddingsVector header, which"
+ + " must be set"),
transformerOut = @BodyType(value = Carrier.LIST, javaType =
"java.util.List<java.lang.Float>",
description = "The vector of the
embedding"))
public class PineconeEmbeddingsDataTypeTransformer extends Transformer {
+ // the metadata field of the text, which the pinecone:rag transformer and
the Pinecone store of LangChain4j read
+ private static final String TEXT_SEGMENT = "text_segment";
+
@Override
public void transform(Message message, DataType fromType, DataType toType)
{
Embedding embedding =
message.getHeader(CamelLangchain4jAttributes.CAMEL_LANGCHAIN4J_EMBEDDING_VECTOR,
Embedding.class);
+ if (embedding == null) {
+ throw new IllegalArgumentException(
+ "Missing embedding vector header '" +
CamelLangchain4jAttributes.CAMEL_LANGCHAIN4J_EMBEDDING_VECTOR
+ + "'. Ensure the
langchain4j-embeddings component is called before this transformer.");
+ }
+ TextSegment text = message.getBody(TextSegment.class);
String indexId = message.getHeader(PineconeVectorDbHeaders.INDEX_ID,
UUID.randomUUID(), String.class);
String indexName =
message.getHeader(PineconeVectorDbHeaders.INDEX_NAME, "embeddings",
String.class);
message.setHeader(PineconeVectorDbHeaders.INDEX_NAME, indexName);
message.setHeader(PineconeVectorDbHeaders.INDEX_ID, indexId);
+ if (text != null) {
+ // added to the metadata the caller may have set, which is kept
+ Struct metadata =
message.getHeader(PineconeVectorDbHeaders.METADATA, Struct.class);
+ Struct.Builder builder = metadata != null ? metadata.toBuilder() :
Struct.newBuilder();
+ builder.putFields(TEXT_SEGMENT,
Value.newBuilder().setStringValue(text.text()).build());
+ message.setHeader(PineconeVectorDbHeaders.METADATA,
builder.build());
+ }
message.setBody(embedding.vectorAsList());
}
}
diff --git
a/components/camel-ai/camel-pinecone/src/test/java/org/apache/camel/component/pinecone/PineconeVectorDbProducerTest.java
b/components/camel-ai/camel-pinecone/src/test/java/org/apache/camel/component/pinecone/PineconeVectorDbProducerTest.java
index 9fa7f5f6ab5d..8213f1a4766d 100644
---
a/components/camel-ai/camel-pinecone/src/test/java/org/apache/camel/component/pinecone/PineconeVectorDbProducerTest.java
+++
b/components/camel-ai/camel-pinecone/src/test/java/org/apache/camel/component/pinecone/PineconeVectorDbProducerTest.java
@@ -21,7 +21,10 @@ import java.math.BigDecimal;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
+import java.util.Collections;
+import java.util.LinkedHashMap;
import java.util.List;
+import java.util.Map;
import java.util.UUID;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.stream.Stream;
@@ -30,6 +33,8 @@ import com.google.protobuf.Struct;
import com.google.protobuf.Value;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpServer;
+import dev.langchain4j.data.embedding.Embedding;
+import dev.langchain4j.data.segment.TextSegment;
import io.grpc.Server;
import io.grpc.ServerBuilder;
import io.grpc.stub.StreamObserver;
@@ -40,11 +45,13 @@ import io.pinecone.proto.UpdateRequest;
import io.pinecone.proto.UpdateResponse;
import io.pinecone.proto.UpsertRequest;
import io.pinecone.proto.UpsertResponse;
+import io.pinecone.proto.Vector;
import io.pinecone.proto.VectorServiceGrpc;
import io.pinecone.unsigned_indices_model.QueryResponseWithUnsignedIndices;
import org.apache.camel.CamelContext;
import org.apache.camel.Exchange;
import org.apache.camel.InvalidPayloadException;
+import org.apache.camel.ai.CamelLangchain4jAttributes;
import org.apache.camel.builder.RouteBuilder;
import org.apache.camel.spi.DataType;
import org.apache.camel.test.junit6.CamelTestSupport;
@@ -67,6 +74,9 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
* header, UPSERT without the CamelPineconeIndexId header stores the vector
under a random id, and the pinecone:rag data
* type transformer reads the matches of a QUERY. A fake Pinecone, its control
plane on a JDK HttpServer and the data
* plane of the index on a gRPC server, records the requests of the client, so
no Pinecone is needed.
+ * <p>
+ * CAMEL-26300: UPSERT and UPDATE store the CamelPineconeMetadata header, in
which the pinecone:embeddings data type
+ * transformer puts the text that the pinecone:rag transformer reads.
*/
class PineconeVectorDbProducerTest extends CamelTestSupport {
@@ -75,15 +85,19 @@ class PineconeVectorDbProducerTest extends CamelTestSupport
{
// the client keeps a connection per index name for the JVM, so each test
has its own index
private final String indexName = "camel-" + UUID.randomUUID();
private final List<Object> requests = new CopyOnWriteArrayList<>();
+ // the vectors of the fake index by id, which UPSERT stores and QUERY
returns as its matches
+ private final Map<String, Vector> vectors =
Collections.synchronizedMap(new LinkedHashMap<>());
private HttpServer controlPlane;
private Server dataPlane;
@Override
protected CamelContext createCamelContext() throws Exception {
+ vectors.put("vector-1",
Vector.newBuilder().setId("vector-1").addAllValues(VECTOR).setMetadata(text("hello")).build());
dataPlane = ServerBuilder.forPort(0).addService(new
VectorServiceGrpc.VectorServiceImplBase() {
@Override
public void upsert(UpsertRequest request,
StreamObserver<UpsertResponse> reply) {
requests.add(request);
+ request.getVectorsList().forEach(vector ->
vectors.put(vector.getId(), vector));
reply.onNext(UpsertResponse.newBuilder().setUpsertedCount(request.getVectorsCount()).build());
reply.onCompleted();
}
@@ -98,12 +112,12 @@ class PineconeVectorDbProducerTest extends
CamelTestSupport {
@Override
public void query(QueryRequest request,
StreamObserver<QueryResponse> reply) {
requests.add(request);
- Struct metadata = Struct.newBuilder()
- .putFields("text_segment",
Value.newBuilder().setStringValue("hello").build())
- .build();
- reply.onNext(QueryResponse.newBuilder()
-
.addMatches(ScoredVector.newBuilder().setId("vector-1").setScore(0.9f).setMetadata(metadata))
- .build());
+ QueryResponse.Builder response = QueryResponse.newBuilder();
+ synchronized (vectors) {
+ vectors.values().forEach(vector -> response.addMatches(
+
ScoredVector.newBuilder().setId(vector.getId()).setScore(0.9f).setMetadata(vector.getMetadata())));
+ }
+ reply.onNext(response.build());
reply.onCompleted();
}
}).build().start();
@@ -129,6 +143,9 @@ class PineconeVectorDbProducerTest extends CamelTestSupport
{
from("direct:rag")
.to("pinecone:rag?action=QUERY")
.transformDataType(new DataType("pinecone:rag"));
+ from("direct:embeddings")
+ .transformDataType(new DataType("pinecone:embeddings"))
+ .to("pinecone:embeddings?action=UPSERT");
}
};
}
@@ -161,6 +178,8 @@ class PineconeVectorDbProducerTest extends CamelTestSupport
{
UpsertRequest upsert = assertInstanceOf(UpsertRequest.class,
singleRequest());
assertEquals("vector-1", upsert.getVectors(0).getId());
assertEquals(VECTOR, upsert.getVectors(0).getValuesList());
+ // no metadata without the CamelPineconeMetadata header
+ assertFalse(upsert.getVectors(0).hasMetadata());
}
@ParameterizedTest
@@ -175,6 +194,8 @@ class PineconeVectorDbProducerTest extends CamelTestSupport
{
UpdateRequest update = assertInstanceOf(UpdateRequest.class,
singleRequest());
assertEquals("vector-1", update.getId());
assertEquals(VECTOR, update.getValuesList());
+ // no metadata without the CamelPineconeMetadata header
+ assertFalse(update.hasSetMetadata());
}
@ParameterizedTest
@@ -240,6 +261,52 @@ class PineconeVectorDbProducerTest extends
CamelTestSupport {
assertEquals(List.of("hello"), texts);
}
+ @Test
+ void upsertAndUpdateStoreTheMetadataHeader() {
+ // CAMEL-26300: UPSERT stores the CamelPineconeMetadata header as the
metadata of the vector, and UPDATE sets its
+ // fields in the metadata of the vector
+ Struct metadata = text("hello");
+ Exchange upsert = fluentTemplate.to("pinecone:test?action=UPSERT")
+ .withHeader(PineconeVectorDbHeaders.INDEX_ID, "vector-1")
+ .withHeader(PineconeVectorDbHeaders.METADATA, metadata)
+ .withBody(VECTOR)
+ .request(Exchange.class);
+ Exchange update = fluentTemplate.to("pinecone:test?action=UPDATE")
+ .withHeader(PineconeVectorDbHeaders.INDEX_ID, "vector-1")
+ .withHeader(PineconeVectorDbHeaders.METADATA, metadata)
+ .withBody(VECTOR)
+ .request(Exchange.class);
+
+ assertNull(upsert.getException());
+ assertNull(update.getException());
+ assertEquals(2, requests.size(), requests::toString);
+ assertEquals(metadata, assertInstanceOf(UpsertRequest.class,
requests.get(0)).getVectors(0).getMetadata());
+ assertEquals(metadata, assertInstanceOf(UpdateRequest.class,
requests.get(1)).getSetMetadata());
+ }
+
+ @Test
+ void theRagTransformerGivesTheTextTheEmbeddingsTransformerStored() {
+ // CAMEL-26300: the pinecone:embeddings transformer puts the text in
the text_segment metadata that UPSERT stores,
+ // which the pinecone:rag transformer reads from the matches of a
QUERY. Before, the text was not stored, and the
+ // match of the vector gave an empty String.
+ Exchange upsert = fluentTemplate.to("direct:embeddings")
+
.withHeader(CamelLangchain4jAttributes.CAMEL_LANGCHAIN4J_EMBEDDING_VECTOR,
+ new Embedding(new float[] { 0.1f, 0.2f, 0.3f }))
+ // the index of the endpoint, as the transformer gives the
embeddings index without this header
+ .withHeader(PineconeVectorDbHeaders.INDEX_NAME, indexName)
+ .withHeader(PineconeVectorDbHeaders.INDEX_ID, "vector-1")
+ .withBody(TextSegment.from("the source passage"))
+ .request(Exchange.class);
+ assertNull(upsert.getException());
+
+ Object texts = fluentTemplate.to("direct:rag")
+ .withHeader(PineconeVectorDbHeaders.QUERY_INCLUDE_METADATA,
true)
+ .withBody(VECTOR)
+ .request();
+
+ assertEquals(List.of("the source passage"), texts);
+ }
+
@Test
void aBodyThatIsNotAVectorFails() {
Exchange text = fluentTemplate.to("pinecone:test?action=UPSERT")
@@ -271,6 +338,10 @@ class PineconeVectorDbProducerTest extends
CamelTestSupport {
assertEquals(List.of(), requests);
}
+ private static Struct text(String text) {
+ return Struct.newBuilder().putFields("text_segment",
Value.newBuilder().setStringValue(text).build()).build();
+ }
+
private Object singleRequest() {
assertEquals(1, requests.size(), requests::toString);
return requests.get(0);
diff --git
a/components/camel-ai/camel-pinecone/src/test/java/org/apache/camel/component/pinecone/transform/PineconeEmbeddingsDataTypeTransformerTest.java
b/components/camel-ai/camel-pinecone/src/test/java/org/apache/camel/component/pinecone/transform/PineconeEmbeddingsDataTypeTransformerTest.java
index dce686ced532..da84b739d659 100644
---
a/components/camel-ai/camel-pinecone/src/test/java/org/apache/camel/component/pinecone/transform/PineconeEmbeddingsDataTypeTransformerTest.java
+++
b/components/camel-ai/camel-pinecone/src/test/java/org/apache/camel/component/pinecone/transform/PineconeEmbeddingsDataTypeTransformerTest.java
@@ -19,20 +19,30 @@ package org.apache.camel.component.pinecone.transform;
import java.util.Arrays;
import java.util.List;
+import com.google.protobuf.Struct;
+import com.google.protobuf.Value;
import dev.langchain4j.data.embedding.Embedding;
import dev.langchain4j.data.segment.TextSegment;
import org.apache.camel.Exchange;
+import org.apache.camel.Message;
import org.apache.camel.ai.CamelLangchain4jAttributes;
+import org.apache.camel.component.pinecone.PineconeVectorDbHeaders;
import org.apache.camel.impl.DefaultCamelContext;
import org.apache.camel.spi.BodyType.Carrier;
import org.apache.camel.spi.BodyTypes;
import org.apache.camel.spi.DataType;
import org.apache.camel.support.DefaultExchange;
+import org.apache.camel.support.SimpleTypeConverter;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
class PineconeEmbeddingsDataTypeTransformerTest {
@@ -40,28 +50,76 @@ class PineconeEmbeddingsDataTypeTransformerTest {
private final PineconeEmbeddingsDataTypeTransformer transformer = new
PineconeEmbeddingsDataTypeTransformer();
+ @BeforeEach
+ void startContext() {
+ // the converter camel-langchain4j-embeddings brings, which is not on
the classpath of this module
+
camelContext.getTypeConverterRegistry().addTypeConverter(TextSegment.class,
String.class,
+ new SimpleTypeConverter(false, (type, exchange, value) ->
TextSegment.from((String) value)));
+ camelContext.start();
+ }
+
+ @AfterEach
+ void stopContext() {
+ camelContext.stop();
+ }
+
@Test
void theBodyTypesSayTheBody() throws Exception {
- // CAMEL-25928: any body, as transformerIn says, as the vector comes
from a header, and the List of Float
- // transformerOut says
+ // CAMEL-25928: any body, as transformerIn says, of which a
TextSegment or a String is stored as the text, with the
+ // vector in a header, and the List of Float transformerOut says
BodyTypes bodyTypes =
PineconeEmbeddingsDataTypeTransformer.class.getAnnotation(BodyTypes.class);
assertArrayEquals(new Carrier[] { Carrier.ANY },
bodyTypes.transformerIn().value());
assertArrayEquals(new Carrier[] { Carrier.LIST },
bodyTypes.transformerOut().value());
assertEquals("java.util.List<java.lang.Float>",
bodyTypes.transformerOut().javaType());
for (Object body : Arrays.asList(TextSegment.from("hello"), "hello",
5, null)) {
- Exchange exchange = new DefaultExchange(camelContext);
-
exchange.getMessage().setHeader(CamelLangchain4jAttributes.CAMEL_LANGCHAIN4J_EMBEDDING_VECTOR,
- new Embedding(new float[] { 0.1f, 0.2f, 0.3f }));
- exchange.getMessage().setBody(body);
- transformer.transform(exchange.getMessage(), DataType.ANY,
DataType.ANY);
- assertEquals(List.of(0.1f, 0.2f, 0.3f),
exchange.getMessage().getBody(), String.valueOf(body));
+ Message message = transform(body, new Embedding(new float[] {
0.1f, 0.2f, 0.3f }));
+ assertEquals(List.of(0.1f, 0.2f, 0.3f), message.getBody(),
String.valueOf(body));
+ Struct metadata =
message.getHeader(PineconeVectorDbHeaders.METADATA, Struct.class);
+ if (body instanceof TextSegment || body instanceof String) {
+ // CAMEL-26300: the text, in the text_segment metadata the
pinecone:rag transformer reads
+ assertNotNull(metadata, String.valueOf(body));
+ assertEquals("hello",
metadata.getFieldsOrThrow("text_segment").getStringValue(),
String.valueOf(body));
+ } else {
+ // another body stores no text
+ assertNull(metadata, String.valueOf(body));
+ }
}
+ }
+
+ @Test
+ void theTextIsAddedToTheMetadataOfTheHeader() throws Exception {
+ // CAMEL-26300: the metadata the caller set is kept
+ Exchange exchange = new DefaultExchange(camelContext);
+
exchange.getMessage().setHeader(CamelLangchain4jAttributes.CAMEL_LANGCHAIN4J_EMBEDDING_VECTOR,
+ new Embedding(new float[] { 0.1f, 0.2f, 0.3f }));
+ exchange.getMessage().setHeader(PineconeVectorDbHeaders.METADATA,
Struct.newBuilder()
+ .putFields("source",
Value.newBuilder().setStringValue("doc.txt").build())
+ .build());
+ exchange.getMessage().setBody(TextSegment.from("hello"));
+ transformer.transform(exchange.getMessage(), DataType.ANY,
DataType.ANY);
+
+ Struct metadata =
exchange.getMessage().getHeader(PineconeVectorDbHeaders.METADATA, Struct.class);
+ assertEquals(2, metadata.getFieldsCount(), metadata::toString);
+ assertEquals("doc.txt",
metadata.getFieldsOrThrow("source").getStringValue());
+ assertEquals("hello",
metadata.getFieldsOrThrow("text_segment").getStringValue());
+ }
- // a message without the embedding fails
+ @Test
+ void aMessageWithoutTheEmbeddingFailsWithAClearMessage() {
+ // CAMEL-26300: an IllegalArgumentException that names the missing
header, as pgvector:embeddings gives, instead of
+ // a NullPointerException
+ IllegalArgumentException noEmbedding =
assertThrows(IllegalArgumentException.class, () -> transform("hello", null));
+
assertTrue(noEmbedding.getMessage().contains("CamelLangChain4jEmbeddingsVector"),
noEmbedding.getMessage());
+ }
+
+ private Message transform(Object body, Embedding embedding) throws
Exception {
Exchange exchange = new DefaultExchange(camelContext);
- exchange.getMessage().setBody("hello");
- assertThrows(NullPointerException.class,
- () -> transformer.transform(exchange.getMessage(),
DataType.ANY, DataType.ANY));
+ if (embedding != null) {
+
exchange.getMessage().setHeader(CamelLangchain4jAttributes.CAMEL_LANGCHAIN4J_EMBEDDING_VECTOR,
embedding);
+ }
+ exchange.getMessage().setBody(body);
+ transformer.transform(exchange.getMessage(), DataType.ANY,
DataType.ANY);
+ return exchange.getMessage();
}
}
diff --git
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
index 4c5b28b9e0d2..9721138d273a 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
@@ -3646,6 +3646,18 @@ it stayed started without receiving any event. The
events of the time without a
`docker:events` or `docker:stats` consumer now closes its stream: before, the
stream stayed open, and a route that was
stopped and started again received every event once more for each restart.
+=== camel-pinecone - the pinecone:embeddings transformer stores the text
+
+The `pinecone:embeddings` data type transformer now reads the message body.
The text of a LangChain4j `TextSegment`,
+or of a `String` that camel-langchain4j-embeddings converts to one, goes in
the `text_segment` field of the new
+`CamelPineconeMetadata` header, which UPSERT stores as the metadata of the
vector and whose fields UPDATE sets, so the
+`pinecone:rag` transformer now gives the text back. Before, the text was not
stored, and `pinecone:rag` gave an empty
+String for the match of such a vector.
+
+The `pinecone:embeddings` transformer now fails with an
`IllegalArgumentException` that names the
+`CamelLangChain4jEmbeddingsVector` header when the message does not have it,
as `pgvector:embeddings` does. Before, it
+failed with a `NullPointerException`.
+
=== camel-docker - the documented default of the socket option is false
The `socket` option, and the `CamelDockerSocketEnabled` header, were
documented with the default `true`, but their
diff --git
a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/PineconeVectorDbEndpointBuilderFactory.java
b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/PineconeVectorDbEndpointBuilderFactory.java
index 4c64d5d39b6f..e66eea40e14a 100644
---
a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/PineconeVectorDbEndpointBuilderFactory.java
+++
b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/PineconeVectorDbEndpointBuilderFactory.java
@@ -598,6 +598,21 @@ public interface PineconeVectorDbEndpointBuilderFactory {
public String pineconeQueryIncludeMetadata() {
return "CamelPineconeQueryIncludeMetadata";
}
+ /**
+ * The metadata UPSERT stores with the vector, and the fields of the
+ * metadata UPDATE sets. The pinecone:embeddings data type transformer
+ * puts the text of the embedding in its text_segment field, which the
+ * pinecone:rag data type transformer reads.
+ *
+ * The option is a: {@code com.google.protobuf.Struct} type.
+ *
+ * Group: producer
+ *
+ * @return the name of the header {@code PineconeMetadata}.
+ */
+ public String pineconeMetadata() {
+ return "CamelPineconeMetadata";
+ }
}
static PineconeVectorDbEndpointBuilder endpointBuilder(String
componentName, String path) {
class PineconeVectorDbEndpointBuilderImpl extends
AbstractEndpointBuilder implements PineconeVectorDbEndpointBuilder,
AdvancedPineconeVectorDbEndpointBuilder {