Ingestion de RAG consciente de la qualité : acheminement des documents en fonction de leur lisibilité
Un flux de pipelines docling déclaratif qui évalue le niveau de langue, les doublons et la lisibilité selon le score Flesch, puis envoie les documents vers deux index OpenSearch pour la recherche par les clients et les développeurs.
Sources: Architecting a Multi-Tier RAG Pipeline: Quality-Aware Document Routing with docling-pipelines
Mettre une base de connaissances dédiée au service client sous le contrôle d’une recherche par IA échoue généralement au même endroit : tous les fichiers sont traités de la même manière. Un corpus réaliste est un mélange hétérogène comprenant des FAQ bien organisées, des guides de produits structurés, des manuels de développement très détaillés, du texte juridique dupliqué, ainsi que quelques fichiers corrompus ne contenant absolument aucun mot lisible.
Lorsque tout cela est intégré dans un seul système de type RAG conversationnel, la qualité des résultats de recherche diminue. Un client qui souhaite simplement obtenir des instructions se voit servir des extraits de manuels techniques. Envoyer du texte inutile ou des journaux système à un modèle coûteux afin d’en obtenir des résumés ou des traductions revient également à gaspiller de l’argent.
La solution consiste à trier les documents en fonction de leur qualité. Une approche en deux niveaux consiste à examiner chaque fichier, à supprimer les pages vides, à évaluer son degré de lisibilité, puis à diviser le corpus en catégories.
- Niveau 1, recherche par les consommateurs. Les documents très lisibles reçoivent des métadonnées structurelles, comme le nombre de mots, afin de faciliter une recherche conversationnelle destinée aux clients.
- Niveau 2, recherche pour les développeurs et à des fins de référence. Les documents techniques complexes sont indexés séparément pour répondre à des questions techniques avancées.
docling-pipelines rend cela pratique. Il s’agit d’un framework open source sous licence MIT, soutenu par IBM, qui crée et exécute des graphes de traitement de documents directement à partir de configurations JSON. Aucun code Python personnalisé n’est nécessaire, car les opérateurs transmettent les documents les uns aux autres sous forme de tables Apache Arrow en mémoire, ce qui fait que les transformations intermédiaires et les vérifications de routage coûtent presque rien en termes de CPU et de mémoire.
La structure du pipeline
Tout est contenu dans un seul fichier JSON qui décrit un graphe acyclique orienté :
ingestlit des fichiers PDF, TXT, DOCX et HTML depuis le disque.extractextrait du texte structuré à l’aide de la bibliothèque Docling.detect_languageidentifie la langue de chaque document.deduputilise des hachages de contenu pour supprimer les lignes dupliquées.doc_qualitycalcule des métriques structurelles, telles que le nombre total de mots.readabilitydétermine des scores de complexité du texte.quality_branchingchoisit la branche en fonction du score Flesch Reading Ease : un score supérieur à 60 mène à la branche haute qualité, tandis qu’un score égal ou inférieur à 60 mène à la branche basse qualité.
ml_enrichment_high_quality, embeddings_high_quality et vectordb_high_quality. Il ajoute des caractéristiques linguistiques basées sur l’IA, crée des embeddings vectoriels et écrit les résultats dans l’index high_quality_docs.sql_filter_low_quality, embeddings_low_quality et vectordb_low_quality. Il supprime les lignes vides, encode ce qui reste et écrit les résultats dans low_quality_docs.Profilage avant la séparation
Chaque document nécessite un profil avant de pouvoir être acheminé. Après que ingest ait lu les charges utiles et que extract ait transformé le format en texte, chaque opérateur suivant agit comme un transformateur : il examine le tableau et ajoute de nouvelles colonnes au même tableau Arrow en mémoire. D’abord, des vérifications simples utilisant uniquement le processeur sont effectuées : langue, doublons et complexité du texte.
Détection de la langue
Les modèles d’apprentissage automatique ultérieurs ainsi que les formules de lisibilité ont besoin de connaître la langue, c’est pourquoi la détection a lieu en premier.
{
"name": "detect_language",
"type": "lang_detect",
"config": {
"doc_column": "content"
},
"depends_on": [
"extract"
]
}
L’opérateur lit la colonne content et y ajoute lang_name (un code ISO 639-1) ainsi que lang_score. Le fait de l’insérer juste après l’extraction permet à chaque étape sensible au langage suivante d’utiliser ses résultats.
Déduplication
Les documents dupliqués gaspillent les ressources de calcul nécessaires à l’incorporation des données et faussent les résultats de recherche.
{
"name": "dedup",
"type": "ededup",
"config": {
"doc_column": "content",
"doc_id_column": "doc_id_hash"
},
"depends_on": [
"detect_language"
]
}
Cela fait référence au hash du contenu stocké dans doc_id_hash ainsi qu’au texte brut dans content, puis supprime les lignes répétées de la table. Faire cela en amont évite aux étapes suivantes d’effectuer un travail inutile.
Qualité du document
Les vérifications structurelles de base permettent de détecter les pages vides et les layouts inhabituels.
{
"name": "doc_quality",
"type": "doc_quality",
"config": {
"doc_column": "content",
"doc_content_column": "content",
"text_lang": "en"
},
"depends_on": [
"dedup"
]
}
Cela ajoute 11 colonnes structurelles, toutes préfixées par docq_. Celle qui est importante ici est docq_total_words, qui représente le nombre total de mots de chaque document.
Lisibilité
Les formules de lisibilité évaluent la difficulté en fonction de la longueur des mots, des syllabes et de la structure des phrases.
{
"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"
]
}
L’array readability_score_list indique à l’opérateur quels indicateurs calculer et ajouter en tant que colonnes. Le routage repose sur le score de facilité de lecture de Flesch, qui varie de 0 à 100 :
- Un score supérieur à 60 signifie un langage simple et accessible, adapté à un public général de centre d’aide.
- Un score égal ou inférieur à 60 indique un texte technique et dense, qui suppose des connaissances spécialisées.
L’étape de ramification
L’opérateur de ramification divise l’ensemble de données Arrow en chemins de traitement distincts.
{
"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"
]
}
Le routage est contrôlé par un bloc déclaratif criteria_json comprenant quatre champs :
variableindique la colonne à tester, par exempleflesch_reading_ease.operatordéfinit l’opération de comparaison, telle que>ou<=.valuereprésente la valeur seuil, ici60.logical_operatorrelie plusieurs conditions avecANDouOR.
Avec la règle flesch_reading_ease > 60, le moteur évalue les conditions nativement, divise la table en mémoire et exécute chaque branche correspondante.
Parcours de haute qualité : enrichissement, intégration, indexation
Les documents acheminés vers high_quality_branch reçoivent d’abord un enrichissement linguistique grâce à l’intelligence artificielle.
{
"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 analyse le texte à l’aide de modèles conscients du langage et génère des métadonnées telles que le nombre de phrases, les propriétés syllabiques et des listes de noms, tout cela dans le but d’améliorer la récupération des informations. Il dépend de la colonne lang_name issue de l’étape de détection. Comme il s’agit de la partie gourmande en ressources CPU du processus, il ne fonctionne que sur des textes lisible. La valeur de la configuration output_column_prefix ajoute le préfixe ml_ aux nouvelles colonnes, y compris ml_num_words.
Ensuite, le texte est transformé en embeddings de 2560 dimensions. Le modèle utilisé est openai/granite4:latest, fourni par un serveur Ollama local via 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"
]
}
Enfin, les documents sont indexés dans OpenSearch, grâce aux feature_mappings qui relient les colonnes d’Arrow aux champs d’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"
]
}
Chemin de faible qualité : filtrage, incorporation, indexation
Les documents situés sur low_quality_branch sont exemptés de l’étape coûteuse ml_enrichment. Certains d’entre eux peuvent ne pas contenir de mots lisible après extraction, comme des fichiers corrompus ou des scans vides, et l’incorporation d’une chaîne vide ne fait qu’ajouter du bruit à l’espace vectoriel. Un filtre permet de les éliminer :
{
"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"
]
}
Le filtre vérifie la colonne docq_total_words de doc_quality et élimine tout document ne contenant aucun mot.
Ces documents survivants, principalement des documents techniques, sont intégrés au même modèle granite4 et enregistrés dans un index OpenSearch distinct :
{
"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"
]
}
Puisque cette branche a omis l’enrichissement, word_count n’existe pas pour ces documents. La mise en correspondance utilise plutôt le comptage des difficult_words calculés, ce qui est utile pour les requêtes techniques.
Deux index, deux styles de recherche
Des index distincts par niveau de lisibilité permettent à chaque public de rechercher différemment.
Recherche dans le centre d’aide
Pour les utilisateurs finaux, la requête cible high_quality_docs. Seuls les guides faciles à lire (readability_score > 60) et contenant du contenu concret (word_count > 100) y sont conservés :
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
}
}
}
]
}
}
}'
Recherche de référence pour les développeurs
Pour les utilisateurs expérimentés à la recherche de spécifications, la requête vise low_quality_docs. Elle analyse le contenu complexe et conserve les segments contenant de nombreux termes techniques difficiles (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
}
}
}
]
}
}
}'
Lancement
Le dépôt inclut un script de configuration qui crée un environnement virtuel, installe les dépendances, lance des conteneurs OpenSearch locaux et enregistre les modèles Ollama.
Étape 1 : configurer l’environnement
Cloner le dépôt et exécuter le script :
git clone https://github.com/IBM/docling-pipelines.git
cd docling-pipelines
./scripts/setup_docling_pipelines_environment.sh
Ou installez-le directement depuis PyPI :
pip install docling-pipelines
Étape 2 : définir les variables d’environnement
export OLLAMA_API_KEY=ollama
export OPENSEARCH_USERNAME=admin
export OPENSEARCH_PASSWORD=<your-password>
Étape 3 : exécuter le flux
Démarrez le pipeline depuis la ligne de commande :
docling-pipelines --flow-file sample_flows/advanced/branching_quality_routing.json
Ce que l’exécution affiche
La sortie retrace le flux, en enregistrant chaque transformation et décision de routage :
$ 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.
En déplaçant les métriques de qualité et les règles de routage hors du code Python personnalisé vers une configuration déclarative, l’ingestion devient rapide et facile à maintenir. Le coût de traitement est proportionnel à la valeur du document, et l’application RAG reçoit des index propres et adaptés à chaque public cible.
Cette approche par branches fonctionne avec n’importe quelle métrique calculée précédemment dans le flux. Il est possible de modifier les critères pour router en fonction de la langue, du score de risque PII ou d’une étiquette de classificateur personnalisée. Comme la logique du pipeline reste séparée du code d’orchestration, les équipes peuvent l’étendre sans augmenter la charge de maintenance.