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 c72c14faa8cf CAMEL-23953: Add batch operations for langchain4j
embeddings
c72c14faa8cf is described below
commit c72c14faa8cf3541b40c56f12693fa6ece44b1f7
Author: Guillaume Nodet <[email protected]>
AuthorDate: Thu Sep 3 07:24:48 2026 +0200
CAMEL-23953: Add batch operations for langchain4j embeddings
Add batch embedAll/addAll/removeAll support to the LangChain4j embedding
components for efficient RAG ingestion. The embeddings producer detects a
List body and batches via embedAll, preserving text segments through the
TEXT_SEGMENTS header. The embedding store producer gains batch ADD via the
EMBEDDINGS header, caller-supplied IDs (EMBEDDING_ID/EMBEDDING_IDS), size
validation across ids/embeddings/text segments, and batch REMOVE by
Collection<String> body or Filter header. Null/empty REMOVE now throws
IllegalArgumentException instead of clearing the store. Adds shared
CAMEL_LANGCHAIN4J_EMBEDDINGS and CAMEL_LANGCHAIN4J_TEXT_SEGMENTS constants
in core/camel-api, documentation, an upgrade-guide entry, and tests.
Closes #25273
Co-authored-by: Guillaume Nodet <[email protected]>
Co-Authored-By: Claude Opus 4.6 <[email protected]>
---
.../catalog/components/langchain4j-embeddings.json | 6 +-
.../components/langchain4j-embeddingstore.json | 4 +-
.../docs/langchain4j-embeddingstore-component.adoc | 67 +++-
.../embeddings/langchain4j-embeddings.json | 6 +-
.../embeddings/LangChain4jEmbeddingsHeaders.java | 8 +
.../embeddings/LangChain4jEmbeddingsProducer.java | 80 +++-
.../embeddings/LangChain4jEmbeddingsBatchTest.java | 93 +++++
.../embeddingstore/langchain4j-embeddingstore.json | 4 +-
.../docs/langchain4j-embeddingstore-component.adoc | 67 +++-
.../LangChain4jEmbeddingStoreHeaders.java | 13 +-
.../LangChain4jEmbeddingStoreProducer.java | 148 +++++++-
...ngChain4jEmbeddingStoreBatchOperationsTest.java | 416 +++++++++++++++++++++
.../camel/ai/CamelLangchain4jAttributes.java | 14 +
.../ROOT/pages/camel-4x-upgrade-guide-4_23.adoc | 23 ++
...hain4jEmbeddingStoreEndpointBuilderFactory.java | 29 +-
...angChain4jEmbeddingsEndpointBuilderFactory.java | 28 ++
16 files changed, 972 insertions(+), 34 deletions(-)
diff --git
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/langchain4j-embeddings.json
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/langchain4j-embeddings.json
index afc73dbfc056..4f3f59f79c5a 100644
---
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/langchain4j-embeddings.json
+++
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/langchain4j-embeddings.json
@@ -37,8 +37,10 @@
"CamelLangChain4jEmbeddingsRequestModel": { "index": 4, "kind": "header",
"displayName": "", "group": "producer", "label": "", "required": false,
"javaType": "String", "deprecated": false, "deprecationNote": "", "autowired":
false, "secret": false, "description": "The request model name.",
"constantName":
"org.apache.camel.component.langchain4j.embeddings.LangChain4jEmbeddingsHeaders#REQUEST_MODEL"
},
"CamelLangChain4jEmbeddingsResponseModel": { "index": 5, "kind": "header",
"displayName": "", "group": "producer", "label": "", "required": false,
"javaType": "String", "deprecated": false, "deprecationNote": "", "autowired":
false, "secret": false, "description": "The response model name.",
"constantName":
"org.apache.camel.component.langchain4j.embeddings.LangChain4jEmbeddingsHeaders#RESPONSE_MODEL"
},
"CamelLangChain4jEmbeddingsEmbedding": { "index": 6, "kind": "header",
"displayName": "", "group": "producer", "label": "", "required": false,
"javaType": "dev.langchain4j.data.embedding.Embedding", "deprecated": false,
"deprecationNote": "", "autowired": false, "secret": false, "description":
"Embedding representation of a text", "constantName":
"org.apache.camel.component.langchain4j.embeddings.LangChain4jEmbeddingsHeaders#EMBEDDING"
},
- "CamelLangChain4jEmbeddingsVector": { "index": 7, "kind": "header",
"displayName": "", "group": "producer", "label": "", "required": false,
"javaType": "float[]", "deprecated": false, "deprecationNote": "", "autowired":
false, "secret": false, "description": "A dense vector embedding of a text",
"constantName":
"org.apache.camel.component.langchain4j.embeddings.LangChain4jEmbeddingsHeaders#VECTOR"
},
- "CamelLangChain4jEmbeddingsTextSegment": { "index": 8, "kind": "header",
"displayName": "", "group": "producer", "label": "", "required": false,
"javaType": " dev.langchain4j.data.segment.TextSegment", "deprecated": false,
"deprecationNote": "", "autowired": false, "secret": false, "description": "A
TextSegment representation of the vector embedding input text", "constantName":
"org.apache.camel.component.langchain4j.embeddings.LangChain4jEmbeddingsHeaders#TEXT_SEGMENT"
}
+ "CamelLangChain4jEmbeddingsEmbeddings": { "index": 7, "kind": "header",
"displayName": "", "group": "producer", "label": "", "required": false,
"javaType": "java.util.List<dev.langchain4j.data.embedding.Embedding>",
"deprecated": false, "deprecationNote": "", "autowired": false, "secret":
false, "description": "List of embeddings from a batch embedAll operation",
"constantName":
"org.apache.camel.component.langchain4j.embeddings.LangChain4jEmbeddingsHeaders#EMBEDDINGS"
},
+ "CamelLangChain4jEmbeddingsVector": { "index": 8, "kind": "header",
"displayName": "", "group": "producer", "label": "", "required": false,
"javaType": "float[]", "deprecated": false, "deprecationNote": "", "autowired":
false, "secret": false, "description": "A dense vector embedding of a text",
"constantName":
"org.apache.camel.component.langchain4j.embeddings.LangChain4jEmbeddingsHeaders#VECTOR"
},
+ "CamelLangChain4jEmbeddingsTextSegment": { "index": 9, "kind": "header",
"displayName": "", "group": "producer", "label": "", "required": false,
"javaType": " dev.langchain4j.data.segment.TextSegment", "deprecated": false,
"deprecationNote": "", "autowired": false, "secret": false, "description": "A
TextSegment representation of the vector embedding input text", "constantName":
"org.apache.camel.component.langchain4j.embeddings.LangChain4jEmbeddingsHeaders#TEXT_SEGMENT"
},
+ "CamelLangChain4jEmbeddingsTextSegments": { "index": 10, "kind": "header",
"displayName": "", "group": "producer", "label": "", "required": false,
"javaType": "java.util.List<dev.langchain4j.data.segment.TextSegment>",
"deprecated": false, "deprecationNote": "", "autowired": false, "secret":
false, "description": "List of text segments from a batch embedAll operation",
"constantName":
"org.apache.camel.component.langchain4j.embeddings.LangChain4jEmbeddingsHeaders#TEXT_SEGMENTS"
}
},
"properties": {
"embeddingId": { "index": 0, "kind": "path", "displayName": "Embedding
Id", "group": "producer", "label": "", "required": true, "type": "string",
"javaType": "java.lang.String", "deprecated": false, "deprecationNote": "",
"autowired": false, "secret": false, "description": "The id" },
diff --git
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/langchain4j-embeddingstore.json
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/langchain4j-embeddingstore.json
index 8be5095be6f8..055c5f8d1b88 100644
---
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/langchain4j-embeddingstore.json
+++
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/langchain4j-embeddingstore.json
@@ -39,7 +39,9 @@
"CamelLangchain4jEmbeddingStoreAction": { "index": 0, "kind": "header",
"displayName": "", "group": "producer", "label": "", "required": false,
"javaType": "String", "enum": [ "ADD", "REMOVE", "SEARCH" ], "deprecated":
false, "deprecationNote": "", "autowired": false, "secret": false,
"description": "The action to be performed.", "constantName":
"org.apache.camel.component.langchain4j.embeddingstore.LangChain4jEmbeddingStoreHeaders#ACTION"
},
"CamelLangchain4jEmbeddingStoreMaxResults": { "index": 1, "kind":
"header", "displayName": "", "group": "producer", "label": "", "required":
false, "javaType": "Integer", "deprecated": false, "deprecationNote": "",
"autowired": false, "secret": false, "defaultValue": "5", "description":
"Maximum number of search results to return", "constantName":
"org.apache.camel.component.langchain4j.embeddingstore.LangChain4jEmbeddingStoreHeaders#MAX_RESULTS"
},
"CamelLangchain4jEmbeddingStoreMinScore": { "index": 2, "kind": "header",
"displayName": "", "group": "producer", "label": "", "required": false,
"javaType": "Double", "deprecated": false, "deprecationNote": "", "autowired":
false, "secret": false, "description": "Minimum similarity score for search
results", "constantName":
"org.apache.camel.component.langchain4j.embeddingstore.LangChain4jEmbeddingStoreHeaders#MIN_SCORE"
},
- "CamelLangchain4jEmbeddingStoreFilter": { "index": 3, "kind": "header",
"displayName": "", "group": "producer", "label": "", "required": false,
"javaType": "dev.langchain4j.store.embedding.filter.Filter", "deprecated":
false, "deprecationNote": "", "autowired": false, "secret": false,
"description": "Search filter for metadata-based constraints", "constantName":
"org.apache.camel.component.langchain4j.embeddingstore.LangChain4jEmbeddingStoreHeaders#FILTER"
}
+ "CamelLangchain4jEmbeddingStoreFilter": { "index": 3, "kind": "header",
"displayName": "", "group": "producer", "label": "", "required": false,
"javaType": "dev.langchain4j.store.embedding.filter.Filter", "deprecated":
false, "deprecationNote": "", "autowired": false, "secret": false,
"description": "Filter for metadata-based constraints (used in SEARCH and
REMOVE operations)", "constantName":
"org.apache.camel.component.langchain4j.embeddingstore.LangChain4jEmbeddingStoreHeaders#FILTER"
},
+ "CamelLangchain4jEmbeddingStoreEmbeddingId": { "index": 4, "kind":
"header", "displayName": "", "group": "producer", "label": "", "required":
false, "javaType": "String", "deprecated": false, "deprecationNote": "",
"autowired": false, "secret": false, "description": "Caller-supplied embedding
ID for single ADD operations", "constantName":
"org.apache.camel.component.langchain4j.embeddingstore.LangChain4jEmbeddingStoreHeaders#EMBEDDING_ID"
},
+ "CamelLangchain4jEmbeddingStoreEmbeddingIds": { "index": 5, "kind":
"header", "displayName": "", "group": "producer", "label": "", "required":
false, "javaType": "java.util.List<java.lang.String>", "deprecated": false,
"deprecationNote": "", "autowired": false, "secret": false, "description":
"Caller-supplied embedding IDs for batch ADD operations", "constantName":
"org.apache.camel.component.langchain4j.embeddingstore.LangChain4jEmbeddingStoreHeaders#EMBEDDING_IDS"
}
},
"properties": {
"embeddingStoreId": { "index": 0, "kind": "path", "displayName":
"Embedding Store Id", "group": "producer", "label": "", "required": true,
"type": "string", "javaType": "java.lang.String", "deprecated": false,
"deprecationNote": "", "autowired": false, "secret": false, "description": "The
id of the embedding store" },
diff --git
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/langchain4j-embeddingstore-component.adoc
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/langchain4j-embeddingstore-component.adoc
index b31d314d4e12..73ff5410428b 100644
---
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/langchain4j-embeddingstore-component.adoc
+++
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/langchain4j-embeddingstore-component.adoc
@@ -30,8 +30,8 @@ The LangChain4j Embedding Store component offers the
following key features:
The component supports three main operations controlled by the
`CamelLangchain4jEmbeddingStoreAction` header:
-* **ADD** - Store embeddings with optional text segments and metadata
-* **REMOVE** - Delete embeddings by their unique identifier
+* **ADD** - Store embeddings with optional text segments and metadata.
Supports single and batch operations, as well as caller-supplied IDs.
+* **REMOVE** - Delete embeddings by single ID, collection of IDs, or metadata
filter.
* **SEARCH** - Perform similarity search with configurable filters and scoring
== URI format
@@ -151,6 +151,39 @@ YAML::
The response body contains the generated embedding ID.
+==== ADD with Caller-Supplied ID
+
+You can provide your own embedding ID using the
`CamelLangchain4jEmbeddingStoreEmbeddingId` header:
+
+[source,java]
+----
+from("direct:store-with-id")
+ .to("langchain4j-embeddings:embed")
+ .setHeader("CamelLangchain4jEmbeddingStoreEmbeddingId",
constant("my-custom-id"))
+ .to("langchain4j-embeddingstore:myStore?action=ADD");
+----
+
+When both a caller-supplied ID and a text segment are present, the component
preserves both using `addAll` with singleton lists.
+
+==== Batch ADD (addAll)
+
+For RAG ingestion pipelines, batch adding is significantly more efficient.
Send a `List<String>` or `List<TextSegment>` as the body to the
`langchain4j-embeddings` component to produce batch embeddings:
+
+[source,java]
+----
+from("direct:batch-store")
+ // Body is a List<String> or List<TextSegment>
+ .to("langchain4j-embeddings:embed")
+ // After batch embed, the CamelLangChain4jEmbeddingsEmbeddings header
contains List<Embedding>
+ // and the CamelLangChain4jEmbeddingsTextSegments header preserves the
original text segments
+ .to("langchain4j-embeddingstore:myStore?action=ADD");
+// Body: List<String> of generated IDs
+----
+
+You can also supply caller-supplied IDs for the batch via the
`CamelLangchain4jEmbeddingStoreEmbeddingIds` header.
+
+NOTE: The sizes of `EMBEDDING_IDS`, `EMBEDDINGS`, and text segments must all
match. A size mismatch throws `IllegalArgumentException`.
+
=== Searching Embeddings (SEARCH Operation)
Perform similarity search to find relevant content. Without `embeddingModel`,
a pre-computed query embedding must be provided:
@@ -226,7 +259,11 @@ YAML::
=== Removing Embeddings (REMOVE Operation)
-Delete embeddings by their ID:
+The REMOVE operation supports multiple strategies:
+
+==== Remove by Single ID
+
+Delete a single embedding by its ID (body is a `String`):
[tabs]
====
@@ -258,6 +295,30 @@ YAML::
----
====
+==== Remove by Collection of IDs
+
+Delete multiple embeddings by passing a `Collection<String>` as the body:
+
+[source,java]
+----
+from("direct:remove-batch")
+ .setBody(constant(List.of("id-1", "id-2", "id-3")))
+ .to("langchain4j-embeddingstore:myStore?action=REMOVE");
+----
+
+==== Remove by Metadata Filter
+
+Delete all embeddings matching a metadata filter using the
`CamelLangchain4jEmbeddingStoreFilter` header:
+
+[source,java]
+----
+from("direct:remove-by-filter")
+ .setHeader("CamelLangchain4jEmbeddingStoreFilter",
constant(metadataFilter))
+ .to("langchain4j-embeddingstore:myStore?action=REMOVE");
+----
+
+NOTE: A REMOVE with no body and no filter throws `IllegalArgumentException`.
This is intentional — destructive "clear all" operations require explicit
intent.
+
=== Complete RAG Pipeline Example
A complete example showing document ingestion and retrieval:
diff --git
a/components/camel-ai/camel-langchain4j-embeddings/src/generated/resources/META-INF/org/apache/camel/component/langchain4j/embeddings/langchain4j-embeddings.json
b/components/camel-ai/camel-langchain4j-embeddings/src/generated/resources/META-INF/org/apache/camel/component/langchain4j/embeddings/langchain4j-embeddings.json
index afc73dbfc056..4f3f59f79c5a 100644
---
a/components/camel-ai/camel-langchain4j-embeddings/src/generated/resources/META-INF/org/apache/camel/component/langchain4j/embeddings/langchain4j-embeddings.json
+++
b/components/camel-ai/camel-langchain4j-embeddings/src/generated/resources/META-INF/org/apache/camel/component/langchain4j/embeddings/langchain4j-embeddings.json
@@ -37,8 +37,10 @@
"CamelLangChain4jEmbeddingsRequestModel": { "index": 4, "kind": "header",
"displayName": "", "group": "producer", "label": "", "required": false,
"javaType": "String", "deprecated": false, "deprecationNote": "", "autowired":
false, "secret": false, "description": "The request model name.",
"constantName":
"org.apache.camel.component.langchain4j.embeddings.LangChain4jEmbeddingsHeaders#REQUEST_MODEL"
},
"CamelLangChain4jEmbeddingsResponseModel": { "index": 5, "kind": "header",
"displayName": "", "group": "producer", "label": "", "required": false,
"javaType": "String", "deprecated": false, "deprecationNote": "", "autowired":
false, "secret": false, "description": "The response model name.",
"constantName":
"org.apache.camel.component.langchain4j.embeddings.LangChain4jEmbeddingsHeaders#RESPONSE_MODEL"
},
"CamelLangChain4jEmbeddingsEmbedding": { "index": 6, "kind": "header",
"displayName": "", "group": "producer", "label": "", "required": false,
"javaType": "dev.langchain4j.data.embedding.Embedding", "deprecated": false,
"deprecationNote": "", "autowired": false, "secret": false, "description":
"Embedding representation of a text", "constantName":
"org.apache.camel.component.langchain4j.embeddings.LangChain4jEmbeddingsHeaders#EMBEDDING"
},
- "CamelLangChain4jEmbeddingsVector": { "index": 7, "kind": "header",
"displayName": "", "group": "producer", "label": "", "required": false,
"javaType": "float[]", "deprecated": false, "deprecationNote": "", "autowired":
false, "secret": false, "description": "A dense vector embedding of a text",
"constantName":
"org.apache.camel.component.langchain4j.embeddings.LangChain4jEmbeddingsHeaders#VECTOR"
},
- "CamelLangChain4jEmbeddingsTextSegment": { "index": 8, "kind": "header",
"displayName": "", "group": "producer", "label": "", "required": false,
"javaType": " dev.langchain4j.data.segment.TextSegment", "deprecated": false,
"deprecationNote": "", "autowired": false, "secret": false, "description": "A
TextSegment representation of the vector embedding input text", "constantName":
"org.apache.camel.component.langchain4j.embeddings.LangChain4jEmbeddingsHeaders#TEXT_SEGMENT"
}
+ "CamelLangChain4jEmbeddingsEmbeddings": { "index": 7, "kind": "header",
"displayName": "", "group": "producer", "label": "", "required": false,
"javaType": "java.util.List<dev.langchain4j.data.embedding.Embedding>",
"deprecated": false, "deprecationNote": "", "autowired": false, "secret":
false, "description": "List of embeddings from a batch embedAll operation",
"constantName":
"org.apache.camel.component.langchain4j.embeddings.LangChain4jEmbeddingsHeaders#EMBEDDINGS"
},
+ "CamelLangChain4jEmbeddingsVector": { "index": 8, "kind": "header",
"displayName": "", "group": "producer", "label": "", "required": false,
"javaType": "float[]", "deprecated": false, "deprecationNote": "", "autowired":
false, "secret": false, "description": "A dense vector embedding of a text",
"constantName":
"org.apache.camel.component.langchain4j.embeddings.LangChain4jEmbeddingsHeaders#VECTOR"
},
+ "CamelLangChain4jEmbeddingsTextSegment": { "index": 9, "kind": "header",
"displayName": "", "group": "producer", "label": "", "required": false,
"javaType": " dev.langchain4j.data.segment.TextSegment", "deprecated": false,
"deprecationNote": "", "autowired": false, "secret": false, "description": "A
TextSegment representation of the vector embedding input text", "constantName":
"org.apache.camel.component.langchain4j.embeddings.LangChain4jEmbeddingsHeaders#TEXT_SEGMENT"
},
+ "CamelLangChain4jEmbeddingsTextSegments": { "index": 10, "kind": "header",
"displayName": "", "group": "producer", "label": "", "required": false,
"javaType": "java.util.List<dev.langchain4j.data.segment.TextSegment>",
"deprecated": false, "deprecationNote": "", "autowired": false, "secret":
false, "description": "List of text segments from a batch embedAll operation",
"constantName":
"org.apache.camel.component.langchain4j.embeddings.LangChain4jEmbeddingsHeaders#TEXT_SEGMENTS"
}
},
"properties": {
"embeddingId": { "index": 0, "kind": "path", "displayName": "Embedding
Id", "group": "producer", "label": "", "required": true, "type": "string",
"javaType": "java.lang.String", "deprecated": false, "deprecationNote": "",
"autowired": false, "secret": false, "description": "The id" },
diff --git
a/components/camel-ai/camel-langchain4j-embeddings/src/main/java/org/apache/camel/component/langchain4j/embeddings/LangChain4jEmbeddingsHeaders.java
b/components/camel-ai/camel-langchain4j-embeddings/src/main/java/org/apache/camel/component/langchain4j/embeddings/LangChain4jEmbeddingsHeaders.java
index 0849a3cbcd21..c18162c78aa5 100644
---
a/components/camel-ai/camel-langchain4j-embeddings/src/main/java/org/apache/camel/component/langchain4j/embeddings/LangChain4jEmbeddingsHeaders.java
+++
b/components/camel-ai/camel-langchain4j-embeddings/src/main/java/org/apache/camel/component/langchain4j/embeddings/LangChain4jEmbeddingsHeaders.java
@@ -41,10 +41,18 @@ public class LangChain4jEmbeddingsHeaders {
@Metadata(description = "Embedding representation of a text", javaType =
"dev.langchain4j.data.embedding.Embedding")
public static final String EMBEDDING =
CamelLangchain4jAttributes.CAMEL_LANGCHAIN4J_EMBEDDING;
+ @Metadata(description = "List of embeddings from a batch embedAll
operation",
+ javaType =
"java.util.List<dev.langchain4j.data.embedding.Embedding>")
+ public static final String EMBEDDINGS =
CamelLangchain4jAttributes.CAMEL_LANGCHAIN4J_EMBEDDINGS;
+
@Metadata(description = "A dense vector embedding of a text", javaType =
"float[]")
public static final String VECTOR =
CamelLangchain4jAttributes.CAMEL_LANGCHAIN4J_EMBEDDING_VECTOR;
@Metadata(description = "A TextSegment representation of the vector
embedding input text",
javaType = " dev.langchain4j.data.segment.TextSegment")
public static final String TEXT_SEGMENT =
CamelLangchain4jAttributes.CAMEL_LANGCHAIN4J_TEXT_SEGMENT;
+
+ @Metadata(description = "List of text segments from a batch embedAll
operation",
+ javaType =
"java.util.List<dev.langchain4j.data.segment.TextSegment>")
+ public static final String TEXT_SEGMENTS =
CamelLangchain4jAttributes.CAMEL_LANGCHAIN4J_TEXT_SEGMENTS;
}
diff --git
a/components/camel-ai/camel-langchain4j-embeddings/src/main/java/org/apache/camel/component/langchain4j/embeddings/LangChain4jEmbeddingsProducer.java
b/components/camel-ai/camel-langchain4j-embeddings/src/main/java/org/apache/camel/component/langchain4j/embeddings/LangChain4jEmbeddingsProducer.java
index c34424024f91..308ad54740ed 100644
---
a/components/camel-ai/camel-langchain4j-embeddings/src/main/java/org/apache/camel/component/langchain4j/embeddings/LangChain4jEmbeddingsProducer.java
+++
b/components/camel-ai/camel-langchain4j-embeddings/src/main/java/org/apache/camel/component/langchain4j/embeddings/LangChain4jEmbeddingsProducer.java
@@ -16,6 +16,9 @@
*/
package org.apache.camel.component.langchain4j.embeddings;
+import java.util.ArrayList;
+import java.util.List;
+
import dev.langchain4j.data.embedding.Embedding;
import dev.langchain4j.data.segment.TextSegment;
import dev.langchain4j.model.embedding.EmbeddingModel;
@@ -42,9 +45,26 @@ public class LangChain4jEmbeddingsProducer extends
DefaultProducer {
}
@Override
+ @SuppressWarnings("unchecked")
public void process(Exchange exchange) throws Exception {
- final TextSegment in =
exchange.getMessage().getMandatoryBody(TextSegment.class);
final EmbeddingModel model =
getEndpoint().getConfiguration().getEmbeddingModel();
+ final Message message = exchange.getMessage();
+ Object body = message.getBody();
+
+ if (body instanceof List) {
+ List<Object> bodyList = (List<Object>) body;
+ if (bodyList.isEmpty()) {
+ throw new IllegalArgumentException(
+ "Batch embedding requires a non-empty List body");
+ }
+ processBatch(exchange, model, message, bodyList);
+ } else {
+ processSingle(exchange, model, message);
+ }
+ }
+
+ private void processSingle(Exchange exchange, EmbeddingModel model,
Message message) throws Exception {
+ final TextSegment in =
exchange.getMessage().getMandatoryBody(TextSegment.class);
GenAiObservationContext observationContext =
GenAiObservationContext.builder()
.operationName(GenAiOperationName.EMBEDDINGS)
.system(GenAiModelResolver.resolveSystem(exchange.getContext().getClassResolver(),
model))
@@ -54,7 +74,41 @@ public class LangChain4jEmbeddingsProducer extends
DefaultProducer {
GenAiObservation observation = GenAiObservability.start(exchange,
observationContext);
try {
final Response<Embedding> result = model.embed(in);
- populateHeaders(exchange.getMessage(), result, in,
observationContext.requestModel());
+ populateHeaders(message, result, in,
observationContext.requestModel());
+ observation.recordSuccess(GenAiUsage.of(
+ result.tokenUsage() != null ?
result.tokenUsage().inputTokenCount() : null,
+ result.tokenUsage() != null ?
result.tokenUsage().outputTokenCount() : null,
+ result.finishReason(),
+ null));
+ } catch (RuntimeException e) {
+ GenAiErrorSupport.apply(exchange, e);
+ observation.recordError(e);
+ throw e;
+ } finally {
+ observation.close();
+ }
+ }
+
+ private void processBatch(Exchange exchange, EmbeddingModel model, Message
message, List<Object> bodyList)
+ throws Exception {
+ // Convert each element to TextSegment using the type converter
+ List<TextSegment> segments = new ArrayList<>(bodyList.size());
+ for (Object item : bodyList) {
+ TextSegment segment =
exchange.getContext().getTypeConverter().mandatoryConvertTo(TextSegment.class,
item);
+ segments.add(segment);
+ }
+
+ GenAiObservationContext observationContext =
GenAiObservationContext.builder()
+ .operationName(GenAiOperationName.EMBEDDINGS)
+
.system(GenAiModelResolver.resolveSystem(exchange.getContext().getClassResolver(),
model))
+
.requestModel(GenAiModelResolver.resolveModelName(exchange.getContext().getClassResolver(),
model))
+ .componentScheme("langchain4j-embeddings")
+ .build();
+ GenAiObservation observation = GenAiObservability.start(exchange,
observationContext);
+ try {
+ final Response<List<Embedding>> result = model.embedAll(segments);
+
+ populateBatchHeaders(message, result, segments,
observationContext.requestModel());
observation.recordSuccess(GenAiUsage.of(
result.tokenUsage() != null ?
result.tokenUsage().inputTokenCount() : null,
result.tokenUsage() != null ?
result.tokenUsage().outputTokenCount() : null,
@@ -89,4 +143,26 @@ public class LangChain4jEmbeddingsProducer extends
DefaultProducer {
message.setHeader(LangChain4jEmbeddingsHeaders.TEXT_SEGMENT,
textSegment);
message.setHeader(LangChain4jEmbeddingsHeaders.EMBEDDING,
result.content());
}
+
+ private static void populateBatchHeaders(
+ Message message, Response<List<Embedding>> result,
List<TextSegment> segments, String requestModel) {
+ if (requestModel != null) {
+ message.setHeader(LangChain4jEmbeddingsHeaders.REQUEST_MODEL,
requestModel);
+ }
+
+ if (result.finishReason() != null) {
+ message.setHeader(LangChain4jEmbeddingsHeaders.FINISH_REASON,
result.finishReason());
+ }
+
+ if (result.tokenUsage() != null) {
+ message.setHeader(LangChain4jEmbeddingsHeaders.INPUT_TOKEN_COUNT,
result.tokenUsage().inputTokenCount());
+ message.setHeader(LangChain4jEmbeddingsHeaders.OUTPUT_TOKEN_COUNT,
result.tokenUsage().outputTokenCount());
+ message.setHeader(LangChain4jEmbeddingsHeaders.TOTAL_TOKEN_COUNT,
result.tokenUsage().totalTokenCount());
+ }
+
+ List<Embedding> embeddings = result.content();
+ message.setHeader(LangChain4jEmbeddingsHeaders.EMBEDDINGS, embeddings);
+ // Preserve text segments so downstream embedding store can associate
them with embeddings
+ message.setHeader(LangChain4jEmbeddingsHeaders.TEXT_SEGMENTS,
segments);
+ }
}
diff --git
a/components/camel-ai/camel-langchain4j-embeddings/src/test/java/org/apache/camel/component/langchain4j/embeddings/LangChain4jEmbeddingsBatchTest.java
b/components/camel-ai/camel-langchain4j-embeddings/src/test/java/org/apache/camel/component/langchain4j/embeddings/LangChain4jEmbeddingsBatchTest.java
new file mode 100644
index 000000000000..081c32195367
--- /dev/null
+++
b/components/camel-ai/camel-langchain4j-embeddings/src/test/java/org/apache/camel/component/langchain4j/embeddings/LangChain4jEmbeddingsBatchTest.java
@@ -0,0 +1,93 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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
+ *
+ * http://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.apache.camel.component.langchain4j.embeddings;
+
+import java.util.Arrays;
+import java.util.List;
+
+import dev.langchain4j.data.embedding.Embedding;
+import dev.langchain4j.data.segment.TextSegment;
+import
dev.langchain4j.model.embedding.onnx.allminilml6v2.AllMiniLmL6V2EmbeddingModel;
+import org.apache.camel.CamelContext;
+import org.apache.camel.Message;
+import org.apache.camel.test.junit6.CamelTestSupport;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class LangChain4jEmbeddingsBatchTest extends CamelTestSupport {
+
+ @Override
+ protected CamelContext createCamelContext() throws Exception {
+ CamelContext context = super.createCamelContext();
+
+ LangChain4jEmbeddingsComponent component
+ = context.getComponent(LangChain4jEmbeddings.SCHEME,
LangChain4jEmbeddingsComponent.class);
+ component.getConfiguration().setEmbeddingModel(new
AllMiniLmL6V2EmbeddingModel());
+
+ return context;
+ }
+
+ @Test
+ @DisplayName("Batch embedding with List<String> body produces EMBEDDINGS
and TEXT_SEGMENTS headers")
+ @SuppressWarnings("unchecked")
+ void batchEmbeddingFromStringList() {
+ List<String> texts = Arrays.asList("hello world", "goodbye world");
+
+ Message result = fluentTemplate.to("langchain4j-embeddings:batch")
+ .withBody(texts)
+ .request(Message.class);
+
+ List<Embedding> embeddings =
result.getHeader(LangChain4jEmbeddingsHeaders.EMBEDDINGS, List.class);
+ assertThat(embeddings).hasSize(2);
+ assertThat(embeddings.get(0).vector()).hasSize(384);
+ assertThat(embeddings.get(1).vector()).hasSize(384);
+
+ List<TextSegment> segments =
result.getHeader(LangChain4jEmbeddingsHeaders.TEXT_SEGMENTS, List.class);
+ assertThat(segments).hasSize(2);
+ assertThat(segments.get(0).text()).isEqualTo("hello world");
+ assertThat(segments.get(1).text()).isEqualTo("goodbye world");
+ }
+
+ @Test
+ @DisplayName("Batch embedding sets token usage headers")
+ void batchEmbeddingTokenUsage() {
+ List<String> texts = Arrays.asList("test one", "test two");
+
+ Message result = fluentTemplate.to("langchain4j-embeddings:batch")
+ .withBody(texts)
+ .request(Message.class);
+
+ // Token usage should be reported for batch operations
+
assertThat(result.getHeader(LangChain4jEmbeddingsHeaders.EMBEDDINGS)).isNotNull();
+
assertThat(result.getHeader(LangChain4jEmbeddingsHeaders.INPUT_TOKEN_COUNT,
Integer.class)).isNotNull().isPositive();
+
assertThat(result.getHeader(LangChain4jEmbeddingsHeaders.TOTAL_TOKEN_COUNT,
Integer.class)).isNotNull().isPositive();
+ }
+
+ @Test
+ @DisplayName("Single item still uses single path")
+ void singleItemStillUsesSinglePath() {
+ Message result = fluentTemplate.to("langchain4j-embeddings:single")
+ .withBody("hello")
+ .request(Message.class);
+
+ // Single path should set EMBEDDING header (not EMBEDDINGS)
+ assertThat(result.getHeader(LangChain4jEmbeddingsHeaders.EMBEDDING,
Embedding.class)).isNotNull();
+
assertThat(result.getHeader(LangChain4jEmbeddingsHeaders.EMBEDDINGS)).isNull();
+ }
+}
diff --git
a/components/camel-ai/camel-langchain4j-embeddingstore/src/generated/resources/META-INF/org/apache/camel/component/langchain4j/embeddingstore/langchain4j-embeddingstore.json
b/components/camel-ai/camel-langchain4j-embeddingstore/src/generated/resources/META-INF/org/apache/camel/component/langchain4j/embeddingstore/langchain4j-embeddingstore.json
index 8be5095be6f8..055c5f8d1b88 100644
---
a/components/camel-ai/camel-langchain4j-embeddingstore/src/generated/resources/META-INF/org/apache/camel/component/langchain4j/embeddingstore/langchain4j-embeddingstore.json
+++
b/components/camel-ai/camel-langchain4j-embeddingstore/src/generated/resources/META-INF/org/apache/camel/component/langchain4j/embeddingstore/langchain4j-embeddingstore.json
@@ -39,7 +39,9 @@
"CamelLangchain4jEmbeddingStoreAction": { "index": 0, "kind": "header",
"displayName": "", "group": "producer", "label": "", "required": false,
"javaType": "String", "enum": [ "ADD", "REMOVE", "SEARCH" ], "deprecated":
false, "deprecationNote": "", "autowired": false, "secret": false,
"description": "The action to be performed.", "constantName":
"org.apache.camel.component.langchain4j.embeddingstore.LangChain4jEmbeddingStoreHeaders#ACTION"
},
"CamelLangchain4jEmbeddingStoreMaxResults": { "index": 1, "kind":
"header", "displayName": "", "group": "producer", "label": "", "required":
false, "javaType": "Integer", "deprecated": false, "deprecationNote": "",
"autowired": false, "secret": false, "defaultValue": "5", "description":
"Maximum number of search results to return", "constantName":
"org.apache.camel.component.langchain4j.embeddingstore.LangChain4jEmbeddingStoreHeaders#MAX_RESULTS"
},
"CamelLangchain4jEmbeddingStoreMinScore": { "index": 2, "kind": "header",
"displayName": "", "group": "producer", "label": "", "required": false,
"javaType": "Double", "deprecated": false, "deprecationNote": "", "autowired":
false, "secret": false, "description": "Minimum similarity score for search
results", "constantName":
"org.apache.camel.component.langchain4j.embeddingstore.LangChain4jEmbeddingStoreHeaders#MIN_SCORE"
},
- "CamelLangchain4jEmbeddingStoreFilter": { "index": 3, "kind": "header",
"displayName": "", "group": "producer", "label": "", "required": false,
"javaType": "dev.langchain4j.store.embedding.filter.Filter", "deprecated":
false, "deprecationNote": "", "autowired": false, "secret": false,
"description": "Search filter for metadata-based constraints", "constantName":
"org.apache.camel.component.langchain4j.embeddingstore.LangChain4jEmbeddingStoreHeaders#FILTER"
}
+ "CamelLangchain4jEmbeddingStoreFilter": { "index": 3, "kind": "header",
"displayName": "", "group": "producer", "label": "", "required": false,
"javaType": "dev.langchain4j.store.embedding.filter.Filter", "deprecated":
false, "deprecationNote": "", "autowired": false, "secret": false,
"description": "Filter for metadata-based constraints (used in SEARCH and
REMOVE operations)", "constantName":
"org.apache.camel.component.langchain4j.embeddingstore.LangChain4jEmbeddingStoreHeaders#FILTER"
},
+ "CamelLangchain4jEmbeddingStoreEmbeddingId": { "index": 4, "kind":
"header", "displayName": "", "group": "producer", "label": "", "required":
false, "javaType": "String", "deprecated": false, "deprecationNote": "",
"autowired": false, "secret": false, "description": "Caller-supplied embedding
ID for single ADD operations", "constantName":
"org.apache.camel.component.langchain4j.embeddingstore.LangChain4jEmbeddingStoreHeaders#EMBEDDING_ID"
},
+ "CamelLangchain4jEmbeddingStoreEmbeddingIds": { "index": 5, "kind":
"header", "displayName": "", "group": "producer", "label": "", "required":
false, "javaType": "java.util.List<java.lang.String>", "deprecated": false,
"deprecationNote": "", "autowired": false, "secret": false, "description":
"Caller-supplied embedding IDs for batch ADD operations", "constantName":
"org.apache.camel.component.langchain4j.embeddingstore.LangChain4jEmbeddingStoreHeaders#EMBEDDING_IDS"
}
},
"properties": {
"embeddingStoreId": { "index": 0, "kind": "path", "displayName":
"Embedding Store Id", "group": "producer", "label": "", "required": true,
"type": "string", "javaType": "java.lang.String", "deprecated": false,
"deprecationNote": "", "autowired": false, "secret": false, "description": "The
id of the embedding store" },
diff --git
a/components/camel-ai/camel-langchain4j-embeddingstore/src/main/docs/langchain4j-embeddingstore-component.adoc
b/components/camel-ai/camel-langchain4j-embeddingstore/src/main/docs/langchain4j-embeddingstore-component.adoc
index b31d314d4e12..73ff5410428b 100644
---
a/components/camel-ai/camel-langchain4j-embeddingstore/src/main/docs/langchain4j-embeddingstore-component.adoc
+++
b/components/camel-ai/camel-langchain4j-embeddingstore/src/main/docs/langchain4j-embeddingstore-component.adoc
@@ -30,8 +30,8 @@ The LangChain4j Embedding Store component offers the
following key features:
The component supports three main operations controlled by the
`CamelLangchain4jEmbeddingStoreAction` header:
-* **ADD** - Store embeddings with optional text segments and metadata
-* **REMOVE** - Delete embeddings by their unique identifier
+* **ADD** - Store embeddings with optional text segments and metadata.
Supports single and batch operations, as well as caller-supplied IDs.
+* **REMOVE** - Delete embeddings by single ID, collection of IDs, or metadata
filter.
* **SEARCH** - Perform similarity search with configurable filters and scoring
== URI format
@@ -151,6 +151,39 @@ YAML::
The response body contains the generated embedding ID.
+==== ADD with Caller-Supplied ID
+
+You can provide your own embedding ID using the
`CamelLangchain4jEmbeddingStoreEmbeddingId` header:
+
+[source,java]
+----
+from("direct:store-with-id")
+ .to("langchain4j-embeddings:embed")
+ .setHeader("CamelLangchain4jEmbeddingStoreEmbeddingId",
constant("my-custom-id"))
+ .to("langchain4j-embeddingstore:myStore?action=ADD");
+----
+
+When both a caller-supplied ID and a text segment are present, the component
preserves both using `addAll` with singleton lists.
+
+==== Batch ADD (addAll)
+
+For RAG ingestion pipelines, batch adding is significantly more efficient.
Send a `List<String>` or `List<TextSegment>` as the body to the
`langchain4j-embeddings` component to produce batch embeddings:
+
+[source,java]
+----
+from("direct:batch-store")
+ // Body is a List<String> or List<TextSegment>
+ .to("langchain4j-embeddings:embed")
+ // After batch embed, the CamelLangChain4jEmbeddingsEmbeddings header
contains List<Embedding>
+ // and the CamelLangChain4jEmbeddingsTextSegments header preserves the
original text segments
+ .to("langchain4j-embeddingstore:myStore?action=ADD");
+// Body: List<String> of generated IDs
+----
+
+You can also supply caller-supplied IDs for the batch via the
`CamelLangchain4jEmbeddingStoreEmbeddingIds` header.
+
+NOTE: The sizes of `EMBEDDING_IDS`, `EMBEDDINGS`, and text segments must all
match. A size mismatch throws `IllegalArgumentException`.
+
=== Searching Embeddings (SEARCH Operation)
Perform similarity search to find relevant content. Without `embeddingModel`,
a pre-computed query embedding must be provided:
@@ -226,7 +259,11 @@ YAML::
=== Removing Embeddings (REMOVE Operation)
-Delete embeddings by their ID:
+The REMOVE operation supports multiple strategies:
+
+==== Remove by Single ID
+
+Delete a single embedding by its ID (body is a `String`):
[tabs]
====
@@ -258,6 +295,30 @@ YAML::
----
====
+==== Remove by Collection of IDs
+
+Delete multiple embeddings by passing a `Collection<String>` as the body:
+
+[source,java]
+----
+from("direct:remove-batch")
+ .setBody(constant(List.of("id-1", "id-2", "id-3")))
+ .to("langchain4j-embeddingstore:myStore?action=REMOVE");
+----
+
+==== Remove by Metadata Filter
+
+Delete all embeddings matching a metadata filter using the
`CamelLangchain4jEmbeddingStoreFilter` header:
+
+[source,java]
+----
+from("direct:remove-by-filter")
+ .setHeader("CamelLangchain4jEmbeddingStoreFilter",
constant(metadataFilter))
+ .to("langchain4j-embeddingstore:myStore?action=REMOVE");
+----
+
+NOTE: A REMOVE with no body and no filter throws `IllegalArgumentException`.
This is intentional — destructive "clear all" operations require explicit
intent.
+
=== Complete RAG Pipeline Example
A complete example showing document ingestion and retrieval:
diff --git
a/components/camel-ai/camel-langchain4j-embeddingstore/src/main/java/org/apache/camel/component/langchain4j/embeddingstore/LangChain4jEmbeddingStoreHeaders.java
b/components/camel-ai/camel-langchain4j-embeddingstore/src/main/java/org/apache/camel/component/langchain4j/embeddingstore/LangChain4jEmbeddingStoreHeaders.java
index e40318d9db5c..cd5370181676 100644
---
a/components/camel-ai/camel-langchain4j-embeddingstore/src/main/java/org/apache/camel/component/langchain4j/embeddingstore/LangChain4jEmbeddingStoreHeaders.java
+++
b/components/camel-ai/camel-langchain4j-embeddingstore/src/main/java/org/apache/camel/component/langchain4j/embeddingstore/LangChain4jEmbeddingStoreHeaders.java
@@ -41,8 +41,17 @@ public class LangChain4jEmbeddingStoreHeaders {
@Metadata(description = "Minimum similarity score for search results",
javaType = "Double")
public static final String MIN_SCORE =
"CamelLangchain4jEmbeddingStoreMinScore";
- /** Filter to apply to search operations for metadata-based filtering */
- @Metadata(description = "Search filter for metadata-based constraints",
+ /** Filter to apply to search and remove operations for metadata-based
filtering */
+ @Metadata(description = "Filter for metadata-based constraints (used in
SEARCH and REMOVE operations)",
javaType = "dev.langchain4j.store.embedding.filter.Filter")
public static final String FILTER = "CamelLangchain4jEmbeddingStoreFilter";
+
+ /** Caller-supplied embedding ID for single ADD operations */
+ @Metadata(description = "Caller-supplied embedding ID for single ADD
operations", javaType = "String")
+ public static final String EMBEDDING_ID =
"CamelLangchain4jEmbeddingStoreEmbeddingId";
+
+ /** Caller-supplied embedding IDs for batch ADD operations */
+ @Metadata(description = "Caller-supplied embedding IDs for batch ADD
operations",
+ javaType = "java.util.List<java.lang.String>")
+ public static final String EMBEDDING_IDS =
"CamelLangchain4jEmbeddingStoreEmbeddingIds";
}
diff --git
a/components/camel-ai/camel-langchain4j-embeddingstore/src/main/java/org/apache/camel/component/langchain4j/embeddingstore/LangChain4jEmbeddingStoreProducer.java
b/components/camel-ai/camel-langchain4j-embeddingstore/src/main/java/org/apache/camel/component/langchain4j/embeddingstore/LangChain4jEmbeddingStoreProducer.java
index 94d00b908083..a6ef24df2525 100644
---
a/components/camel-ai/camel-langchain4j-embeddingstore/src/main/java/org/apache/camel/component/langchain4j/embeddingstore/LangChain4jEmbeddingStoreProducer.java
+++
b/components/camel-ai/camel-langchain4j-embeddingstore/src/main/java/org/apache/camel/component/langchain4j/embeddingstore/LangChain4jEmbeddingStoreProducer.java
@@ -16,6 +16,7 @@
*/
package org.apache.camel.component.langchain4j.embeddingstore;
+import java.util.Collection;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.stream.Collectors;
@@ -109,55 +110,168 @@ public class LangChain4jEmbeddingStoreProducer extends
DefaultProducer {
}
/**
- * Adds an embedding to the store with optional text segment.
+ * Adds embeddings to the store with optional text segments and
caller-supplied IDs.
*
* <p>
- * Expects the following headers:
+ * Supports both single and batch operations:
+ * </p>
+ *
+ * <p>
+ * <b>Single operation</b> - when the {@code
CamelLangChain4jEmbeddingsEmbedding} header contains a single
+ * {@link Embedding}:
* </p>
* <ul>
- * <li>{@code CamelLangchain4jEmbeddingEmbedding} - The embedding vector
(required)</li>
- * <li>{@code CamelLangchain4jEmbeddingTextSegment} - Associated text
segment (optional)</li>
+ * <li>With caller-supplied ID header ({@code
CamelLangchain4jEmbeddingStoreEmbeddingId}) and text segment: calls
+ * {@code addAll(singletonList(id), singletonList(embedding),
singletonList(textSegment))}</li>
+ * <li>With caller-supplied ID header but no text segment: calls {@code
add(id, embedding)}</li>
+ * <li>With text segment header: calls {@code add(embedding,
textSegment)}</li>
+ * <li>Without text segment: calls {@code add(embedding)}</li>
* </ul>
*
* <p>
- * Returns the generated embedding ID in the message body.
+ * <b>Batch operation</b> - when the {@code
CamelLangChain4jEmbeddingsEmbeddings} header contains a
+ * {@code List<Embedding>}:
* </p>
+ * <ul>
+ * <li>With IDs header ({@code
CamelLangchain4jEmbeddingStoreEmbeddingIds}) and text segments: calls
+ * {@code addAll(ids, embeddings, textSegments)}</li>
+ * <li>With IDs header but no text segments: loops with {@code add(id,
embedding)}</li>
+ * <li>With text segments: calls {@code addAll(embeddings,
textSegments)}</li>
+ * <li>Without text segments: calls {@code addAll(embeddings)}</li>
+ * </ul>
*
* @param exchange the Camel exchange containing the embedding data
* @throws Exception if the add operation fails
*/
+ @SuppressWarnings("unchecked")
private void add(Exchange exchange) throws Exception {
+ final Message in = exchange.getMessage();
LangChain4jEmbeddingStoreConfiguration config =
getEndpoint().getConfiguration();
+ EmbeddingStore<TextSegment> store = config.getEmbeddingStore();
+
+ // Check for batch embeddings header first
+ List<Embedding> embeddings =
in.getHeader(LangChain4jEmbeddingsHeaders.EMBEDDINGS, List.class);
+ if (embeddings != null) {
+ addBatch(in, store, embeddings);
+ return;
+ }
+ // Single embedding path — use resolveEmbedding for auto-embedding
support
EmbeddingResult resolved = resolveEmbedding(exchange);
+ // Check for caller-supplied ID
+ String callerId =
in.getHeader(LangChain4jEmbeddingStoreHeaders.EMBEDDING_ID, String.class);
String id;
- if (resolved.textSegment() != null) {
- id = config.getEmbeddingStore().add(resolved.embedding(),
resolved.textSegment());
+
+ if (callerId != null) {
+ if (resolved.textSegment() != null) {
+ // Use addAll with singleton lists to preserve both ID and
text segment
+ store.addAll(List.of(callerId), List.of(resolved.embedding()),
List.of(resolved.textSegment()));
+ } else {
+ store.add(callerId, resolved.embedding());
+ }
+ id = callerId;
+ } else if (resolved.textSegment() != null) {
+ id = store.add(resolved.embedding(), resolved.textSegment());
} else {
- id = config.getEmbeddingStore().add(resolved.embedding());
+ id = store.add(resolved.embedding());
}
- exchange.getMessage().setBody(id);
+ in.setBody(id);
+ }
+
+ @SuppressWarnings("unchecked")
+ private void addBatch(Message in, EmbeddingStore<TextSegment> store,
List<Embedding> embeddings) {
+ List<String> callerIds =
in.getHeader(LangChain4jEmbeddingStoreHeaders.EMBEDDING_IDS, List.class);
+
+ // Resolve text segments from header (set by batch embeddings
producer) or body
+ List<TextSegment> textSegments =
in.getHeader(LangChain4jEmbeddingsHeaders.TEXT_SEGMENTS, List.class);
+ if (textSegments == null) {
+ Object body = in.getBody();
+ if (body instanceof List && !((List<?>) body).isEmpty() &&
((List<?>) body).get(0) instanceof TextSegment) {
+ textSegments = (List<TextSegment>) body;
+ }
+ }
+
+ // Validate sizes match when caller IDs are provided
+ if (callerIds != null && callerIds.size() != embeddings.size()) {
+ throw new IllegalArgumentException(
+ "EMBEDDING_IDS size (" + callerIds.size() + ") must match
EMBEDDINGS size (" + embeddings.size() + ")");
+ }
+ if (textSegments != null && textSegments.size() != embeddings.size()) {
+ throw new IllegalArgumentException(
+ "TEXT_SEGMENTS size (" + textSegments.size() + ") must
match EMBEDDINGS size (" + embeddings.size() + ")");
+ }
+
+ List<String> ids;
+ if (callerIds != null && textSegments != null) {
+ store.addAll(callerIds, embeddings, textSegments);
+ ids = callerIds;
+ } else if (callerIds != null) {
+ // No addAll(ids, embeddings) overload in langchain4j, so loop
with add(id, embedding)
+ for (int i = 0; i < embeddings.size(); i++) {
+ store.add(callerIds.get(i), embeddings.get(i));
+ }
+ ids = callerIds;
+ } else if (textSegments != null) {
+ ids = store.addAll(embeddings, textSegments);
+ } else {
+ ids = store.addAll(embeddings);
+ }
+
+ in.setBody(ids);
}
/**
- * Removes an embedding from the store by its ID.
+ * Removes embeddings from the store. Supports multiple removal strategies:
*
- * <p>
- * Expects the embedding ID as the message body (String).
- * </p>
+ * <ul>
+ * <li><b>By filter</b>: when the {@code
CamelLangchain4jEmbeddingStoreFilter} header is set, removes all embeddings
+ * matching the filter via {@code removeAll(Filter)}</li>
+ * <li><b>By ID list</b>: when the body is a {@code Collection<String>},
removes all specified embeddings via
+ * {@code removeAll(Collection)}</li>
+ * <li><b>By single ID</b>: when the body is a single {@code String},
removes that embedding via
+ * {@code remove(id)}</li>
+ * </ul>
*
- * @param exchange the Camel exchange containing the embedding ID to
remove
+ * @param exchange the Camel exchange containing removal parameters
* @throws Exception if the remove operation fails
*/
+ @SuppressWarnings("unchecked")
private void remove(Exchange exchange) throws Exception {
final Message in = exchange.getMessage();
- String id = in.getBody(String.class);
+ EmbeddingStore<TextSegment> store =
getEndpoint().getConfiguration().getEmbeddingStore();
+
+ // Check for filter-based removal first
+ Filter filter = in.getHeader(LangChain4jEmbeddingStoreHeaders.FILTER,
Filter.class);
+ if (filter != null) {
+ store.removeAll(filter);
+ return;
+ }
- getEndpoint().getConfiguration().getEmbeddingStore().remove(id);
+ Object body = in.getBody();
- Message out = exchange.getMessage();
+ // Batch removal by collection of IDs
+ if (body instanceof Collection) {
+ Collection<String> ids = (Collection<String>) body;
+ if (ids.isEmpty()) {
+ throw new IllegalArgumentException(
+ "REMOVE action requires a non-empty Collection<String>
body for batch ID removal");
+ }
+ store.removeAll(ids);
+ return;
+ }
+
+ // Single ID removal
+ String id = in.getBody(String.class);
+ if (id != null && !id.isEmpty()) {
+ store.remove(id);
+ return;
+ }
+
+ throw new IllegalArgumentException(
+ "REMOVE action requires either: a String body (single ID), a
Collection<String> body (batch IDs), "
+ + "or a
CamelLangchain4jEmbeddingStoreFilter header (filter-based removal)");
}
/**
diff --git
a/components/camel-ai/camel-langchain4j-embeddingstore/src/test/java/org/apache/camel/component/langchain4j/embeddingstore/LangChain4jEmbeddingStoreBatchOperationsTest.java
b/components/camel-ai/camel-langchain4j-embeddingstore/src/test/java/org/apache/camel/component/langchain4j/embeddingstore/LangChain4jEmbeddingStoreBatchOperationsTest.java
new file mode 100644
index 000000000000..b8e79ccbf660
--- /dev/null
+++
b/components/camel-ai/camel-langchain4j-embeddingstore/src/test/java/org/apache/camel/component/langchain4j/embeddingstore/LangChain4jEmbeddingStoreBatchOperationsTest.java
@@ -0,0 +1,416 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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
+ *
+ * http://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.apache.camel.component.langchain4j.embeddingstore;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+
+import dev.langchain4j.data.embedding.Embedding;
+import dev.langchain4j.data.segment.TextSegment;
+import dev.langchain4j.store.embedding.filter.Filter;
+import dev.langchain4j.store.embedding.inmemory.InMemoryEmbeddingStore;
+import org.apache.camel.CamelContext;
+import org.apache.camel.Exchange;
+import
org.apache.camel.component.langchain4j.embeddings.LangChain4jEmbeddingsHeaders;
+import org.apache.camel.test.junit6.CamelTestSupport;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class LangChain4jEmbeddingStoreBatchOperationsTest extends CamelTestSupport {
+
+ private RecordingEmbeddingStore embeddingStore;
+
+ @Override
+ protected CamelContext createCamelContext() throws Exception {
+ CamelContext context = super.createCamelContext();
+ embeddingStore = new RecordingEmbeddingStore();
+
+ LangChain4jEmbeddingStoreComponent component = context.getComponent(
+ LangChain4jEmbeddingStore.SCHEME,
LangChain4jEmbeddingStoreComponent.class);
+ component.getConfiguration().setEmbeddingStore(embeddingStore);
+
+ return context;
+ }
+
+ // ---- ADD: caller-supplied ID ----
+
+ @Test
+ @DisplayName("ADD with caller-supplied ID uses add(id, embedding)")
+ void addWithCallerSuppliedId() {
+ Embedding embedding = Embedding.from(new float[] { 0.1f, 0.2f, 0.3f });
+
+ Exchange result = fluentTemplate.to("langchain4j-embeddingstore:test")
+ .withHeader(LangChain4jEmbeddingStoreHeaders.ACTION,
LangChain4jEmbeddingStoreAction.ADD)
+ .withHeader(LangChain4jEmbeddingsHeaders.EMBEDDING, embedding)
+ .withHeader(LangChain4jEmbeddingStoreHeaders.EMBEDDING_ID,
"my-custom-id")
+ .request(Exchange.class);
+
+ assertThat(result.getException()).isNull();
+
assertThat(result.getMessage().getBody(String.class)).isEqualTo("my-custom-id");
+ assertThat(embeddingStore.getAddWithIdInvocations()).isEqualTo(1);
+
assertThat(embeddingStore.getLastCallerSuppliedId()).isEqualTo("my-custom-id");
+ }
+
+ // ---- ADD: caller-supplied ID with text segment ----
+
+ @Test
+ @DisplayName("ADD with caller-supplied ID and text segment uses
addAll(singletonList) to preserve both")
+ void addWithCallerSuppliedIdAndTextSegment() {
+ Embedding embedding = Embedding.from(new float[] { 0.1f, 0.2f, 0.3f });
+ TextSegment segment = TextSegment.from("hello world");
+
+ Exchange result = fluentTemplate.to("langchain4j-embeddingstore:test")
+ .withHeader(LangChain4jEmbeddingStoreHeaders.ACTION,
LangChain4jEmbeddingStoreAction.ADD)
+ .withHeader(LangChain4jEmbeddingsHeaders.EMBEDDING, embedding)
+ .withHeader(LangChain4jEmbeddingsHeaders.TEXT_SEGMENT, segment)
+ .withHeader(LangChain4jEmbeddingStoreHeaders.EMBEDDING_ID,
"my-custom-id")
+ .request(Exchange.class);
+
+ assertThat(result.getException()).isNull();
+
assertThat(result.getMessage().getBody(String.class)).isEqualTo("my-custom-id");
+ // Should use addAll with singleton lists to preserve text segment
with caller ID
+ assertThat(embeddingStore.getAddAllWithIdsInvocations()).isEqualTo(1);
+ }
+
+ // ---- ADD: batch operations ----
+
+ @Test
+ @DisplayName("ADD with EMBEDDINGS header and no body calls
addAll(embeddings)")
+ void addBatchEmbeddingsOnly() {
+ List<Embedding> embeddings = Arrays.asList(
+ Embedding.from(new float[] { 0.1f, 0.2f }),
+ Embedding.from(new float[] { 0.3f, 0.4f }));
+
+ Exchange result = fluentTemplate.to("langchain4j-embeddingstore:test")
+ .withHeader(LangChain4jEmbeddingStoreHeaders.ACTION,
LangChain4jEmbeddingStoreAction.ADD)
+ .withHeader(LangChain4jEmbeddingsHeaders.EMBEDDINGS,
embeddings)
+ .request(Exchange.class);
+
+ assertThat(result.getException()).isNull();
+ assertThat(embeddingStore.getAddAllInvocations()).isEqualTo(1);
+
+ @SuppressWarnings("unchecked")
+ List<String> ids = result.getMessage().getBody(List.class);
+ assertThat(ids).hasSize(2);
+ }
+
+ @Test
+ @DisplayName("ADD with EMBEDDINGS header and TextSegment body calls
addAll(embeddings, textSegments)")
+ void addBatchEmbeddingsWithTextSegments() {
+ List<Embedding> embeddings = Arrays.asList(
+ Embedding.from(new float[] { 0.1f, 0.2f }),
+ Embedding.from(new float[] { 0.3f, 0.4f }));
+ List<TextSegment> segments = Arrays.asList(
+ TextSegment.from("hello"),
+ TextSegment.from("world"));
+
+ Exchange result = fluentTemplate.to("langchain4j-embeddingstore:test")
+ .withHeader(LangChain4jEmbeddingStoreHeaders.ACTION,
LangChain4jEmbeddingStoreAction.ADD)
+ .withHeader(LangChain4jEmbeddingsHeaders.EMBEDDINGS,
embeddings)
+ .withBody(segments)
+ .request(Exchange.class);
+
+ assertThat(result.getException()).isNull();
+
assertThat(embeddingStore.getAddAllWithSegmentsInvocations()).isEqualTo(1);
+
+ @SuppressWarnings("unchecked")
+ List<String> ids = result.getMessage().getBody(List.class);
+ assertThat(ids).hasSize(2);
+ }
+
+ @Test
+ @DisplayName("ADD with EMBEDDINGS header and TEXT_SEGMENTS header calls
addAll(embeddings, textSegments)")
+ void addBatchEmbeddingsWithTextSegmentsHeader() {
+ List<Embedding> embeddings = Arrays.asList(
+ Embedding.from(new float[] { 0.1f, 0.2f }),
+ Embedding.from(new float[] { 0.3f, 0.4f }));
+ List<TextSegment> segments = Arrays.asList(
+ TextSegment.from("hello"),
+ TextSegment.from("world"));
+
+ Exchange result = fluentTemplate.to("langchain4j-embeddingstore:test")
+ .withHeader(LangChain4jEmbeddingStoreHeaders.ACTION,
LangChain4jEmbeddingStoreAction.ADD)
+ .withHeader(LangChain4jEmbeddingsHeaders.EMBEDDINGS,
embeddings)
+ .withHeader(LangChain4jEmbeddingsHeaders.TEXT_SEGMENTS,
segments)
+ .request(Exchange.class);
+
+ assertThat(result.getException()).isNull();
+
assertThat(embeddingStore.getAddAllWithSegmentsInvocations()).isEqualTo(1);
+ }
+
+ @Test
+ @DisplayName("ADD with EMBEDDINGS header, caller IDs, and TextSegment body
calls addAll(ids, embeddings, textSegments)")
+ void addBatchWithCallerIdsAndTextSegments() {
+ List<String> callerIds = Arrays.asList("id-1", "id-2");
+ List<Embedding> embeddings = Arrays.asList(
+ Embedding.from(new float[] { 0.1f, 0.2f }),
+ Embedding.from(new float[] { 0.3f, 0.4f }));
+ List<TextSegment> segments = Arrays.asList(
+ TextSegment.from("hello"),
+ TextSegment.from("world"));
+
+ Exchange result = fluentTemplate.to("langchain4j-embeddingstore:test")
+ .withHeader(LangChain4jEmbeddingStoreHeaders.ACTION,
LangChain4jEmbeddingStoreAction.ADD)
+ .withHeader(LangChain4jEmbeddingsHeaders.EMBEDDINGS,
embeddings)
+ .withHeader(LangChain4jEmbeddingStoreHeaders.EMBEDDING_IDS,
callerIds)
+ .withBody(segments)
+ .request(Exchange.class);
+
+ assertThat(result.getException()).isNull();
+ assertThat(embeddingStore.getAddAllWithIdsInvocations()).isEqualTo(1);
+
+ @SuppressWarnings("unchecked")
+ List<String> ids = result.getMessage().getBody(List.class);
+ assertThat(ids).containsExactly("id-1", "id-2");
+ }
+
+ @Test
+ @DisplayName("ADD with EMBEDDINGS header and caller IDs but no text
segments loops with add(id, embedding)")
+ void addBatchWithCallerIdsNoTextSegments() {
+ List<String> callerIds = Arrays.asList("id-a", "id-b");
+ List<Embedding> embeddings = Arrays.asList(
+ Embedding.from(new float[] { 0.5f, 0.6f }),
+ Embedding.from(new float[] { 0.7f, 0.8f }));
+
+ Exchange result = fluentTemplate.to("langchain4j-embeddingstore:test")
+ .withHeader(LangChain4jEmbeddingStoreHeaders.ACTION,
LangChain4jEmbeddingStoreAction.ADD)
+ .withHeader(LangChain4jEmbeddingsHeaders.EMBEDDINGS,
embeddings)
+ .withHeader(LangChain4jEmbeddingStoreHeaders.EMBEDDING_IDS,
callerIds)
+ .request(Exchange.class);
+
+ assertThat(result.getException()).isNull();
+ // Should call add(id, embedding) for each pair, not addAll(embeddings)
+ assertThat(embeddingStore.getAddWithIdInvocations()).isEqualTo(2);
+ assertThat(embeddingStore.getAddAllInvocations()).isEqualTo(0);
+
+ @SuppressWarnings("unchecked")
+ List<String> ids = result.getMessage().getBody(List.class);
+ assertThat(ids).containsExactly("id-a", "id-b");
+ }
+
+ // ---- ADD: size mismatch validation ----
+
+ @Test
+ @DisplayName("ADD with mismatched EMBEDDING_IDS and EMBEDDINGS sizes
throws IllegalArgumentException")
+ void addBatchSizeMismatchThrows() {
+ List<String> callerIds = Arrays.asList("id-1"); // 1 ID
+ List<Embedding> embeddings = Arrays.asList(
+ Embedding.from(new float[] { 0.1f }),
+ Embedding.from(new float[] { 0.2f })); // 2 embeddings
+
+ Exchange result = fluentTemplate.to("langchain4j-embeddingstore:test")
+ .withHeader(LangChain4jEmbeddingStoreHeaders.ACTION,
LangChain4jEmbeddingStoreAction.ADD)
+ .withHeader(LangChain4jEmbeddingsHeaders.EMBEDDINGS,
embeddings)
+ .withHeader(LangChain4jEmbeddingStoreHeaders.EMBEDDING_IDS,
callerIds)
+ .request(Exchange.class);
+
+
assertThat(result.getException()).isInstanceOf(IllegalArgumentException.class);
+ assertThat(result.getException().getMessage()).contains("EMBEDDING_IDS
size");
+ }
+
+ // ---- REMOVE: batch by ID list ----
+
+ @Test
+ @DisplayName("REMOVE with Collection<String> body calls removeAll(ids)")
+ void removeBatchByIds() {
+ // Pre-populate
+ String id1 = embeddingStore.add(Embedding.from(new float[] { 0.1f }));
+ String id2 = embeddingStore.add(Embedding.from(new float[] { 0.2f }));
+ embeddingStore.resetCounters();
+
+ List<String> ids = Arrays.asList(id1, id2);
+ Exchange result = fluentTemplate.to("langchain4j-embeddingstore:test")
+ .withHeader(LangChain4jEmbeddingStoreHeaders.ACTION,
LangChain4jEmbeddingStoreAction.REMOVE)
+ .withBody(ids)
+ .request(Exchange.class);
+
+ assertThat(result.getException()).isNull();
+ assertThat(embeddingStore.getRemoveAllByIdsInvocations()).isEqualTo(1);
+ }
+
+ // ---- REMOVE: by filter ----
+
+ @Test
+ @DisplayName("REMOVE with FILTER header calls removeAll(filter)")
+ void removeByFilter() {
+ Filter filter = new Filter() {
+ @Override
+ public boolean test(Object object) {
+ return true;
+ }
+ };
+
+ Exchange result = fluentTemplate.to("langchain4j-embeddingstore:test")
+ .withHeader(LangChain4jEmbeddingStoreHeaders.ACTION,
LangChain4jEmbeddingStoreAction.REMOVE)
+ .withHeader(LangChain4jEmbeddingStoreHeaders.FILTER, filter)
+ .request(Exchange.class);
+
+ assertThat(result.getException()).isNull();
+
assertThat(embeddingStore.getRemoveAllByFilterInvocations()).isEqualTo(1);
+ }
+
+ // ---- REMOVE: null body and no filter throws ----
+
+ @Test
+ @DisplayName("REMOVE with null body and no filter throws
IllegalArgumentException")
+ void removeWithNoBodyOrFilterThrows() {
+ Exchange result = fluentTemplate.to("langchain4j-embeddingstore:test")
+ .withHeader(LangChain4jEmbeddingStoreHeaders.ACTION,
LangChain4jEmbeddingStoreAction.REMOVE)
+ .request(Exchange.class);
+
+
assertThat(result.getException()).isInstanceOf(IllegalArgumentException.class);
+ assertThat(result.getException().getMessage()).contains("REMOVE action
requires");
+ }
+
+ // ---- REMOVE: single ID still works ----
+
+ @Test
+ @DisplayName("REMOVE with single String body still calls remove(id)")
+ void removeSingleIdStillWorks() {
+ String id = embeddingStore.add(Embedding.from(new float[] { 0.9f }));
+ embeddingStore.resetCounters();
+
+ Exchange result = fluentTemplate.to("langchain4j-embeddingstore:test")
+ .withHeader(LangChain4jEmbeddingStoreHeaders.ACTION,
LangChain4jEmbeddingStoreAction.REMOVE)
+ .withBody(id)
+ .request(Exchange.class);
+
+ assertThat(result.getException()).isNull();
+ assertThat(embeddingStore.getRemoveSingleInvocations()).isEqualTo(1);
+ }
+
+ /**
+ * Extended recording store that tracks which method variants are called.
+ */
+ private static final class RecordingEmbeddingStore extends
InMemoryEmbeddingStore<TextSegment> {
+
+ private int addWithIdInvocations;
+ private int addAllInvocations;
+ private int addAllWithSegmentsInvocations;
+ private int addAllWithIdsInvocations;
+ private int removeSingleInvocations;
+ private int removeAllByIdsInvocations;
+ private int removeAllByFilterInvocations;
+ private int removeAllInvocations;
+ private String lastCallerSuppliedId;
+
+ @Override
+ public String add(Embedding embedding) {
+ return super.add(embedding);
+ }
+
+ @Override
+ public void add(String id, Embedding embedding) {
+ addWithIdInvocations++;
+ lastCallerSuppliedId = id;
+ super.add(id, embedding);
+ }
+
+ @Override
+ public List<String> addAll(List<Embedding> embeddings) {
+ addAllInvocations++;
+ return super.addAll(embeddings);
+ }
+
+ @Override
+ public List<String> addAll(List<Embedding> embeddings,
List<TextSegment> embedded) {
+ addAllWithSegmentsInvocations++;
+ return super.addAll(embeddings, embedded);
+ }
+
+ @Override
+ public void addAll(List<String> ids, List<Embedding> embeddings,
List<TextSegment> embedded) {
+ addAllWithIdsInvocations++;
+ super.addAll(ids, embeddings, embedded);
+ }
+
+ @Override
+ public void remove(String id) {
+ removeSingleInvocations++;
+ super.remove(id);
+ }
+
+ @Override
+ public void removeAll(Collection<String> ids) {
+ removeAllByIdsInvocations++;
+ super.removeAll(ids);
+ }
+
+ @Override
+ public void removeAll(Filter filter) {
+ removeAllByFilterInvocations++;
+ super.removeAll(filter);
+ }
+
+ @Override
+ public void removeAll() {
+ removeAllInvocations++;
+ super.removeAll();
+ }
+
+ void resetCounters() {
+ addWithIdInvocations = 0;
+ addAllInvocations = 0;
+ addAllWithSegmentsInvocations = 0;
+ addAllWithIdsInvocations = 0;
+ removeSingleInvocations = 0;
+ removeAllByIdsInvocations = 0;
+ removeAllByFilterInvocations = 0;
+ removeAllInvocations = 0;
+ lastCallerSuppliedId = null;
+ }
+
+ int getAddWithIdInvocations() {
+ return addWithIdInvocations;
+ }
+
+ int getAddAllInvocations() {
+ return addAllInvocations;
+ }
+
+ int getAddAllWithSegmentsInvocations() {
+ return addAllWithSegmentsInvocations;
+ }
+
+ int getAddAllWithIdsInvocations() {
+ return addAllWithIdsInvocations;
+ }
+
+ int getRemoveSingleInvocations() {
+ return removeSingleInvocations;
+ }
+
+ int getRemoveAllByIdsInvocations() {
+ return removeAllByIdsInvocations;
+ }
+
+ int getRemoveAllByFilterInvocations() {
+ return removeAllByFilterInvocations;
+ }
+
+ int getRemoveAllInvocations() {
+ return removeAllInvocations;
+ }
+
+ String getLastCallerSuppliedId() {
+ return lastCallerSuppliedId;
+ }
+ }
+}
diff --git
a/core/camel-api/src/main/java/org/apache/camel/ai/CamelLangchain4jAttributes.java
b/core/camel-api/src/main/java/org/apache/camel/ai/CamelLangchain4jAttributes.java
index d0bc0d11a8be..1b5d2550e585 100644
---
a/core/camel-api/src/main/java/org/apache/camel/ai/CamelLangchain4jAttributes.java
+++
b/core/camel-api/src/main/java/org/apache/camel/ai/CamelLangchain4jAttributes.java
@@ -25,4 +25,18 @@ public interface CamelLangchain4jAttributes {
String CAMEL_LANGCHAIN4J_EMBEDDING = "CamelLangChain4jEmbeddingsEmbedding";
String CAMEL_LANGCHAIN4J_EMBEDDING_VECTOR =
"CamelLangChain4jEmbeddingsVector";
String CAMEL_LANGCHAIN4J_TEXT_SEGMENT =
"CamelLangChain4jEmbeddingsTextSegment";
+
+ /**
+ * List of embeddings from a batch embedAll operation.
+ *
+ * @since 4.23
+ */
+ String CAMEL_LANGCHAIN4J_EMBEDDINGS =
"CamelLangChain4jEmbeddingsEmbeddings";
+
+ /**
+ * List of text segments from a batch embedAll operation.
+ *
+ * @since 4.23
+ */
+ String CAMEL_LANGCHAIN4J_TEXT_SEGMENTS =
"CamelLangChain4jEmbeddingsTextSegments";
}
diff --git
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
index df7de54e3453..14da60b11062 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
@@ -670,6 +670,29 @@ Camel then no longer controls how the payload is read.
The legacy `sse` `transportType` has been removed. It follows the support
removal in
https://github.com/langchain4j/langchain4j/commit/7f4e99fd637137ab9d3bf116eb62047ac757a818[langchain4j-core
1.19.0].
+=== camel-langchain4j-embeddings
+
+The `langchain4j-embeddings` component now supports **batch embedding**. When
the message body is a `List`
+(of `String` or `TextSegment`), the producer calls `model.embedAll()` instead
of `model.embed()`.
+The resulting `List<Embedding>` is set in the
`CamelLangChain4jEmbeddingsEmbeddings` header, and the
+original text segments are preserved in the
`CamelLangChain4jEmbeddingsTextSegments` header for downstream
+use by the embedding store component.
+
+=== camel-langchain4j-embeddingstore
+
+The ADD operation now supports **batch operations** (via `addAll`) and
**caller-supplied IDs**.
+When the `CamelLangChain4jEmbeddingsEmbeddings` header is present with a
`List<Embedding>`,
+the component uses batch operations. Caller-supplied IDs are provided via the
+`CamelLangchain4jEmbeddingStoreEmbeddingId` (single) or
`CamelLangchain4jEmbeddingStoreEmbeddingIds`
+(batch) headers.
+
+The REMOVE operation now supports **batch removal by collection of IDs** (pass
a `Collection<String>`
+as the body) and **filter-based removal** (set a `Filter` in the
+`CamelLangchain4jEmbeddingStoreFilter` header). Previously, passing a null or
empty body to REMOVE
+would pass `null` to the underlying store's `remove()` method, which threw an
opaque exception
+from langchain4j. It now throws `IllegalArgumentException` with a clear
message listing the
+expected inputs.
+
=== camel-ftp, camel-sftp, camel-ftps, camel-mina-sftp, camel-azure-files,
camel-smb
The remote-file consumers now ensure the path resolved for a polled file stays
within the directory being
diff --git
a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/LangChain4jEmbeddingStoreEndpointBuilderFactory.java
b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/LangChain4jEmbeddingStoreEndpointBuilderFactory.java
index b1f863714ebf..976676bc233c 100644
---
a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/LangChain4jEmbeddingStoreEndpointBuilderFactory.java
+++
b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/LangChain4jEmbeddingStoreEndpointBuilderFactory.java
@@ -430,7 +430,8 @@ public interface
LangChain4jEmbeddingStoreEndpointBuilderFactory {
return "CamelLangchain4jEmbeddingStoreMinScore";
}
/**
- * Search filter for metadata-based constraints.
+ * Filter for metadata-based constraints (used in SEARCH and REMOVE
+ * operations).
*
* The option is a: {@code
* dev.langchain4j.store.embedding.filter.Filter} type.
@@ -443,6 +444,32 @@ public interface
LangChain4jEmbeddingStoreEndpointBuilderFactory {
public String langchain4jEmbeddingStoreFilter() {
return "CamelLangchain4jEmbeddingStoreFilter";
}
+ /**
+ * Caller-supplied embedding ID for single ADD operations.
+ *
+ * The option is a: {@code String} type.
+ *
+ * Group: producer
+ *
+ * @return the name of the header {@code
+ * Langchain4jEmbeddingStoreEmbeddingId}.
+ */
+ public String langchain4jEmbeddingStoreEmbeddingId() {
+ return "CamelLangchain4jEmbeddingStoreEmbeddingId";
+ }
+ /**
+ * Caller-supplied embedding IDs for batch ADD operations.
+ *
+ * The option is a: {@code java.util.List<java.lang.String>} type.
+ *
+ * Group: producer
+ *
+ * @return the name of the header {@code
+ * Langchain4jEmbeddingStoreEmbeddingIds}.
+ */
+ public String langchain4jEmbeddingStoreEmbeddingIds() {
+ return "CamelLangchain4jEmbeddingStoreEmbeddingIds";
+ }
}
static LangChain4jEmbeddingStoreEndpointBuilder endpointBuilder(String
componentName, String path) {
class LangChain4jEmbeddingStoreEndpointBuilderImpl extends
AbstractEndpointBuilder implements LangChain4jEmbeddingStoreEndpointBuilder,
AdvancedLangChain4jEmbeddingStoreEndpointBuilder {
diff --git
a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/LangChain4jEmbeddingsEndpointBuilderFactory.java
b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/LangChain4jEmbeddingsEndpointBuilderFactory.java
index 13151faabfde..3672e2d13884 100644
---
a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/LangChain4jEmbeddingsEndpointBuilderFactory.java
+++
b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/LangChain4jEmbeddingsEndpointBuilderFactory.java
@@ -295,6 +295,20 @@ public interface
LangChain4jEmbeddingsEndpointBuilderFactory {
public String langChain4jEmbeddingsEmbedding() {
return "CamelLangChain4jEmbeddingsEmbedding";
}
+ /**
+ * List of embeddings from a batch embedAll operation.
+ *
+ * The option is a: {@code
+ * java.util.List<dev.langchain4j.data.embedding.Embedding>} type.
+ *
+ * Group: producer
+ *
+ * @return the name of the header {@code
+ * LangChain4jEmbeddingsEmbeddings}.
+ */
+ public String langChain4jEmbeddingsEmbeddings() {
+ return "CamelLangChain4jEmbeddingsEmbeddings";
+ }
/**
* A dense vector embedding of a text.
*
@@ -321,6 +335,20 @@ public interface
LangChain4jEmbeddingsEndpointBuilderFactory {
public String langChain4jEmbeddingsTextSegment() {
return "CamelLangChain4jEmbeddingsTextSegment";
}
+ /**
+ * List of text segments from a batch embedAll operation.
+ *
+ * The option is a: {@code
+ * java.util.List<dev.langchain4j.data.segment.TextSegment>} type.
+ *
+ * Group: producer
+ *
+ * @return the name of the header {@code
+ * LangChain4jEmbeddingsTextSegments}.
+ */
+ public String langChain4jEmbeddingsTextSegments() {
+ return "CamelLangChain4jEmbeddingsTextSegments";
+ }
}
static LangChain4jEmbeddingsEndpointBuilder endpointBuilder(String
componentName, String path) {
class LangChain4jEmbeddingsEndpointBuilderImpl extends
AbstractEndpointBuilder implements LangChain4jEmbeddingsEndpointBuilder,
AdvancedLangChain4jEmbeddingsEndpointBuilder {