Accueil / Articles

Filtrage des métadonnées pour Databricks RAG à l’aide d’ai_extract et d’ai_prep_search

Fusionner ai_parse_document, ai_extract v2.1 et ai_prep_search afin que chaque morceau contienne des métadonnées saisies, permettant à la recherche vectorielle de filtrer avant de classer par similarité.

Éditeur : REACTAPP.TOP7 min de lecture1713 mots

Sources: Adding ai_extract to the mix: building a unified RAG pipeline with three Databricks SQL AI functions

ai_prep_search fait déjà beaucoup de choses. Les documents analysés y sont introduits, puis des fragments enrichis sur le plan sémantique et contextuel en sortent, prêts à être utilisés dans une recherche vectorielle. Le flux est simple : analyser les documents, préparer les fragments, les aplatir dans une table Delta et la mettre en index.

Cependant, quelque chose manque lorsque ce pipeline est appliqué à un corpus de documents, dans ce cas un ensemble de manuels d’Orion Robotics. Les morceaux eux-mêmes sont de bonne qualité. Le champ chunk_to_embed contient le contexte documentaire adéquat, et chunk_to_retrieve est suffisamment propre pour être transmis à un LLM. Le problème apparaît au moment de la requête, car il n’existe pas de méthode fiable pour filtrer. Une demande telle que "trouvez-moi le guide de firmware pour le contrôleur Orion Motion" doit être satisfaite uniquement grâce à la similarité sémantique, puisque l’index ne dispose d’aucune colonne structurée comme document_type, product_name ou target_audience.

Pour la récupération pilotée par des agents, il existe un véritable manque. La recherche sémantique pure fonctionne bien lors d’une démonstration. En production, un agent doit réduire l’espace de recherche avant d’exécuter des comparaisons de similarité vectorielle, et les filtres de métadonnées font précisément cela. La solution consiste à ajouter ai_extract v2.1 au pipeline.

Pourquoi les filtres de métadonnées sont importants

La recherche hybride dans RAG signifie généralement une recherche vectorielle combinée à une correspondance de mots-clés telle que BM25. C’est utile, mais pour des collections de documents structurés, un autre type d’hybridation est plus important : la recherche vectorielle associée au filtrage des métadonnées.

Imaginez un agent qui sait déjà que l’utilisateur a besoin d’un manuel pour le Bras d’Orion. Classer 10 000 fragments provenant de tous types de documents serait gaspiller des ressources. Une meilleure approche consiste d’abord à réduire les données aux documents pertinents, puis à laisser la similarité vectorielle trier les meilleures correspondances au sein de cet ensemble plus restreint. Databricks Vector Search permet cela grâce à des colonnes de filtrage déclarées lors de la création de l’index.

Le problème, c’est que les métadonnées doivent être propres, cohérentes et correctement typées. Si document_type indique "Manuel d’utilisation" dans une ligne, "manual" dans une autre et rien dans une troisième, les filtres ne fonctionnent pas correctement. La qualité des résultats de recherche est alors médiocre, et des heures sont consacrées à leur débogage, alors que la véritable cause réside dans des métadonnées incohérentes créées précédemment.

ai_extract v2.1 remédie à ce problème. Il prend en charge le VARIANT structuré généré par ai_parse_document et restitue des métadonnées typées vérifiées contre un schéma. Les champs énumératifs définissent des valeurs fixes, les champs de chaîne gèrent l’extraction de texte libre et les tableaux couvrent les champs à plusieurs valeurs. Comme l’extraction suit un schéma, la sortie reste uniforme d’un document à l’autre, même lorsque leurs layouts diffèrent.

Trois fonctions, trois tâches

