DS DevShelfHub Projects · AI tools
Tutorials / RAG & Vector DBs / Document Ingestion
RAG Pipeline Beginner · 10 min read Page 3 of 23

Document Ingestion

By DevShelfHub

Loading documents from various sources, extracting metadata, handling different file formats, and preprocessing data for RAG.

Series progress3 / 23
RAG Document Ingestion — RAG pipeline tutorial

Overview: Data Sources

RAG requires pulling documents from various sources. They can be:

File-based

PDFs, Word, TXT, JSON, CSV, HTML

Web-based

Websites, APIs, URLs

Database

PostgreSQL, MySQL, MongoDB

Knowledge

Notion, Confluence, Google Docs

Loading Documents

LangChain and LlamaIndex provide loaders for dozens of formats. They all return Document objects with page_content and metadata.

LangChain Example

Python
from langchain_community.document_loaders import (
    PyPDFLoader,
    TextLoader,
    WebBaseLoader,
    UnstructuredExcelLoader
)

# Load a PDF
loader = PyPDFLoader("report.pdf")
docs = loader.load()  # Returns list of Document objects

# Load a webpage
loader = WebBaseLoader("https://example.com/docs")
docs = loader.load()

# Load plain text
loader = TextLoader("notes.txt")
docs = loader.load()

# Load Excel
loader = UnstructuredExcelLoader("data.xlsx")
docs = loader.load()

Document Structure

YAML
Document {
  page_content: "Tax deductions include...",
  metadata: {
    "source": "tax_guide.pdf",
    "page": 3,
    "author": "IRS",
    "date": "2024-01-01"
  }
}

Extracting and Enriching Metadata

Metadata is crucial for filtering, attribution, and debugging. Always preserve and enhance it.

Key Metadata to Preserve:

source

Where the document came from (filename, URL, table name)

timestamp

When it was created/modified (for versioning)

author

Who created it (for trust/permissions)

category

Type/topic (FAQ, policy, research, etc)

permissions

Who can access it (for multi-tenant RAG)

Pro tip: Add a chunk_id field when chunking. It helps trace which chunk led to the final answer and enables precise deletions during updates.

Handling Different File Formats

📄 PDFs

Challenge: Images, tables, forms, multiple columns.

Python
from langchain_community.document_loaders import PyPDFLoader
loader = PyPDFLoader("document.pdf")
docs = loader.load()
# Problem: Loses formatting, images as "Document page X"

Better: Use PyMuPDF or pdfplumber for layout-aware extraction. For scanned PDFs, use OCR (pytesseract).

📊 Excel / CSV

Challenge: Tabular data, headers, types.

Python
import pandas as pd
df = pd.read_csv("data.csv")
# Convert to documents
docs = [
  Document(
    page_content=f"Name: {row['name']}, Age: {row['age']}",
    metadata={"source": "data.csv", "row": i}
  )
  for i, row in df.iterrows()
]

Tip: For structured data, consider storing in a database and querying by filters, not RAG.

🌐 Web Pages

Challenge: HTML/CSS noise, JavaScript content, dynamic pages.

Python
from langchain_community.document_loaders import WebBaseLoader
loader = WebBaseLoader("https://example.com/docs")
docs = loader.load()
# Often includes navigation, ads, etc.

Better: Use BeautifulSoup to clean HTML. Extract only main content. For JavaScript-heavy sites, use Playwright.

🗄️ Databases

Challenge: Queries, joins, permissions, realtime updates.

Python
import psycopg2
conn = psycopg2.connect("dbname=mydb")
cursor = conn.cursor()
cursor.execute("SELECT * FROM articles")
docs = [
  Document(
    page_content=row['content'],
    metadata={"id": row['id'], "title": row['title']}
  )
  for row in cursor.fetchall()
]

Note: Keep chunk_id aligned with DB row IDs for easy deletion on update.

Preprocessing & Cleaning

Raw documents often contain noise that hurts embedding quality. Clean them before ingestion.

Python
import re

def clean_text(text):
    # Remove extra whitespace
    text = re.sub(r'\s+', ' ', text).strip()

    # Remove URLs (optional, depends on use case)
    text = re.sub(r'https?://\S+', '', text)

    # Remove email addresses (optional)
    text = re.sub(r'\S+@\S+', '', text)

    # Remove special characters (keep some for code)
    text = re.sub(r'[^\w\s\.\,\!\?\-]', '', text)

    return text

# Apply to documents
for doc in documents:
    doc.page_content = clean_text(doc.page_content)

Common Issues to Fix:

Extra whitespace/newlines from PDF extraction

Page headers/footers repeated on every page

Navigation text from web pages (menu items, etc)

Encoding issues (special characters, mojibake)

Private information that shouldn't be indexed

Notes

Loader selection matters more than you think

PyPDFLoader is convenient but loses layout information — tables become scrambled text, multi-column layouts merge incorrectly. Use pdfplumber or PyMuPDF when your PDFs have structured content. For scanned PDFs, budget for an OCR step; pytesseract works locally but AWS Textract or Google Document AI produce far better results on complex layouts.

Hash documents before embedding to avoid duplicates

Store a SHA-256 hash of each document's content in a simple key-value store (Redis or even a SQLite table) before calling the embedding API. On re-ingestion, skip any document whose hash already exists. Without this, incremental refreshes silently double-embed the same content, degrading retrieval precision and burning API budget.

Web loaders need aggressive cleaning

WebBaseLoader returns the full DOM text including navigation menus, cookie banners, and footer links. Always strip boilerplate with BeautifulSoup's get_text(separator=' ', strip=True) targeting only the main content element. For JavaScript-rendered pages, swap to PlaywrightURLLoader — async rendering adds 2–3 seconds per page but is unavoidable for SPAs.

Attach a chunk_id before the vector store, not after

Set a deterministic chunk_id (e.g., f"{source_hash}_{chunk_index}") during the loading step, before chunking. If you try to reconstruct IDs later, you lose the ability to do targeted deletes when a single document is updated — you end up re-ingesting everything instead.

RAG Document Ingestion FAQ

What file formats can a RAG system ingest?

Most RAG frameworks support PDF, DOCX, HTML, Markdown, plain text, CSV, and JSON out of the box. For images and tables within PDFs, you need document AI tools like AWS Textract or Azure Document Intelligence.

How do I extract text from PDFs for RAG?

Use PyPDF2 or pdfplumber for text-only PDFs. For scanned PDFs, use OCR via Tesseract or a hosted API. LangChain's PyPDFLoader and LlamaIndex's PDFReader handle most cases with one line of code.

What is document metadata and why does it matter in RAG?

Metadata includes source URL, file name, author, date, and page number. Storing it alongside chunks lets you filter retrieval results (e.g., "only search documents from 2024"), cite sources, and debug which documents were retrieved.

How should I handle duplicate documents in RAG ingestion?

Hash the document content before ingestion. If the hash already exists in your tracking store, skip re-embedding. This prevents duplicate chunks from cluttering retrieval results and wasting embedding API costs.

Can I ingest web pages into a RAG pipeline?

Yes. Use LangChain's WebBaseLoader or a Firecrawl API integration to scrape and clean web pages. Strip navigation, ads, and boilerplate before chunking, and store the URL as metadata for source citation.