Saltar al contenido
Lección 4 de 12

Procesamiento de Documentos

7 min read

El Desafio de la Ingestion

Antes de poder embeber y buscar documentos, necesitas extraer texto limpio de ellos. Esto suena simple pero es una de las partes mas subestimadas de construir un sistema RAG. Los documentos del mundo real vienen en formatos desordenados: PDFs multi-columna con encabezados y pies de pagina, paginas HTML saturadas con navegacion y anuncios, imagenes escaneadas incrustadas en documentos Word, archivos de codigo con anidamiento complejo.

La calidad de tu texto extraido determina directamente la calidad de tus embeddings y, en ultima instancia, la calidad de tus respuestas. Basura entra, basura sale -- esto aplica en ningun lugar con mas fuerza que en RAG.

Procesamiento de PDFs

Los PDFs son el formato de documento mas comun en sistemas RAG empresariales, y tambien son los mas dificiles de procesar bien. Un PDF es fundamentalmente un formato visual -- describe donde dibujar caracteres en una pagina, no la estructura logica del texto.

PyPDF (Simple y Rapido)

PyPDF es una biblioteca pura de Python que extrae texto de la mayoria de los PDFs estandar. Funciona bien para PDFs nativos de texto (documentos creados digitalmente) pero tiene problemas con documentos escaneados y disenos complejos.

from langchain_community.document_loaders import PyPDFLoader

# Cargar un PDF y dividir por paginas
loader = PyPDFLoader("manual_empresa.pdf")
pages = loader.load()

for page in pages:
    print(f"Pagina {page.metadata['page']}: {len(page.page_content)} caracteres")
    print(page.page_content[:200])
    print("---")

Unstructured (Robusto y Consciente del Diseno)

La biblioteca unstructured maneja documentos complejos entendiendo elementos de diseno como titulos, encabezados, tablas y listas. Puede procesar PDFs, DOCX, HTML, imagenes (via OCR) y mas.

from langchain_community.document_loaders import UnstructuredPDFLoader

# El modo de alta resolucion detecta elementos de diseno
loader = UnstructuredPDFLoader(
    "informe_anual.pdf",
    mode="elements",  # Dividir en elementos (titulos, texto, tablas)
    strategy="hi_res"  # Usar modelo de deteccion de diseno
)
elements = loader.load()

for elem in elements[:5]:
    print(f"Tipo: {elem.metadata.get('category', 'desconocido')}")
    print(f"Texto: {elem.page_content[:100]}")
    print("---")

Manejo de PDFs Escaneados

Los PDFs escaneados contienen imagenes, no texto. Necesitas OCR (Reconocimiento Optico de Caracteres) para extraer texto. La biblioteca unstructured se integra con Tesseract OCR, y opciones mas avanzadas incluyen Azure Document Intelligence y AWS Textract.

# Usando unstructured con OCR para documentos escaneados
from langchain_community.document_loaders import UnstructuredPDFLoader

loader = UnstructuredPDFLoader(
    "contrato_escaneado.pdf",
    strategy="ocr_only"  # Forzar procesamiento OCR
)
docs = loader.load()

Consejo: Para sistemas en produccion que procesan muchos PDFs, considera Azure Document Intelligence o Amazon Textract. Manejan tablas, formularios y escritura a mano significativamente mejor que el OCR de codigo abierto.

Procesamiento de HTML

Las paginas web son una fuente comun para sistemas RAG -- sitios de documentacion, bases de conocimiento y publicaciones de blog viven en la web. El desafio es separar el contenido real de la navegacion, barras laterales, pies de pagina y anuncios.

Enfoque con BeautifulSoup

from langchain_community.document_loaders import WebBaseLoader
import bs4

# Cargar una pagina web y extraer solo contenido del articulo
loader = WebBaseLoader(
    web_paths=["https://docs.example.com/guia"],
    bs_kwargs=dict(
        parse_only=bs4.SoupStrainer(
            class_=("post-content", "article-body", "main-content")
        )
    ),
)
docs = loader.load()

