med-rag answers questions about a folder of PDFs in plain language. It reads the documents once, keeps the text and the embeddings in a database on your machine, and then, for every question, retrieves the passages that bear on it and hands them to a language model. It works with a model running locally through Ollama, or with Claude through the API.
Indexing runs on the CPU, with an embedding model of about 50MB in ONNX and no GPU. With the Claude backend the machine does nothing heavier, so any ordinary laptop is enough. (Running the model locally through Ollama needs more memory, so use a model at least as capable as gemma3:12b).
It was written for an archive of medical reports large enough that finding a single value meant opening the files one after another until it turned up. It also handles simpler plain pages, where there are no tables to reconstruct.
I also used med-rag while writing my thesis and preparing it for publication, to retrieve the references across the dozens of PDFs I had accumulated. It was better than searching each PDF with Ctrl+F or using an LLM that, by reading every PDF entirely, would have cost a huge amount of tokens.
Source and instructions are available at
github.com/leonardocppn/med-rag.
Indexing runs once per document. The profiler measures the layout and derives the parsing parameters, the parser extracts every text element with its coordinates and classifies it as title, header, footer, table or body, and what survives is embedded by a small multilingual model and written to a local ChromaDB collection. A scanned book comes out of the profiler with an impossible column density, produced by the OCR noise, and gets a parameter set that keeps the footnotes and skips the column and title rules. Narrative documents can skip the layout work entirely with --plain.
Asking runs per question. The question is embedded the same way, the nearest chunks come back, and a cross-encoder can re-rank them when precision matters more than speed. The model receives those passages with the page they came from, and streams its answer to the terminal.
Each PDF gets its own collection, and several can be grouped into a named corpus that is then queried as one document.
Reading documents in:
Asking questions:
Looking at the machinery:
Every command takes a PDF path or a corpus name in the same position, so the syntax is the same in both cases.
Python 3.10+ and the packages declared in pyproject.toml: pdfplumber for the PDFs, fastembed and sentence-transformers for the embeddings, chromadb to store them, rank-bm25 for the keyword search, scikit-learn for the clustering, and the two client libraries. An Anthropic API key is needed for the Claude backend. The local backend needs a running Ollama on the machine.
Then index a folder and ask it something. The first run downloads the embedding model, about 50MB, from HuggingFace:
--corpus puts every PDF of the folder in one searchable collection, and a name is assigned automatically if you leave it out. --rerank passes the retrieved chunks through a cross-encoder before they reach the model. It costs about a second per question, and another 100MB the first time. --model claude sends the question and the retrieved passages to the API; without it the answer comes from the local model and nothing leaves the machine.
Source and instructions: github.com/leonardocppn/med-rag (MIT licence).