n4nAI

RAG on PDFs: LangChain vs LlamaIndex parsers compared

A hands-on comparison of LangChain and LlamaIndex PDF parsers for RAG, covering extraction quality, chunking, performance, and when to use each.

n4n Team5 min read1,099 words

Audio narration

Coming soon — every post will get a voice note here.

If you’re building RAG on PDFs, the parser decision shapes everything downstream: retrieval precision, citation accuracy, and how much preprocessing debt you inherit. LangChain and LlamaIndex both wrap multiple PDF backends, but they optimize for different defaults and expose different knobs. This comparison cuts through the documentation to show where each actually wins.

Parsing architecture differences

LangChain treats PDF parsing as a document loader problem. You pick a loader — PyPDFLoader, PDFMinerLoader, UnstructuredPDFLoader, or PyMuPDFLoader — and each returns a list of Document objects with page_content and metadata. The loader choice is explicit and swappable, but chunking happens separately via text splitters.

from langchain_community.document_loaders import PyMuPDFLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter

loader = PyMuPDFLoader("report.pdf")
docs = loader.load()

splitter = RecursiveCharacterTextSplitter(
    chunk_size=1000,
    chunk_overlap=200,
    separators=["\n\n", "\n", ". ", " ", ""]
)
chunks = splitter.split_documents(docs)

LlamaIndex frames parsing as a node parser problem. You configure a PDFReader (wrapping pymupdf, pdfminer, or unstructured) and then apply a NodeParser that understands document structure. The SimpleNodeParser is the default, but SentenceWindowNodeParser and HierarchicalNodeParser preserve more context.

from llama_index.core import SimpleDirectoryReader
from llama_index.core.node_parser import SentenceWindowNodeParser

reader = SimpleDirectoryReader(input_files=["report.pdf"])
documents = reader.load_data()

parser = SentenceWindowNodeParser.from_defaults(
    window_size=3,
    window_metadata_key="window",
    original_text_metadata_key="original_text"
)
nodes = parser.get_nodes_from_documents(documents)

The practical difference: LangChain gives you raw text chunks with page numbers. LlamaIndex gives you nodes that can carry sentence windows, hierarchical relationships, and custom metadata fields that the retriever can exploit.

Extraction quality on real documents

Both frameworks delegate to the same underlying libraries, so raw text extraction quality converges when you use the same backend. The divergence appears in structure preservation.

PyMuPDF (fitz) via either framework handles columns, tables, and mixed layouts best. pdfminer.six struggles with multi-column layouts but preserves reading order better for single-column academic papers. unstructured adds OCR fallback and table extraction but adds latency and an external dependency.

LangChain’s UnstructuredPDFLoader with strategy="hi_res" extracts tables as markdown and preserves header hierarchy:

from langchain_community.document_loaders import UnstructuredPDFLoader

loader = UnstructuredPDFLoader(
    "financial_report.pdf",
    strategy="hi_res",
    infer_table_structure=True
)
docs = loader.load()
# Tables appear as markdown in page_content

LlamaIndex’s PDFReader with pymupdf returns text blocks with bbox coordinates, letting you filter headers/footers programmatically:

from llama_index.readers.file import PDFReader

reader = PDFReader()
docs = reader.load_data(file="financial_report.pdf")
# Each doc.text includes content; doc.metadata has 'page_label', 'file_path'
# For bbox access, use pymupdf directly and wrap

For scanned PDFs, both require OCR. LangChain’s UnstructuredPDFLoader with strategy="ocr_only" works out of the box. LlamaIndex needs you to pipe through pytesseract or use the UnstructuredReader integration.

Chunking strategies and metadata

This is where retrieval quality lives or dies.

LangChain’s RecursiveCharacterTextSplitter is the workhorse. It splits by separators recursively, respecting chunk size and overlap. It’s predictable but blind to semantic boundaries. MarkdownHeaderTextSplitter and HTMLHeaderTextSplitter add structure awareness but only work if your parser emits markdown/HTML.

from langchain_text_splitters import MarkdownHeaderTextSplitter

headers_to_split_on = [
    ("#", "header_1"),
    ("##", "header_2"),
    ("###", "header_3"),
]
splitter = MarkdownHeaderTextSplitter(headers_to_split_on=headers_to_split_on)
md_chunks = splitter.split_text(markdown_text)

LlamaIndex’s SentenceWindowNodeParser stores a rolling window of sentences around each chunk. At retrieval time, you can fetch the window for context while embedding only the central sentence. This is a genuine retrieval advantage for dense vectors.

from llama_index.core.node_parser import SentenceWindowNodeParser
from llama_index.core.postprocessor import MetadataReplacementPostProcessor

parser = SentenceWindowNodeParser.from_defaults(window_size=3)
nodes = parser.get_nodes_from_documents(documents)

# At query time
postprocessor = MetadataReplacementPostProcessor(target_metadata_key="window")
# Replaces node.text with the window content for the LLM

HierarchicalNodeParser builds a tree of nodes (document → sections → paragraphs → sentences) and lets you retrieve at multiple granularities. LangChain has no direct equivalent — you’d need to run multiple splitters and manage relationships yourself.

Metadata handling: LangChain stuffs everything into Document.metadata dict. LlamaIndex uses typed MetadataMode (ALL, EMBED, LLM, NONE) so you control what goes into embeddings vs. what the LLM sees. This matters when you have noisy metadata (filenames, timestamps) that hurts embedding quality.

Performance and cost

Parsing is CPU-bound and single-threaded in both frameworks. For a 200-page PDF with mixed layouts:

Backend Time (approx) Memory Notes
PyMuPDF 2-4 seconds Low Fastest, best layout
pdfminer.six 8-15 seconds Medium Pure Python, slower
unstructured (hi_res) 30-120 seconds High Calls libmagic, poppler, tesseract
unstructured (fast) 5-10 seconds Medium Rule-based, no OCR