# Limpiar el texto extraido
for doc in docs:
    # Eliminar espacios en blanco excesivos
    doc.page_content = " ".join(doc.page_content.split())
    print(doc.page_content[:300])

Procesamiento de Multiples Paginas

Para sitios de documentacion con muchas paginas, usa un sitemap o rastreador recursivo:

from langchain_community.document_loaders import SitemapLoader

# Cargar todas las paginas desde un sitemap
loader = SitemapLoader(
    "https://docs.example.com/sitemap.xml",
    filter_urls=["https://docs.example.com/guia/"],  # Solo paginas de la guia
)
docs = loader.load()
print(f"Cargadas {len(docs)} paginas")

Procesamiento de Markdown

Los archivos Markdown son el formato mas facil de procesar porque ya son texto plano con estructura ligera. Son comunes en repositorios de documentacion, wikis y bases de conocimiento.

from langchain_community.document_loaders import DirectoryLoader, TextLoader
from pathlib import Path

# Cargar todos los archivos Markdown de un directorio
loader = DirectoryLoader(
    "./docs",
    glob="**/*.md",
    loader_cls=TextLoader,
    loader_kwargs={"encoding": "utf-8"}
)
docs = loader.load()

# Cada documento incluye la ruta de origen en metadatos
for doc in docs:
    print(f"Origen: {doc.metadata['source']}")
    print(f"Longitud: {len(doc.page_content)} caracteres")

Procesamiento de Archivos de Codigo

Los archivos de codigo necesitan manejo especial porque su estructura porta significado. La indentacion, los limites de funciones y las declaraciones de importacion son importantes para entender el codigo.

from langchain_community.document_loaders import DirectoryLoader, TextLoader

# Cargar archivos Python de un proyecto
loader = DirectoryLoader(
    "./src",
    glob="**/*.py",
    loader_cls=TextLoader,
    loader_kwargs={"encoding": "utf-8"}
)
code_docs = loader.load()

# Agregar metadatos de lenguaje
for doc in code_docs:
    doc.metadata["language"] = "python"
    doc.metadata["file_type"] = "code"

Cubriremos el procesamiento de codigo en profundidad en la Leccion 9.

Extraccion de Metadatos

Buenos metadatos hacen la recuperacion dramaticamente mejor. Permiten filtrado (buscar solo documentos de marketing), proporcionan contexto al LLM (esta informacion proviene del informe anual 2024) y mejoran la citacion de fuentes.

Campos de Metadatos Esenciales

Cada documento en tu sistema RAG deberia llevar como minimo:

  • source -- de donde proviene el documento (ruta de archivo, URL, ID de base de datos)
  • title -- el titulo del documento o seccion
  • date -- cuando se creo o actualizo el documento por ultima vez
  • content_type -- que tipo de contenido es (politica, guia, codigo, FAQ)

Extraccion Automatica de Metadatos

from datetime import datetime
from pathlib import Path

def enrich_metadata(doc, base_path: str = ""):
    """Agregar metadatos utiles a un documento cargado."""
    source = doc.metadata.get("source", "")
    path = Path(source)

    # Extraer de la ruta del archivo
    doc.metadata["filename"] = path.name
    doc.metadata["extension"] = path.suffix
    doc.metadata["directory"] = str(path.parent)

    # Extraer titulo del primer encabezado
    lines = doc.page_content.split("\n")
    for line in lines:
        if line.startswith("# "):
            doc.metadata["title"] = line.lstrip("# ").strip()
            break

    # Agregar marca de tiempo de procesamiento
    doc.metadata["indexed_at"] = datetime.now().isoformat()

    # Estimar tipo de contenido
    if path.suffix in [".py", ".js", ".ts", ".go"]:
        doc.metadata["content_type"] = "code"
    elif path.suffix in [".md", ".mdx"]:
        doc.metadata["content_type"] = "documentation"
    elif path.suffix == ".pdf":
        doc.metadata["content_type"] = "document"

    return doc

