Del formato nativo a texto plano. Cada formato pierde algo distinto.
pierde: maquetación, tablas, orden de lecturaQuitar lo que se coló: cabeceras repetidas, menús, caracteres invisibles.
pierde: nada, si los patrones están bien elegidosPartir con la estrategia que corresponda al tipo de documento.
pierde: contexto que cruzaba la frontera del chunkAdjuntar de dónde viene cada chunk y qué es.
pierde: todo lo que no se anote aquí, para siempreLo primero que conviene desmontar es la idea de que un loader «lee» un documento. Un loader hace una traducción con pérdidas, y la pérdida depende del formato:
| Formato | Qué se complica |
|---|---|
| PDF nativo | El orden de lectura en dos columnas, y las tablas, que salen como texto corrido |
| PDF escaneado | Necesita OCR: si la imagen es ilegible para una persona, también lo será para el modelo |
| HTML | Menús, pies y banners se cuelan como texto legítimo en cada página |
| DOCX | Índices, notas al pie y encabezados repetidos en cada página |
| CSV / Excel | Decidir si cada fila es un documento o si la tabla entera es uno |
| Markdown | Poco: es el único formato donde la jerarquía sobrevive intacta |
La limpieza no es cosmética: cada trozo de ruido acaba siendo parte de un chunk, se convierte en vector y compite por aparecer en los resultados. Una cabecera que se repite en 200 páginas produce 200 chunks que se parecen entre sí y a nada más.
| Problema | Cómo se quita | Por qué importa |
|---|---|---|
| Caracteres no imprimibles | Regex sobre los rangos de control | Ensucian el texto sin verse |
| Espacios y saltos múltiples | Normalizar a uno / a dos | Inflan el tamaño del chunk sin aportar |
| Cabeceras y pies repetidos | Detectar el patrón y eliminarlo | Generan chunks casi idénticos |
| Navegación de HTML | Selectores CSS que excluyen esos bloques | El menú aparece en todas las páginas |
| Chunks duplicados | Hash del contenido y descartar repetidos | Ocupan sitio en el top-K sin añadir nada |
La deduplicación merece un momento aparte, porque no es evidente que haga falta hasta que ocurre: un mismo párrafo puede llegar por dos rutas —el manual y su versión en la wiki— y ocupar dos de los tres huecos que tenías para responder. La solución es barata (un hash del contenido antes de indexar) y el ahorro es doble: menos embeddings que pagar y más variedad en el top-K.
Cada chunk se guarda con un diccionario al lado. Lo que pongas ahí es todo lo que podrás usar después para filtrar y para citar; lo que no pongas no existe.
{
"source": "documentos/manual-v2.pdf", # de dónde salió
"page": 15, # dónde estaba
"section": "Configuración > Avanzado", # jerarquía, si la había
"doc_type": "manual", # para filtrar por categoría
"created_at": "2026-01-15", # para descartar lo viejo
"chunk_index": 3 # para reconstruir el orden
}
Con eso puestos, dos cosas dejan de ser imposibles. La primera es citar: una respuesta que dice «según el manual v2, página 15» se puede comprobar, y una que no dice de dónde viene hay que creérsela. La segunda es filtrar antes de buscar —solo manuales, solo de este año, solo de esta sección—, que suele mejorar el retrieval más que cualquier estrategia sofisticada, porque reduce el espacio de búsqueda en lugar de refinar el ranking dentro de él.
Y hay un tercer uso, el que salva depuraciones: source y chunk_index
te permiten volver del chunk malo al documento original y ver qué pasó al trocearlo.
Antes de indexar nada, escoge tres chunks al azar e imprime, para cada uno, el texto completo y su diccionario de metadata. Pregúntate dos cosas: ¿se entiende este fragmento leído a solas? y ¿podría, con esta metadata, decirle a alguien exactamente de dónde salió?
Si la respuesta a la primera es no, el problema está en el troceo. Si la respuesta a la segunda es no, el problema es que estás a punto de indexar un corpus del que no podrás citar nada — y eso no se arregla luego sin volver a empezar.