New to Kestra?
Use blueprints to kickstart your first workflows.
Sync a MinIO bucket into Qdrant with Tika and Ollama in Kestra. Handles new, changed and deleted files, and heals failed runs. Local, no API keys.
Most RAG demos index a folder once and never look at it again. Documents change, get deleted and sometimes fail to parse, and the index quietly drifts from the source. This flow keeps a Qdrant collection in sync with a MinIO bucket, so dropping, replacing or removing a file is all it takes to update the knowledge base. Text extraction (Apache Tika), embeddings (Ollama) and storage all run on your machine, so documents never leave it.
Pair it with local-rag-knowledge-base-ask, which answers questions over the same collection with a local LLM and refuses the ones your documents do not cover.
This blueprint was created by kruxarth.
on_document_change (io.kestra.plugin.minio.Trigger) polls the bucket every 30 seconds and fires for files that are new or whose ETag changed (on: CREATE_OR_UPDATE). Files stay in the bucket (action: NONE), because the bucket is the source of truth. hourly_reconciliation (Schedule) runs the same flow every hour without any file event.ensure_bucket and create_collection_if_missing (If) create the bucket, the Qdrant collection (768 dimensions, cosine) and a keyword index on source on the first run, so there is nothing to set up by hand.plan_sync compares list_bucket with the sources that have chunks in Qdrant (indexed_sources, a facet query) and outputs two lists:deleted: sources with chunks but no file. The MinIO trigger cannot see deletions, so this is how they are caught.to_index: files reported by the trigger, plus any file still missing from the index.purge_deleted_documents (If) removes the chunks of deleted files in one Qdrant call.index_documents (Loop) processes one file at a time:download (io.kestra.plugin.minio.Download) and extract_text (io.kestra.plugin.tika.Parse) turn PDF, DOCX, Markdown and anything else Tika reads into plain text.has_text (If) skips files with no text, such as scanned PDFs or corrupt files, with a WARN log, so an empty or broken file never replaces a good version.delete_previous_chunks removes the old version of the file, embed_and_store (io.kestra.plugin.ai.rag.IngestDocument) splits it into 1,000-character chunks and embeds them with nomic-embed-text, tagging each chunk with its source key, and assert_document_indexed checks that chunks really landed in Qdrant.publish_manifest writes the indexed files and their chunk counts to the knowledge_base_manifest KV entry.ERROR log naming the failing task, and a manifest you can read from the KV Store.concurrency queues runs so two of them never index the same file at once.MinIO, Qdrant and Ollama, reachable from Kestra:
docker run -d --name minio -p 9000:9000 -p 9001:9001 -e MINIO_ROOT_USER=minioadmin -e MINIO_ROOT_PASSWORD=minioadmin cgr.dev/chainguard/minio server /data --console-address :9001
docker run -d --name qdrant -p 6333:6333 -p 6334:6334 -e QDRANT__SERVICE__API_KEY=change-me qdrant/qdrant
docker run -d --name ollama -p 11434:11434 ollama/ollama
docker exec ollama ollama pull nomic-embed-text
The flow's variables point at localhost. If Kestra itself runs in Docker, replace localhost with host.docker.internal (add extra_hosts: ["host.docker.internal:host-gateway"] to the Kestra service on Linux).
MINIO_ACCESS_KEY and MINIO_SECRET_KEY: credentials with read and list access to the bucket, and permission to create it on the first run.QDRANT_API_KEY: the value of QDRANT__SERVICE__API_KEY (the Qdrant plugin requires one).knowledge-base bucket and the knowledge_base collection, and publishes an empty manifest. Until then, the trigger logs that the bucket does not exist.http://localhost:9001. Within 30 seconds an execution indexes them: open KV Store in the company.ai namespace to see knowledge_base_manifest with one entry per file.WARN on every run until they are fixed or removed. To read scanned PDFs, install Tesseract on the worker and set ocrOptions on extract_text.delete_previous_chunks and embed_and_store.embedding_model means a different vector size: update embedding_dimension, delete the collection and let the next run rebuild it.prefix or regexp on the trigger, or run one collection per team.log_failure message to Slack or email instead of the logs.io.kestra.plugin.ai.embeddings store.