Finding application notes fast with a targeted RAG
A good question deserves an answer you can find in seconds, not an hour of scanning PDFs. 🎯
Why I built it
In my role as an Agilent product specialist, a large part of the job is business development: answering technical questions, preparing demonstrations, and helping customers and colleagues find the right information fast. Application notes are among the richest sources of technical truth — real methods, reagents, expected results, instrument conditions. But they are scattered: different formats, different locations, written for different audiences.
Searching for a precise fact — the mobile phase of a method, an injection volume, a recommended column — used to mean opening several documents and scanning them. ⏱️ It worked, but it was slow, and the same question got answered again and again.
I wanted a system that retrieves technical information quickly, reliably, and privately, directly from my corpus of application notes. That is the idea behind retrieval-augmented generation (RAG).
How the strategy was designed
Before any code, I set the constraints that shape the whole stack. Four principles:
- Private by design. 🛡️ The corpus stays on my own machines. No proprietary document is sent to a third party.
- Focused, not exhaustive. I index a curated set of application notes, not everything. Precision beats coverage.
- Traceable answers. Every answer points to its source note.
- Cheap and simple to run. The pipeline is automatic and incremental — no data team required.
The system is built around the questions I actually answer, and around the fact that the data must stay where I control it.
The stack: what I installed
The whole thing runs on-prem, on a small commodity machine — no cloud, no heavy GPU farm. The tools:
| Layer | Tool | Role |
|---|---|---|
| Document extraction | PyMuPDF (fitz) | reads PDFs, splits them into chapters using the table of contents |
| Text format | Markdown | one structured .md per document — readable, diffable, indexable |
| Image handling | hash dedup + local VLM | drops repeated logos, captions the useful screenshots |
| Index | local, incremental (SQLite-backed) | each chunk stored with its source reference |
| Generation | a lightweight LLM | writes the final answer from the retrieved chunk |
Deployment principle: everything that touches the documents runs locally; only the final, decontextualized answer step may call a lightweight model with a single retrieved chunk. Nothing sensitive leaves the box.
How the RAG is fed (ingestion pipeline)
Feeding the system is a short automated chain. New documents are deposited in a folder and processed incrementally — only new files get treated.
# 1) drop appnote-xyz.pdf into docs-src/
# 2) run ingestion — the pipeline handles only what's new
python pipeline.py ingest docs-src/appnote-xyz.pdf
Which produces:
docs-src/
└─ appnote-xyz.pdf (source, untouched)
images/ (extracted figures)
docs-md/
└─ appnote-xyz.md (structured Markdown)
rag/
├─ index.db (local index, incremental)
└─ INDEX.md (top-level table of contents)
The pipeline for each document:
- Extract 🔧 — PyMuPDF reads the PDF and cuts it along the table of contents, so a method and its results stay in the same block instead of being split mid-sentence.
- Handle images 🖼️ — figures are pulled out and deduplicated by hash: a logo repeated on every page is removed once; only genuinely useful captures are kept. Those are then read by a local vision-language model, which writes a short caption — so image content becomes searchable text instead of a dead attachment.
- Convert to Markdown 📝 — the cleaned text and captions are written as a structured
.md(one per document), which is both human-readable and easy to index. - Index 🗂️ — chunks are added to the local index with a reference to their source. Because it's incremental, adding note #50 takes seconds, not a full rebuild.
# illustrative config (simplified)
rag:
input: docs-src/
output: docs-md/
index: rag/index.db
chunk:
mode: toc # split on the PDF table of contents
images:
dedup: hash # remove repeated logos
caption: vlm-local # caption useful captures, locally
incremental: true
Interrogating it
Asking a question is the reverse path:
Your question
→ find the most relevant chunk(s) (by meaning, not just keywords)
→ send ONLY that chunk to the LLM
→ short answer + source reference
I get a concise answer and the note it comes from. When precision matters — a customer decision, a demonstration — I open the source and confirm. The system is an entry point, not a substitute.
Why this approach works
- The right corpus. Only trusted application notes are indexed, so results stay relevant; a broad index would bury good answers under noise.
- Human-in-the-loop. The model proposes, the specialist confirms.
The goal is not to "cheat" the answer. It's to remove the friction of finding it — then verify it in the source.
In short
A targeted, on-prem RAG over a trusted corpus: PyMuPDF + Markdown + a local VLM for figures, incremental indexing, retrieval that sends only the relevant chunk to a lightweight model. The payoff: fast answers, cited sources, private data, near-zero maintenance — hours of document scanning turned into seconds.