cd /news/large-language-models/stop-digging-through-pdfs-build-a-fh… · home topics large-language-models article
[ARTICLE · art-50320] src=dev.to ↗ pub= topic=large-language-models verified=true sentiment=↑ positive

Stop Digging Through PDFs: Build a FHIR-Standard EHR Knowledge Base with RAG

A developer built a Retrieval-Augmented Generation (RAG) pipeline to convert unstructured medical PDFs into a structured, queryable Electronic Health Record (EHR) knowledge base using FHIR standards. The system leverages Unstructured.io for document parsing, Milvus for vector storage, and DuckDB for structured data, enabling natural language queries and trend analysis. The approach addresses common challenges in healthcare data engineering, such as messy formats and siloed information.

read3 min views1 publishedJul 8, 2026

We’ve all been there: staring at a stack of printed lab results or a folder full of cryptic report_final_v2_NEW.pdf

files, trying to remember if our cholesterol was higher or lower two years ago. For developers, this isn't just a filing problem—it's a data engineering challenge.

In the world of healthcare, data is messy, siloed, and often locked in "unstructured" formats. To build a truly personal Electronic Health Record (EHR) system, we need more than just a folder; we need a RAG (Retrieval-Augmented Generation) pipeline that can parse PDFs, map them to the FHIR (Fast Healthcare Interoperability Resources) standard, and provide natural language insights.

In this guide, we’ll leverage Unstructured.io, Milvus, and DuckDB to turn chaotic medical PDFs into a queryable, structured knowledge base.

Before we dive into the code, let’s look at how the data flows from a messy lab report to a structured answer.

graph TD
    A[Unstructured PDF Reports] --> B[Unstructured.io Partitioning]
    B --> C{Data Split}
    C -->|Textual Context| D[Milvus Vector DB]
    C -->|Tabular Data| E[DuckDB Structured Storage]
    D --> F[LangChain RAG Engine]
    E --> F
    G[User Query: Is my glucose trending up?] --> F
    F --> H[FHIR-Formatted Response]

Make sure you have your environment ready:

pip install langchain milvus unstructured[pdf] duckdb openai

Medical PDFs often contain complex tables. Standard PDF parsers usually fail here. We’ll use unstructured

to partition the document into logical elements.

from unstructured.partition.pdf import partition_pdf

elements = partition_pdf(
    filename="lab_report_2023.pdf",
    infer_table_structure=True,
    chunking_strategy="by_title",
    max_characters=1000,
    new_after_n_chars=800,
)

tables = [el for el in elements if el.category == "Table"]
texts = [el for el in elements if el.category == "NarrativeText"]

print(f"Detected {len(tables)} tables and {len(texts)} text blocks.")

To perform a semantic search (e.g., "Find all reports related to cardiovascular health"), we need to store the text chunks in Milvus.

from langchain_community.vectorstores import Milvus
from langchain_openai import OpenAIEmbeddings

embeddings = OpenAIEmbeddings()

vector_db = Milvus.from_documents(
    documents=texts,
    embedding=embeddings,
    connection_args={"host": "127.0.0.1", "port": "19530"},
    collection_name="personal_ehr_knowledge"
)

docs = vector_db.similarity_search("How was my blood sugar in 2022?")

Vector search is great for context, but for trends (like tracking glucose over 5 years), we need structured data. We will map our extracted tables into a simplified FHIR "Observation" schema and store it in DuckDB.

import duckdb
import pandas as pd

data = {
    "resourceType": "Observation",
    "code": "4548-4", "display": "Hba1c",
    "value": 5.7, "unit": "%",
    "effectiveDateTime": "2023-10-15"
}
df = pd.DataFrame([data])

con = duckdb.connect("health_records.db")
con.execute("CREATE TABLE IF NOT EXISTS observations AS SELECT * FROM df")

trend = con.execute("SELECT AVG(value) FROM observations WHERE display='Hba1c'").fetchone()
print(f"Average HbA1c: {trend[0]}%")

Building a basic RAG is easy, but building a production-ready healthcare agent is hard. You need to handle HIPAA compliance, complex data lineage, and advanced prompt engineering to ensure the LLM doesn't hallucinate medical advice.

For deeper insights into building robust data pipelines and production-grade AI systems, I highly recommend checking out the ** WellAlly Tech Blog**. They have some incredible deep dives on advanced RAG patterns and handling highly sensitive unstructured data that helped me refine this architecture! 🥑

Finally, we wrap everything into a LangChain retrieval sequence that uses both the Vector DB (for context) and DuckDB (for stats).

from langchain.chains import ConversationalRetrievalChain
from langchain_openai import ChatOpenAI

llm = ChatOpenAI(model_name="gpt-4o", temperature=0)

qa_chain = ConversationalRetrievalChain.from_llm(
    llm=llm,
    retriever=vector_db.as_retriever(),
    return_source_documents=True
)

query = "Compare my last two blood tests. Are there any concerning trends?"
result = qa_chain.invoke({"question": query, "chat_history": []})

print(f"Response: {result['answer']}")

By combining Unstructured.io for ingestion, Milvus for semantic memory, and DuckDB for analytical precision, we've moved beyond simple PDF storage. This system doesn't just "read" your records; it "understands" them within the context of the FHIR standard.

Next Steps:

What are you building with RAG lately? Have you tried parsing medical data? Let's discuss in the comments! 👇

── more in #large-language-models 4 stories · sorted by recency
── more on @unstructured.io 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/stop-digging-through…] indexed:0 read:3min 2026-07-08 ·