LangChain4j Ingest

JVM since3.39.0 Native since3.39.0 ๐ŸงชExperimental

Declarative AI document ingestion: point a knowledge base at a folder via configuration; splitting, embedding and storing are handled under the hood

Maven coordinates

Or add the coordinates to your existing project:

<dependency>
    <groupId>org.apache.camel.quarkus</groupId>
    <artifactId>camel-quarkus-langchain4j-ingest</artifactId>
</dependency>

Check the User guide for more information about writing Camel Quarkus applications.

Usage

Ingesting a directory

Point a pipeline at a folder and the files in it become a knowledge base. No route, no ingestion code:

quarkus.camel.langchain4j.ingest.products.source.directory=/var/data/product-docs
quarkus.camel.langchain4j.ingest.products.embedding-store=products
quarkus.camel.langchain4j.ingest.products.embedding-model=my-model

Each file is read as UTF-8 text โ€” there is no format parsing, so convert a PDF or DOCX before it reaches the pipeline โ€” split into overlapping segments (max-segment-size, max-overlap-size), embedded in batches and written to the store; a document is held in memory whole while it is split. embedding-store and embedding-model name CDI beans and may be omitted when the application has exactly one of each. Every segment carries camel_quarkus_pipeline and camel_quarkus_document_id metadata, so retrieval can cite which document an answer came from. Apart from enabled and the source.* settings shown here, properties are fixed at build time. A pipeline declared through runtime properties alone โ€” nothing but a source.directory, say โ€” is invisible to build-time validation; its checks, including the clash with an equally named @Ingest pipeline, report at startup instead.

This experimental extension ingests what it is given and keeps no record of it: a document ingested twice leaves two copies in the store, a restart re-reads the whole directory (the duplicate register is in-memory, sized for 100,000 files), and a polled consumer re-reads its source on every poll. Keeping the store in step with a changing source โ€” skipping unchanged documents, replacing edited ones, removing deleted ones โ€” needs that record and arrives with the synchronising engine in a later release.

Other sources, declared in Java

Any Camel consumer can feed a pipeline โ€” the roughly 300 components, each with its own options and its own documentation. Such a pipeline is declared in Java with @Ingest and the Camel Endpoint DSL:

@Ingest("events")
IngestPipeline events() {
    return IngestPipeline.from(Source.endpoint(dsl -> dsl.kafka("ingest-events").groupId("ingest"))
                    .documentId("CamelKafkaKey"))
            .embeddingStore("events");
}

Typing dsl. lists a factory for every component, each completing its own typed options. The method runs once at startup; it must return IngestPipeline, take no parameters and use a name no configured pipeline uses. enabled=false in configuration switches a Java-declared pipeline off. A component missing from the classpath fails at startup with an error naming the extension artifact that provides it โ€” the DSL compiles regardless, since its factories all ship in one artifact.

Ingestion needs a stable id per document, and where it lives is the consumer’s business: documentId names the header โ€” the record key CamelKafkaKey above, CamelAwsS3Key for S3 โ€” or gives a simple-language expression. Without it, the pipeline expects the CamelIngestDocumentId header and fails the exchange when it is absent. Mind each component’s own defaults, too: the aws2-s3 consumer deletes objects after reading them unless deleteAfterRead(false) is set โ€” a knowledge base reads its source, it does not consume it.

After such a pipeline ingests a document, the exchange body is replaced with the IngestResult, so a request-reply caller receives the outcome of its call.

The same pipeline can be declared purely in properties: source.uri takes the consumer URI as written, source.document-id the header name โ€” or a simple-language expression written $simple{...}, the one form MicroProfile Config leaves untouched. The URI is fixed at build time by design โ€” a runtime-overridable consumer URI would be arbitrary component invocation โ€” while property placeholders inside it still resolve at startup, keeping credentials and endpoints runtime configuration. Treat runtime configuration as the trust boundary it is: the directory and the id expression decide what the process reads into an often external store.

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
quarkus.camel.langchain4j.ingest.s3docs.embedding-store=products

When ingestion fails

A failure while splitting, embedding or storing โ€” a rate-limited model, an unreachable store โ€” propagates to the consumer; there is no dead-letter channel in this increment. For a directory pipeline the file stays where it is and is retried on the next poll, because the duplicate-protection key is only committed on success โ€” which also means a permanently failing file is retried forever, loudly. For a consumer-fed pipeline the component’s own error handling applies: a request-reply caller receives the exception, while a Kafka consumer with default settings logs the failure and commits the offset, so the record is dropped โ€” and since this engine keeps no record either, nothing remembers it. A record whose configured document-id resolves to nothing (a Kafka record without a key, say) fails the same way, one exchange at a time.

LangChain4j usage

Dependency management

In order to ensure alignment across all Quarkus and LangChain4j related dependencies, it is recommended to import the LangChain4j BOM as below:

<dependencyManagement>
  <dependencies>
    <dependency>
      <groupId>dev.langchain4j</groupId>
      <artifactId>langchain4j-bom</artifactId>
      <version>1.19.0</version>
      <type>pom</type>
      <scope>import</scope>
    </dependency>
  </dependencies>
  ...
</dependencyManagement>

Note that the import order is paramount when using maven dependencyManagement. As such, one might need to import the langchain4j-bom before other related Camel and Quarkus BOMs.

Quarkus LangChain4j support

This extension is designed and tested to work together with the Quarkus LangChain4j extensions. The EmbeddingStore and EmbeddingModel beans a pipeline writes through are ordinary CDI beans shared by both stacks โ€” including stores declared through Quarkus LangChain4j configuration โ€” and with the RAG augmentor bridge an @RegisterAiService interface answers from the store a pipeline filled, without glue code. The integration tests run both stacks in one application.

Additional Camel Quarkus configuration

Configuration property Type Default

The Camel consumer URI feeding this pipeline: any component, with its own options. Setting it is what makes the pipeline consume from that component; leaving it unset makes the pipeline read the directory named by the runtime source.directory property instead. Fixed at build time by design โ€” a runtime-overridable consumer URI would be arbitrary component invocation. Property placeholders inside it still resolve at startup, so credentials and endpoints remain runtime configuration.

string

Name of the EmbeddingStore bean to write to. When not set, the only one present is used.

string

Name of the EmbeddingModel bean to embed with. When not set, the only one present is used.

string

Maximum size of one segment, in characters.

int

500

How much of the previous segment each segment repeats, in characters. Overlap keeps a sentence split across a boundary retrievable from either side.

int

50

Whether this pipeline starts. Useful to switch ingestion off in dev mode.

boolean

true

The directory to ingest documents from, for a pipeline that has no source.uri. A path is a deployment concern, so unlike the URI it stays runtime configuration. Setting both is an error.

string

Whether subdirectories are ingested too, when reading a directory.

boolean

true

Where the document id lives in the exchange the consumer delivers: normally the name of a header, such as CamelAwsS3Key for an S3 consumer or CamelKafkaKey for a Kafka one. For an id that is not a plain header, write a simple-language expression in the $simple{...} form โ€” MicroProfile Config passes it through untouched, while a ${...} in a properties file would be consumed as a config expansion before Camel ever saw it. When not set, a pipeline reading a directory uses the file name, and one consuming from a component uses the CamelIngestDocumentId header.

string

Configuration property fixed at build time. All other configuration properties are overridable at runtime.