Les rôles des trois fonctions ne se chevauchent pas, il est donc utile de les séparer avant d’examiner le notebook.

  1. ai_parse_document v2.0 transforme des fichiers binaires bruts (PDF, DOCX, images) en un VARIANT structuré, en identifiant les blocs de texte, les titres, les tableaux, les figures, les boîtes délimitantes ainsi que la hiérarchie des sections. Il connaît la structure interne d’un document mais ignore complètement de quoi il traite.
  • ai_extract v2.1 prend le VARIANT analysé et, en suivant un schéma fourni par l’utilisateur, extrait les métadonnées au niveau du document. Il ne comprend le sujet que parce que le schéma indique ce qu’il doit rechercher. Son résultat est un nouveau VARIANT contenant des champs typés tels que document_title, document_type, product_name et target_audience, ainsi que tous les autres champs définis. La version 2.1 a introduit des scores de confiance et des citations, et c’est la version à utiliser.
  • ai_prep_search divise le VARIANT analysé en blocs sémantiques prêts pour la recherche, en utilisant les indications structurelles issues de l’analyse (boîtes délimitantes, types d’éléments, hiérarchie des sections). Il laisse de côté les métadonnées au niveau du document, car celles-ci relèvent de ai_extract.
  • Chaque fonction a une seule responsabilité, et le pipeline les combine.

    Étapes par étapes dans le carnet

    Le carnet fonctionne sur le corpus Orion Robotics, qui se trouve dans un volume de Unity Catalog et contient des PDF, des fichiers DOCX ainsi que des images. La première étape consiste en la parsing :

    SELECT
      path,
      ai_parse_document(
        content,
        MAP('version', '2.0')
      ) AS parsed
    FROM READ_FILES(
      '/Volumes/<YOUR_CATALOG>/<YOUR_SCHEMA>/orion_docs/',
      format => 'binaryFile'
    )
    

    Chaque fichier du volume est lu en tant que données binaires et converti en un VARIANT structuré. Il est judicieux de choisir explicitement la version 2.0, car ce schéma inclut les coordonnées de la boîte délimitante ainsi que les descriptions des figures dont ai_prep_search a besoin pour prendre des décisions de segmentation. Rester sur la version par défaut permettrait à Databricks de modifier le fonctionnement sous-jacent du pipeline.

    L’exécution d’échantillon a produit ce résultat :

    La prochaine requête extrait les métadonnées à partir du résultat analysé :

    WITH parsed_documents AS (
      SELECT path, ai_parse_document(content, MAP('version', '2.0')) AS parsed
      FROM READ_FILES('/Volumes/<YOUR_CATALOG>/<YOUR_SCHEMA>/orion_docs/', format => 'binaryFile')
    )
    SELECT
      path,
      ai_extract(
        parsed,
        '{
          "document_title":  {"type": "string", "description": "Main title or heading of the document"},
          "document_type":   {"type": "enum", "labels": ["manual", "datasheet", "guide", "report", "specification", "policy", "other"]},
          "product_name":    {"type": "string", "description": "Primary product or system referenced"},
          "version":         {"type": "string", "description": "Document or product version if mentioned"},
          "key_topics":      {"type": "array", "items": {"type": "string"}},
          "target_audience": {"type": "enum", "labels": ["engineer", "operator", "manager", "customer", "general"]}
        }',
        MAP(
          'version', '2.1',
          'instructions', 'These are technical documents from Orion Robotics. Extract metadata that would help users filter and find relevant documents.'
        )
      ) AS metadata
    FROM parsed_documents
    

    Certaines choix nécessitent des explications. À la fois document_type et target_audience sont des enums, car les filtres requièrent un ensemble fermé de valeurs. S’ils étaient saisis en texte libre, ils pourraient revenir sous la forme de « manual », « User Manual » ou « technical manual », ce qui entraverait la correspondance. L’option instructions fournit des informations de contexte : dans ce cas, il s’agit de documents techniques émanant d’Orion Robotics, ce qui améliore l’extraction. De plus, ai_extract accepte directement la valeur VARIANT issue de ai_parse_document, sans conversion en texte brut.

    Résultat de l’exécution de l’exemple :

    Viennent ensuite les étapes de segmentation :

    WITH parsed_documents AS ( ... )
    SELECT
      path,
      ai_prep_search(parsed) AS chunked
    FROM parsed_documents
    

    ai_prep_search utilise la même VERSION analysée. Les documents sont divisés en blocs stockés dans un tableau document.contents, et chaque bloc expose chunk_id, chunk_position, chunk_to_retrieve et chunk_to_embed. Les deux derniers ne contiennent pas intentionnellement le même texte.

    chunk_to_embed inclut du contexte supplémentaire tel que le titre du document, l’en-tête de section et les détails de la page, afin que le modèle d’incorporation puisse placer correctement le bloc. chunk_to_retrieve est un texte plus simplifié, généralement celui affiché aux utilisateurs ou transmis au LLM.

    Cette séparation constitue une conception judicieuse. De nombreux pipelines RAG incorporent et répondent à partir du même texte, mais un texte riche en contexte convient mieux aux modèles d’incorporation tandis qu’un texte simplifié est plus adapté à la lecture ; il est donc préférable de conserver les deux.

    Exemple de sortie :

    Assemblage de tous les éléments

    C’est dans la requête combinée que toutes ces parties se rencontrent :

    WITH parsed_documents AS (
      SELECT path, ai_parse_document(content, MAP('version', '2.0')) AS parsed
      FROM READ_FILES('/Volumes/<YOUR_CATALOG>/<YOUR_SCHEMA>/orion_docs/', format => 'binaryFile')
    ),
    doc_metadata AS (
      SELECT
        path,
        parsed,
        ai_extract(
          parsed,
          '{ ... }',
          MAP('version', '2.1', 'instructions', 'These are technical documents from Orion Robotics. Extract metadata for search filtering.')
        ) AS metadata
      FROM parsed_documents
    ),
    doc_chunks AS (
      SELECT path, ai_prep_search(parsed) AS chunked
      FROM doc_metadata
    )
    SELECT
      chunk.value:chunk_id::STRING          AS chunk_id,
      chunk.value:chunk_position::INT       AS chunk_position,
      chunk.value:chunk_to_retrieve::STRING AS chunk_to_retrieve,
      chunk.value:chunk_to_embed::STRING    AS chunk_to_embed,
      doc_chunks.chunked:document.source_uri::STRING AS source_uri,
      dm.metadata:response:document_title::STRING    AS document_title,
      dm.metadata:response:document_type::STRING     AS document_type,
      dm.metadata:response:product_name::STRING      AS product_name,
      dm.metadata:response:version::STRING           AS doc_version,
      dm.metadata:response:target_audience::STRING   AS target_audience
    FROM
      doc_chunks
      JOIN doc_metadata dm ON doc_chunks.path = dm.path,
      LATERAL variant_explode(doc_chunks.chunked:document.contents) AS chunk
    

    L’étape LATERAL variant_explode décompose l’array de blocs imbriqués en lignes. L’opération JOIN sur le chemin associe les métadonnées au niveau du document à chaque bloc de ce dernier. Le résultat est une ligne par bloc, contenant à la fois les champs de récupération et les colonnes de métadonnées filtrables. C’est cette structure qui est stockée dans la table Delta et utilisée pour alimenter l’index de recherche vectorielle.

    Résultat du test de fonctionnement :

    Le tableau est matérialisé de la manière suivante :

    CREATE OR REPLACE TABLE <YOUR_CATALOG>.<YOUR_SCHEMA>.orion_docs_rag_chunks AS
    -- [full CTE chain from Step 4]
    

    Avec le tableau en place, un index Mosaic AI Vector Search Delta Sync est créé avec les paramètres suivants :

    1. Colonne source d’incorporation : chunk_to_embed
    2. Clé primaire : chunk_id
    3. Champs filtrables : doc_version ainsi que document_type, product_name et target_audience

    Le chunk_id reste stable d’une exécution à l’autre et est composé de l’ID du document et de la position du chunk. Grâce à cela, les mises à jour incrémentielles par MERGE sur la table Delta laissent inchangés les chunks concernés au lieu de les réindexer, ce qui représente une véritable économie lorsque le corpus est mis à jour selon un calendrier prédéfini.

    Les avantages au moment de la requête

    Les bénéfices apparaissent lorsque l’utilisateur pose une question, et non au moment du chargement. Supposons que la requête soit « Trouver le guide de mise à jour logicielle pour le Orion Motion Controller ». La récupération peut désormais se faire en deux étapes :

    1. Déterminer les filtres de métadonnées correspondants, tels que document_type = “guide” et product_name = “Orion Motion Controller”.
    2. Intégrer la requête et effectuer la recherche uniquement parmi les documents qui répondent à ces filtres.

    L’absence de ai_extract oblige l’agent à se fier uniquement à l’similitude sémantique. Cela peut suffire pour quelques documents similaires, mais devient peu fiable lorsqu’il s’agit de milliers de fichiers couvrant de nombreux produits, types et publics. Les métadonnées basées sur des énumérations résolvent ce problème : l’agent n’a pas besoin de deviner s’il s’agit d’un manuel ou d’un guide et peut simplement filtrer par document_type = “manual” pour obtenir des résultats exacts.

    Limites et coûts

    ai_prep_search ne permet aucun contrôle sur le découpage en blocs. La taille des blocs, leur chevauchement ainsi que l’algorithme utilisé ne peuvent pas être définis. Pour de nombreux cas d’utilisation RAG avec des documents, cela est acceptable, car le découpage sémantique accompagné de contexte supplémentaire est souvent supérieur aux divisions à taille fixe simples. Un contrôle précis des blocs parent-enfant ou du chevauchement exige néanmoins une logique personnalisée.

    ai_extract augmente les coûts et le temps de traitement, car chaque document nécessite une appel supplémentaire à un LLM. Delta Sync peut sauter les documents inchangés, mais l’ingestion complète initiale reste coûteuse ; il est donc judicieux de tester sur un échantillon avant d’analyser l’ensemble du corpus.

    Utilisez la version 2.0 de ai_parse_document lorsque ses résultats VARIANT sont transmis à ai_prep_search ou à ai_extract. Cette version fournit des boîtes de délimitation, des types d’éléments ainsi que des descriptions des figures. Le texte brut peut être transmis à ai_extract, mais les avantages structurels liés à la parsing sont alors perdus.

    Fermeture du design

    L’ajout de ai_extract v2.1 ne nécessite que peu de code supplémentaire, mais modifie significativement l’architecture, car les blocs contiennent désormais des métadonnées propres, typées et filtrables, ce qui rend la recherche hybride plus fiable. Le processus reste composé de trois étapes claires :

    1. ai_parse_document capture la structure du document.
    2. ai_extract recueille des métadonnées utiles.
    3. ai_prep_search crée des blocs sémantiques.

    Cette séparation s’avère bénéfique pour des documents provenant de produits, de versions, de publics et de types différents. Sans filtres de métadonnées, la recherche repose principalement sur une recherche par similarité. Avec eux, l’agent affine d’abord les résultats avant de les classer.

    Cette approche ne conviendra pas à tout. Les métadonnées au niveau des blocs, la récupération parent-enfant ou le découpage personnalisé nécessitent encore du code personnalisé. Cependant, pour de nombreux pipelines RAG de documents Databricks, c’est un point de départ plus propre et plus simple. De plus, les blocs doivent toujours être testés et évalués, et on ne doit jamais leur faire confiance aveuglément.