Chunking and Document Preprocessing

Retrieval quality is decided before the first query. Preprocessing turns messy PDFs and HTML into clean text. Chunking cuts that text into units you embed and fetch. Bad chunks—too huge, too tiny, or split mid-table—make even a perfect vector database look broken.

Intuition

An embedding is one point summarizing a chunk. A 40-page handbook in one chunk becomes oatmeal: every topic blended. Three words alone lack context. You want chunks that are one coherent idea, large enough to answer something, small enough to stay focused.

Overlap exists because answers often straddle boundaries. A sentence at the end of chunk 4 may need the first lines of chunk 5.

flowchart LR R[Raw docs] --> C[Clean / extract] C --> S[Structure-aware split] S --> K[Chunks + metadata] K --> E[Embed + index]

How it works

Preprocessing checklist

Document types and tools

Document type Suggested approach Why
PDF plain prose pypdf Fast when the file is mostly text
PDF with tables pdfplumber or Camelot Preserves row/column structure
Scanned / image PDF OCR (optical character recognition) No text layer to extract otherwise
Complex layout layout-aware parser (unstructured, docling) Reading order matters
HTML pages BeautifulSoup, trafilatura Strip boilerplate; keep useful content

OCR turns scanned images of text into machine-readable text. If a PDF is just an image, a plain extractor may return empty strings—nothing to embed.

Quick probe before parsing:

from pypdf import PdfReader

def pdf_has_text_layer(path, sample=3):
    reader = PdfReader(path)
    return any((p.extract_text() or "").strip() for p in reader.pages[:sample])

Chunking strategies

Strategy How it works Best when
Fixed-size Split by token count with overlap Baseline or uniform prose
Sliding window Move a window with 10–20% overlap Answers cross boundaries
Structure-aware Split on headings, clauses, sections Markdown, legal text, code, API docs
Semantic chunking Split when topic similarity drops Topic-shifting docs; quality over cost
Recursive splitting Try structure first, then smaller pieces General-purpose practical choice
Parent–child Index small child chunks; return larger parent for context Balance precision and completeness

Common starting point: about 512 tokens with 10–20% overlap. Match chunk size to the kind of answer you expect—small for FAQ facts, larger for narrative or code.

When RAG may not be needed

If the knowledge base is tiny and already fits in the model's context window, you may get a simpler result by putting the text directly in the prompt instead of building a retrieval system.

In code

Structure-aware splitting over Markdown headings with word-based overlap.

import re
from dataclasses import dataclass

@dataclass
class Chunk:
    id: str
    text: str
    section: str
    source: str

def split_markdown(text: str, source: str, max_words: int = 80, overlap: int = 15) -> list[Chunk]:
    parts = re.split(r"(?m)^(#{1,3} .+)$", text)
    sections: list[tuple[str, str]] = []
    current_title = "intro"
    buf = []
    for part in parts:
        if re.match(r"^#{1,3} ", part or ""):
            if buf:
                sections.append((current_title, " ".join(buf).strip()))
                buf = []
            current_title = part.lstrip("#").strip()
        elif part and part.strip():
            buf.append(part.strip())
    if buf:
        sections.append((current_title, " ".join(buf).strip()))

    chunks: list[Chunk] = []
    n = 0
    for title, body in sections:
        words = body.split()
        if not words:
            continue
        start = 0
        while start < len(words):
            end = min(start + max_words, len(words))
            piece = " ".join(words[start:end])
            chunks.append(Chunk(f"{source}_{n}", piece, title, source))
            n += 1
            if end == len(words):
                break
            start = max(0, end - overlap)
    return chunks

Production code usually counts real tokenizer tokens, not words.

What goes wrong

One-line summary

Preprocess to clean, structured text, then chunk with coherent boundaries, sensible size, overlap, and metadata—because embeddings can only retrieve what you gave them.

Key terms