PDF parsing with OpenDataLoader
Upgrade PDF ingestion from flat text to structured document understanding.
Audience: operators who want better answers from PDF documents — especially tables, structured data, and multi-page documents. What you will accomplish: enable ODL, understand the three value layers, and diagnose issues.
What OpenDataLoader does
The default PDF parser (PyPDFLoader) extracts raw text — tables become garbled columns,
headings lose their hierarchy, and lists become loose words. OpenDataLoader (ODL) solves
this in three layers:
Step 1: Layer 1: Content
ODL outputs Markdown — tables become pipe tables, headings become
# markers, lists become bullet points. The LLM receives structured text instead of noise.Step 2: Layer 2: Structure
ODL’s JSON output exposes heading hierarchy, element types, and bounding boxes. The system builds two chunk levels: L1 (section-level) and L2 (element-level), with parent links for context expansion.
Step 3: Layer 3: Intelligence
Element-aware retrieval boosts table chunks for table-like queries and section chunks for overview queries. Context expansion pulls the full section when a paragraph-level match is found.
Prerequisites
- Java 11+ must be on the server’s
$PATH(java -version). - The
opendataloader_pdfPython package (pip install opendataloader-pdf). - Both are optional — when absent, the system falls back to PyPDF automatically.
Quick start
ODL is auto-detected. When Java 11+ and the package are present, PDFs automatically use ODL without any configuration change:
curl -X POST http://127.0.0.1:8000/api/v1/ingest/upload \
-F "file=@annual_report.pdf"If it fails: If Java is missing, the ingest silently falls back to PyPDF. Check the status endpoint to see which parser was used.
Check the parser used:
curl http://127.0.0.1:8000/api/v1/ingest/status/annual_reportLook for "parser": "opendataloader" in the response. If you see "fallback_used": true,
ODL failed and PyPDF was used instead.
Three tracks (value layers)
Track 1: Markdown content (default, no config needed)
When ODL is active, PDF chunks store Markdown fragments instead of raw text. The existing
RecursiveCharacterTextSplitter splits on \n\n and \n, which Markdown headings and
section breaks naturally provide.
What improves:
- Tables appear as pipe tables (
| Column | Value |) - Headings preserve hierarchy (
# Title,## Section) - Lists stay as bullet points (
- item)
Track 2: Hierarchical chunking + retrieval (opt-in)
Enable by setting RETRIEVAL_STRATEGY=hierarchical.
Two chunk levels are indexed:
| Level | Granularity | Use case |
|---|---|---|
| L1 | One chunk per heading section | Overview questions, “what is this document about?” |
| L2 | One chunk per element (paragraph, table, list) | Specific questions, “what is the pricing?” |
L2 chunks carry a parent_chunk_id pointing to their L1 section. The retrieval strategy:
- Fetches both L1 and L2 candidates
- Boosts table chunks when the query contains table-like terms (
table,row,column,compare,vs) - Boosts L1 section chunks when the query contains overview terms (
overview,summary,what is,about) - Expands L2 results to their L1 parent when the result set has room, giving the LLM full section context
Track 3: Hybrid OCR + enrichment (optional sidecar)
For scanned PDFs, formulas, and chart descriptions, run the hybrid sidecar:
docker compose --profile hybrid upThis starts an odl-hybrid service on port 5002. Enable in .env:
ODL_HYBRID=docling-fast
ODL_HYBRID_MODE=auto # auto = triage (simple pages stay local, complex pages go to AI)
# or ODL_HYBRID_MODE=full # all pages processed by the AI backendAdditional enrichment options:
ODL_ENRICH_FORMULA=true # Extract LaTeX formulas (requires full mode)
ODL_ENRICH_PICTURES=true # Describe charts and images (requires full mode)Both enrichment flags require ODL_HYBRID_MODE=full — the AI backend must process every
page to find formulas and pictures.
Configuration reference
| Variable | Default | Purpose |
|---|---|---|
PDF_PARSER | (empty) | Override: pypdf or opendataloader. Empty = auto-detect. |
PDF_PARSER_FALLBACK | true | Fall back to PyPDF when ODL fails. |
ODL_FORMAT | json,markdown | Output formats passed to ODL. |
ODL_READING_ORDER | xycut | Reading-order algorithm. |
ODL_USE_STRUCT_TREE | false | Use native PDF structure tags when present. |
ODL_INCLUDE_HEADER_FOOTER | false | Include header/footer elements in output. |
ODL_HYBRID | (empty) | Hybrid backend: docling-fast or empty (disabled). |
ODL_HYBRID_MODE | auto | auto (triage) or full (all pages to AI). |
ODL_HYBRID_URL | (empty) | URL of the hybrid sidecar (default: http://odl-hybrid:5002 in Compose). |
ODL_HYBRID_FALLBACK | false | Fall back to local Java when hybrid server is unreachable. |
ODL_ENRICH_FORMULA | false | Extract LaTeX formulas (requires hybrid_mode=full). |
ODL_ENRICH_PICTURES | false | Describe charts/images (requires hybrid_mode=full). |
RETRIEVAL_STRATEGY | mmr | Set to hierarchical for element-aware retrieval. |
Per-request overrides
When ingesting, you can override the parser and hybrid mode per request:
curl -X POST http://127.0.0.1:8000/api/v1/ingest \
-H "Content-Type: application/json" \
-d '{
"file_name": "scan",
"s3_url": "https://example.com/scan.pdf",
"parser": "opendataloader",
"hybrid_mode": "full",
"pages": "1-10"
}'| Field | Type | Description |
|---|---|---|
parser | pypdf / opendataloader | Force a specific parser. |
hybrid_mode | auto / full | Override the deployment default. |
pages | string | Process only specific pages, e.g. "1-10" or "1,3,5-7". |
Citations with ODL
When ODL fields are present, citations include additional context:
| Field | Example | Description |
|---|---|---|
section | "Financial Highlights" | The heading of the containing section. |
element_type | "table" | Type: paragraph, table, list, heading, formula. |
page_end | 4 | Last page for multi-page elements (e.g. tables spanning pages). |
bbox | [72.0, 680.5, 540.25, 740.1] | Bounding box in PDF points [left, bottom, right, top]. |
These fields are optional — old chunks without them simply show null, and older
clients are unaffected.
Troubleshooting
Java not found
java -version
# → "java: command not found"Install Java 11+ (see the operator guide). When PDF_PARSER_FALLBACK=true (default), the
system silently falls back to PyPDF. Set PDF_PARSER_FALLBACK=false to make Java absence a
hard error.
Hybrid server not reachable
If ODL_HYBRID is set but the sidecar is down:
- With
ODL_HYBRID_FALLBACK=true→ falls back to local Java (no OCR/enrichment) - With
ODL_HYBRID_FALLBACK=false→ ingest fails with a clear error
Enrichment produces empty chunks
Formula and picture enrichment require ODL_HYBRID_MODE=full. In auto mode, the triage
router may skip complex pages. The config validator raises a startup error if enrichment is
enabled with auto mode.
Manually forcing PyPDF
Set PDF_PARSER=pypdf in .env or pass "parser": "pypdf" in the ingest request.
Hierarchical retrieval not activating
RETRIEVAL_STRATEGY=hierarchical is required. The default is mmr. Without this setting,
L1/L2 chunks are retrieved but without element-type boosting or context expansion.
Verify your result
- Verify: You can verify ODL is active by checking
parser=opendataloaderin the ingest status. - Verify: You understand the three tracks: Markdown (default), Hierarchical (opt-in), Hybrid (optional sidecar).
- Verify: You can force PyPDF fallback per-request or globally.
- Verify: You know enrichment requires
hybrid_mode=full.
Related next steps
- See how retrieval strategies work in Retrieval.
- Configure all ODL variables in Configuration.
- Learn about document ingestion in Ingesting documents.
- Read the operator troubleshooting guide in the project repo:
docs/odl-operator-guide.md.