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-website.git


The following commit(s) were added to refs/heads/main by this push:
     new 1f68ed5d Blog: Document Ingestion for RAG in Camel 4.23 (#1848)
1f68ed5d is described below

commit 1f68ed5d733f42af07a20a14ee1a093b95e6a724
Author: Jiří Ondrušek <[email protected]>
AuthorDate: Tue Nov 3 18:08:21 2026 +0100

    Blog: Document Ingestion for RAG in Camel 4.23 (#1848)
    
    * Blog: ingest documents into a vector store with Camel and LangChain4j
    
    Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
    
    * Blog: address review feedback
    
    - Date: 2026-11-03
    - Camel Quarkus: two parts only, 3.x (released 3.39.0 and 3.40.0) and
      4.x (4.0.0, not released yet), each fact once
    - 3.x: text engine, properties pipeline, other consumers, metadata
      keys, register, native mode, RAG example linked at its 3.40.0 tag
    - 4.x: the extension drives the component and composes the Kamelets;
      new properties, route names, parsers (the example adds only the
      parser line), filters and media (a 4.x feature, no PR link), native
      Kamelets and Tika
    - Version attribution: source.uri and @Ingest since 3.39.0;
      persistent register and IdempotentRepository reflection since 3.40.0
    - Summary (preview, intro) and "Use It in Your Project" follow the
      3.x/4.x split, with release names instead of `main`; the preview no
      longer lists Camel Main
    - Headings: the second "Try It" is now "Use It in Your Project"
    - Run It Anywhere: install Camel JBang or the Camel CLI Launcher, both
      pinned to 4.23.0, then one shared camel run command (no
      --camel-version, --kamelets-version=4.23.0); tested with both on
      JDK 25
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
    
    ---------
    
    Co-authored-by: jiriOndrusek-agent <[email protected]>
    Co-authored-by: Claude Sonnet 5.5 <[email protected]>
---
 .../11/camel-document-ingestion-rag/featured.jpg   | Bin 0 -> 208295 bytes
 .../2026/11/camel-document-ingestion-rag/index.md  | 320 +++++++++++++++++++++
 2 files changed, 320 insertions(+)

diff --git a/content/blog/2026/11/camel-document-ingestion-rag/featured.jpg 
b/content/blog/2026/11/camel-document-ingestion-rag/featured.jpg
new file mode 100644
index 00000000..49b745d9
Binary files /dev/null and 
b/content/blog/2026/11/camel-document-ingestion-rag/featured.jpg differ
diff --git a/content/blog/2026/11/camel-document-ingestion-rag/index.md 
b/content/blog/2026/11/camel-document-ingestion-rag/index.md
new file mode 100644
index 00000000..cd0f1ba8
--- /dev/null
+++ b/content/blog/2026/11/camel-document-ingestion-rag/index.md
@@ -0,0 +1,320 @@
+---
+title: "Document Ingestion for RAG in Camel 4.23: From Folder to Vector Store"
+date: 2026-11-03
+draft: false
+authors: [JiriOndrusek]
+categories: ["AI", "Camel"]
+preview: "Camel 4.23 adds the langchain4j-ingest component and four ingestion 
Kamelets. Turn a folder, an S3 bucket or a Kafka topic into a RAG knowledge 
base of documents, images, audio or video, from Java, YAML or Camel JBang (with 
a small bean file): parse, filter, split, embed and store. Camel Quarkus 3.x 
ingests text from a few properties; 4.0.0 will do all of it."
+---
+
+Camel 4.23 can turn a folder of documents into a knowledge base for 
retrieval-augmented generation (RAG). The new `camel-langchain4j-ingest` 
component splits a message body, embeds the segments and writes them to a 
LangChain4j `EmbeddingStore`. Four new Kamelets in `camel-kamelets` 4.23 add 
the rest of the pipeline: a directory source, two parsers and a sink.
+
+All of it runs from the Java DSL, the YAML DSL and Camel Main. Camel JBang 
runs it too, once you define the store and the model as beans in a small Java 
file ([how to define 
beans](/manual/camel-jbang-beans.html#_using_camel_dependency_injection)). 
Camel Quarkus declares a pipeline in a few properties: text only in 3.x, all of 
it in 4.0.0 (not released yet).
+
+This post covers the component, the Kamelets and the Camel Quarkus extension.
+
+## The Problem: Ingestion Is Plumbing
+
+A RAG application is only as good as what is in its store. Filling the store 
is the part every project writes again: read the files, get the text out, split 
it, embed it, store it, and don't do it all again on the next poll.
+
+Three things make it harder than it looks:
+
+- **Formats.** Knowledge lives in PDFs, office files, images, audio and video, 
not plain text.
+- **Repetition.** Every poll lists the whole folder, so the source keeps a 
list of the files it has already read and skips them (the register). Without 
it, a running route embeds the whole directory again and again.
+- **Hygiene.** Drafts, confidential files and oversized uploads should never 
reach the store.
+
+## The Solution: `langchain4j-ingest`
+
+The [`langchain4j-ingest`](/components/next/langchain4j-ingest-component.html) 
component is a producer. It takes the message body, splits it into overlapping 
segments, embeds them in batches and writes them to the store. Any Camel 
consumer can feed it, and an optional parser goes in front:
+
+```
+┌──────────────────┐    ┌──────────────────┐    ┌──────────────────────┐
+│      source      │    │      parser      │    │  langchain4j-ingest  │
+│ file, S3, Kafka, │───▶│    (optional)    │───▶│    filter · split    │
+│   any consumer   │    │  Tika | Docling  │    │    embed · store     │
+└──────────────────┘    └──────────────────┘    └──────────────────────┘
+```
+
+Retrieval is served by the `langchain4j-embeddingstore` component or any 
LangChain4j retriever over the same store.
+
+Text is not the only input. With `modality=media` the component embeds an 
image, an audio clip, a video or a PDF whole, as one vector, using a multimodal 
embedding model, so a folder of photos is retrievable like a folder of 
documents (see below). Several LangChain4j providers embed images today; audio 
needs a model of your own.
+
+### A Folder in One Route
+
+```java
+from("file:/var/data/product-docs?noop=true&recursive=true&readLock=changed&charset=UTF-8")
+    .to("langchain4j-ingest:products?documentIdHeader=CamelFileName");
+```
+
+That is a complete pipeline. By default, segments are 500 characters with a 50 
character overlap, embedded in batches of 32. `noop=true` leaves the files in 
place, because a knowledge base reads its source, and makes the file consumer 
remember what it has read. `readLock=changed` waits for a file that is still 
being copied.
+
+Two limits. The route reads files as UTF-8 text, so a PDF fails on every poll 
or lands in the store as raw PDF syntax. Limit it with 
`antInclude=**/*.md,**/*.txt`, or parse first with the Kamelets below. And its 
register holds only 1000 files: in a bigger folder, every file is embedded 
again on every poll, unless you give the `file` endpoint a sized 
`idempotentRepository` (see Good to Know).
+
+`embeddingStore` and `embeddingModel` are autowired when the registry holds 
exactly one bean of each type. Otherwise name the bean: 
`embeddingStore=#bean:name`. On Camel Main, `camel.beans.*` binds a pair that 
needs no API key (add 
`dev.langchain4j:langchain4j-embeddings-all-minilm-l6-v2`):
+
+```properties
+camel.beans.store=#class:dev.langchain4j.store.embedding.inmemory.InMemoryEmbeddingStore
+camel.beans.model=#class:dev.langchain4j.model.embedding.onnx.allminilml6v2.AllMiniLmL6V2EmbeddingModel
+```
+
+Every segment carries `camel_ingest_pipeline` and `camel_ingest_document_id` 
metadata, so an answer can cite its document. The message body becomes an 
`IngestResult` whose outcome is `ingested`, `empty`, `skipped` or `filtered`.
+
+### Any Consumer as the Source
+
+The document id lives wherever the consumer puts it. `documentIdHeader` names 
it:
+
+```java
+from("aws2-s3://product-docs?region=eu-west-1&deleteAfterRead=false")
+    .to("langchain4j-ingest:s3docs?documentIdHeader=CamelAwsS3Key");
+```
+
+Mind each consumer's defaults: `aws2-s3` deletes objects after reading them 
unless `deleteAfterRead=false`. For sources that redeliver, such as Kafka, add 
`idempotentRepository=#bean:register`. A duplicate id is answered `skipped`, 
and the first write wins, so an updated document with the same id is skipped 
too: a Kafka topic of updates never updates the store.
+
+### Filters and Size Limits
+
+```java
+from("file:knowledge-base?noop=true")
+    .to("langchain4j-ingest:docs?documentIdHeader=CamelFileName"
+        + 
"&includeId=**/*.md,**/*.txt&excludeId=**/draft-*&minDocumentSize=100");
+```
+
+`includeId` and `excludeId` take comma-separated Ant-style patterns for the 
document id. Exclusion wins. They run before the body is read, so a rejected 
document costs nothing. `minDocumentSize` drops documents too short to be 
useful, and `documentFilter=#bean:name` takes any `Predicate`. All of them 
answer `filtered`.
+
+`maxDocumentSize` is a limit, not a filter. The pipeline holds a document in 
memory whole, so an oversized one fails the exchange. Set it when the source 
can deliver large payloads.
+
+### Images, and Other Media with Your Own Model
+
+Not everything should become text. With `modality=media` the body is read as 
bytes and embedded whole, as one vector, by a multimodal embedding model:
+
+```java
+from("file:/var/data/photos?noop=true&recursive=true&readLock=changed&antInclude=**/*.jpg,**/*.png")
+    
.to("langchain4j-ingest:photos?modality=media&documentIdHeader=CamelFileName");
+```
+
+The MIME type comes from the file extension, or from `contentType`. The model 
must list that medium in `supportedContentTypes()`, and a text-only model fails 
the start. The vector is stored with a placeholder segment whose text is the 
document id, so retrieval cites it like any other.
+
+Several LangChain4j providers embed images. As of LangChain4j 1.21 none embeds 
audio out of the box, so that takes an `EmbeddingModel` of your own. Video and 
PDF need a model that declares them as well.
+
+## Kamelets: The Same Pipeline, Declared
+
+Parsing is the fiddly part. A parser copies document metadata over the message 
headers, so a route has to capture the document id before the parse, or a 
crafted document could forge its own. `camel-kamelets` 4.23 ships parsers that 
do it for you, with a directory source and a sink to match:
+
+- 
[`langchain4j-ingest-file-source`](/camel-kamelets/next/langchain4j-ingest-file-source.html)
 watches a directory. Files stay in place, unchanged files are remembered, and 
a file still being copied is waited for.
+- 
[`tika-extract-text-action`](/camel-kamelets/next/tika-extract-text-action.html)
 extracts plain text in-process. Each format needs its Tika parser module, such 
as `tika-parser-pdf-module`.
+- [`docling-convert-action`](/camel-kamelets/next/docling-convert-action.html) 
converts documents to structure-preserving markdown with Docling, OCR included. 
It uses a local CLI or a [Docling 
Serve](https://github.com/docling-project/docling-serve) instance.
+- 
[`langchain4j-ingest-sink`](/camel-kamelets/next/langchain4j-ingest-sink.html) 
wraps the component and takes its ingest options, such as `includeId` and 
`modality`.
+
+A folder of PDFs in the YAML DSL:
+
+```yaml
+- route:
+    from:
+      uri: kamelet:langchain4j-ingest-file-source
+      parameters:
+        directory: /var/data/reports
+      steps:
+        - to:
+            uri: kamelet:tika-extract-text-action
+        - to:
+            uri: kamelet:langchain4j-ingest-sink
+            parameters:
+              pipelineName: reports
+```
+
+Filters and media are sink parameters (`includeId`, `modality: media`). 
`include` and `exclude` on the source narrow the files before they are read. To 
parse with Docling Serve instead, swap the Tika step:
+
+```yaml
+        - to:
+            uri: kamelet:docling-convert-action
+            parameters:
+              useDoclingServe: true
+              doclingServeUrl: http://docling-serve:5001
+```
+
+### Run It Anywhere
+
+Camel JBang resolves the Kamelets' dependencies, but you bind the store and 
the model yourself. Save the PDF route above as `ingest.camel.yaml` and add a 
`Beans.java`:
+
+```java
+import 
dev.langchain4j.model.embedding.onnx.allminilml6v2.AllMiniLmL6V2EmbeddingModel;
+import dev.langchain4j.store.embedding.inmemory.InMemoryEmbeddingStore;
+import org.apache.camel.BindToRegistry;
+import org.apache.camel.Configuration;
+
+@Configuration
+public class Beans {
+
+    @BindToRegistry
+    public InMemoryEmbeddingStore<?> store() {
+        return new InMemoryEmbeddingStore<>();
+    }
+
+    @BindToRegistry
+    public AllMiniLmL6V2EmbeddingModel model() {
+        // JBang's context class loader cannot see the --dep jar that holds 
the model file
+        
Thread.currentThread().setContextClassLoader(AllMiniLmL6V2EmbeddingModel.class.getClassLoader());
+        return new AllMiniLmL6V2EmbeddingModel();
+    }
+}
+```
+
+To see a result, add `ask.camel.yaml`, a route that asks the store one 
question ten seconds after the start:
+
+```yaml
+- route:
+    from:
+      uri: timer:ask?delay=10000&repeatCount=1
+      steps:
+        - setBody:
+            expression:
+              constant:
+                expression: How much did revenue grow?
+        - to:
+            uri: langchain4j-embeddingstore:reports?action=SEARCH&maxResults=1
+        - split:
+            expression:
+              simple:
+                expression: "${body}"
+            steps:
+              - log:
+                  message: "${body.embedded.text} 
(${body.embedded.metadata.getString('camel_ingest_document_id')})"
+```
+
+Install Camel JBang or the Camel CLI Launcher. Either gives you the `camel` 
command:
+
+- Camel JBang (JBang asks you to trust github.com/apache/camel):
+
+  ```bash
+  jbang app install --force --fresh -Dcamel.jbang.version=4.23.0 
-Dcamel-kamelets.version=4.23.0 camel@apache/camel
+  ```
+
+- The [Camel CLI Launcher](/blog/2026/07/camel-cli-launcher-installers/), 
which needs a JDK 17+:
+
+  ```bash
+  curl -fsSL https://camel.apache.org/install.sh | sh -s -- --version 4.23.0
+  ```
+
+Both run the three files with the same command, which adds the PDF module and 
the model (it runs in-process, no API key):
+
+```bash
+camel run ingest.camel.yaml ask.camel.yaml Beans.java 
--kamelets-version=4.23.0 \
+    
--dep=org.apache.tika:tika-parser-pdf-module:4.1.0,dev.langchain4j:langchain4j-embeddings-all-minilm-l6-v2:1.21.0-beta31
+```
+
+The installs pin 4.23.0 because Camel JBang defaults to the 4.22 LTS, and the 
launcher runs only the version it was installed with (it rejects 
`--camel-version`). The launcher bundles the 4.22 Kamelets, hence 
`--kamelets-version=4.23.0`.
+
+For a `q3-report.pdf` in `/var/data/reports`, the log shows the matching 
passage and the document it came from:
+
+```
+Revenue grew twelve percent in the third quarter, while support tickets fell 
by a third. (q3-report.pdf)
+```
+
+On Camel Main, add `camel-langchain4j-ingest`, `camel-yaml-dsl`, 
`camel-kamelet`, `camel-file`, `camel-tika`, `tika-parser-pdf-module` and 
`org.apache.camel.kamelets:camel-kamelets`.
+
+### Good to Know
+
+- **No record.** The pipeline keeps no record of what it wrote. A document 
removed from the source stays in the store, and a document ingested again lands 
on top of its old segments.
+- **Registers.** The Kamelet source remembers a file by path, modification 
time and size, so an edited file is read again. A plain `file` endpoint with 
`noop=true` remembers it by path only, so an edit is ignored. The register 
lives in memory and holds 1000 entries. After a restart it is empty and the 
whole folder is ingested again. In a folder of more than 1000 files the oldest 
entries fall out, so files come back on every poll. For a real knowledge base 
give the source a persistent reg [...]
+
+  ```properties
+  
camel.beans.register=#class:org.apache.camel.support.processor.idempotent.FileIdempotentRepository
+  
camel.beans.register.fileStore=#class:java.io.File('/var/data/ingest/register.dat')
+  camel.beans.register.cacheSize=100000
+  ```
+
+  Name it on the Kamelet source:
+
+  ```yaml
+  parameters:
+    directory: /var/data/reports
+    idempotentRepository: "#bean:register"
+  ```
+
+  A plain consumer takes it in the URI: 
`file:/var/data/product-docs?noop=true&idempotentRepository=#bean:register`.
+- **Failures.** A failing file on a directory source is retried on every poll.
+- **Support level.** The component and the Kamelets are Preview.
+
+## Camel Quarkus 3.x
+
+The `camel-quarkus-langchain4j-ingest` extension is experimental and has been 
part of Camel Quarkus since 3.39.0. In 3.x it ingests text with an engine of 
its own. A directory pipeline reads each file as UTF-8 text, so a PDF has to be 
converted first. A pipeline is a handful of properties:
+
+```properties
+quarkus.camel.langchain4j.ingest.reports.source.directory=/var/data/reports
+quarkus.camel.langchain4j.ingest.reports.embedding-store=reports
+quarkus.camel.langchain4j.ingest.reports.embedding-model=my-model
+```
+
+`embedding-store` and `embedding-model` name CDI beans by their `@Named` 
value. Leave them out when the application has exactly one of each. Segment 
size and overlap are properties too.
+
+Any other consumer feeds a pipeline with `source.uri` and 
`source.document-id`, or in Java with `@Ingest` and the Endpoint DSL:
+
+```properties
+quarkus.camel.langchain4j.ingest.s3docs.source.uri=aws2-s3://product-docs?region=eu-west-1&deleteAfterRead=false
+quarkus.camel.langchain4j.ingest.s3docs.source.document-id=CamelAwsS3Key
+```
+
+```java
+@Ingest("events")
+IngestPipeline events() {
+    return IngestPipeline.from(Source.endpoint(dsl -> 
dsl.kafka("ingest-events").groupId("ingest"))
+                    .documentId("CamelKafkaKey"))
+            .embeddingStore("events");
+}
+```
+
+Segments carry the metadata keys of the component, `camel_ingest_pipeline` and 
`camel_ingest_document_id`, since 3.40.0 (3.39.0 writes 
`camel_quarkus_pipeline` and `camel_quarkus_document_id`). The extension sizes 
the file register at 100,000 keys. It lives in memory, so a restart ingests the 
folder once more. Since 3.40.0, `source.idempotent-repository` names a 
persistent one.
+
+The extension supports native mode, and CI runs its integration tests against 
the native executable. Since 3.40.0 it registers the `IdempotentRepository` 
classes it finds for reflection, so a `#class:` register works too.
+
+The 
[`langchain4j-ingest-rag`](https://github.com/apache/camel-quarkus-examples/tree/3.40.0/langchain4j-ingest-rag)
 example, pinned to 3.40.0, shows a properties-only pipeline feeding a RAG 
assistant.
+
+## Camel Quarkus 4.x
+
+In Camel Quarkus 4.0.0, which is not released yet, the extension drives the 
component and composes each pipeline from the Kamelets. The 3.x properties and 
`@Ingest` stay. Batch size, `max-document-size` and a `document-splitter` bean 
are new properties. Still no route, no ingestion code. The full options are in 
the [extension 
reference](/camel-quarkus/next/reference/extensions/langchain4j-ingest.html).
+
+A pipeline named `reports` runs as the route `langchain4j-ingest-reports` plus 
one route per Kamelet it uses, suffixed `-source`, `-parser` and `-sink`. To 
pause a directory pipeline, stop its `-source` route.
+
+### Parsers
+
+One property adds a parser to the `reports` pipeline above:
+
+```properties
+quarkus.camel.langchain4j.ingest.reports.parser=tika
+```
+
+`parser` is `tika` or `docling`. Tika needs `camel-quarkus-tika` and a parser 
module such as `tika-parser-pdf-module`. Docling needs `camel-quarkus-docling`, 
`camel.component.docling.use-docling-serve=true` and 
`camel.component.docling.docling-serve-url`. A missing parser extension fails 
the build and names the artifact.
+
+### Filters and Media
+
+The component's filters and media modality are also available as properties:
+
+```properties
+quarkus.camel.langchain4j.ingest.products.filter.include-id=**/*.pdf,**/*.md
+quarkus.camel.langchain4j.ingest.products.filter.exclude-id=**/draft-*
+quarkus.camel.langchain4j.ingest.products.filter.min-document-size=100
+
+quarkus.camel.langchain4j.ingest.photos.source.directory=/var/data/photos
+quarkus.camel.langchain4j.ingest.photos.modality=media
+quarkus.camel.langchain4j.ingest.photos.embedding-model=my-multimodal-model
+quarkus.camel.langchain4j.ingest.photos.filter.include-id=**/*.png,**/*.jpg
+```
+
+`filter.document-filter` names a `Predicate` bean and `content-type` sets the 
MIME type. Media cannot be combined with `parser` or `document-splitter`. With 
a parser, the id patterns run before the parse, so an excluded document never 
reaches Tika or Docling Serve.
+
+### Native Mode
+
+The Kamelets the pipelines use are always embedded in the native executable, 
even if `quarkus.camel.kamelet.identifiers` is narrowed. Tika's HTML, plain 
text and PDF modules work in native mode.
+
+## Try It Yourself
+
+A runnable example is in the 
[`camel-langchain4j-ingest-demo`](https://github.com/jiriOndrusek/camel-langchain4j-ingest-demo)
 repository. Clone it and run `./run.sh`. It needs Java 21+ and JBang, and runs 
fully local, with no API key and no Docker.
+
+## Use It in Your Project
+
+The component and the Kamelets ship in Apache Camel 4.23 and `camel-kamelets` 
4.23.0.
+
+1. Add `camel-langchain4j-ingest` and bind an `EmbeddingModel` and an 
`EmbeddingStore` in the registry.
+2. Write the route in the DSL you prefer, with the Tika or Docling Kamelet if 
you parse.
+3. On Quarkus, add `camel-quarkus-langchain4j-ingest` and declare the pipeline 
in `application.properties`. Camel Quarkus 3.x ingests text, and 4.0.0 (not 
released yet) adds parsers, filters and media.
+
+Your documents are now a knowledge base. Declare the pipeline, drop in the 
files.

Reply via email to