Accueil / Articles

Enrichissement des métadonnées dans RAG : filtres, citations, fraîcheur et contrôle d’accès

Pourquoi un système RAG renvoie la politique de la région incorrecte, et comment les métadonnées sources, organisationnelles, temporelles et de sécurité permettent de corriger la recherche lorsqu’elles sont ajoutées au moment de l’ingestion.

Éditeur : REACTAPP.TOP14 min de lecture3163 mots

Sources: Metadata Enrichment in RAG: The Secret Ingredient for Better Retrieval

Supposons qu’une équipe ait développé pour l’entreprise un chatbot de génération assistée par recherche. Des jours ont été consacrés au choix du modèle d’embedding, une base de données vectorielle a été configurée et un grand modèle de langage a été intégré par-dessus. Satisfaits du résultat, quelqu’un tape une question concernant la politique de congé de maternité pour le personnel du bureau européen.

La réponse arrive rapidement : les employées ont droit à 20 semaines de congé de maternité payé. Tout semble correct. Cependant, une vérification des documents originaux révèle que le texte récupéré provient de la politique destinée aux employés aux États-Unis, et non en Europe. Le système de recherche a trouvé un extrait sur le congé de maternité, mais n’a pas su à quelle région il appartenait, car le contexte environnant avait été perdu lors du découpage et de l’indexation.

Ce problème fait partie des échecs les plus fréquents dans les systèmes RAG. La plupart des discussions sur RAG portent sur les embeddings, les stratégies de segmentation et les bases de données vectorielles, tandis qu’un facteur ayant une influence majeure sur la qualité de la récupération est négligé : l’enrichissement des métadonnées. Les métadonnées permettent à un système de récupération de comprendre non seulement ce que dit un segment, mais aussi d’où il provient, à qui il s’adresse, quand il a été écrit et quelle est sa place parmi les autres documents. Dans de nombreuses applications d’entreprise, elles permettent de distinguer un résultat simplement pertinent du résultat véritablement exact.

Quelles sont les failles de la récupération basée uniquement sur le contenu ?

Regardez comment fonctionne typiquement un système RAG destiné aux débutants. Une organisation dispose de trois documents de politique interne : un manuel pour les employés aux États-Unis, un autre pour l’Europe et un troisième pour l’Asie-Pacifique. Tous trois contiennent une section sur le congé de maternité. Après le découpage en fragments, la base de données vectorielle stocke des phrases presque identiques qui ne diffèrent que par le nombre de semaines : 20 semaines dans l’un, 16 dans un autre et 26 dans le troisième.

Pour un modèle d’incorporation, ces fragments sont extrêmement similaires, car ils traitent tous du même sujet. Si l’on pose des questions sur le congé de maternité, le système de récupération peut afficher n’importe lequel d’entre eux. Il comprend le sens sémantique mais ne perçoit pas automatiquement le contexte professionnel. C’est là que les métadonnées deviennent essentielles.

Qu’est-ce que les métadonnées ?

Les métadonnées sont des données sur des données. Dans un système RAG, elles décrivent un fragment sans faire partie de son contenu principal. Elles englobent tout ce qui est vrai à propos d’un texte, sans être ce que le texte dit lui-même.

Prenons le fragment de l’exemple d’ouverture :

Employees are entitled to 20 weeks of paid maternity leave.

Seule, cette phrase est claire et complète, et son contenu fournit une réponse. Cependant, elle ne précise pas de quel document elle provient, quelle politique régionale elle décrit, qui en assure la mise à jour ou quand elle a été révisée pour la dernière fois. Le texte répond à ce que dit la politique. Il ne peut pas indiquer si c’est la bonne politique pour la situation de cette personne, ce qui est précisément ce qui s’est passé lorsqu’un élément américain a été renvoyé pour une question européenne.

Voici maintenant le même élément avec des métadonnées ajoutées :

{
  "source": "EU_HR_Policy.pdf",
  "region": "Europe",
  "department": "Human Resources",
  "last_updated": "2025-03-15"
}

