Jerem Flow

Finding application notes fast with a targeted RAG

· 4 mins read · #rag #retrieval-augmented-generation #llm #embeddings #pymupdf #local-llm #knowledge-management

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:

  1. Private by design. 🛡️ The corpus stays on my own machines. No proprietary document is sent to a third party.
  2. Focused, not exhaustive. I index a curated set of application notes, not everything. Precision beats coverage.
  3. Traceable answers. Every answer points to its source note.
  4. 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:

  1. 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.
  2. 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.
  3. 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.
  4. 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.

← Back