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
unstructuredfor enterprise formats (PPTX, DOCX, HTML, emails). TheUnstructuredPDFLoaderis a single import that handles OCR, tables, and layout detection. - LlamaIndex: Native
PDFReaderwithpymupdfis zero-dependency beyond fitz. TheNodeParserabstractions compose cleanly withVectorStoreIndex,KnowledgeGraphIndex, andSummaryIndexwithout adapter code. - Both: Support
langchain_experimental/llama_index.core.ingestionpipelines 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:
UnstructuredPDFLoaderwithstrategy="fast"gets you structured markdown in seconds, andRecursiveCharacterTextSplitteris 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
UnstructuredPDFLoaderto validate extraction quality, then migrate to LlamaIndex’sHierarchicalNodeParserfor 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.