Aucun de ces quatre champs ne fait partie de la phrase. Le mot « Europe » ainsi que le nom du fichier n’apparaissent nulle part dans le texte réel. Pourtant, le système de récupération dispose désormais de bien plus d’éléments sur lesquels travailler. Le fragment contient un contexte qui peut être utilisé pour la récupération, le filtrage, le classement et la génération de réponses, et ce contexte reste distinct du contenu sans s’y mélanger.

Ce séparement est à l’origine de la puissance des métadonnées. Par conception, le modèle d’incorporation ne code que le page_content du fragment et ne voit jamais les métadonnées. Les métadonnées ne sont pas destinées à influencer la manière dont un fragment est incorporé. Elles existent afin que toutes les autres parties du système, y compris les filtres, les vérifications d’accès, les citations et la logique de classement, disposent d’éléments concrets sur lesquels agir, quelles que soient les conclusions du modèle d’incorporation concernant la similarité sémantique.

Un modèle mental simple : les métadonnées constituent l’ensemble des étiquettes associées à chaque élément de connaissance. Sans étiquettes, chaque fragment n’est qu’un sens flottant, dépourvu de contexte, et la récupération doit se fier uniquement à la similarité, comme c’était le cas lorsque trois politiques régionales semblaient presque identiques pour le modèle d’incorporation. Avec des étiquettes, la devinette est remplacée par quelque chose que le système peut vérifier directement. La question passe de savoir si un fragment sonne similaire à la requête à savoir s’il appartient réellement à la situation abordée.

Comment les métadonnées améliorent la récupération

Revenons au cas du congé de maternité et suivons l’étape de récupération avec et sans métadonnées.

En l’absence de métadonnées, une requête concernant le personnel européen est effectuée sur l’ensemble de la base de données vectorielle. Seule la similarité sémantique compte, et comme les trois blocs régionaux sont presque identiques à un embedding, le système de récupération peut afficher avec assurance du texte de politique provenant de la région incorrecte. Aucun élément du score de similarité ne laisse supposer un problème.

Un système disposant de métadonnées peut quant à lui appliquer un filtre avant ou en même temps que la recherche par similarité :

filter = {
    "region": "Europe"
}

La recherche est désormais limitée aux blocs étiquetés « Europe ». Au lieu d’analyser chaque document de politique, le moteur de recherche ne prend en compte que le sous-ensemble pertinent, ce qui réduit le nombre de candidats avant même que la similarité ne puisse entraîner une erreur de sélection.

Le lieu où a lieu le filtrage a de l’importance. La plupart des bases de données vectorielles en production prennent en charge le pré-filtrage, qui applique d’abord le filtre de métadonnées afin que la recherche de similarité ne s’exécute que sur les blocs correspondants. Cela est plus rapide et plus fiable que le post-filtrage, où la recherche de similarité complète est effectuée sur l’ensemble des données et les résultats non correspondants sont éliminés par la suite. Le post-filtrage peut gaspiller des ressources informatiques sur des régions irrelevantes et, dans certains cas extrêmes, retourner moins de résultats que demandé lorsque trop de résultats pertinents sont filtrés en fin de processus. Qdrant et Weaviate sont tous deux conçus pour prendre en charge un pré-filtrage efficace.

Voici le scénario de congé de maternité détaillé du début à la fin :

from sentence_transformers import SentenceTransformer
import numpy as np

embedding_model = SentenceTransformer("all-MiniLM-L6-v2")
chunks = [
    {"text": "Employees are entitled to 20 weeks of paid maternity leave.", "region": "United States"},
    {"text": "Employees are entitled to 16 weeks of paid maternity leave.", "region": "Europe"},
    {"text": "Employees are entitled to 26 weeks of paid maternity leave.", "region": "Asia-Pacific"},
]

for c in chunks:
    c["embedding"] = embedding_model.encode(c["text"])

query = "What is the maternity leave policy for employees in the European office?"
query_embedding = embedding_model.encode(query)

def search(chunks, query_embedding, region_filter=None):
    candidates = chunks if region_filter is None else [c for c in chunks if c["region"] == region_filter]
    scored = [(np.dot(query_embedding, c["embedding"]), c) for c in candidates]
    scored.sort(key=lambda x: x[0], reverse=True)
    return scored[0][1]

