Documentos Office para RAG
Las pipelines de Retrieval-Augmented Generation viven y mueren por la calidad de la ingesta. Office Oxide te da tres salidas (texto, Markdown, IR estructurado) que mapean directamente a las tres cosas que una pipeline RAG necesita: cuerpo para embeddings, estructura para chunking, metadatos para citas.
Elige tu salida
| Objetivo | Usa |
|---|---|
| Embeddings más baratos, mínimo coste de token | plain_text() |
| Chunks con estructura preservada (mejor calidad de retrieval) | to_markdown() |
| Chunk + cita por section/diapositiva/celda | to_ir() |
Para la mayoría de proyectos, to_markdown() es el sweet spot: preserva títulos (límites naturales de chunk), las tablas siguen siendo consultables y es lo bastante pequeño como para embeber sin disparar los tokens.
Chunking consciente de títulos desde Markdown
La salida Markdown usa # / ## / ### para los títulos de origen. Cortar por ahí da chunks semánticamente coherentes “gratis”.
from office_oxide import Document
def chunk_by_heading(md: str, level: int = 2):
chunks, current = [], []
for line in md.splitlines():
if line.startswith("#" * level + " "):
if current:
chunks.append("\n".join(current))
current = [line]
else:
current.append(line)
if current:
chunks.append("\n".join(current))
return chunks
with Document.open("report.docx") as doc:
md = doc.to_markdown()
chunks = chunk_by_heading(md, level=2)
for c in chunks:
print(len(c), c[:60].replace("\n", " "))
Chunking basado en IR para precisión de cita
Si necesitas citar diapositiva 3 o hoja “Q4 Forecast” en el contexto recuperado, recorre el IR. Cada section trae el localizador natural:
from office_oxide import Document
with Document.open("deck.pptx") as doc:
ir = doc.to_ir()
chunks = []
for i, section in enumerate(ir["sections"], 1):
title = section.get("title") or f"Diapositiva {i}"
body = []
for el in section["elements"]:
if el["kind"] == "Heading":
body.append("# " + el["text"])
elif el["kind"] == "Paragraph":
body.append(" ".join(r["text"] for r in el["runs"]))
elif el["kind"] == "Table":
for row in el["rows"]:
body.append(" | ".join(row))
chunks.append({
"source": "deck.pptx",
"locator": f"slide:{i}",
"title": title,
"text": "\n".join(body),
})
Ahora tus chunks recuperados llevan un localizador preciso para citas (slide:3 / sheet:Q4 Forecast / section:2).
Integración con LangChain
from langchain_core.documents import Document as LCDoc
from office_oxide import Document
def load_office(path: str) -> list[LCDoc]:
with Document.open(path) as doc:
ir = doc.to_ir()
out = []
for i, section in enumerate(ir["sections"], 1):
body_lines = []
for el in section["elements"]:
if el["kind"] == "Paragraph":
body_lines.append(" ".join(r["text"] for r in el["runs"]))
elif el["kind"] == "Heading":
body_lines.append(el["text"])
if not body_lines:
continue
out.append(LCDoc(
page_content="\n".join(body_lines),
metadata={
"source": path,
"section_index": i,
"section_title": section.get("title"),
},
))
return out
docs = load_office("report.docx")
Tíralo en Chroma.from_documents(docs, embedder) (o cualquier vectorstore) como siempre.
Integración con LlamaIndex
from llama_index.core import Document as LIDoc
from office_oxide import Document
def load_office(path: str) -> list[LIDoc]:
with Document.open(path) as doc:
md = doc.to_markdown()
return [LIDoc(text=md, metadata={"source": path})]
Para nodos por section, usa el patrón IR de arriba y pasa cada chunk como un LIDoc separado.
Tablas — la parte difícil
Los LLM manejan bien tablas pequeñas en formato Markdown. Las tablas grandes (50+ filas) van mejor resumidas o paginadas:
def summarize_table(rows: list[list[str]]) -> str:
headers = rows[0]
body = rows[1:]
return f"Tabla con columnas {headers} y {len(body)} filas. Muestra: {body[:3]}"
Para dashboards (XLSX), considera extraer resúmenes por hoja en vez de volcados completos de celdas — el LLM saca más provecho de “La hoja ‘Q4’ totaliza ingresos de $4,2M en 12 regiones” que de 5.000 valores de celda.
Rendimiento y coste
| Op | Tiempo por archivo (DOCX, mediana) | Notas |
|---|---|---|
plain_text() |
0,8 ms | lo más barato |
to_markdown() |
~1,5 ms | recomendado para RAG |
to_ir() |
~1,2 ms | cuando necesitas estructura |
Un corpus de un millón de documentos se extrae en ~25 minutos single-thread, ~3 minutos en 8 núcleos. El coste dominante en tu pipeline RAG serán las llamadas a la API de embedding, no el parseo de Office.
Véase también
- Extracción Markdown — spec completa de salida
- IR estructurado — esquema para chunking consciente de citas
- Procesamiento por lotes — patrones de paralelismo