LESSON 73 Β· REAL-WORLD PROJECTS

PDF Question Answering

Build a practical PDF question-answering system that extracts document text, creates useful chunks, retrieves relevant passages, and uses an LLM to answer questions from the source material.

28 min readβ€’Intermediateβ€’Pythonβ€’RAG Project

How PDF Question Answering Works

PDFUpload document
ExtractRead text + pages
ChunkSplit into passages
EmbedCreate vectors
RetrieveFind relevant chunks
LLMAnswer with context
iImportant: The LLM should not be asked to β€œread the PDF” directly. Your application extracts and prepares the document, retrieves relevant evidence for the question, and then sends that evidence to the model.
1

What Are We Building?

We are building a small RAG application for asking questions about a PDF. The user uploads a document, asks a question, and receives an answer grounded in relevant passages from that document.

User experience

Upload a PDF, ask a natural-language question, and see a concise answer with source references.

Application behavior

Extract, clean, chunk, index, retrieve, prompt, answer, and expose the supporting evidence.

2

Understand the End-to-End Architecture

Keep ingestion and question answering as two separate flows. The PDF is processed once during ingestion; questions reuse the prepared index.

UploadPDF file
ParseExtract text
ChunkSplit content
IndexStore vectors
QueryRetrieve + answer
3

Separate Ingestion from Querying

A common design mistake is re-reading and embedding the PDF every time a user asks a question. Instead, create the index once and reuse it.

Ingestion

PDF β†’ text β†’ chunks β†’ embeddings β†’ vector index.

Query

Question β†’ embedding β†’ retrieval β†’ prompt β†’ answer.

Response

Answer + source metadata such as page number and chunk ID.

4

Extract Text from a PDF

PDFs are layout-oriented files, so extraction quality matters. Start with a library that can return text page by page, then preserve page metadata for citations.

</> Python
from pathlib import Path


def extract_pages(pdf_path: str) -> list[dict]:
    # Replace this demo reader with your PDF library.
    text = Path(pdf_path).read_text(encoding="utf-8")
    return [{"page": 1, "text": text}]

pages = extract_pages("manual.txt")
print(pages[0]["page"])

For a real PDF, use a PDF parser such as PyMuPDF or another suitable document extraction library.

5

Preserve Page Metadata

Do not throw away where a passage came from. Page numbers, document IDs, section names, and chunk IDs make answers easier to verify and debug.

document_idIdentifies the uploaded document.
pagePoints to the source page.
chunk_idUniquely identifies the passage.
textStores the retrieved content.
sectionOptional heading or section name.
sourceOriginal file or URI reference.
6

Clean Extracted Text

Extraction can produce repeated headers, footers, broken whitespace, and line breaks. Clean obvious noise without destroying meaningful structure.

</> Python
import re


def clean_text(text: str) -> str:
    text = re.sub(r"[ \\t]+", " ", text)
    text = re.sub(r"\\n{3,}", "\\n\\n", text)
    return text.strip()

cleaned = clean_text("Page title   \\n\\n\\n\\n  Useful content")
print(cleaned)
7

Choose a Chunking Strategy

Chunking determines the units that retrieval can return. For PDFs, page-aware recursive or semantic chunking is often more useful than blindly cutting every N characters.

Too small

Retrieval may return fragments without enough context to answer the question.

Too large

Chunks may contain unrelated sections and increase prompt tokens.

8

Create Chunks with Metadata

Each chunk should carry both its text and source information. This lets you return citations and filter results later.

</> Python
def make_chunks(pages: list[dict], size: int = 800) -> list[dict]:
    chunks = []
    for page in pages:
        text = page["text"]
        for start in range(0, len(text), size):
            chunks.append({
                "chunk_id": f"p{page['page']}-{start}",
                "page": page["page"],
                "text": text[start:start + size],
            })
    return chunks

chunks = make_chunks(pages)
print(len(chunks))
9

Generate Embeddings

Convert each chunk into a vector representation so semantically related questions and passages can be compared.

Pipeline: chunk text β†’ embedding model β†’ vector β†’ vector index. Store the embedding model/version with your index so you can reproduce results.
10

Store Vectors in an Index

A vector database or local vector index lets you search many chunks efficiently. Store the chunk text and metadata alongside the vector or in a linked document store.

VectorEmbedding used for similarity search.
PayloadText, page, document ID, and metadata.
IndexData structure optimized for nearest-neighbor search.
11

Embed the User's Question

The query must be represented in the same embedding space as the indexed chunks.

</> Python
def embed(text: str) -> list[float]:
    # Demo placeholder for an embedding model.
    return [float(len(text)), 0.5]

question = "What is the refund policy?"
query_vector = embed(question)
print(query_vector)
12

Retrieve the Most Relevant Chunks

Use similarity search to get a candidate set, such as the top 5–10 chunks. Retrieval quality is often the biggest factor in whether a RAG answer is grounded.

Query1 question
Top-K5–10 chunks
FilterDocument
RerankOptional
ContextBest evidence
13

Build a Grounded Prompt

Tell the model that the supplied context is the source of truth. Make the citation metadata visible to the model and instruct it not to invent unsupported details.

