Guides

Turn PDFs into RAG-ready chunks

Updated 2026-10-28 · Uses Doc to Markdown for AI Agents

Retrieval quality depends on what goes into your vector database. Text pulled straight out of a PDF often interleaves columns, repeats page headers, flattens tables into number soup and loses headings. This guide turns documents into clean Markdown chunks that keep tables intact and carry the page and heading they came from.

Steps

  1. Collect public document links. PDF, DOCX, PPTX, XLSX/XLS, HTML, CSV or TXT links. Google Docs, Drive, Dropbox and GitHub share links work if shared publicly.
  2. Pick a chunk strategy. Use size (heading-aware, about 512 tokens with 50 overlap) for most RAG work; section, page or none for special cases.
  3. Run the conversion. Call sourcewire/doc-to-markdown through the Apify API, a client library or MCP.
  4. Keep only successful items. Check status: ok items have chunks; error and needs_ocr items explain why and are not charged.
  5. Embed chunks with their metadata. Store each chunk's text with url, page, pageEnd and headingPath so answers can cite their source.
  6. Skip unchanged files next time. Compare each document's sha256 with the last run and only re-embed documents that changed.

Choosing a chunk strategy

chunkStrategyUse it when
size (default)General RAG. Heading-aware chunks of about chunkSize tokens (default 512) with chunkOverlap (default 50). Tables are never cut mid-row.
sectionDocuments with strong headings, like policies or manuals, where one section is one answer.
pageYou need page-level citations, or one chunk per slide or spreadsheet sheet.
noneYou only want the full Markdown, for example to summarize a document in one prompt.

Long tables are split by rows with the header row repeated in each chunk, so every chunk can be read on its own.

Convert and prepare chunks in Python

# pip install apify-client
import os
from apify_client import ApifyClient

client = ApifyClient(os.environ["APIFY_TOKEN"])
run = client.actor("sourcewire/doc-to-markdown").call(run_input={
    "urls": [
        "https://www.irs.gov/pub/irs-pdf/fw9.pdf",
        # add your own public links here
    ],
    "chunkStrategy": "size",
    "chunkSize": 512,
    "chunkOverlap": 50,
    "includeMarkdown": False,   # chunks only, smaller response
})

records = []
for doc in client.dataset(run["defaultDatasetId"]).iterate_items():
    if doc["status"] != "ok":
        print("skipped", doc["url"], doc["errorCode"])  # not charged
        continue
    for ch in doc["chunks"]:
        records.append({
            "id": f'{doc["sha256"]}-{ch["index"]}',
            "text": ch["text"],
            "metadata": {
                "url": doc["url"],
                "title": doc.get("title"),
                "page": ch.get("page"),
                "pageEnd": ch.get("pageEnd"),
                "headingPath": ch.get("headingPath"),
                "hasTable": ch.get("hasTable"),
            },
        })

print(len(records), "chunks ready to embed")
# Next: embed records[i]["text"] with your embedding model and upsert
# into your vector database along with records[i]["metadata"].

What a chunk looks like

{
  "index": 0,
  "text": "# W-9\n\n# Request for Taxpayer ...",
  "page": 1,
  "pageEnd": 1,
  "headingPath": "W-9 > Request for Taxpayer",
  "hasTable": false,
  "charCount": 1890,
  "tokenEstimate": 472
}

Prepending headingPath to the text before embedding (for example "W-9 > Request for Taxpayer\n\n" + text) often helps retrieval, because short chunks gain the context of the section they sit in.

Handle failures without paying for them

Every item has the same shape. Check status:

  • ok: converted and charged once, plus one extra event per additional 50 PDF pages or slides.
  • error: see errorCode, such as NOT_FOUND, ACCESS_DENIED, FILE_TOO_LARGE, UNSUPPORTED_FORMAT, ENCRYPTED_PDF or TIMEOUT. Not charged.
  • needs_ocr: a scanned PDF with no text layer. Run OCR first. Not charged. Partly scanned PDFs are converted, with the image-only pages listed in scannedPages.

Tips

  • Use includePages: true when you want the Markdown of each PDF page, slide or sheet for citations.
  • Use estimatedTokens to decide whether a document fits in context or needs chunking.
  • Use sha256 to skip documents that haven't changed since your last run.
  • Set maxPages and maxDocuments, or a maximum cost per run in Apify Console, to keep costs predictable.

Cost: $0.005 per converted document, $5 per 1,000. Open Doc to Markdown on the Apify Store.