Construyendo un Cargador Multi-Fuente

En produccion, raramente cargas documentos de una sola fuente. Un sistema RAG tipico ingesta de multiples formatos y ubicaciones. Aqui hay un patron para construir un cargador unificado:

from langchain_community.document_loaders import (
    PyPDFLoader,
    TextLoader,
    DirectoryLoader,
    WebBaseLoader,
)

def load_all_documents(config: dict) -> list:
    """Cargar documentos de multiples fuentes."""
    all_docs = []

    # Cargar PDFs
    if "pdf_dir" in config:
        loader = DirectoryLoader(
            config["pdf_dir"],
            glob="**/*.pdf",
            loader_cls=PyPDFLoader
        )
        pdf_docs = loader.load()
        for doc in pdf_docs:
            doc.metadata["source_type"] = "pdf"
        all_docs.extend(pdf_docs)

    # Cargar documentos Markdown
    if "docs_dir" in config:
        loader = DirectoryLoader(
            config["docs_dir"],
            glob="**/*.md",
            loader_cls=TextLoader,
            loader_kwargs={"encoding": "utf-8"}
        )
        md_docs = loader.load()
        for doc in md_docs:
            doc.metadata["source_type"] = "markdown"
        all_docs.extend(md_docs)

    # Cargar paginas web
    if "urls" in config:
        loader = WebBaseLoader(web_paths=config["urls"])
        web_docs = loader.load()
        for doc in web_docs:
            doc.metadata["source_type"] = "web"
        all_docs.extend(web_docs)

    # Enriquecer todos los metadatos
    all_docs = [enrich_metadata(doc) for doc in all_docs]

    print(f"Cargados {len(all_docs)} documentos de {len(config)} fuentes")
    return all_docs

# Uso
docs = load_all_documents({
    "pdf_dir": "./data/pdfs",
    "docs_dir": "./data/docs",
    "urls": [
        "https://docs.example.com/faq",
        "https://docs.example.com/inicio-rapido",
    ]
})

Manejo de Tablas y Datos Estructurados

Las tablas en PDFs y HTML son uno de los desafios mas complicados. Cuando la extraccion de texto aplana una tabla, las relaciones fila-columna se pierden y el LLM no puede interpretar los datos correctamente.

Estrategias para tablas:

  • Preservar estructura. Usa unstructured con estrategia hi_res para detectar y extraer tablas en formato HTML o Markdown.
  • Describir tablas. Convierte tablas a descripciones en lenguaje natural: "En Q3 2024, los ingresos fueron $12M, un aumento del 15% respecto a Q2."
  • Separar fragmentos de tablas. Almacena tablas como fragmentos propios con metadatos indicando que son datos tabulares.
  • Usar modelos de vision. Para tablas complejas, captura la tabla y usa un LLM de vision para describir su contenido.

Consejos para Procesamiento de Documentos en Produccion

  • Deduplica. Verifica documentos duplicados antes de indexar. El mismo contenido de diferentes fuentes contaminara tus resultados de busqueda.
  • Limpia agresivamente. Elimina encabezados, pies de pagina, numeros de pagina y texto boilerplate. Agregan ruido sin agregar informacion.
  • Preserva los titulos. Los titulos de seccion proporcionan contexto crucial. Incluyelos en cada fragmento o almacenalos como metadatos.
  • Rastrea versiones. Cuando los documentos se actualizan, re-indexalos y elimina entradas obsoletas. Un sistema RAG respondiendo con documentos desactualizados es peor que no responder en absoluto.
  • Registra todo. Registra cuantos documentos se cargaron, cuantos fallaron y por que. Los fallos de parsing son inevitables y necesitas visibilidad sobre ellos.

El procesamiento de documentos no es glamoroso, pero es donde muchos sistemas RAG triunfan o fracasan. En la siguiente leccion, aprenderas como dividir estos documentos procesados en fragmentos optimizados para la recuperacion.