print("Without metadata filter:")
result = search(chunks, query_embedding)
print(f"  Returned: '{result['text']}' (region: {result['region']})")
print("\nWith metadata filter (region='Europe'):")
result = search(chunks, query_embedding, region_filter="Europe")
print(f"  Returned: '{result['text']}' (region: {result['region']})")

Cela reproduit directement l’échec initial. Sans filtre, la similarité seule choisit le fragment américain simplement parce qu’il obtient un score légèrement plus élevé, et non parce qu’il convient à la question. Une fois le filtre régional appliqué, l’espace de recherche se réduit au seul fragment européen et la bonne réponse apparaît, quel que soit le degré de similarité des autres embeddings.

La précision s’améliore considérablement, et l’écart s’agrandit à mesure que le corpus grandit. Parmi trois documents, un modèle d’embedding pourrait classer le bon premier par pur hasard. Avec des centaines de documents couvrant de nombreuses régions, départements et années, la similarité seule devient peu fiable, et le filtrage sur les métadonnées est le seul moyen fiable pour éliminer tout ce qui n’appartient pas.

Quatre types de métadonnées utiles

Tous les métadonnées ne sont pas également utiles. Quatre catégories apparaissent fréquemment dans les systèmes d’entreprise réels, et chacune résout un problème différent.

Métadonnées de source

Celles-ci indiquent d’où proviennent les informations. Les champs typiques sont le nom du fichier, le numéro de page, l’URL et l’ID du document. Elles sont importantes lorsque les utilisateurs souhaitent des citations ou veulent vérifier une réponse. Voici comment une citation est créée :

def format_citation(metadata):
    return f"Source: {metadata['source']}, Page {metadata['page']}"

chunk_metadata = {"source": "Employee_Handbook_2025.pdf", "page": 42}
print(format_citation(chunk_metadata))

Qui affiche :

Source: Employee_Handbook_2025.pdf, Page 42

Cette chaîne de citation n’est pas générée par le LLM qui tente de deviner d’où provient une réponse. Il s’agit d’une recherche déterministe sur des champs enregistrés au moment du chargement du document. La confiance augmente lorsque les utilisateurs peuvent retracer l’origine des informations, ce qui dépend du fait que les métadonnées soient conservées du début jusqu’à la réponse finale.

Métadonnées organisationnelles

Les connaissances d’une entreprise sont généralement réparties entre différents départements et unités commerciales tels que les ressources humaines, la finance, le juridique et l’ingénierie. En étiquetant les données de cette manière, il est possible de limiter la recherche à la partie pertinente de l’entreprise avant de procéder à une comparaison de similarité, en utilisant le même mécanisme de filtrage présenté ci-dessus :

filter = {"department": "Finance"}

Rechercher dans la documentation technique des réponses à une question financière est une perte de temps, mais il y a un autre problème : une requête financière analysée sur du texte technique peut renvoyer un extrait clairement erroné et hors sujet qui obtient pourtant un score trompeusement élevé en termes de similarité brute.

Métadonnées temporelles

Les informations évoluent avec le temps, à mesure que les politiques sont mises à jour, que les procédures changent et que les réglementations se modifient. Sans timestamps, un système de récupération ne peut pas distinguer une politique en vigueur d’une ancienne. Il sait quels extraits sont sémantiquement proches de la requête, mais pas lesquels sont encore en application.

Considérez deux extraits concernant la même politique mais avec des dates de mise à jour différentes :

chunks = [
    {"text": "Employees receive 15 vacation days per year.", "last_updated": "2023-01-10"},
    {"text": "Employees receive 20 vacation days per year.", "last_updated": "2025-03-15"},
]

Les deux répondront probablement de manière très similaire à une question sur les jours de congé, car le sujet et la formulation sont presque identiques. En enregistrant une date et une heure, le système peut choisir délibérément la réponse la plus récente plutôt que de laisser le hasard décider :

most_recent = max(chunks, key=lambda c: c["last_updated"])
print(most_recent["text"])

Résultat :

Employees receive 20 vacation days per year.

Cela est crucial dans les industries où les informations changent fréquemment. Sans vérification explicite de la date de dernière mise à jour, une politique ancienne et sa version remplacée sont fonctionnellement indiscernables pour un système qui ne prend en compte que la similarité.

Metadata de sécurité