Prompt pattern: Answer the question using only the context below. If the context does not contain the answer, say that the document does not provide enough information. Include the relevant page number.
14

Assemble Context in Python

Keep retrieved chunks structured until the final prompt is built.

</> Python
def build_context(results: list[dict]) -> str:
    parts = []
    for item in results:
        parts.append(
            f"[Page {item['page']} | {item['chunk_id']}]\\n{item['text']}"
        )
    return "\\n\\n".join(parts)

context = build_context(chunks[:2])
print(context)
15

Generate the Answer

The LLM receives the question plus retrieved evidence. Keep temperature and output length appropriate for factual document QA.

Question

What does the document say about returns?

Evidence

Only the retrieved passages relevant to returns.

Answer

A concise response grounded in those passages.

16

Return Sources with the Answer

A good PDF QA interface should show the evidence used to answer. This makes the system more trustworthy and gives the user a way to verify the response.

  • Answer text
  • Document name
  • Page number
  • Relevant excerpt
  • Confidence or retrieval score when appropriate
  • Link to the source page when your viewer supports it
17

Handle Questions the PDF Cannot Answer

Retrieval can return weak matches even when the answer is absent. Add a relevance threshold or evaluation step so the application can refuse to guess.

</> Python
def should_answer(results: list[dict], threshold: float = 0.72) -> bool:
    if not results:
        return False
    return results[0].get("score", 0.0) >= threshold

results = [{"score": 0.81}]
print(should_answer(results))
18

Support Multiple PDFs

When users upload several documents, add a document ID to every chunk. Then you can search across all documents or filter to one selected document.

Single PDF modeRestrict retrieval to the active document.
Multi-PDF modeSearch across a user's document collection.
Tenant isolationNever retrieve another user's documents.
Document filtersLimit retrieval by document ID or type.
VersioningTrack replaced or updated files.
DeletionRemove vectors when a document is deleted.
19

PDFs Are Not Always Text

Scanned PDFs may contain images instead of machine-readable text. Tables, diagrams, columns, and unusual layouts can also reduce extraction quality.

Text PDF

Use text extraction and normal RAG techniques when the text layer is reliable.

Scanned PDF

Use OCR first, then validate the extracted text before indexing it.

20

Improve Retrieval Quality

If answers are weak, do not immediately change the LLM. First inspect whether the correct evidence was retrieved.

  • Improve chunk boundaries
  • Adjust chunk overlap
  • Change embedding model
  • Increase candidate Top-K
  • Add metadata filtering
  • Use reranking
21

Control Context Size and Cost

Sending ten large chunks for every question can increase latency and token cost. Retrieve a reasonable candidate set, rerank when needed, and send only the strongest evidence.

RetrieveFind a wider candidate set.
RerankOrder the best evidence.
CompressKeep only useful context.
22

Add Security and Privacy Controls

Uploaded documents can contain sensitive information. Treat the PDF pipeline as an application boundary, not just a demo script.

  • Validate file type and size
  • Scan uploads before processing
  • Isolate users and tenants
  • Protect stored documents and vectors
  • Limit who can query each document
  • Do not expose secrets in prompts or logs
23

Evaluate the PDF QA System

Create questions with known answers and source pages. Evaluate both retrieval and generation separately.

Retrieval metrics

Recall@K, Precision@K, MRR, and NDCG help measure whether useful evidence was retrieved.

Answer metrics

Correctness, faithfulness, relevance, and citation accuracy evaluate the final response.

Human review

Sample difficult questions and verify the answer against the source PDF.

24

Mini Project: Build a PDF Q&A Assistant

Build a small application that accepts a PDF and answers questions using retrieved passages.

Step 1 β€” Upload

Accept one PDF and assign a document ID.

Step 2 β€” Ingest

Extract pages, clean text, and create metadata-rich chunks.

Step 3 β€” Index

Generate embeddings and store the chunks in a vector index.

Step 4 β€” Retrieve

Embed the question and retrieve the strongest passages.

Step 5 β€” Answer

Send only the relevant context to the LLM.

Step 6 β€” Cite

Return the answer with page and document references.

30-Second Recap

01Extract PDF text and preserve page metadata.
02Chunk content so retrieval can return useful evidence.
03Embed and index chunks for semantic search.
04Retrieve relevant passages for each question.
05Prompt the LLM with grounded context.
06Return source pages and evaluate the whole pipeline.

Key Takeaways

  • PDF Question Answering is a practical RAG use case.
  • Ingestion and querying should be separate application flows.
  • Source metadata is essential for citations and debugging.
  • Retrieval quality matters as much as model quality.
  • Never force the model to answer when the PDF does not contain enough evidence.
  • Security, evaluation, and document lifecycle management matter in production.

Quick Quiz

1. Why store page numbers with chunks?

To let users verify where the retrieved evidence came from.

2. Why separate ingestion from querying?

To avoid repeatedly parsing and embedding the same PDF.

3. What should happen when retrieval finds no reliable evidence?

The application should say that the document does not provide enough information instead of guessing.

4. What is the main job of the vector index?

Efficiently find semantically similar chunks for a question.