Qualitätsbewusste RAG-Eingabe: Dokumente nach Lesbarkeit routen
Ein deklarativer Docling-Pipelines-Fluss, der die Sprache sowie Duplikate und die Flesch-Lesbarkeit bewertet, bevor er die Dokumente an zwei OpenSearch-Indizes zur Suche durch Kunden und Entwickler sendet.
Quellen: Architecting a Multi-Tier RAG Pipeline: Quality-Aware Document Routing with docling-pipelines
Der Umzug einer Kundensupport-Wissensdatenbank in eine KI-Suche geht in der Regel an derselben Stelle schief: Jeder Datei wird dieselbe Behandlung zuteil. Ein realistisches Korpus ist eine Mischung aus verschiedenen Inhalten – es enthält ordentliche FAQs, strukturierte Produktanleitungen, umfangreiche Entwicklerhandbücher, duplizierte Rechtstexte sowie einige beschädigte Dateien ohne lesbare Wörter.
Wenn all das in ein einziges konversationsbasiertes RAG-System eingefüllt wird, sinkt die Qualität der Informationsabrufung. Ein Kunde, der nur eine einfache Anleitung sucht, erhält stattdessen Auszüge aus einem Ingenieurhandbuch. Das Versenden von unbrauchbarem Text oder Systemprotokollen an ein teures Modell zur Zusammenfassung oder Übersetzung ist ebenfalls Geldverschwendung.
Die Lösung besteht darin, die Dokumente nach Qualität zu sortieren. Ein zweistufiges Design prüft jede Datei, entfernt leere Seiten, misst die Lesbarkeit und teilt das Korpus anschließend auf:
- Ebene 1, Suchfunktion für Endverbraucher. Sehr gut lesbare Dokumente erhalten strukturelle Metadaten wie Wortzählungen, um eine für Kunden zugängliche, konversationelle Suche zu unterstützen.
- Ebene 2, Suchfunktion für Entwickler und Referenzen. Dichtes technisches Material wird in einen eigenen Index aufgenommen, um fortgeschrittene technische Fragen zu beantworten.
docling-pipelines macht dies praktikabel. Es handelt sich um ein open-source-Framework unter der MIT-Lizenz, das von IBM unterstützt wird und Dokumentverarbeitungsgraphen direkt aus JSON-Konfigurationen erstellt und ausführt. Es ist kein benutzerdefinierter Python-Code erforderlich, da die Prozessoren die Dokumente als in-Memory-Apache-Arrow-Tabellen aneinander weitergeben; dadurch verursachen Zwischenvorgänge sowie Routing-Prüfungen fast keinen Aufwand in Bezug auf CPU und Speicher.
Die Struktur des Pipelines
Alles befindet sich in einer einzigen JSON-Datei, die einen gerichteten azyklischen Graphen beschreibt:
ingestliest PDF-, TXT-, DOCX- und HTML-Dateien von der Festplatte ein.extractextrahiert mithilfe der Docling-Bibliothek strukturierten Text.detect_languageermittelt die Sprache jedes Dokuments.dedupverwendet Inhaltshashes, um doppelte Einträge zu entfernen.doc_qualityberechnet strukturelle Metriken wie die Gesamtzahl der Wörter.readabilityermittelt Komplexitätswerte des Textes.quality_branchingleitet basierend auf dem Flesch Reading Ease-Score weiter: Werte über 60 führen zum High-Quality-Pfad, Werte von 60 oder darunter zum Low-Quality-Pfad.
ml_enrichment_high_quality, embeddings_high_quality und vectordb_high_quality. Er fügt auf ML basierende linguistische Merkmale hinzu, erstellt Vektordarstellungen und schreibt in den Index high_quality_docs.sql_filter_low_quality, embeddings_low_quality und vectordb_low_quality. Er entfernt leere Zeilen, embeddet den verbleibenden Inhalt und schreibt in low_quality_docs.Profilierung vor der Aufteilung
Jedes Dokument benötigt ein Profil, bevor es weitergeleitet werden kann. Nachdem ingest die Payloads gelesen hat und extract das Layout in Text umwandelt, fungiert jeder nachfolgende Operator als Transformator: Er inspiziert die Tabelle und fügt neue Spalten zur gleichen in-Memory Arrow-Tabelle hinzu. Zuerst werden kostengünstige, rein CPU-basierte Überprüfungen durchgeführt: Sprache, Duplikate und Textkomplexität.
Spracherkennung
Spätere ML-Modelle sowie Formeln zur Lesbarkeit benötigen Kenntnisse über die Sprache, daher erfolgt die Erkennung zuerst.
{
"name": "detect_language",
"type": "lang_detect",
"config": {
"doc_column": "content"
},
"depends_on": [
"extract"
]
}
Der Operator liest die content-Spalte und fügt lang_name (einen ISO 639-1-Code) sowie lang_score hinzu. Durch die Einfügung unmittelbar nach der Extraktion kann jeder nachfolgende schreibsprachabhängige Schritt deren Ausgabe nutzen.
Deduplikation
Duplicate-Dokumente verschwenden Rechenressourcen für das Einbetten und verfälschen die Suchergebnisse.
{
"name": "dedup",
"type": "ededup",
"config": {
"doc_column": "content",
"doc_id_column": "doc_id_hash"
},
"depends_on": [
"detect_language"
]
}
Es bezieht sich auf den in doc_id_hash gespeicherten Inhaltshash sowie auf den Rohtext in content und entfernt anschließend wiederholte Zeilen aus der Tabelle. Dadurch werden die späteren Schritte vor unnötiger Arbeit bewahrt.
Dokumentqualität
Grundlegende strukturelle Überprüfungen erkennen leere Seiten sowie ungewöhnliche Layouts.
{
"name": "doc_quality",
"type": "doc_quality",
"config": {
"doc_column": "content",
"doc_content_column": "content",
"text_lang": "en"
},
"depends_on": [
"dedup"
]
}
Dies fügt 11 Strukturspalten hinzu, alle mit docq_ vorangestellt. Die hier wichtige ist docq_total_words, die Gesamtanzahl der Wörter pro Dokument.
Lesbarkeit
Formeln zur Lesbarkeitsbewertung beurteilen die Schwierigkeit anhand der Wortlänge, Silbenanzahl und Satzstruktur.
{
"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"
]
}
Der Array readability_score_list gibt dem Operator an, welche Metriken berechnet und als Spalten hinzugefügt werden sollen. Die Routenbildung stützt sich auf die Flesch-Lesbarkeitswertung, die von 0 bis 100 reicht:
- Mehr als 60 bedeutet eine einfache, zugängliche Sprache, die für ein allgemeines Publikum in Help-Centern geeignet ist.
- 60 oder weniger bedeutet dichte, technische Prosa, die ein Fachwissen voraussetzt.
Der Verzweigungs-Schritt
Der Verzweigungsoperator teilt das Arrow-Datensatz in separate Verarbeitungspfade auf.
{
"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"
]
}
Die Routierung wird durch einen deklarativen criteria_json-Block mit vier Feldern gesteuert:
variablegibt den zu prüfenden Spaltennamen an, zum Beispielflesch_reading_ease.operatorlegt den Vergleich fest, wie z. B.>oder<=.valueist die Schwellenwert, hier60.logical_operatorverknüpft mehrere Bedingungen mitANDoderOR.
Mit der Regel flesch_reading_ease > 60 bewertet der Motor die Bedingungen direkt, teilt die Tabelle im Arbeitsspeicher auf und führt jede nachfolgende Abzweigung aus.
Hochwertiger Pfad: Erweitern, einbetten, indizieren
Dokumente, die über den high_quality_branch gesendet werden, erhalten zunächst eine sprachliche ML-Erweiterung.
{
"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 analysiert den Text mithilfe sprachbewusster Modelle und erzeugt Metadaten wie Satzzahl, Silbenmerkmale sowie Substantivlisten – alles zur Verbesserung der Suche. Dabei wird von der Spalte lang_name aus dem Erkennungsschritt abhängig. Da es sich um den rechenintensiven Teil des Ablaufs handelt, wird er nur bei lesbarer Texte ausgeführt. Die Einstellung output_column_prefix fügt neuen Spalten das Präfix ml_ hinzu, wozu auch ml_num_words gehört.
Anschließend werden aus dem Text 2560-dimensionalen Embeddings erzeugt. Das verwendete Modell ist openai/granite4:latest, das über einen lokalen Ollama-Server mittels LiteLLM bereitgestellt wird:
{
"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"
]
}
Zuletzt werden die Dokumente in OpenSearch indiziert, wobei feature_mappings die Arrow-Spalten mit den OpenSearch-Feldern verknüpfen:
{
"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"
]
}
Niedrigwertiger Pfad: Filtern, Einbetten, Indizieren
Dokumente auf low_quality_branch überspringen den kostspieligen Schritt der ml_enrichment. Einige davon enthalten nach der Extraktion möglicherweise keine lesbaren Wörter, beispielsweise beschädigte Dateien oder leere Scans – das Einbetten einer leeren Zeichenkette fügt dem Vektorraum lediglich Störungen hinzu. Ein Filter entfernt sie:
{
"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"
]
}
Der Filter überprüft die Spalte docq_total_words aus doc_quality und entfernt alle Dokumente mit null Wörtern.
Die übrig gebliebenen Dokumente, hauptsächlich technisches Material, werden mit demselben granite4-Modell verarbeitet und in einen separaten OpenSearch-Index geschrieben:
{
"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"
]
}
Da diese Abteilung die Verfeinerung übersprungen hat, existiert für diese Dokumente kein word_count. Stattdessen wird die berechnete Anzahl an difficult_words verwendet, was bei technischen Abfragen hilft.
Zwei Indizes, zwei Abfragestile
Separate Indizes für verschiedene Lesbarkeitsstufen ermöglichen es den jeweiligen Zielgruppen, unterschiedlich zu suchen.
Suche im Hilfesystem
Für Endbenutzer erfolgt die Abfrage bei high_quality_docs. Dabei werden nur leicht verständliche Anleitungen (readability_score > 60) mit echtem Inhalt (word_count > 100) berücksichtigt:
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
}
}
}
]
}
}
}'
Referenzsuche für Entwickler
Für Experten, die nach Spezifikationen suchen, richtet sich die Abfrage auf low_quality_docs. Dabei werden komplexe Materialien untersucht und Abschnitte mit vielen schwierigen technischen Begriffen (difficult_words > 5) beibehalten:
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
}
}
}
]
}
}
}'
Ausführen
Das Repository enthält ein Setup-Skript, das eine virtuelle Umgebung erstellt, Abhängigkeiten installiert, lokale OpenSearch-Container startet und die Ollama-Modelle registriert.
Schritt 1: Umgebung einrichten
Klonen Sie das Repository und führen Sie das Skript aus:
git clone https://github.com/IBM/docling-pipelines.git
cd docling-pipelines
./scripts/setup_docling_pipelines_environment.sh
Oder installieren Sie direkt über PyPI:
pip install docling-pipelines
Schritt 2: Umgebungsvariablen festlegen
export OLLAMA_API_KEY=ollama
export OPENSEARCH_USERNAME=admin
export OPENSEARCH_PASSWORD=<your-password>
Schritt 3: Den Ablauf ausführen
Führen Sie die Pipeline von der Befehlszeile aus aus:
docling-pipelines --flow-file sample_flows/advanced/branching_quality_routing.json
Was die Ausführung ausgibt
Die Ausgabe verfolgt den Ablauf und protokolliert jede Transformation sowie jede Routing-Entscheidung:
$ 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.
Durch den Umzug der Qualitätsmetriken und Routing-Regeln aus benutzerdefiniertem Python in eine deklarative Konfiguration wird die Datenaufnahme schneller und leichter zu warten. Die Verarbeitungskosten hängen vom Dokumentwert ab, und die RAG-Anwendung erhält saubere, auf den jeweiligen Zielgruppen zugeschnittene Indizes.
Die Verzweigungsmethode funktioniert mit jeder zuvor im Prozess berechneten Metrik. Ändern Sie die Kriterien, um nach Sprache, PII-Risikowert oder einem benutzerdefinierten Klassifizierungslabel zu routen. Da die Pipeline-Logik getrennt vom Orchestrierungskod bleibt, können Teams die Pipeline erweitern, ohne dass die Wartungslast zunimmt.