De nombreuses organisations ont besoin d’un contrôle d’accès basé sur les rôles : les documents RH ne sont visibles que par le personnel RH, les rapports financiers uniquement par les équipes financières. Les metadata permettent d’imposer cela. Un enregistrement peut contenir des champs tels que ceux-ci :

chunk_metadata = {
    "access_level": "confidential",
    "allowed_roles": ["HR_Manager", "HR_Admin"]
}

Avant la récupération, le système compare le rôle de l’utilisateur qui demande l’accès aux rôles autorisés à voir ce fragment :

def can_access(user_role, chunk_metadata):
    return user_role in chunk_metadata["allowed_roles"]

print(can_access("HR_Manager", chunk_metadata))   # True
print(can_access("Engineering", chunk_metadata))  # False

Résultat :

True
False

Dans un pipeline réel, cette vérification s’effectue en tant que filtre lors de la récupération des données, et non en tant que mesure supplémentaire une fois que le LLM dispose déjà du contenu. Un fragment que l’utilisateur n’a pas le droit de voir ne doit jamais parvenir jusqu’à la requête. Sans métadonnées, il est extrêmement difficile de créer un système RAG sécurisé, car aucun signal fiable n’est associé à chaque fragment permettant au système de comparer avec les permissions de l’utilisateur avant de décider s’il faut le récupérer.

Ajout de métadonnées lors de l’ingestion

Les métadonnées ne apparaissent pas d’elles-mêmes. Elles doivent être ajoutées lors de l’ingestion des documents, et si elles ne sont pas capturées à ce stade, elles ne peuvent pas être récupérées, car au moment où un fragment atteint l’étape d’incorporation ou de stockage, le lien vers sa source a déjà disparu.

Prenons un PDF de politique RH qui est chargé. Le pipeline d’ingestion habituel effectue ces étapes dans l’ordre suivant :

  1. Charger le document.
  2. Extraire le texte.
  3. Diviser le contenu en fragments.
  4. Joindre les métadonnées.
  5. Générer les embeddings.
  6. Écrire les chunks, les embeddings et les métadonnées dans la base de données vectorielle.

Le joignement des métadonnées a lieu après le découpage en chunks et non avant, et l’ordre est important. Les métadonnées doivent être associées à chaque chunk individuel dans lequel un document est divisé, plutôt que d’être jointes une seule fois au document dans son ensemble.

from langchain_core.documents import Document

doc = Document(
    page_content="Employees receive 20 weeks of maternity leave.",
    metadata={
        "department": "HR",
        "region": "Europe",
        "source": "EU_HR_Policy.pdf"
    }
)

print(doc.page_content)
print(doc.metadata)

Résultat :

Employees receive 20 weeks of maternity leave.
{'department': 'HR', 'region': 'Europe', 'source': 'EU_HR_Policy.pdf'}

C’est simple tant que le document est suffisamment petit pour rester dans une seule partie. Cependant, les documents réels sont divisés en de nombreuses parties, et les métadonnées doivent être copiées sur chacune d’elles :

def chunk_with_metadata(document, chunk_size=60):
    text = document.page_content
    chunks = [text[i:i+chunk_size] for i in range(0, len(text), chunk_size)]
    return [
        Document(page_content=chunk_text, metadata=document.metadata)
        for chunk_text in chunks
    ]

source_doc = Document(
    page_content="Employees receive 20 weeks of maternity leave. Eligibility begins after six months of employment.",
    metadata={"department": "HR", "region": "Europe", "source": "EU_HR_Policy.pdf"}
)
chunked_docs = chunk_with_metadata(source_doc)
for i, chunk in enumerate(chunked_docs, 1):
    print(f"Chunk {i}: {chunk.page_content!r}")
    print(f"  Metadata: {chunk.metadata}\n")

Résultat :

Chunk 1: 'Employees receive 20 weeks of maternity leave. Eligibility b'
  Metadata: {'department': 'HR', 'region': 'Europe', 'source': 'EU_HR_Policy.pdf'}
Chunk 2: 'egins after six months of employment.'
  Metadata: {'department': 'HR', 'region': 'Europe', 'source': 'EU_HR_Policy.pdf'}

