Inicio / Artículos

Ingestión de RAG consciente de la calidad: Enrutamiento de documentos según su legibilidad

Un flujo declarativo de pipelines docling que evalúa el nivel lingüístico, las duplicaciones y la legibilidad según Flesch, y luego envía los documentos a dos índices de OpenSearch para búsquedas por clientes y desarrolladores.

Editor: REACTAPP.TOP8 min de lectura1893 palabras

Fuentes: Architecting a Multi-Tier RAG Pipeline: Quality-Aware Document Routing with docling-pipelines

Mover una base de conocimientos de soporte al cliente a un sistema de búsqueda basado en IA suele fallar en el mismo punto: todos los archivos reciben el mismo tratamiento. Un corpus realista es una mezcla heterogénea que incluye preguntas frecuentes bien organizadas, guías de productos estructuradas, manuales técnicos extensos, texto legal duplicado y algunos archivos dañados sin palabras legibles en absoluto.

Al introducir todo eso en un único sistema conversacional de RAG, la calidad de la recuperación de información disminuye. Un cliente que busca una instrucción sencilla recibe fragmentos de un manual técnico. Enviar texto sin valor o registros del sistema a un modelo costoso para que realice resúmenes o traducciones también representa un desperdicio de dinero.

La solución consiste en clasificar los documentos según su calidad. Un diseño en dos niveles permite analizar cada archivo, eliminar páginas vacías, medir su legibilidad y luego dividir el corpus en categorías.

  1. Nivel 1, búsqueda por parte de los consumidores. Los documentos muy fáciles de leer reciben metadatos estructurales, como el recuento de palabras, para facilitar búsquedas conversacionales orientadas al cliente.
  2. Nivel 2, búsqueda para desarrolladores y como referencia. El material técnico denso se incluye en su propio índice para responder a preguntas técnicas avanzadas.

docling-pipelines hace que esto sea práctico. Se trata de un framework de código abierto con licencia MIT respaldado por IBM que crea y ejecuta flujos de procesamiento de documentos directamente a partir de configuraciones en JSON. No se necesita código personalizado en Python, ya que los operadores intercambian documentos entre sí como tablas Apache Arrow en memoria, por lo que las transformaciones intermedias y las verificaciones de enrutamiento apenas consumen recursos de CPU y memoria.

La estructura del pipeline

