For a large corpus of complex PDFs, I’d build the pipeline around layout-aware parsing first, embeddings second. The biggest mistake is extracting plain text and then trying to recover tables/layout during chunking.
Recommended architecture
PDFs
│
├─► classify: born-digital vs scanned
│
▼
Layout-aware parser
│
├─ text blocks + reading order
├─ headings/sections
├─ tables → structured representation
├─ figures/captions
├─ page coordinates
└─ OCR where necessary
│
▼
Canonical document representation
│
├─ paragraphs
├─ sections
├─ tables
└─ figures
│
▼
Semantic / structure-aware chunking
│
├─ prose chunks
├─ table chunks
└─ figure/context chunks
│
▼
Embeddings + metadata
│
▼
Vector DB + document/object store
Parser choice
For a self-hosted/open-source pipeline, github.com is particularly well suited. It explicitly handles PDF layout, reading order, tables, formulas and OCR, and maintains a structured document representation rather than reducing everything to a text blob.
Its table pipeline is especially relevant: it can reconstruct merged and borderless tables, and its table extraction has configurable accuracy/cell-matching behavior.
If you're comfortable with a managed cloud service, Azure Document Intelligence's Layout model is another strong architecture. It returns paragraphs, roles, tables, cells, bounding regions, reading structure and OCR information, making it straightforward to preserve provenance.
How I'd represent the extracted data
Don't embed the PDF page wholesale. Convert it into objects roughly like:
{
"document_id": "report-123",
"page": 17,
"section": "3.2 Revenue",
"type": "table",
"content": "Region | 2024 | 2025\nNorth | $10M | $13M\n...",
"metadata": {
"bbox": [72, 310, 520, 690],
"table_id": "table-17-2"
}
}
For prose:
{
"document_id": "report-123",
"page": 18,
"section": "3.2 Revenue",
"type": "paragraph",
"content": "...",
"metadata": {
"parent_section": "3.2 Revenue"
}
}
That metadata becomes extremely valuable for retrieval and citations.
Tables need special treatment
Don't flatten a table into arbitrary text chunks. Preserve:
- table title/caption
- column headers
- row headers
- merged-cell relationships
- page/document location
- surrounding explanatory paragraph
- units and footnotes
For embedding, I'd create a retrieval-friendly textual representation of each table, while also storing the original structured table separately.
For example:
Table: Revenue by region, 2024–2025
Units: USD millions
North: 2024 = 10, 2025 = 13
South: 2024 = 8, 2025 = 11
West: 2024 = 14, 2025 = 15
Source: Annual Report, page 17
This makes queries such as "What was North revenue in 2025?" much more likely to retrieve the relevant table.
For numerical/analytical questions, I'd also consider routing retrieved tables to a structured-data/LLM reasoning step rather than relying exclusively on vector similarity.
Chunking
Use semantic + structural chunking, rather than fixed 500-token windows.
A good hierarchy is:
Document
└── Section
├── paragraph
├── paragraph
├── table
├── paragraph
└── figure
Then make chunks around sections and coherent concepts, with modest overlap where necessary.
I would also keep:
chunk_id
document_id
page_start / page_end
section_path
element_type
parent_element
bbox
source_uri
This lets retrieval return the right passage and lets your application jump back to the exact PDF page.
What I'd choose
| Requirement | Pipeline |
|---|
| Open source / self-hosted | Docling → structured JSON/Markdown → semantic chunker → embeddings → vector DB |
| Azure-heavy infrastructure | Azure Document Intelligence Layout → canonical representation → chunking → embeddings |
| Mostly simple digital PDFs | Native PDF extraction may be sufficient |
| Scanned PDFs | Layout parser + OCR |
| Financial/scientific PDFs with difficult tables | Layout-aware parser + dedicated table representation |
| Highest retrieval quality | Hybrid vector + keyword/metadata retrieval |
The important architectural principle is "structure first, vectors second." Keep the parsed representation as your source of truth and treat embeddings as an index over that representation.
If you're processing hundreds of thousands/millions of PDFs, I'd additionally make parsing an asynchronous, idempotent batch stage and cache its output before embedding. That prevents expensive re-parsing whenever you change your embedding model or chunking strategy.
If you're comfortable with a managed cloud service, Azure Document Intelligence's Layout model is another strong architecture. It returns paragraphs, roles, tables, cells, bounding regions, reading structure and OCR information, making it straightforward to preserve provenance.
How I'd represent the extracted data
Don't embed the PDF page wholesale. Convert it into objects roughly like:
{
"document_id": "report-123",
"page": 17,
"section": "3.2 Revenue",
"type": "table",
"content": "Region | 2024 | 2025\nNorth | $10M | $13M\n...",
"metadata": {
"bbox": [72, 310, 520, 690],
"table_id": "table-17-2"
}
}
For prose:
{
"document_id": "report-123",
"page": 18,
"section": "3.2 Revenue",
"type": "paragraph",
"content": "...",
"metadata": {
"parent_section": "3.2 Revenue"
}
}
That metadata becomes extremely valuable for retrieval and citations.
Tables need special treatment
Don't flatten a table into arbitrary text chunks. Preserve:
- table title/caption
- column headers
- row headers
- merged-cell relationships
- page/document location
- surrounding explanatory paragraph
- units and footnotes
For embedding, I'd create a retrieval-friendly textual representation of each table, while also storing the original structured table separately.
For example:
Table: Revenue by region, 2024–2025
Units: USD millions
North: 2024 = 10, 2025 = 13
South: 2024 = 8, 2025 = 11
West: 2024 = 14, 2025 = 15
Source: Annual Report, page 17
This makes queries such as "What was North revenue in 2025?" much more likely to retrieve the relevant table.
For numerical/analytical questions, I'd also consider routing retrieved tables to a structured-data/LLM reasoning step rather than relying exclusively on vector similarity.
Chunking
Use semantic + structural chunking, rather than fixed 500-token windows.
A good hierarchy is:
Document
└── Section
├── paragraph
├── paragraph
├── table
├── paragraph
└── figure
Then make chunks around sections and coherent concepts, with modest overlap where necessary.
I would also keep:
chunk_id
document_id
page_start / page_end
section_path
element_type
parent_element
bbox
source_uri
This lets retrieval return the right passage and lets your application jump back to the exact PDF page.
What I'd choose
| Requirement | Pipeline |
|---|
| Open source / self-hosted | Docling → structured JSON/Markdown → semantic chunker → embeddings → vector DB |
| Azure-heavy infrastructure | Azure Document Intelligence Layout → canonical representation → chunking → embeddings |
| Mostly simple digital PDFs | Native PDF extraction may be sufficient |
| Scanned PDFs | Layout parser + OCR |
| Financial/scientific PDFs with difficult tables | Layout-aware parser + dedicated table representation |
| Highest retrieval quality | Hybrid vector + keyword/metadata retrieval |
The important architectural principle is "structure first, vectors second." Keep the parsed representation as your source of truth and treat embeddings as an index over that representation.
If you're processing hundreds of thousands/millions of PDFs, I'd additionally make parsing an asynchronous, idempotent batch stage and cache its output before embedding. That prevents expensive re-parsing whenever you change your embedding model or chunking strategy.