Bien que ces deux fragments soient des parties distinctes d’un même document, chacun hérite des mêmes valeurs pour la région, le département et la source. C’est ce qui permet au fonctionnement des exemples de filtrage présentés ci-dessus : quel que soit le mode de division du texte, chaque fragment peut toujours indiquer qu’il fait partie de la politique RH européenne. Chacun d’eux contient désormais des informations contextuelles précieuses, obtenues grâce à la récupération, au filtrage et à la citation.

Permettre à un LLM de générer les métadonnées

Les systèmes RAG modernes utilisent de plus en plus des LLM pour générer des métadonnées, plutôt que de compter entièrement sur l’étiquetage manuel. Au lieu qu’une personne lise chaque document et y attribue manuellement des étiquettes, un modèle peut analyser un document et déterminer des champs tels que le sujet, le département, la catégorie, les mots-clés et un résumé. Le code ci-dessous montre le prompt utilisé pour générer ces métadonnées, ainsi que le résultat obtenu.

def generate_metadata(text, llm):
    prompt = f"""Analyze the following document and return metadata as JSON with these fields:
topic, category, and keywords (a list of 3-5 relevant terms).

Document:
{text}
Respond with only the JSON, no other text."""
    response = llm.generate(prompt)
    return response

sample_text = "Employees are entitled to 20 weeks of paid maternity leave, with eligibility beginning after six months of continuous employment. Additional unpaid leave may be requested with manager approval."
# In practice, llm.generate() would call an actual model (Claude, GPT, etc.)
# Below is the kind of output this prompt is designed to produce:

example_output = {
    "topic": "Employee Benefits",
    "category": "HR Policy",
    "keywords": ["maternity leave", "eligibility", "employee benefits"]
}

print(example_output)

Résultat :

{'topic': 'Employee Benefits', 'category': 'HR Policy', 'keywords': ['maternity leave', 'eligibility', 'employee benefits']}

Ces métadonnées sont attachées au chunk exactement comme le seraient des métadonnées assignées manuellement. Seule la partie qui décide des valeurs diffère. Le résultat est une expérience de recherche plus riche avec moins d’effort manuel, surtout à grande échelle : étiqueter un petit nombre de documents à la main est possible, mais étiqueter des dizaines de milliers de chunks dans une base de connaissances en croissance ne l’est généralement pas. Un nombre croissant de pipelines avancés traitent une telle étape comme un prétraitement de routine, exécuté une fois lors de l’ingestion, en même temps que le chunking et l’embedding.

Une précaution pratique : vérifier manuellement les résultats générés par les LLM, en particulier au début. Un modèle peut parfois attribuer un sujet ou une catégorie qui ne correspond pas au contenu réel, et contrairement à une erreur humaine sur un seul document, une erreur automatisée peut se propager silencieusement à de nombreux documents en même temps si la demande présente un défaut systémique.

Filtrage des métadonnées et recherche hybride

Les métadonnées sont particulièrement puissantes lorsqu’elles sont combinées à la recherche sémantique, bien que la précision concernant cette combinaison soit utile, étant donné que le terme « hybride » est utilisé pour désigner plus d’une technique dans RAG.

Au sens strict, la récupération hybride consiste à associer deux méthodes de recherche, à savoir la recherche vectorielle dense et la recherche par mots-clés épars (BM25), à les exécuter en parallèle et à fusionner les résultats. Le filtrage par métadonnées est une chose différente : il ne s’agit pas d’une autre méthode de recherche, mais d’un moyen de réduire le nombre de candidats avant ou en même temps que la méthode, ou la combinaison de méthodes, que vous utilisez déjà.

La recherche vectorielle consiste à déterminer quels fragments sont sémantiquement similaires à la requête. Le filtrage par métadonnées permet de savoir quels fragments appartiennent à la bonne catégorie, région, département ou période. Ces deux approches sont indépendantes, de sorte qu’un filtre fonctionne tout aussi bien avec une simple recherche vectorielle, avec BM25 seul, ou avec un système de récupération hybride complet. Il s’agit d’une couche supplémentaire qui peut être ajoutée à n’importe lequel de ces systèmes.

