This is an automated email from the ASF dual-hosted git repository.

davsclaus pushed a commit to branch CAMEL-26204-embeddingstore-body
in repository https://gitbox.apache.org/repos/asf/camel.git

commit 645db45c2cf914c81820abbeded3b5200339d8ae
Author: Claus Ibsen <[email protected]>
AuthorDate: Tue Nov 3 00:21:15 2026 +0100

    CAMEL-26204: camel-langchain4j-embeddingstore - auto-embed a TextSegment 
body as is and a List body as a batch, and REMOVE takes only a text id
    
    With an embeddingModel and no embedding header, ADD and SEARCH read the body
    as a String, so a TextSegment body was embedded and stored as its toString()
    and a List body was embedded and stored as one text such as "[a, b]".
    A TextSegment body is now embedded and stored as is, with its metadata. ADD
    embeds a List of String or TextSegment as a batch, one entry per element,
    and returns the ids; SEARCH fails on a List body.
    
    REMOVE converted any body to a String, so a Map or an Integer body removed
    a made-up id and nothing failed. It now fails unless the body is text (a
    String, bytes or a stream) or a Collection of String ids.
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
---
 .../components/langchain4j-embeddingstore.json     |   2 +-
 .../docs/langchain4j-embeddingstore-component.adoc |  15 ++-
 .../embeddingstore/langchain4j-embeddingstore.json |   2 +-
 .../docs/langchain4j-embeddingstore-component.adoc |  15 ++-
 .../LangChain4jEmbeddingStoreEndpoint.java         |  10 +-
 .../LangChain4jEmbeddingStoreProducer.java         | 115 ++++++++++++++++---
 .../LangChain4jEmbeddingStoreAutoEmbedTest.java    | 123 +++++++++++++++++++++
 ...ngChain4jEmbeddingStoreBatchOperationsTest.java |  67 +++++++++++
 .../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc    |  12 ++
 9 files changed, 333 insertions(+), 28 deletions(-)

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 03dd71668c8d..ab1f4f1a8078 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
@@ -44,7 +44,7 @@
     "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"
 }
   },
   "bodyTypes": {
-    "producerIn": { "carriers": [ "any" ], "byOption": [ { "option": "action", 
"value": "REMOVE", "carriers": [ "text", "list" ] } ], "description": "ADD and 
SEARCH take the embedding from the CamelLangChain4jEmbeddingsEmbedding header 
(a batch ADD from the CamelLangChain4jEmbeddingsEmbeddings header) and ignore 
the body, but with an embeddingModel and no such header they embed the body 
read as a String, so no body fails and another body, a TextSegment too, is 
embedded as its toString(). [...]
+    "producerIn": { "carriers": [ "any" ], "byOption": [ { "option": "action", 
"value": "REMOVE", "carriers": [ "text", "list" ] } ], "description": "ADD and 
SEARCH take the embedding from the CamelLangChain4jEmbeddingsEmbedding header 
(a batch ADD from the CamelLangChain4jEmbeddingsEmbeddings header) and ignore 
the body, but with an embeddingModel and no such header they embed the body: a 
TextSegment as is with its metadata, another body read as a String, so no body 
fails. ADD embeds a  [...]
     "producerOut": { "carriers": [ "text", "list", "unchanged" ], "byOption": 
[ { "option": "action", "value": "ADD", "carriers": [ "text", "list" ] }, { 
"option": "action", "value": "REMOVE", "carriers": [ "unchanged" ] }, { 
"option": "action", "value": "SEARCH", "carriers": [ "list" ] } ], 
"description": "By the action, from the endpoint or the 
CamelLangchain4jEmbeddingStoreAction header: the id of the added embedding as a 
String for ADD, a List of the ids for a batch ADD, and a List o [...]
   },
   "properties": {
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 3b16a2653206..7655bfd3181d 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
@@ -78,6 +78,15 @@ When an `EmbeddingModel` is configured (either explicitly or 
via autowiring from
 
 If the `CamelLangChain4jEmbeddingsEmbedding` header is present, it always 
takes precedence over auto-embedding.
 
+The body is embedded as follows:
+
+* A `TextSegment` body is embedded and stored as is, with its metadata.
+* Any other single body is read as a `String`, and stored as a `TextSegment` 
of that text.
+* For ADD, a `List` of `String` or `TextSegment` is embedded and stored as a 
batch, one entry per element, and the
+  response body is the `List` of the generated IDs (the 
`CamelLangchain4jEmbeddingStoreEmbeddingIds` header supplies
+  your own IDs). A `List` with another element type, or an empty `List`, fails 
with `IllegalArgumentException`.
+* For SEARCH, the body is one query: a `List` body fails with 
`IllegalArgumentException`.
+
 [tabs]
 ====
 Java::
@@ -263,7 +272,8 @@ The REMOVE operation supports multiple strategies:
 
 ==== Remove by Single ID
 
-Delete a single embedding by its ID (body is a `String`):
+Delete a single embedding by its ID (body is a `String`, or bytes or a stream 
read as a `String`). Another body type,
+such as a `Map` or a number, fails with `IllegalArgumentException`:
 
 [tabs]
 ====
@@ -297,7 +307,8 @@ YAML::
 
 ==== Remove by Collection of IDs
 
-Delete multiple embeddings by passing a `Collection<String>` as the body:
+Delete multiple embeddings by passing a `Collection<String>` as the body. A 
collection with an element that is not a
+`String` fails with `IllegalArgumentException`:
 
 [source,java]
 ----
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 03dd71668c8d..ab1f4f1a8078 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
@@ -44,7 +44,7 @@
     "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"
 }
   },
   "bodyTypes": {
-    "producerIn": { "carriers": [ "any" ], "byOption": [ { "option": "action", 
"value": "REMOVE", "carriers": [ "text", "list" ] } ], "description": "ADD and 
SEARCH take the embedding from the CamelLangChain4jEmbeddingsEmbedding header 
(a batch ADD from the CamelLangChain4jEmbeddingsEmbeddings header) and ignore 
the body, but with an embeddingModel and no such header they embed the body 
read as a String, so no body fails and another body, a TextSegment too, is 
embedded as its toString(). [...]
+    "producerIn": { "carriers": [ "any" ], "byOption": [ { "option": "action", 
"value": "REMOVE", "carriers": [ "text", "list" ] } ], "description": "ADD and 
SEARCH take the embedding from the CamelLangChain4jEmbeddingsEmbedding header 
(a batch ADD from the CamelLangChain4jEmbeddingsEmbeddings header) and ignore 
the body, but with an embeddingModel and no such header they embed the body: a 
TextSegment as is with its metadata, another body read as a String, so no body 
fails. ADD embeds a  [...]
     "producerOut": { "carriers": [ "text", "list", "unchanged" ], "byOption": 
[ { "option": "action", "value": "ADD", "carriers": [ "text", "list" ] }, { 
"option": "action", "value": "REMOVE", "carriers": [ "unchanged" ] }, { 
"option": "action", "value": "SEARCH", "carriers": [ "list" ] } ], 
"description": "By the action, from the endpoint or the 
CamelLangchain4jEmbeddingStoreAction header: the id of the added embedding as a 
String for ADD, a List of the ids for a batch ADD, and a List o [...]
   },
   "properties": {
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 3b16a2653206..7655bfd3181d 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
@@ -78,6 +78,15 @@ When an `EmbeddingModel` is configured (either explicitly or 
via autowiring from
 
 If the `CamelLangChain4jEmbeddingsEmbedding` header is present, it always 
takes precedence over auto-embedding.
 
+The body is embedded as follows:
+
+* A `TextSegment` body is embedded and stored as is, with its metadata.
+* Any other single body is read as a `String`, and stored as a `TextSegment` 
of that text.
+* For ADD, a `List` of `String` or `TextSegment` is embedded and stored as a 
batch, one entry per element, and the
+  response body is the `List` of the generated IDs (the 
`CamelLangchain4jEmbeddingStoreEmbeddingIds` header supplies
+  your own IDs). A `List` with another element type, or an empty `List`, fails 
with `IllegalArgumentException`.
+* For SEARCH, the body is one query: a `List` body fails with 
`IllegalArgumentException`.
+
 [tabs]
 ====
 Java::
@@ -263,7 +272,8 @@ The REMOVE operation supports multiple strategies:
 
 ==== Remove by Single ID
 
-Delete a single embedding by its ID (body is a `String`):
+Delete a single embedding by its ID (body is a `String`, or bytes or a stream 
read as a `String`). Another body type,
+such as a `Map` or a number, fails with `IllegalArgumentException`:
 
 [tabs]
 ====
@@ -297,7 +307,8 @@ YAML::
 
 ==== Remove by Collection of IDs
 
-Delete multiple embeddings by passing a `Collection<String>` as the body:
+Delete multiple embeddings by passing a `Collection<String>` as the body. A 
collection with an element that is not a
+`String` fails with `IllegalArgumentException`:
 
 [source,java]
 ----
diff --git 
a/components/camel-ai/camel-langchain4j-embeddingstore/src/main/java/org/apache/camel/component/langchain4j/embeddingstore/LangChain4jEmbeddingStoreEndpoint.java
 
b/components/camel-ai/camel-langchain4j-embeddingstore/src/main/java/org/apache/camel/component/langchain4j/embeddingstore/LangChain4jEmbeddingStoreEndpoint.java
index 8c1b63b80c98..ee08455c318d 100644
--- 
a/components/camel-ai/camel-langchain4j-embeddingstore/src/main/java/org/apache/camel/component/langchain4j/embeddingstore/LangChain4jEmbeddingStoreEndpoint.java
+++ 
b/components/camel-ai/camel-langchain4j-embeddingstore/src/main/java/org/apache/camel/component/langchain4j/embeddingstore/LangChain4jEmbeddingStoreEndpoint.java
@@ -50,10 +50,12 @@ import org.apache.camel.support.DefaultEndpoint;
                                   description = "ADD and SEARCH take the 
embedding from the"
                                                 + " 
CamelLangChain4jEmbeddingsEmbedding header (a batch ADD from the"
                                                 + " 
CamelLangChain4jEmbeddingsEmbeddings header) and ignore the body, but with"
-                                                + " an embeddingModel and no 
such header they embed the body read as a String,"
-                                                + " so no body fails and 
another body, a TextSegment too, is embedded as its"
-                                                + " toString(). REMOVE takes 
the id as text or a Collection of ids, unless the"
-                                                + " 
CamelLangchain4jEmbeddingStoreFilter header is set"),
+                                                + " an embeddingModel and no 
such header they embed the body: a TextSegment as"
+                                                + " is with its metadata, 
another body read as a String, so no body fails. ADD"
+                                                + " embeds a List of String or 
TextSegment as a batch, one entry per element, and"
+                                                + " SEARCH fails on a List. 
REMOVE takes the id as text or a Collection of String"
+                                                + " ids, unless the 
CamelLangchain4jEmbeddingStoreFilter header is set, and fails"
+                                                + " on another body"),
            producerOut = @BodyType(value = { Carrier.TEXT, Carrier.LIST, 
Carrier.UNCHANGED },
                                    byOption = {
                                            @BodyType.When(option = "action", 
value = "ADD",
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 a6ef24df2525..260cb98f0ab2 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,9 @@
  */
 package org.apache.camel.component.langchain4j.embeddingstore;
 
+import java.io.InputStream;
+import java.io.Reader;
+import java.util.ArrayList;
 import java.util.Collection;
 import java.util.List;
 import java.util.concurrent.ExecutorService;
@@ -32,6 +35,7 @@ import org.apache.camel.CamelContext;
 import org.apache.camel.Exchange;
 import org.apache.camel.Message;
 import org.apache.camel.NoSuchHeaderException;
+import org.apache.camel.StreamCache;
 import 
org.apache.camel.component.langchain4j.embeddings.LangChain4jEmbeddingsHeaders;
 import org.apache.camel.support.DefaultProducer;
 
@@ -140,6 +144,12 @@ public class LangChain4jEmbeddingStoreProducer extends 
DefaultProducer {
      * <li>Without text segments: calls {@code addAll(embeddings)}</li>
      * </ul>
      *
+     * <p>
+     * <b>Auto-embedding</b> - with an {@code embeddingModel} and no embedding 
header, a {@code TextSegment} body is
+     * embedded and stored as is, and a {@code Collection} of {@code String} 
or {@code TextSegment} is embedded and
+     * stored as a batch.
+     * </p>
+     *
      * @param  exchange  the Camel exchange containing the embedding data
      * @throws Exception if the add operation fails
      */
@@ -152,7 +162,18 @@ public class LangChain4jEmbeddingStoreProducer extends 
DefaultProducer {
         // Check for batch embeddings header first
         List<Embedding> embeddings = 
in.getHeader(LangChain4jEmbeddingsHeaders.EMBEDDINGS, List.class);
         if (embeddings != null) {
-            addBatch(in, store, embeddings);
+            addBatch(in, store, embeddings, resolveTextSegments(in));
+            return;
+        }
+
+        // Auto-embed a collection body as a batch, one entry per element (the 
embedding and text segment headers
+        // take precedence over the body, as for a single embedding)
+        if (config.getEmbeddingModel() != null && 
in.getHeader(LangChain4jEmbeddingsHeaders.EMBEDDING) == null
+                && in.getHeader(LangChain4jEmbeddingsHeaders.TEXT_SEGMENT) == 
null
+                && in.getBody() instanceof Collection<?> collection) {
+            List<TextSegment> textSegments = toTextSegments(collection);
+            List<Embedding> embedded = 
config.getEmbeddingModel().embedAll(textSegments).content();
+            addBatch(in, store, embedded, textSegments);
             return;
         }
 
@@ -181,9 +202,7 @@ public class LangChain4jEmbeddingStoreProducer extends 
DefaultProducer {
     }
 
     @SuppressWarnings("unchecked")
-    private void addBatch(Message in, EmbeddingStore<TextSegment> store, 
List<Embedding> embeddings) {
-        List<String> callerIds = 
in.getHeader(LangChain4jEmbeddingStoreHeaders.EMBEDDING_IDS, List.class);
-
+    private static List<TextSegment> resolveTextSegments(Message in) {
         // 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) {
@@ -192,6 +211,34 @@ public class LangChain4jEmbeddingStoreProducer extends 
DefaultProducer {
                 textSegments = (List<TextSegment>) body;
             }
         }
+        return textSegments;
+    }
+
+    private static List<TextSegment> toTextSegments(Collection<?> collection) {
+        if (collection.isEmpty()) {
+            throw new IllegalArgumentException("ADD with auto-embedding 
requires a non-empty List body");
+        }
+        List<TextSegment> textSegments = new ArrayList<>(collection.size());
+        int index = 0;
+        for (Object element : collection) {
+            if (element instanceof TextSegment textSegment) {
+                textSegments.add(textSegment);
+            } else if (element instanceof String text) {
+                textSegments.add(TextSegment.from(text));
+            } else {
+                throw new IllegalArgumentException(
+                        "ADD with auto-embedding requires a List of String or 
TextSegment, but the element at index "
+                                                   + index + " is " + 
typeOf(element));
+            }
+            index++;
+        }
+        return textSegments;
+    }
+
+    @SuppressWarnings("unchecked")
+    private void addBatch(
+            Message in, EmbeddingStore<TextSegment> store, List<Embedding> 
embeddings, List<TextSegment> textSegments) {
+        List<String> callerIds = 
in.getHeader(LangChain4jEmbeddingStoreHeaders.EMBEDDING_IDS, List.class);
 
         // Validate sizes match when caller IDs are provided
         if (callerIds != null && callerIds.size() != embeddings.size()) {
@@ -252,17 +299,27 @@ public class LangChain4jEmbeddingStoreProducer extends 
DefaultProducer {
         Object body = in.getBody();
 
         // Batch removal by collection of IDs
-        if (body instanceof Collection) {
-            Collection<String> ids = (Collection<String>) body;
-            if (ids.isEmpty()) {
+        if (body instanceof Collection<?> collection) {
+            if (collection.isEmpty()) {
                 throw new IllegalArgumentException(
                         "REMOVE action requires a non-empty Collection<String> 
body for batch ID removal");
             }
-            store.removeAll(ids);
+            for (Object id : collection) {
+                if (!(id instanceof String)) {
+                    throw new IllegalArgumentException(
+                            "REMOVE action requires a Collection<String> body 
for batch ID removal, but it contains "
+                                                       + typeOf(id));
+                }
+            }
+            store.removeAll((Collection<String>) collection);
             return;
         }
 
-        // Single ID removal
+        // Single ID removal, the id is text (a String, or bytes or a stream 
to read as a String)
+        if (body != null && !isText(body)) {
+            throw new IllegalArgumentException(
+                    "REMOVE action requires the ID as a String body, but the 
body is " + typeOf(body));
+        }
         String id = in.getBody(String.class);
         if (id != null && !id.isEmpty()) {
             store.remove(id);
@@ -350,15 +407,28 @@ public class LangChain4jEmbeddingStoreProducer extends 
DefaultProducer {
         TextSegment textSegment = 
in.getHeader(LangChain4jEmbeddingsHeaders.TEXT_SEGMENT, TextSegment.class);
 
         if (embedding == null && config.getEmbeddingModel() != null) {
-            String text = in.getBody(String.class);
-            if (text == null) {
-                throw new IllegalArgumentException(
-                        "Message body cannot be converted to String for 
auto-embedding. "
-                                                   + "Either set the body to a 
text value or provide a pre-computed embedding via the "
-                                                   + 
LangChain4jEmbeddingsHeaders.EMBEDDING + " header.");
-            }
-            if (textSegment == null) {
-                textSegment = TextSegment.from(text);
+            Object body = in.getBody();
+            if (body instanceof TextSegment segment) {
+                if (textSegment == null) {
+                    textSegment = segment;
+                }
+            } else {
+                if (textSegment == null && body instanceof Collection) {
+                    // ADD embeds a collection as a batch, a search takes one 
query
+                    throw new IllegalArgumentException(
+                            "Auto-embedding requires one text as the body (a 
String or a TextSegment), but the body is "
+                                                       + typeOf(body));
+                }
+                String text = in.getBody(String.class);
+                if (text == null) {
+                    throw new IllegalArgumentException(
+                            "Message body cannot be converted to String for 
auto-embedding. "
+                                                       + "Either set the body 
to a text value or provide a pre-computed embedding via the "
+                                                       + 
LangChain4jEmbeddingsHeaders.EMBEDDING + " header.");
+                }
+                if (textSegment == null) {
+                    textSegment = TextSegment.from(text);
+                }
             }
             embedding = 
config.getEmbeddingModel().embed(textSegment).content();
         }
@@ -373,6 +443,15 @@ public class LangChain4jEmbeddingStoreProducer extends 
DefaultProducer {
         return new EmbeddingResult(embedding, textSegment);
     }
 
+    private static boolean isText(Object body) {
+        return body instanceof CharSequence || body instanceof byte[] || body 
instanceof InputStream
+                || body instanceof Reader || body instanceof StreamCache;
+    }
+
+    private static String typeOf(Object value) {
+        return value == null ? "null" : value.getClass().getName();
+    }
+
     private record EmbeddingResult(Embedding embedding, TextSegment 
textSegment) {
     }
 
diff --git 
a/components/camel-ai/camel-langchain4j-embeddingstore/src/test/java/org/apache/camel/component/langchain4j/embeddingstore/LangChain4jEmbeddingStoreAutoEmbedTest.java
 
b/components/camel-ai/camel-langchain4j-embeddingstore/src/test/java/org/apache/camel/component/langchain4j/embeddingstore/LangChain4jEmbeddingStoreAutoEmbedTest.java
index 0f5adf0b96ee..082ad11c00d2 100644
--- 
a/components/camel-ai/camel-langchain4j-embeddingstore/src/test/java/org/apache/camel/component/langchain4j/embeddingstore/LangChain4jEmbeddingStoreAutoEmbedTest.java
+++ 
b/components/camel-ai/camel-langchain4j-embeddingstore/src/test/java/org/apache/camel/component/langchain4j/embeddingstore/LangChain4jEmbeddingStoreAutoEmbedTest.java
@@ -18,10 +18,12 @@ package 
org.apache.camel.component.langchain4j.embeddingstore;
 
 import java.util.List;
 
+import dev.langchain4j.data.document.Metadata;
 import dev.langchain4j.data.embedding.Embedding;
 import dev.langchain4j.data.segment.TextSegment;
 import dev.langchain4j.model.embedding.EmbeddingModel;
 import 
dev.langchain4j.model.embedding.onnx.allminilml6v2.AllMiniLmL6V2EmbeddingModel;
+import dev.langchain4j.store.embedding.EmbeddingMatch;
 import dev.langchain4j.store.embedding.EmbeddingSearchRequest;
 import dev.langchain4j.store.embedding.inmemory.InMemoryEmbeddingStore;
 import org.apache.camel.CamelContext;
@@ -67,6 +69,9 @@ class LangChain4jEmbeddingStoreAutoEmbedTest extends 
CamelTestSupport {
                 from("direct:search")
                         
.to("langchain4j-embeddingstore:test?action=SEARCH&returnTextContent=true");
 
+                from("direct:search-matches")
+                        .to("langchain4j-embeddingstore:test?action=SEARCH");
+
                 from("direct:add-no-model")
                         .to("no-model-store:test?action=ADD");
             }
@@ -158,4 +163,122 @@ class LangChain4jEmbeddingStoreAutoEmbedTest extends 
CamelTestSupport {
         assertThat(results).isNotEmpty();
         assertThat(results.get(0)).contains("Apache Camel");
     }
+
+    @Test
+    void addTextSegmentBodyStoresTheSegment() {
+        TextSegment segment = TextSegment.from("I like football.", 
Metadata.from("source", "notes"));
+
+        String id = fluentTemplate.to("direct:add")
+                .withBody(segment)
+                .request(String.class);
+
+        List<EmbeddingMatch<TextSegment>> matches = search("I like football.", 
5);
+        assertThat(matches).hasSize(1);
+        assertThat(matches.get(0).embeddingId()).isEqualTo(id);
+        assertThat(matches.get(0).embedded().text()).isEqualTo("I like 
football.");
+        
assertThat(matches.get(0).embedded().metadata().getString("source")).isEqualTo("notes");
+        // the embedding is the one of the segment text, not of its toString
+        assertThat(matches.get(0).score()).isGreaterThan(0.99);
+    }
+
+    @Test
+    void addListOfStringsBodyAddsOneEntryPerElement() {
+        List<String> ids = fluentTemplate.to("direct:add")
+                .withBody(List.of("I like football.", "The weather is good 
today."))
+                .request(List.class);
+
+        assertThat(ids).hasSize(2);
+        List<EmbeddingMatch<TextSegment>> matches = search("sports", 5);
+        assertThat(matches).hasSize(2);
+        assertThat(matches).extracting(m -> m.embedded().text())
+                .containsExactly("I like football.", "The weather is good 
today.");
+        
assertThat(matches).extracting(EmbeddingMatch::embeddingId).containsExactlyInAnyOrderElementsOf(ids);
+    }
+
+    @Test
+    void addListOfTextSegmentsBodyKeepsTheMetadata() {
+        List<TextSegment> segments = List.of(
+                TextSegment.from("I like football.", Metadata.from("index", 
"1")),
+                TextSegment.from("The weather is good today.", 
Metadata.from("index", "2")));
+
+        List<String> ids = fluentTemplate.to("direct:add")
+                .withBody(segments)
+                .withHeader(LangChain4jEmbeddingStoreHeaders.EMBEDDING_IDS, 
List.of("id-1", "id-2"))
+                .request(List.class);
+
+        assertThat(ids).containsExactly("id-1", "id-2");
+        List<EmbeddingMatch<TextSegment>> matches = search("sports", 5);
+        assertThat(matches).hasSize(2);
+        assertThat(matches.get(0).embeddingId()).isEqualTo("id-1");
+        
assertThat(matches.get(0).embedded().metadata().getString("index")).isEqualTo("1");
+        assertThat(matches.get(1).embeddingId()).isEqualTo("id-2");
+        
assertThat(matches.get(1).embedded().metadata().getString("index")).isEqualTo("2");
+    }
+
+    @Test
+    void addListBodyWithTextSegmentHeaderStoresTheHeaderSegment() {
+        String id = fluentTemplate.to("direct:add")
+                .withBody(List.of("I like football.", "The weather is good 
today."))
+                .withHeader(LangChain4jEmbeddingsHeaders.TEXT_SEGMENT, 
TextSegment.from("XYZZY-marker-42"))
+                .request(String.class);
+
+        List<EmbeddingMatch<TextSegment>> matches = search("XYZZY-marker-42", 
5);
+        assertThat(matches).hasSize(1);
+        assertThat(matches.get(0).embeddingId()).isEqualTo(id);
+        
assertThat(matches.get(0).embedded().text()).isEqualTo("XYZZY-marker-42");
+    }
+
+    @Test
+    void addListWithAnElementThatIsNotTextFails() {
+        Exchange result = fluentTemplate.to("direct:add")
+                .withBody(List.of("I like football.", 42))
+                .send();
+
+        assertThat(result.getException())
+                .isInstanceOf(IllegalArgumentException.class)
+                .hasMessageContaining("index 1")
+                .hasMessageContaining("java.lang.Integer");
+        assertThat(search("sports", 5)).isEmpty();
+    }
+
+    @Test
+    void addEmptyListFails() {
+        Exchange result = fluentTemplate.to("direct:add")
+                .withBody(List.of())
+                .send();
+
+        assertThat(result.getException())
+                .isInstanceOf(IllegalArgumentException.class)
+                .hasMessageContaining("non-empty List body");
+    }
+
+    @Test
+    void searchWithTextSegmentBody() {
+        TextSegment segment = TextSegment.from("Apache Camel is a powerful 
integration framework");
+        embeddingStore.add(embeddingModel.embed(segment).content(), segment);
+
+        List<EmbeddingMatch<TextSegment>> matches = 
fluentTemplate.to("direct:search-matches")
+                .withBody(segment)
+                .request(List.class);
+
+        assertThat(matches).hasSize(1);
+        assertThat(matches.get(0).score()).isGreaterThan(0.99);
+    }
+
+    @Test
+    void searchWithListBodyFails() {
+        Exchange result = fluentTemplate.to("direct:search")
+                .withBody(List.of("sports", "weather"))
+                .send();
+
+        assertThat(result.getException())
+                .isInstanceOf(IllegalArgumentException.class)
+                .hasMessageContaining("one text");
+    }
+
+    private List<EmbeddingMatch<TextSegment>> search(String text, int 
maxResults) {
+        Embedding queryEmbedding = embeddingModel.embed(text).content();
+        return embeddingStore.search(
+                
EmbeddingSearchRequest.builder().queryEmbedding(queryEmbedding).maxResults(maxResults).build()).matches();
+    }
 }
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
index b8e79ccbf660..3983ed538c89 100644
--- 
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
@@ -16,9 +16,11 @@
  */
 package org.apache.camel.component.langchain4j.embeddingstore;
 
+import java.nio.charset.StandardCharsets;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.List;
+import java.util.Map;
 
 import dev.langchain4j.data.embedding.Embedding;
 import dev.langchain4j.data.segment.TextSegment;
@@ -296,6 +298,64 @@ class LangChain4jEmbeddingStoreBatchOperationsTest extends 
CamelTestSupport {
         assertThat(embeddingStore.getRemoveSingleInvocations()).isEqualTo(1);
     }
 
+    // ---- REMOVE: the id must be text ----
+
+    @Test
+    @DisplayName("REMOVE with a byte[] body removes the id read as a String")
+    void removeSingleIdFromBytes() {
+        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.getBytes(StandardCharsets.UTF_8))
+                .request(Exchange.class);
+
+        assertThat(result.getException()).isNull();
+        assertThat(embeddingStore.getRemoveSingleInvocations()).isEqualTo(1);
+        assertThat(embeddingStore.getLastRemovedId()).isEqualTo(id);
+    }
+
+    @Test
+    @DisplayName("REMOVE with a Map body fails and removes nothing")
+    void removeWithMapBodyThrows() {
+        Exchange result = fluentTemplate.to("langchain4j-embeddingstore:test")
+                .withHeader(LangChain4jEmbeddingStoreHeaders.ACTION, 
LangChain4jEmbeddingStoreAction.REMOVE)
+                .withBody(Map.of("id", "abc"))
+                .request(Exchange.class);
+
+        
assertThat(result.getException()).isInstanceOf(IllegalArgumentException.class)
+                .hasMessageContaining("REMOVE action requires the ID as a 
String body");
+        assertThat(embeddingStore.getRemoveSingleInvocations()).isZero();
+    }
+
+    @Test
+    @DisplayName("REMOVE with an Integer body fails and removes nothing")
+    void removeWithIntegerBodyThrows() {
+        Exchange result = fluentTemplate.to("langchain4j-embeddingstore:test")
+                .withHeader(LangChain4jEmbeddingStoreHeaders.ACTION, 
LangChain4jEmbeddingStoreAction.REMOVE)
+                .withBody(42)
+                .request(Exchange.class);
+
+        
assertThat(result.getException()).isInstanceOf(IllegalArgumentException.class)
+                .hasMessageContaining("java.lang.Integer");
+        assertThat(embeddingStore.getRemoveSingleInvocations()).isZero();
+    }
+
+    @Test
+    @DisplayName("REMOVE with a Collection of non-String ids fails and removes 
nothing")
+    void removeWithCollectionOfIntegersThrows() {
+        Exchange result = fluentTemplate.to("langchain4j-embeddingstore:test")
+                .withHeader(LangChain4jEmbeddingStoreHeaders.ACTION, 
LangChain4jEmbeddingStoreAction.REMOVE)
+                .withBody(List.of(1, 2))
+                .request(Exchange.class);
+
+        
assertThat(result.getException()).isInstanceOf(IllegalArgumentException.class)
+                .hasMessageContaining("Collection<String>")
+                .hasMessageContaining("java.lang.Integer");
+        assertThat(embeddingStore.getRemoveAllByIdsInvocations()).isZero();
+    }
+
     /**
      * Extended recording store that tracks which method variants are called.
      */
@@ -310,6 +370,7 @@ class LangChain4jEmbeddingStoreBatchOperationsTest extends 
CamelTestSupport {
         private int removeAllByFilterInvocations;
         private int removeAllInvocations;
         private String lastCallerSuppliedId;
+        private String lastRemovedId;
 
         @Override
         public String add(Embedding embedding) {
@@ -344,6 +405,7 @@ class LangChain4jEmbeddingStoreBatchOperationsTest extends 
CamelTestSupport {
         @Override
         public void remove(String id) {
             removeSingleInvocations++;
+            lastRemovedId = id;
             super.remove(id);
         }
 
@@ -375,6 +437,7 @@ class LangChain4jEmbeddingStoreBatchOperationsTest extends 
CamelTestSupport {
             removeAllByFilterInvocations = 0;
             removeAllInvocations = 0;
             lastCallerSuppliedId = null;
+            lastRemovedId = null;
         }
 
         int getAddWithIdInvocations() {
@@ -412,5 +475,9 @@ class LangChain4jEmbeddingStoreBatchOperationsTest extends 
CamelTestSupport {
         String getLastCallerSuppliedId() {
             return lastCallerSuppliedId;
         }
+
+        String getLastRemovedId() {
+            return lastRemovedId;
+        }
     }
 }
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 10f5a47c1b89..03bd3a495d65 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
@@ -611,6 +611,18 @@ which embeds its own relocated copy of Vert.x. The `vertx` 
option of `camel-aran
 See the 
https://github.com/fabric8io/kubernetes-client/blob/main/doc/MIGRATION-v8.md[Kubernetes
 client 8 migration guide]
 for the other changes.
 
+=== camel-langchain4j-embeddingstore - auto-embedding a TextSegment or a List 
body, and the REMOVE id
+
+With an `embeddingModel` and no `CamelLangChain4jEmbeddingsEmbedding` header, 
ADD and SEARCH now embed a `TextSegment`
+body as is, with its metadata. Before, they embedded and stored its 
`toString()`.
+
+ADD now embeds a `List` of `String` or `TextSegment` as a batch, one entry per 
element, and returns the `List` of the
+IDs. Before, the whole `List` was embedded and stored as one text (such as 
`[a, b]`), and the body was one ID. A `List`
+with another element type fails. SEARCH fails on a `List` body.
+
+REMOVE now fails when the body is not text (a `String`, bytes or a stream) or 
a `Collection` of `String` IDs. Before, a
+`Map` or a number body removed the ID made of its `toString()`, which usually 
removed nothing.
+
 === camel-resilience4j, camel-microprofile-fault-tolerance - a circuit breaker 
in a doTry
 
 A circuit breaker in the steps of a `doTry` no longer ends that `doTry` for 
the steps after it. Before, when the

Reply via email to