Fragmentado padre-hijo e indexado jerárquico para una mejor recuperación RAG
Por qué los fragmentos pequeños ganan la competencia pero pierden la respuesta, y cómo el fragmentado padre-hijo, la expansión por eliminación de duplicados y el indexado jerárquico restauran el contexto en la recuperación RAG.
Fuentes: Parent–Child Chunking and Hierarchical Indexing in RAG: A Practical Guide to Better Retrieval
Cuando un sistema RAG devuelve una respuesta que está casi correcta pero carece del detalle crucial, el punto débil suele ser la forma en que se dividió el texto, y no las representaciones vectoriales. Dos técnicas abordan esto directamente: el particionamiento padre-hijo, que busca fragmentos pequeños pero lee los grandes, y la indexación jerárquica, que reduce la búsqueda siguiendo el esquema propio del documento. Ambas tienen como objetivo brindar a un modelo de lenguaje precisión y contexto al mismo tiempo.
Por qué la división es el paso difícil
El proceso básico es sencillo: dividir los documentos en fragmentos, incrustarlos, almacenarlos, obtener los k mejores resultados para una consulta y entregárselos a un modelo. Cuando surge un problema, suele deberse al primer paso del proceso. Los fragmentos demasiado grandes diluyen la relevancia y distorsionan la incrustación, mientras que los fragmentos demasiado pequeños eliminan las oraciones circundantes de las que depende una pregunta. La división por tamaño fijo, recursiva, semántica o por ventana de oraciones son intentos de equilibrar esa tensión dentro de una sola unidad de texto.
Vale la pena probar un enfoque diferente: dejar de buscar un tamaño perfecto y utilizar dos tamaños juntos, cada uno ajustado para una tarea distinta. Antes de implementar esa solución, es útil identificar con precisión dónde falla un único tamaño.
Un pequeño ejemplo de fallo
Considere un pasaje de dos oraciones de un documento técnico. La primera oración dice que el caché hace que un sistema sea mucho más rápido. La segunda advierte que una mala invalidación del caché puede dejar datos obsoletos.
Un divisor de tamaño reducido podría dividirlo en dos fragmentos. El fragmento A es la oración que elogia el rendimiento, y el fragmento B es la oración sobre los datos obsoletos causados por una mala invalidación. Ahora pregúntese: ¿cuál es un riesgo del caché? El script a continuación incluye la pregunta y ambos fragmentos, y luego los compara mediante similitud de coseno.
from sentence_transformers import SentenceTransformer
import numpy as np
model = SentenceTransformer("all-MiniLM-L6-v2")
def cosine_similarity(a, b):
return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))
query = "What is a risk of caching?"
chunk_a = "Caching improves system performance significantly."
chunk_b = "However, improper cache invalidation can lead to stale data issues."
query_embedding = model.encode(query)
score_a = cosine_similarity(query_embedding, model.encode(chunk_a))
score_b = cosine_similarity(query_embedding, model.encode(chunk_b))
print(f"Chunk A similarity: {score_a:.4f} - '{chunk_a}'")
print(f"Chunk B similarity: {score_b:.4f} - '{chunk_b}'")
Al ejecutarlo, se muestra la similitud de cada fragmento con la pregunta.
Chunk A similarity: 0.5891 — 'Caching improves system performance significantly.'
Chunk B similarity: 0.5103 — 'However, improper cache invalidation can lead to stale data issues.'
El fragmento A obtiene una puntuación más alta, alrededor de 0.59 frente a aproximadamente 0.51, aunque es el fragmento B el que responde a la pregunta. Ambos mencionan el caché, por lo que parecen estar relacionados, y el modelo de embedding no tiene motivo especial para vincular la palabra “riesgo” con una oración sobre datos obsoletos que nunca utiliza esa palabra. Un recuperador que solo tome la mejor coincidencia pasaría confiadamente la mitad incorrecta del pasaje al modelo.
La lección es que la similitud semántica no garantiza la completitud. Un fragmento puede considerarse apropiado para el tema y, aun así, carecer de la información que la pregunta necesita; cuanto más pequeño sea el fragmento, mayor es la probabilidad de que esa información se encuentre en una oración vecina.
Fragmentación padre-hijo
La idea es almacenar al mismo tiempo dos niveles de detalle del mismo texto, cada uno con una función diferente.
Los fragmentos infantiles son pequeños y precisos. Suelen contener de 100 a 400 tokens y están diseñados para realizar búsquedas con un alto índice de recuperación. Su pequeño tamaño genera vectores más nítidos, ya que menos material no relacionado diluye lo que representa cada vector. Sin embargo, reducirlos a un puñado de palabras tiene efectos negativos, ya que un fragmento podría carecer del significado independiente necesario para una coincidencia fiable.
Los bloques padre son grandes y contextuales. A menudo abarcan de 800 a 3000 tokens o más, cubriendo toda una sección o unidad lógica, y existen para razonar y no para hacer coincidencias. En el ejemplo de caché, un bloque padre que contiene todo el pasaje incluye ambas partes de la idea, de modo que sea cual sea el bloque hijo que haga coincidencia, el modelo sigue viendo la imagen completa. El límite superior proviene de la ventana de contexto del modelo, ya que varios bloques padre grandes reunidos en una sola solicitud pueden ocupar todo el espacio que esa solicitud debe contener.
La relación se puede resumir en una frase: los bloques hijos son los que se buscan, y los bloques padre son los que se leen. Un bloque hijo debe ser lo suficientemente preciso como para ganar la competencia de similitud, y su bloque padre asegura que el modelo reciba el contexto del cual depende la respuesta de ese bloque hijo.
Así es en la práctica
Imagínese una sección titulada “Optimización del rendimiento de la base de datos”. Un divisor padre-hijo funciona en dos pasos. Primero identifica el límite del padre, toda la sección que abarca el indexado, la optimización de consultas, el caché y las compensaciones. Luego divide ese mismo texto en pequeñas secciones hijos dentro de él, por ejemplo:
- una sección hijo sobre cómo el indexado mejora la velocidad de las consultas al reducir el tiempo de escaneo,
- una sección hijo sobre cómo el caché reduce los cálculos repetidos, aunque con el riesgo de que la información quede desactualizada,
- una sección hijo sobre cómo reescribir las consultas SQL para optimizarlas.
Lo que hace que este esquema funcione es que cada sección hijo recuerda de dónde proviene. El método más sencillo es almacenar el texto completo del padre en los metadatos de cada sección hijo, como en esta función de indexado.
def build_parent_child_index(document_section, parent_text, child_splitter, embedding_model):
child_chunks = child_splitter(parent_text)
records = []
for child_text in child_chunks:
records.append({
"text": child_text,
"embedding": embedding_model.encode(child_text),
"parent_text": parent_text # full parent stored directly on every child
})
return records
parent_text = """Indexing improves query speed by reducing scan time across large tables.
Caching reduces repeated computation but may introduce staleness if not invalidated properly.
Query optimization involves rewriting SQL queries to use more efficient execution plans."""
child_texts = [
"Indexing improves query speed by reducing scan time across large tables.",
"Caching reduces repeated computation but may introduce staleness if not invalidated properly.",
"Query optimization involves rewriting SQL queries to use more efficient execution plans.",
]
records = [
{"text": t, "parent_text": parent_text} for t in child_texts
]
for r in records:
print(f"Child: {r['text'][:50]}...")
print(f" → linked to parent ({len(r['parent_text'])} chars)\n")
Duplicar el elemento padre en cada hijo requiere más espacio de almacenamiento, pero no necesita una búsqueda separada en el momento de la consulta, ya que el padre ya se encuentra en el registro del hijo. La alternativa es almacenar solo un parent_id y recuperar el padre cuando sea necesario, lo cual ahorra espacio a gran escala pero añade un paso adicional de búsqueda. De cualquier manera, cada hijo sigue siendo buscable de forma independiente, al igual que cualquier otro grupo de datos, y cuenta con un camino directo hacia su contexto completo.
Recuperar lo pequeño, expandir lo grande
Esto invierte el flujo habitual. El RAG estándar sigue el orden de recuperar, enviar y responder. La versión padre-hijo sigue el orden de recuperar un hijo, expandirlo a su padre, enviar el contexto enriquecido y responder. A continuación se muestra cómo se implementa este flujo, continuando con el ejemplo de caché.
def parent_child_retrieve(query, child_records, embedding_model, top_k=3):
query_embedding = embedding_model.encode(query)
scored = [
(np.dot(query_embedding, r["embedding"]) /
(np.linalg.norm(query_embedding) * np.linalg.norm(r["embedding"])), r)
for r in child_records
]
scored.sort(key=lambda x: x[0], reverse=True)
top_children = scored[:top_k]
# Expand each matched child to its parent - but deduplicate first
seen_parents = set()
expanded_context = []
for score, record in top_children:
parent = record["parent_text"]
if parent not in seen_parents:
expanded_context.append(parent)
seen_parents.add(parent)
return expanded_context
for r in records:
r["embedding"] = model.encode(r["text"])
context = parent_child_retrieve("What is a risk of caching?", records, model, top_k=2)
print("Context sent to the LLM:\n")
for c in context:
print(c)
Preste atención a la verificación de seen_parents. Cuando los dos hijos superiores pertenecen al mismo padre, lo cual es común ya que los fragmentos adyacentes de una misma sección suelen tener puntuaciones similares, expandir cada uno por separado enviaría el mismo texto del padre dos veces y desperdiciaría contexto. Al eliminar duplicados en el nivel del padre, cada fragmento de contexto aparece una sola vez, independientemente de cuántos hijos coincidan.
En el caso del caché, el modelo ahora recibe la oración que ganó la coincidencia junto con la explicación que la rodea, de modo que ambas partes llegan juntas, sin importar qué hijo obtuvo la posición más alta.
Por qué funciona
- Precisión con contexto. Los hijos encuentran el lugar adecuado, y los padres aportan lo que un solo hijo no puede transmitir.
- Razonamiento mejorado. Los modelos tienen dificultades con fragmentos desconectados, y un padre restablece la continuidad narrativa.
Indexación jerárquica
Si la relación padre-hijo es de dos niveles, la indexación jerárquica puede ser de muchos niveles. En lugar de un padre y sus hijos, se crea una estructura que refleja cómo está organizado el documento. Una jerarquía de ejemplo podría verse así.
Document: "Cloud Architecture Guide"
Section: Networking
Subsection: Load Balancing
Chunk: Round-robin method
Chunk: Least connections method
Subsection: CDN usage
Section: Security
Subsection: IAM policies
Subsection: Encryption
Tenga en cuenta de dónde proviene esta jerarquía. Algunos sistemas enrutan según categorías, como departamento, tema o etiquetas de metadatos asignadas de forma independiente de cualquier documento en particular. Aquí, la jerarquía proviene de los propios títulos y secciones del documento, de la forma en que su autor lo organizó. Ambos reducen el espacio de búsqueda antes de realizarla, pero uno organiza toda una base de conocimientos por temas y el otro organiza un único documento mediante su índice.
La mayoría de los documentos no son texto plano. Los títulos y subsecciones ya codifican cómo se relacionan las ideas. El indexado jerárquico mantiene esa estructura y permite al sistema encontrar la sección adecuada, luego la subsección y finalmente el fragmento exacto, de manera similar a como una persona revisa un índice antes de leer detenidamente.
Recuperación en acción
Supongamos que la consulta es “¿Cómo funciona el equilibrio de carga por menor número de conexiones?”. El sistema encuentra la sección de Redes, se enfoca en la subsección de Equilibrio de Carga y luego obtiene el fragmento que explica que el método de menor número de conexiones elige al servidor con el menor número de conexiones activas.
Cada paso de enfoque requiere un mecanismo real, ya que nada de esto ocurre por sí solo. Las opciones incluyen un clasificador entrenado que predice a qué sección pertenece una consulta, una llamada a un LLM que lee la consulta y selecciona la sección más probable de una lista conocida, o una coincidencia basada en reglas con los títulos de las secciones y las palabras clave. Cualquiera que sea la opción elegida se ejecuta en cada nivel, una vez para la sección y otra vez para la subsección, antes de que comience la búsqueda a nivel de fragmento.
¿Vale la pena la profundidad adicional? Dos niveles son suficientes para la mayoría de los documentos. Una jerarquía multinivel completa resulta útil en materiales largos y profundamente estructurados, donde una sección contiene tantos subtemas distintos que buscar todo al mismo tiempo diluiría la relevancia. Una página corta de Preguntas Frecuentes o un artículo sobre un único tema rara vez necesita más de dos niveles, mientras que una guía de arquitectura de cien páginas con docenas de subsecciones podría necesitarlo. Al igual que con cualquier decisión de indexación, mida los efectos antes de añadir complejidad.
Consejos rápidos de ajuste
Comience con tamaños de hijos cercanos al medio del rango, alrededor de 200 tokens, y verifique la capacidad de recuperación con un puñado de preguntas reales antes de cambiar algo. Registre qué padres se expanden para cada consulta para poder identificar las secciones que siempre se incluyen y que podrían ser demasiado amplias. Limite la cantidad de padres enviados al modelo, ya que tres o cuatro secciones extensas ya pueden llenar una ventana de contexto modesta, y mantenga el texto original de los hijos disponible para citaciones, de modo que los usuarios puedan ver exactamente qué oración coincidió.
Comparación de los dos
- Alcance: el enfoque padre-hijo funciona con dos niveles de detalle del mismo texto, mientras que la indexación jerárquica modela todo el esquema.
- Principal ventaja: el enfoque padre-hijo rescata el contexto alrededor de una coincidencia, mientras que la jerarquía reduce el área desde donde comienza la búsqueda.
- Costo: la relación padre-hijo requiere enlaces entre hijos y padres, mientras que la jerarquía necesita detección de estructura y un paso de enrutamiento en cada nivel.
- Adecuación: la relación padre-hijo es adecuada para la mayoría de los documentos largos, mientras que la jerarquía es adecuada para fuentes grandes y profundamente estructuradas.
Se combinan bien, ya que una jerarquía puede terminar con bloques de tipo padre-hijo en su parte inferior.
Un pipeline listo para usar
- Ingesta. Analizar los documentos y detectar los títulos. El cargador debe conservar la información de los títulos, es decir, qué texto pertenece a qué sección, en lugar de unificar todo en un único flujo, ya que las etapas siguientes dependen de ello.
- División en bloques. Crear bloques padre a partir de secciones completas y luego dividir cada uno en bloques hijo, identificando primero el límite del padre.
- Indexación. Incorpore los elementos hijos y almacene los enlaces de los padres, ya sea copiando el texto del padre en los metadatos de cada hijo o almacenando un
parent_idpara su búsqueda. - Consulta. Incorpore la pregunta, busque entre los elementos hijos, seleccione las coincidencias principales, obtenga sus padres eliminando duplicados, compile el contexto enriquecido y envíelo al modelo.
Errores que evitar
- Solo elementos hijos. Obtiene precisión pero pierde el contexto de razonamiento, como ocurre en el ejemplo de caché, donde el bloque B nunca recibe la oración que completa la respuesta.
- Solo elementos padres. Mantiene el contexto pero pierde la precisión en la recuperación, ya que al incrustar una sección con múltiples temas como un único vector se desdibuja de la misma manera que lo haría un bloque demasiado grande.
- Ignorar la estructura. Si el cargador descarta los títulos, no queda ningún límite entre secciones que el enrutamiento jerárquico pueda utilizar, por bueno que sea el enrutador.
- Metadatos débiles. Sin enlaces parentales explícitos como
parent_textoparent_id, la expansión se vuelve imposible posteriormente. Los metadatos deben capturarse al crear el fragmento, ya que no se pueden recuperar una vez descartados.
Donde da los mejores resultados
Estos patrones son especialmente útiles en documentos largos, estructurados e interconectados: bases de conocimiento empresariales, documentación interna, bots de soporte, archivos legales y de cumplimiento, wikis de ingeniería y asistentes de investigación. En este tipo de materiales, ningún tamaño de fragmento único puede satisfacer tanto la precisión como el contexto.
RAG está pasando de un enfoque simple de búsqueda y generación a un modelo que primero examina conocimientos organizados y luego razona sobre lo que ha encontrado. El agrupamiento por relaciones padre-hijo mejora la comprensión local, de modo que dondequiera que aparezca una coincidencia, la explicación correspondiente también se presenta. La indexación jerárquica facilita la navegación global, ya que la búsqueda comienza en el área adecuada en lugar de revisar todo. El agrupamiento plano es similar a arrancar páginas de un libro y confiar en que el modelo adivine la trama. Estas dos técnicas mantienen el libro encuadernado mientras permiten buscar una línea específica. En la práctica, esto significa menos respuestas que estén casi correctas, lo cual es el fallo que más frustra a los usuarios, y una capa de recuperación cuyo comportamiento es más fácil de explicar cuando alguien pregunta por qué un pasaje en particular llegó al modelo.
Leer más
- Enriquecimiento de metadatos en RAG: filtros, citaciones, frescura y control de acceso — Por qué un sistema RAG devuelve la política de la región incorrecta, y cómo los metadatos de origen, organizacionales, temporales y de seguridad corrigen la recuperación de información al adjuntarse durante la ingestión.
- PageIndex: RAG sin vectores, basado en razonamiento con un índice en forma de árbol — PageIndex reemplaza a las bases de datos vectoriales por un índice en forma de árbol navegado por un LLM. Conozca cómo funciona, ejemplos de código en Python, resultados de pruebas de rendimiento y cuándo utilizarlo.