Ensemble, ils améliorent considérablement la qualité de la récupération, comme l’exemple du congé de maternité le montre. Le système cesse de chercher de manière aveugle dans l’ensemble de la base de connaissances et recherche de manière ciblée au sein du sous-ensemble le plus pertinent, la similarité ne servant qu’à déterminer le classement au sein de cet ensemble déjà restreint et adapté. C’est pourquoi les systèmes RAG d’entreprise modernes ne se fient que rarement aux seuls embeddings : les embeddings déterminent ce qui est similaire, mais les métadonnées déterminent ce qui est éligible à être pris en compte.

L’élément souvent négligé

De nombreuses organisations consacrent beaucoup d’efforts au choix des modèles d’incorporation et des bases de données vectorielles. Ces choix sont importants, mais en pratique, la récupération échoue souvent pour une raison bien plus simple : des informations contextuelles essentielles n’ont jamais été capturées, quel que soit le niveau de qualité du modèle ou de la base de données.

Imaginez une bibliothèque où chaque livre a perdu son titre, son auteur, sa date de publication et sa catégorie, ne laissant que du texte brut. Trouver quoi que ce soit serait très difficile, même avec un système de rayonnage et un catalogue de recherche excellents. Stocker des fragments sans métadonnées revient au même : l’infrastructure de récupération peut être parfaite, mais elle ne sera d’aucune aide si aucun contexte n’est fourni pour qu’elle puisse agir.

Les métadonnées fournissent la structure organisationnelle qui permet aux systèmes de recherche de naviguer dans de grandes bases de connaissances. À mesure que les collections passent de centaines de documents à des millions de fragments, leur importance ne cesse de croître. À petite échelle, un modèle d’incorporation peut classer le bon fragment en premier par pur hasard, comme cela a presque été le cas avec trois documents de politique. Dans une véritable base de connaissances d’entreprise, ce hasard s’épuise rapidement. De nombreuses équipes d’entreprise indiquent que l’amélioration de la qualité des métadonnées apporte des gains plus importants que le passage à un modèle d’incorporation plus récent, ce qui mérite d’être retenu compte tenu de l’attention accordée au choix des modèles et de celle bien moindre portée à la stratégie des métadonnées.

Conclusion

Enrichir les fragments avec des métadonnées compte parmi les meilleures façons d’améliorer les résultats RAG, et il reçoit bien moins d’attention que les embeddings, les bases de données vectorielles et les LLM. Les embeddings capturent le sens du contenu, tandis que les métadonnées capturent le contexte qui l’entoure ; ensemble, ils permettent à un système de trouver non seulement des informations similaires, mais aussi les bonnes informations. Que ce soit pour suivre les sources, filtrer par région, assurer la sécurité, enregistrer des timestamps ou ajouter des étiquettes générées par l’IA, les métadonnées aident à transformer une configuration de recherche de base en une plateforme de connaissances prête à être utilisée en production.

Ainsi, lors de l’évaluation d’un pipeline RAG, la question n’est pas seulement de savoir quel modèle d’embedding utiliser, mais aussi quels métadonnées doivent être associés aux fragments, car dans de nombreux systèmes réels, de meilleures métadonnées signifient une meilleure recherche, une meilleure recherche signifie de meilleures réponses, et de meilleures réponses signifient une meilleure expérience pour les utilisateurs.

Lectures complémentaires

  • Le découpage parent-enfant et l’indexation hiérarchique pour une meilleure récupération de données dans RAG — Pourquoi de petits fragments permettent de trouver des correspondances mais pas les réponses, et comment le découpage parent-enfant, l’expansion par suppression des doublons et l’indexation hiérarchique restituent le contexte dans la récupération de données RAG.
  • Recette Spring AI : Filtrer les résultats RAG avec des métadonnées selon le titre du jeu — Une démarche pas à pas Spring AI qui met des étiquettes sur les documents en fonction de leur titre, utilise un LLM pour sélectionner la bonne valeur parmi les questions, puis filtre la recherche vectorielle en fonction de celle-ci.
  • La récupération d’informations sur une wiki technique : BM25, les fenêtres denses sécurisées par les tokens et RRF — Récupération axée sur l’évaluation pour une wiki technique : cibles basées sur des preuves exactes, une ligne de référence BM25, un bug de troncature de MiniLM, les fenêtres de tokens et les résultats de fusion par rang réciproque.