Todo está contenido en un único archivo de flujo JSON que describe un grafo acíclico dirigido:

  1. ingest lee archivos PDF, TXT, DOCX y HTML desde el disco.
  2. extract extrae texto estructurado con la biblioteca Docling.
  3. detect_language identifica el idioma de cada documento.
  4. dedup utiliza hashes de contenido para eliminar filas duplicadas.
  5. doc_quality calcula métricas estructurales, como el recuento total de palabras.
  6. readability calcula puntajes de complejidad del texto.
  7. quality_branching toma como criterio el puntaje Flesch Reading Ease: valores superiores a 60 se dirigen al ramo de alta calidad, mientras que 60 o menos van al ramo de baja calidad.
  • La ruta de alta calidad ejecuta ml_enrichment_high_quality, embeddings_high_quality y vectordb_high_quality. Agrega características lingüísticas basadas en ML, crea embeddings vectoriales y escribe los resultados en el índice high_quality_docs.
  • La ruta de baja calidad ejecuta sql_filter_low_quality, embeddings_low_quality y vectordb_low_quality. Elimina las filas vacías, incrusta el contenido restante y escribe los resultados en low_quality_docs.
  • Perfilado antes de la división

    Cada documento necesita un perfil antes de que se pueda enrutar. Después de que ingest lea los datos y extract convierta el formato en texto, cada operador posterior funciona como un transformador: inspecciona la tabla y agrega nuevas columnas a la misma tabla Arrow en memoria. Primero se realizan verificaciones simples que solo requieren procesamiento de CPU: el idioma, las duplicaciones y la complejidad del texto.

    Detección de idioma

    Los modelos de ML y las fórmulas de legibilidad posteriores necesitan conocer el idioma, por lo que la detección se realiza primero.

    {
          "name": "detect_language",
          "type": "lang_detect",
          "config": {
            "doc_column": "content"
          },
          "depends_on": [
            "extract"
          ]
        }
    

    El operador lee la columna content y agrega lang_name (un código ISO 639-1) y lang_score. Al colocarlos justo después de la extracción, cada paso posterior sensible al idioma puede utilizar sus resultados.

    Deduplicación

    Los documentos duplicados desperdician recursos de procesamiento y distorsionan los resultados de búsqueda.

    {
          "name": "dedup",
          "type": "ededup",
          "config": {
            "doc_column": "content",
            "doc_id_column": "doc_id_hash"
          },
          "depends_on": [
            "detect_language"
          ]
        }
    

    Hace referencia al hash del contenido almacenado en doc_id_hash y al texto sin procesar en content, y luego elimina las filas repetidas de la tabla. Al hacerlo temprano, se evita que los pasos posteriores realicen trabajo inútil.

    Calidad del documento

    Las verificaciones estructurales básicas detectan páginas en blanco y diseños inusuales.

    {
          "name": "doc_quality",
          "type": "doc_quality",
          "config": {
            "doc_column": "content",
            "doc_content_column": "content",
            "text_lang": "en"
          },
          "depends_on": [
            "dedup"
          ]
        }
    

    Esto agrega 11 columnas estructurales, todas con el prefijo docq_. La que es importante aquí es docq_total_words, que representa el recuento total de palabras de cada documento.

    Legibilidad

    Las fórmulas de legibilidad evalúan la dificultad en función de la longitud de las palabras, las sílabas y la estructura de las oraciones.

    {
          "name": "readability",
          "type": "readability",
          "config": {
            "doc_column": "content",
            "readability_score_list": [
              "flesch_reading_ease",
              "flesch_kincaid_grade",
              "gunning_fog",
              "difficult_words",
              "dale_chall_readability_score"
            ]
          },
          "depends_on": [
            "doc_quality"
          ]
        }
    

    El array readability_score_list indica al operador qué métricas calcular y añadir como columnas. El enrutamiento se basa en la puntuación de facilidad de lectura de Flesch, que va de 0 a 100:

    • Más de 60 significa un lenguaje sencillo y accesible adecuado para un público general de centros de ayuda.
    • 60 o menos significa un texto técnico y denso que presupone conocimientos especializados.

    El paso de ramificación

    El operador de ramificación divide el conjunto de datos Arrow en rutas de procesamiento separadas.

    {
          "name": "quality_branching",
          "type": "branching",
          "config": {
            "branches": {
              "high_quality_branch": {
                "link_name": "High Quality Documents",
                "criteria_json": {
                  "criteria_list": [
                    {
                      "variable": "flesch_reading_ease",
                      "operator": ">",
                      "value": 60
                    }
                  ],
                  "logical_operator": "AND"
                }
              },
              "low_quality_branch": {
                "link_name": "Low Quality Documents",
                "criteria_json": {
                  "criteria_list": [
                    {
                      "variable": "flesch_reading_ease",
                      "operator": "<=",
                      "value": 60
                    }
                  ],
                  "logical_operator": "AND"
                }
              }
            }
          },
          "depends_on": [
            "readability"
          ]
        }
    

    El enrutamiento está controlado por un bloque declarativo criteria_json que cuenta con cuatro campos:

    • variable indica la columna a verificar, por ejemplo flesch_reading_ease.
    • operator especifica la operación de comparación, como > o <=.
    • value representa el umbral, en este caso 60.
    • logical_operator combina varias condiciones mediante AND o OR.

    Con la regla flesch_reading_ease > 60, el motor evalúa las condiciones de forma nativa, divide la tabla en memoria y ejecuta cada rama posterior.

    Ruta de alta calidad: enriquecimiento, incrustación e indexación

    Los documentos enviados a high_quality_branch reciben primero un enriquecimiento mediante técnicas de ML lingüístico.

    {
          "name": "ml_enrichment_high_quality",
          "type": "ml_enrichment",
          "config": {
            "doc_column": "content",
            "lang_column": "lang_name",
            "output_column_prefix": "ml_",
            "error_column_name": "enrichment_error"
          },
          "depends_on": [
            "quality_branching.high_quality_branch"
          ]
        }
    

    ml_enrichment analiza el texto con modelos conscientes del idioma y genera metadatos como el recuento de oraciones, las propiedades silábicas y listas de sustantivos, todo ello con el objetivo de mejorar la recuperación de información. Depende de la columna lang_name del paso de detección. Dado que esta es la parte del proceso que consume muchos recursos de la CPU, solo se ejecuta con texto legible. La configuración output_column_prefix añade el prefijo ml_ a las nuevas columnas, entre las cuales se encuentra ml_num_words.

    A continuación, el texto se convierte en embeddings de 2560 dimensiones. El modelo utilizado es openai/granite4:latest, proporcionado por un servidor local Ollama a través de LiteLLM:

    {
          "name": "embeddings_high_quality",
          "type": "embeddings",
          "config": {
            "embeddings_column": "embeddings",
            "overlap_ratio": 0.2,
            "doc_column": "content",
            "provider_config": {
              "api_base": "http://localhost:11434/v1",
              "api_key": "${OLLAMA_API_KEY}",
              "model_id": "openai/granite4:latest"
            },
            "provider": "litellm"
          },
          "depends_on": [
            "ml_enrichment_high_quality"
          ]
        }
    

    Por último, los documentos se indexan en OpenSearch, donde feature_mappings relaciona las columnas de Arrow con los campos de OpenSearch:

    {
          "name": "vectordb_high_quality",
          "type": "vectordb",
          "config": {
            "provider": "opensearch",
            "doc_id_column": "doc_id_hash",
            "embeddings_column": "embeddings",
            "vector_dimension": 2560,
            "create_index": "true",
            "provider_config": {
              "index_name": "high_quality_docs",
              "host": "localhost",
              "port": 9200,
              "username": "${OPENSEARCH_USERNAME}",
              "password": "${OPENSEARCH_PASSWORD}",
              "use_ssl": false,
              "verify_certs": false,
              "batch_size": 100,
              "engine": "faiss",
              "algorithm": "hnsw",
              "space_type": "l2"
            },
            "feature_mappings": [
              {"feature_name": "doc_id_hash", "mapped_column_name": "pk"},
              {"feature_name": "name", "mapped_column_name": "doc_name"},
              {"feature_name": "content", "mapped_column_name": "text"},
              {"feature_name": "embeddings", "mapped_column_name": "vector_embeddings"},
              {"feature_name": "lang_name", "mapped_column_name": "language"},
              {"feature_name": "flesch_reading_ease", "mapped_column_name": "readability_score"},
              {"feature_name": "ml_num_words", "mapped_column_name": "word_count"}
            ]
          },
          "depends_on": [
            "embeddings_high_quality"
          ]
        }
    

    Ruta de baja calidad: filtrado, incrustación e indexación

    Los documentos en low_quality_branch omiten el costoso paso de ml_enrichment. Algunos de ellos pueden no contener palabras legibles tras la extracción, como archivos dañados o escaneos en blanco; incrustar una cadena vacía solo añade ruido al espacio vectorial. Un filtro los elimina:

    {
          "name": "sql_filter_low_quality",
          "type": "sql_filter",
          "config": {
            "criteria_json": {
              "criteria_list": [
                {
                  "variable": "docq_total_words",
                  "operator": ">",
                  "value": 0
                }
              ],
              "logical_operator": "AND"
            }
          },
          "depends_on": [
            "quality_branching.low_quality_branch"
          ]
        }
    

    El filtro verifica la columna docq_total_words de doc_quality y descarta cualquier documento que contenga cero palabras.

    Los documentos que sobreviven, en su mayoría material técnico, se incluyen con el mismo modelo granite4 y se escriben en un índice separado de OpenSearch:

    {
          "name": "vectordb_low_quality",
          "type": "vectordb",
          "config": {
            "provider": "opensearch",
            "doc_id_column": "doc_id_hash",
            "embeddings_column": "embeddings",
            "vector_dimension": 2560,
            "create_index": "true",
            "provider_config": {
              "index_name": "low_quality_docs",
              "host": "localhost",
              "port": 9200,
              "username": "${OPENSEARCH_USERNAME}",
              "password": "${OPENSEARCH_PASSWORD}",
              "use_ssl": false,
              "verify_certs": false,
              "batch_size": 100,
              "engine": "faiss",
              "algorithm": "hnsw",
              "space_type": "l2"
            },
            "feature_mappings": [
              {"feature_name": "doc_id_hash", "mapped_column_name": "pk"},
              {"feature_name": "name", "mapped_column_name": "doc_name"},
              {"feature_name": "content", "mapped_column_name": "text"},
              {"feature_name": "embeddings", "mapped_column_name": "vector_embeddings"},
              {"feature_name": "lang_name", "mapped_column_name": "language"},
              {"feature_name": "flesch_reading_ease", "mapped_column_name": "readability_score"},
              {"feature_name": "difficult_words", "mapped_column_name": "difficult_words"}
            ]
          },
          "depends_on": [
            "embeddings_low_quality"
          ]
        }
    

    Dado que esta rama omitió el proceso de enriquecimiento, word_count no existe para estos documentos. En su lugar, la asignación utiliza el recuento calculado de difficult_words, lo cual es útil para consultas técnicas.

    Dos índices, dos estilos de consulta

    Los índices separados por nivel de legibilidad permiten que cada grupo de usuarios realice búsquedas de manera diferente.

    Búsqueda en el centro de ayuda

    Para los usuarios finales, la consulta se dirige a high_quality_docs. Este índice solo incluye guías fáciles de leer (readability_score > 60) que contengan contenido sustancial (word_count > 100):

    curl -XGET "http://localhost:9200/high_quality_docs/_search" \
      -H 'Content-Type: application/json' -d'
    {
      "query": {
        "bool": {
          "must": [
            {
              "knn": {
                "vector_embeddings": {
                  "vector": [0.015, -0.023, 0.004],
                  "k": 10
                }
              }
            }
          ],
          "filter": [
            {
              "range": {
                "readability_score": {
                  "gt": 60
                }
              }
            },
            {
              "range": {
                "word_count": {
                  "gt": 100
                }
              }
            }
          ]
        }
      }
    }'
    

    Búsqueda de referencia para desarrolladores

    Para usuarios expertos que buscan especificaciones, la consulta se dirige a low_quality_docs. Analiza el material complejo y conserva los segmentos que contienen muchos términos técnicos difíciles (difficult_words > 5):

    curl -XGET "http://localhost:9200/low_quality_docs/_search" \
      -H 'Content-Type: application/json' -d'
    {
      "query": {
        "bool": {
          "must": [
            {
              "knn": {
                "vector_embeddings": {
                  "vector": [0.015, -0.023, 0.004],
                  "k": 10
                }
              }
            }
          ],
          "filter": [
            {
              "range": {
                "difficult_words": {
                  "gt": 5
                }
              }
            }
          ]
        }
      }
    }'
    

    Ejecutarlo

    El repositorio incluye un script de configuración que crea un entorno virtual, instala las dependencias, inicia los contenedores locales de OpenSearch y registra los modelos de Ollama.

    Paso 1: configurar el entorno

    Clona el repositorio y ejecuta el script:

    git clone https://github.com/IBM/docling-pipelines.git
    cd docling-pipelines
    ./scripts/setup_docling_pipelines_environment.sh
    

    O instálalo directamente desde PyPI:

    pip install docling-pipelines
    

    Paso 2: establecer variables de entorno

    export OLLAMA_API_KEY=ollama
    export OPENSEARCH_USERNAME=admin
    export OPENSEARCH_PASSWORD=<your-password>
    

    Paso 3: ejecutar el flujo

    Inicie la tubería desde la línea de comandos:

    docling-pipelines --flow-file sample_flows/advanced/branching_quality_routing.json
    

    Qué imprime la ejecución

    La salida registra el flujo, anotando cada transformación y decisión de enrutamiento:

    $ docling-pipelines --flow-file sample_flows/advanced/branching_quality_routing.json
    [INFO] [ingest] Loading files from tests/fixtures/customer_support_docs/
    [INFO] [ingest] Ingested 12 files.
    [INFO] [extract] Running extract_operator using docling_library...
    [INFO] [extract] Extracted text from 12 documents.
    [INFO] [detect_language] Running lang_detect...
    [INFO] [detect_language] Complete. lang_name assigned.
    [INFO] [dedup] Running ededup...
    [INFO] [dedup] Removed 2 duplicate documents. 10 documents remaining.
    [INFO] [doc_quality] Running doc_quality...
    [INFO] [doc_quality] Computed docq_* features for 10 documents.
    [INFO] [readability] Running readability...
    [INFO] [readability] Computed readability features for 10 documents.
    [INFO] [quality_branching] Branching 10 documents...
    [INFO] [quality_branching] Routed 6 documents to link 'High Quality Documents' (flesch_reading_ease > 60).
    [INFO] [quality_branching] Routed 4 documents to link 'Low Quality Documents' (flesch_reading_ease <= 60).
    [INFO] [ml_enrichment_high_quality] Running ml_enrichment on 6 documents...
    [INFO] [embeddings_high_quality] Generating embeddings with model 'openai/granite4:latest'...
    [INFO] [vectordb_high_quality] Uploaded 6 documents to OpenSearch index 'high_quality_docs'.
    [INFO] [sql_filter_low_quality] Running sql_filter on 4 documents...
    [INFO] [sql_filter_low_quality] Filtered out 0 empty documents. 4 documents remaining.
    [INFO] [embeddings_low_quality] Generating embeddings with model 'openai/granite4:latest'...
    [INFO] [vectordb_low_quality] Uploaded 4 documents to OpenSearch index 'low_quality_docs'.
    [INFO] Flow execution finished successfully in 4.23s.
    

    Al trasladar las métricas de calidad y las reglas de enrutamiento del código Python personalizado a una configuración declarativa, la incorporación de datos se vuelve rápida y fácil de mantener. El costo de procesamiento depende del valor del documento, y la aplicación RAG recibe índices limpios y adaptados para cada público.

    La idea de ramificación funciona con cualquier métrica calculada anteriormente en el flujo. Se pueden cambiar los criterios para enrutar según el idioma, la puntuación de riesgo de PII o una etiqueta de clasificador personalizado. Dado que la lógica del pipeline permanece separada del código de orquestación, los equipos pueden ampliar el pipeline sin aumentar la carga de mantenimiento.

    Lecturas relacionadas