New to Kestra?
Use blueprints to kickstart your first workflows.
Build a RAG chat-with-your-data system with Kestra, Elasticsearch, and OpenAI. Retrieve relevant documents and generate grounded, factual AI answers.
Build a chat-with-your-data system that answers natural-language questions over your own documents using the Retrieval-Augmented Generation (RAG) pattern. This Kestra blueprint wires Elasticsearch full-text retrieval to an OpenAI large language model so users get factual, grounded answers instead of hallucinations. It solves the core RAG problem: an LLM alone does not know your private knowledge base, and a search index alone returns raw documents rather than a clean answer. Together, keyword retrieval plus generation turns an FAQ index into a conversational assistant for support, documentation, and internal knowledge.
search task (io.kestra.plugin.elasticsearch.Search) runs a bool query against the course_questions index, using a multi_match over the question, text, and section fields and a term filter on course. It returns the top five matching documents for the user's question.context_template task (io.kestra.plugin.core.debug.Return) loops over outputs.search.rows with a Pebble for block to assemble the retrieved sections, questions, and text into a single context string.generate_response task (io.kestra.plugin.openai.ChatCompletion) sends a teaching-assistant prompt with the user question and the built context to the gpt-4o model, capped at maxTokens: 500, instructed to answer only from context and return NONE when the answer is absent.log_output task (io.kestra.plugin.core.log.Log) extracts the generated answer from choices with a jq expression and logs it.Two inputs drive each run: a free-text question and a select_a_zoomcamp SELECT that scopes retrieval to one course.
Elasticsearch and OpenAI have no shared scheduler that can chain retrieval, prompt assembly, and generation into one governed pipeline. Kestra declares the whole flow in YAML, passes outputs between tasks with templated expressions, and gives you event triggers, retries on transient API or cluster errors, and full execution lineage with per-task logs and outputs. That is the gap the underlying tools cannot fill on their own: a single, observable, replayable definition of the entire RAG chain.
http://localhost:9200/) with a course_questions indexChatCompletion taskThis example uses inline configuration rather than Kestra secrets: the Elasticsearch hosts value and the OpenAI apiKey are set directly on the tasks. For any real deployment, replace the placeholder apiKey with {{ secret('OPENAI_API_KEY') }} and store the value in your secret backend.
docker run -it --rm --name elasticsearch -m 2G -p 9200:9200 -p 9300:9300 \
-e "discovery.type=single-node" -e "xpack.security.enabled=false" \
docker.elastic.co/elasticsearch/elasticsearch:8.14.3
course_questions index:curl -X PUT "http://localhost:9200/course_questions" \
-H "Content-Type: application/json" -d'
{ "mappings": { "properties": {
"text": { "type": "text" }, "section": { "type": "text" },
"question": { "type": "text" }, "course": { "type": "keyword" } } } }'
curl -X POST "http://localhost:9200/course_questions/_bulk" \
-H "Content-Type: application/json" \
--data-binary @<(curl -s \
https://huggingface.co/datasets/kestra/datasets/raw/main/json/zoomcamp_faq.json \
| jq -c '.[] | {"index":{}}, .')
generate_response task, then execute the flow with a question.indexes and the query fields at your own Elasticsearch index to chat with internal docs, tickets, or product content.multi_match for a vector or hybrid query to add semantic retrieval.NONE.Thanks to Faithful Adeda for contributing this example.