Back

RAG Microservice

Solo build, July 2025. Source on GitHub
RAG Microservice banner
My Role

Solo - API, queue, indexing pipeline, retrieval and prompt

Stack

Python · FastAPI · LangChain
OpenAI embeddings + GPT-4 · Qdrant
Redis Queue (RQ) · Docker Compose

Timeline & Status

Built July 2025
Reviewed again November 2026

Overview

Long PDFs are hard to search and easy to misquote. This service lets you ask a document a question and get an answer built only from the passages that matched, with page numbers you can check.

It’s the classic retrieval-augmented generation (RAG) loop, split into services: an indexing script that chunks and embeds the PDF into Qdrant, a FastAPI layer that queues every question instead of waiting on the model, and a worker that retrieves, prompts and answers.

It’s also the project I learned the most from re-reading. The last section is an honest review of what I’d fix now.

HOW IT WORKS

Two paths: index once, ask often.

Indexing.

A PDF is loaded page by page, so every chunk keeps its page label. LangChain’s recursive splitter cuts it into 1,000-character chunks with 200 characters of overlap, so a sentence split across a boundary still appears whole in one of them.

Each chunk is embedded with OpenAI’s text-embedding-3-large (3,072 dimensions) and stored in a Qdrant collection that uses cosine distance.

Asking.

POST /chat puts the question on a Redis queue and returns a job id immediately. A separate RQ worker embeds the question, pulls the five closest chunks from Qdrant, and sends them to GPT-4 with the question. The client polls GET /result/{id} until the answer is ready.

GROUNDED BY THE PROMPT
The model is told to answer only from the retrieved context, to say clearly when the context doesn’t cover the question, and to cite page numbers for specific details.

DECISIONS

Why a queue in front of the model.

The API should never wait on an LLM.

Embedding a question, searching and generating a GPT-4 answer takes seconds. Doing that inside the request would tie up an API worker for every question and turn any model slowness into timeouts.

With a queue, the API stays fast and stateless, and workers scale separately from the API: more questions means more workers, not a bigger web server.

Everything in one compose file.

Redis (Valkey), Qdrant, the API and the worker are four services in one docker-compose file, with volumes so the vector index and the queue survive restarts.

A YEAR LATER

Reviewing my own code like a stranger's PR.

Why this section exists.

I built this in July 2025 and called it production-ready in the README. Reading it again in November 2026, after running FetchJobs in production, I’d block that claim in review. These are the findings, each one checked against the code.

01
Failures report as success
The worker catches every exception and returns the error as a string. The queue sees a finished job, so /result says “completed” with an error message as the answer, and its “failed” branch is never reached.
02
Neither start path runs
docker-compose builds from a Dockerfile the repo doesn’t have. And python main.py exits at once: uvicorn refuses reload=True when it’s given an app object instead of an import string.
03
No way to add a document
Indexing is a script with a hardcoded PDF path. A real service needs an upload endpoint that queues indexing the same way it queues questions.
04
No evals
Chunk size, overlap and top-5 were never measured. A small set of questions with known answer pages would turn each of those into a number.
05
Temperature 0.7 for grounded answers
An answer meant to quote a document should be close to deterministic. 0 to 0.2 fits better.
06
The worker needs an index to start
The worker connects to the Qdrant collection when it’s imported, so it fails to start until indexing has run once.
WHAT CHANGED
The shape still holds up: queue in front of the model, retrieval separate from generation. What changed is my bar. “Production-ready” now means it starts, fails loudly, and has a number behind every tuning choice.