Startseite / Artikel

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.

Verlag: REACTAPP.TOP8 Min. Lesezeit1893 Wörter

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:

  1. 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.
  2. 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:

  1. ingest liest PDF-, TXT-, DOCX- und HTML-Dateien von der Festplatte ein.
  2. extract extrahiert mithilfe der Docling-Bibliothek strukturierten Text.
  3. detect_language ermittelt die Sprache jedes Dokuments.
  4. dedup verwendet Inhaltshashes, um doppelte Einträge zu entfernen.
  5. doc_quality berechnet strukturelle Metriken wie die Gesamtzahl der Wörter.
  6. readability ermittelt Komplexitätswerte des Textes.
  7. quality_branching leitet 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.
  • Der hochwertige Weg führt durch 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.
  • Der niedrigwertige Weg führt durch 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:

    • variable gibt den zu prüfenden Spaltennamen an, zum Beispiel flesch_reading_ease.
    • operator legt den Vergleich fest, wie z. B. > oder <=.
    • value ist die Schwellenwert, hier 60.
    • logical_operator verknüpft mehrere Bedingungen mit AND oder OR.

    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.