Chunking overhead is negligible (<100ms for 10k chunks) in both.

Embedding cost dominates. LlamaIndex’s sentence window means you embed fewer tokens per logical unit (one sentence vs. 1000-char chunk), but you store more nodes. LangChain’s larger chunks mean fewer embedding calls but more tokens per call. At current embedding prices (text-embedding-3-small at $0.02/1M tokens), the difference is pennies per thousand pages — optimize for retrieval quality first.

If you’re parsing at scale, both frameworks support async loading. LangChain: aload(). LlamaIndex: aload_data(). Neither parallelizes the actual PDF parsing — you need a process pool or Ray for that.

Ergonomics and debugging

LangChain’s document model is simpler: list of Document objects, each with page_content (str) and metadata (dict). You can print(doc.page_content[:200]) and immediately see what the splitter produced. The RecursiveCharacterTextSplitter has a split_text method you can unit test in isolation.

LlamaIndex’s Node objects carry more: text, metadata, relationships (prev/next/parent/child), embedding, score. Powerful, but verbose. Debugging means inspecting node.get_content(metadata_mode=MetadataMode.ALL) and tracing relationships.

# LangChain: trivial inspection
for i, chunk in enumerate(chunks[:3]):
    print(f"Chunk {i}: {chunk.metadata.get('page')}, {len(chunk.page_content)} chars")
    print(chunk.page_content[:150])

# LlamaIndex: richer but more ceremony
for node in nodes[:3]:
    print(f"Node {node.node_id}: page={node.metadata.get('page_label')}")
    print(f"  Text: {node.get_content()[:150]}")
    print(f"  Window: {node.metadata.get('window', '')[:150]}")
    print(f"  Relationships: {list(node.relationships.keys())}")

LangChain wins for quick iteration. LlamaIndex wins when you need the retrieval pipeline to understand document structure.

Ecosystem and integrations

Both integrate with every major vector store. LangChain has more community-contributed loaders (Confluence, Notion, Slack, etc.) but many are unmaintained. LlamaIndex’s LlamaHub agents are similarly hit-or-miss.

For PDF-specific work, the differentiators are:

  • LangChain: Better integration with unstructured for enterprise formats (PPTX, DOCX, HTML, emails). The UnstructuredPDFLoader is a single import that handles OCR, tables, and layout detection.
  • LlamaIndex: Native PDFReader with pymupdf is zero-dependency beyond fitz. The NodeParser abstractions compose cleanly with VectorStoreIndex, KnowledgeGraphIndex, and SummaryIndex without adapter code.
  • Both: Support langchain_experimental / llama_index.core.ingestion pipelines for idempotent, incremental ingestion with docstore tracking.

If you’re building a multi-format ingestion pipeline, LangChain’s loader uniformity reduces boilerplate. If you’re PDF-first and want hierarchical retrieval, LlamaIndex’s node parsers save custom code.

Comparison table

Dimension LangChain LlamaIndex
Primary abstraction Document loaders + text splitters Readers + node parsers
Best PDF backend PyMuPDFLoader / UnstructuredPDFLoader PDFReader (pymupdf)
Structure preservation MarkdownHeaderTextSplitter (if markdown) HierarchicalNodeParser, SentenceWindowNodeParser
Metadata control Dict on Document Typed MetadataMode (EMBED/LLM/NONE)
Table extraction Via unstructured (hi_res) Via pymupdf bbox + custom logic
OCR support Built into UnstructuredPDFLoader Requires separate OCR pipeline
Incremental ingestion IngestionPipeline (experimental) IngestionPipeline + docstore
Async support aload(), asplit_documents() aload_data(), aget_nodes_from_documents()
Debugging ergonomics Simple Document objects Rich Node objects with relationships
Retrieval primitives Vector store retrievers VectorIndexRetriever + postprocessors (window replacement, rerank)
Multi-format pipeline Uniform loader interface Reader per format, more config

Which to choose

Choose LangChain when:

  • You ingest heterogeneous formats (PDF, DOCX, HTML, Notion, Slack) and want one loader pattern across all of them.
  • Your team already uses LangChain for chains/agents and you want consistent primitives.
  • You need fast iteration: UnstructuredPDFLoader with strategy="fast" gets you structured markdown in seconds, and RecursiveCharacterTextSplitter is predictable.
  • You don’t need hierarchical retrieval or sentence-window context expansion.

Choose LlamaIndex when:

  • PDFs are your primary or only source and you want retrieval that understands document hierarchy.
  • Sentence-window retrieval matters: you embed fine-grained units but present expanded context to the LLM.
  • You need metadata filtering at embedding time (exclude noisy fields from vectors, keep them for the LLM).
  • You’re building a multi-index system (vector + knowledge graph + summary) over the same parsed nodes.

Use both when:

  • Prototyping: start with LangChain’s UnstructuredPDFLoader to validate extraction quality, then migrate to LlamaIndex’s HierarchicalNodeParser for production retrieval.
  • Different pipelines for different document types: LangChain for messy enterprise formats, LlamaIndex for clean technical PDFs where hierarchy pays off.

The parser is not the product. But the wrong parser makes the product worse in ways that show up at query time — missing table data, hallucinated citations, chunks that split mid-sentence. Pick the abstraction that matches your retrieval strategy, not the one with more GitHub stars.

Tagsragpdf-parsinglangchainllamaindex

Written by

n4n Team

The team building n4n — a single OpenAI-compatible API in front of 240+ models, with automatic fallback, load balancing and pay-per-token metering.

More from n4n Team →

All langchain vs llamaindex for rag posts →