CAMEL-23953: Add batch operations for langchain4j embeddings and embeddingstore - #25273
CAMEL-23953: Add batch operations for langchain4j embeddings and embeddingstore#25273gnodet wants to merge 3 commits into
Conversation
|
🌟 Thank you for your contribution to the Apache Camel project! 🌟 🐫 Apache Camel Committers, please review the following items:
|
apupier
left a comment
There was a problem hiding this comment.
several files require a regen
modified: catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/langchain4j-embeddings.json
modified: catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/langchain4j-embeddingstore.json
modified: catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/langchain4j-embeddings-component.adoc
modified: catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/langchain4j-embeddingstore-component.adoc
modified: dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/LangChain4jEmbeddingStoreEndpointBuilderFactory.java
modified: dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/LangChain4jEmbeddingsEndpointBuilderFactory.java
|
🧪 CI tested the following changed modules:
🔬 Scalpel shadow comparison — Scalpel: 560 tested, 27 compile-only — current: 558 all testedMaveniverse Scalpel detected 587 affected modules (current approach: 558).
|
gnodet
left a comment
There was a problem hiding this comment.
The batch operations design is well-structured and addresses a real performance concern for RAG pipelines. Tests are well-written using AssertJ and a recording store pattern, and documentation with Java/YAML examples is good. However, there are several issues that should be addressed:
[HIGH] Destructive removeAll() fallback — The remove() method now calls store.removeAll() (clearing the entire store) when body is null/empty and no filter is set. Previously this would throw IllegalArgumentException via langchain4j's ensureNotBlank() — a safe failure mode. Making the most destructive operation the default fallback when no input is provided is a data safety risk. Consider requiring explicit intent (e.g., a dedicated header flag or a distinct action like CLEAR).
[MEDIUM] Caller-supplied IDs silently discarded in addBatch() — When EMBEDDING_IDS header is set but no TextSegment body is provided, the code falls through to store.addAll(embeddings) which generates new IDs, silently ignoring the user-provided IDs. Should either loop with add(id, embedding) or throw an error explaining that caller-supplied IDs require text segments for batch operations.
[MEDIUM] FQCN for ArrayList — new java.util.ArrayList<>() used without importing ArrayList, violating the project's no-FQCN convention.
[MEDIUM] EMBEDDINGS header not in CamelLangchain4jAttributes — The new cross-component header constant uses a hardcoded string instead of referencing CamelLangchain4jAttributes in core/camel-api, breaking consistency with the existing EMBEDDING, VECTOR, TEXT_SEGMENT pattern.
[LOW] Missing upgrade guide entry — No entry in the upgrade guide for the behavioral change in REMOVE operation (null body previously threw exception, now clears the store).
This review was generated by an AI agent and may contain inaccuracies. Please verify all suggestions before applying.
Claude Code on behalf of Guillaume Nodet
| * {@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> | ||
| * <li><b>Clear all</b>: when the body is null/empty and no filter is set, removes all embeddings from the store via |
There was a problem hiding this comment.
[HIGH] When the body is null/empty and no filter is set, this falls through to store.removeAll(), silently clearing the entire embedding store. Previously, remove(null) would call ensureNotBlank(id, "id") in langchain4j's default EmbeddingStore.remove(), which threw an IllegalArgumentException — a safe failure mode.
A destructive "clear all" operation should require explicit intent, not be the default fallback when nothing else matches. Consider requiring a dedicated action (e.g., CLEAR) or a confirmation header like CamelLangchain4jEmbeddingStoreClearAll=true.
There was a problem hiding this comment.
Claude Code on behalf of gnodet
Fixed in f267c76. The remove() method no longer falls through to store.removeAll() when body is null/empty. It now throws IllegalArgumentException with a clear message requiring either a String body (single ID), Collection body (batch IDs), or a Filter header. The "Clear All" section has been removed from the documentation.
|
Claude Code on behalf of gnodet All review findings have been addressed in commit f267c76:
Regarding the upgrade guide point: since the REMOVE-with-null-body behavior never existed in a released version (it was introduced in this PR's first commit and corrected in this follow-up), there's no behavioral change for existing users to migrate from — so no upgrade guide entry is needed. |
gnodet
left a comment
There was a problem hiding this comment.
Thorough batch-operations implementation for both embedding and embedding-store components. The caller-supplied-ID support and batch REMOVE are welcome additions. A few data-flow issues stood out that could affect RAG ingestion pipelines:
1. Text segments lost in batch embed → store pipeline (medium)
In LangChain4jEmbeddingsProducer.processBatch(), the message body is overwritten with List<Embedding> at line 96 (message.setBody(embeddings)). Unlike the single-document path — which preserves the original text segment via the TEXT_SEGMENT header — the batch path sets no TEXT_SEGMENTS header. When this flows downstream to the embedding store producer, addBatch() checks whether the body is List<TextSegment> but finds List<Embedding> instead, falling through to addAll(embeddings) and storing embeddings without their text segments.
This is the exact chaining pipeline documented in the PR's own langchain4j-embeddingstore-component.adoc (embed → store), making it a real data-loss scenario for RAG pipelines — search results will return embeddings but not the original text.
2. Size mismatch between callerIds and embeddings not validated (medium)
In LangChain4jEmbeddingStoreProducer.addBatch(), when callerIds != null && textSegments == null, the loop iterates embeddings.size() times and accesses callerIds.get(i) without a size-equality check. If callerIds.size() < embeddings.size(), this throws an unhelpful IndexOutOfBoundsException. If callerIds.size() > embeddings.size(), trailing IDs are silently ignored and incorrectly included in the response body. A defensive size check with a clear error message would improve debuggability.
3. Caller-supplied ID path silently drops text segment (medium)
In the single-add path, when callerId != null, store.add(callerId, embedding) is called and the text-segment check is skipped entirely. The langchain4j EmbeddingStore API has no add(String id, Embedding, TextSegment) method, which explains the current code. However, addAll(List<String>, List<Embedding>, List<TextSegment>) exists and could be used with singleton lists as a workaround. The PR's documentation example shows a chain that sets EMBEDDING_ID after embedding — this exact pipeline loses the text segment.
This review was generated by an AI agent and may contain inaccuracies. Please verify all suggestions before applying.
Claude Code on behalf of @gnodet
…ddingstore - LangChain4jEmbeddingsProducer: support List<TextSegment> body for embedAll() batch embedding in a single API call - LangChain4jEmbeddingStoreProducer ADD: support addAll() batch variants, caller-supplied IDs via EMBEDDING_ID/EMBEDDING_IDS headers - LangChain4jEmbeddingStoreProducer REMOVE: support removeAll(Collection), removeAll(Filter) via existing FILTER header, and removeAll() to clear store - Add EMBEDDINGS header constant for batch embedding results - Add EMBEDDING_ID and EMBEDDING_IDS header constants for caller-supplied IDs - Add unit tests for all batch operations (11 new tests) - Update documentation for both components Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Regenerate generated sources for the new EMBEDDINGS, EMBEDDING_ID, and EMBEDDING_IDS headers added to the langchain4j embedding components. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- [HIGH] Remove destructive removeAll() fallback when body is null/empty and no filter set. Now throws IllegalArgumentException requiring explicit ID(s) or filter. Prevents accidental store clearing. - [MEDIUM] Honor caller-supplied IDs when no text segments are provided in batch ADD. Loops with add(id, embedding) instead of silently discarding user-provided IDs. - [MEDIUM] Fix FQCN: replace java.util.ArrayList with proper import. - [MEDIUM] Add CAMEL_LANGCHAIN4J_EMBEDDINGS constant to CamelLangchain4jAttributes in core/camel-api for cross-component consistency with existing EMBEDDING, VECTOR, TEXT_SEGMENT pattern. - Add test for batch ADD with caller IDs but no text segments. - Update removeClearAll test to expect IllegalArgumentException. - Remove "Clear All" section from embeddingstore documentation. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
f267c76 to
a3c3ecb
Compare
davsclaus
left a comment
There was a problem hiding this comment.
Thank you for this well-structured addition of batch operations to the langchain4j embedding components — the RecordingEmbeddingStore test pattern is particularly good, and the documentation with Java/YAML tabs is solid.
Two medium-severity issues remain from the prior self-review rounds that should be addressed before merging.
This review focuses on project rules and conventions. It does not replace specialized AI review tools (CodeRabbit, Sourcery) or static analyzers (SonarCloud).
This review was generated by an AI agent and may contain inaccuracies. Please verify all suggestions before applying.
Claude Code on behalf of davsclaus
| 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++) { |
There was a problem hiding this comment.
[MEDIUM] Size mismatch between callerIds and embeddings not validated
This loop iterates embeddings.size() times and accesses callerIds.get(i) without checking callerIds.size() == embeddings.size(). If the caller provides fewer IDs than embeddings, this throws an unhelpful IndexOutOfBoundsException. If more IDs than embeddings, trailing IDs are silently ignored but returned in the response body.
Consider adding a size-equality check before the loop:
| for (int i = 0; i < embeddings.size(); i++) { | |
| if (callerIds.size() != embeddings.size()) { | |
| throw new IllegalArgumentException( | |
| "EMBEDDING_IDS size (" + callerIds.size() + ") must match EMBEDDINGS size (" + embeddings.size() + ")"); | |
| } | |
| for (int i = 0; i < embeddings.size(); i++) { |
The same validation would also be useful before the addAll(callerIds, embeddings, textSegments) call above, since langchain4j's error message for mismatched sizes is less informative.
|
|
||
| List<Embedding> embeddings = result.content(); | ||
| message.setHeader(LangChain4jEmbeddingsHeaders.EMBEDDINGS, embeddings); | ||
| message.setBody(embeddings); |
There was a problem hiding this comment.
[MEDIUM] Text segments lost in batch embed-to-store pipeline
The body is overwritten with List<Embedding> and no TEXT_SEGMENTS header is set to preserve the input text segments. The single path preserves the original text via the TEXT_SEGMENT header, but the batch path has no equivalent.
When chained with the embedding store producer (the exact pipeline documented in this PR's own langchain4j-embeddingstore-component.adoc), text segments are lost — the store sees List<Embedding> as body, not List<TextSegment>, so addBatch() falls through to addAll(embeddings) without text.
This means search results will return embeddings but not the original text, which is a data-loss concern for RAG pipelines.
Consider preserving the segments via a TEXT_SEGMENTS header (or as the body, with embeddings only in the EMBEDDINGS header) to mirror the single-item behavior.
| return; | ||
| } | ||
|
|
||
| throw new IllegalArgumentException( |
There was a problem hiding this comment.
[LOW] Missing upgrade guide entry for REMOVE behavior change
This IllegalArgumentException is an improvement over the previous behavior (passing null to store.remove(null)), but it's a behavioral change that could affect existing routes.
Per project conventions, behavioral changes should be documented in docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_22.adoc.
|
|
||
| if (in.getHeader(LangChain4jEmbeddingsHeaders.TEXT_SEGMENT) != null) { | ||
| if (callerId != null) { | ||
| store.add(callerId, embedding); |
There was a problem hiding this comment.
[LOW] Caller-supplied ID path silently drops text segment on single add
When callerId is set, store.add(callerId, embedding) is called and the text-segment check is skipped entirely. If a TEXT_SEGMENT header is also present, it's silently lost.
The langchain4j API has no add(String id, Embedding, TextSegment) overload, but addAll(List<String>, List<Embedding>, List<TextSegment>) exists and could be used with singleton lists to preserve the text segment. Worth documenting this limitation at minimum.
Summary
Add batch operation support to the LangChain4j embeddings and embedding store components, addressing CAMEL-23953.
The upstream LangChain4j API provides batch variants (
embedAll,addAll,removeAll) but the Camel components only wired single-item calls. This PR adds:Embeddings component (
camel-langchain4j-embeddings):EmbeddingModel.embedAll()when body is aList<String>orList<TextSegment>EMBEDDINGSheader (CamelLangChain4jEmbeddingsEmbeddings) for the resultingList<Embedding>String/TextSegmentbody still works as beforeEmbedding store component (
camel-langchain4j-embeddingstore):addAll()whenEMBEDDINGSheader containsList<Embedding>EMBEDDING_IDheader →add(id, embedding)EMBEDDING_IDSheader (with or without text segments)removeAll(Collection<String>)when body is aCollection<String>removeAll(Filter)whenFILTERheader is set on REMOVE actionCross-component consistency:
CAMEL_LANGCHAIN4J_EMBEDDINGSconstant toCamelLangchain4jAttributesincore/camel-api, consistent with existingEMBEDDING,VECTOR,TEXT_SEGMENTpatternTest plan
LangChain4jEmbeddingsBatchTest— 3 tests covering batch embedAll, single backward compat, token usageLangChain4jEmbeddingStoreBatchOperationsTest— 9 tests usingRecordingEmbeddingStoreto verify correct method dispatch:IllegalArgumentException🤖 Generated with Claude Code
Claude Code on behalf of gnodet