Aller au contenu

Créer un pipeline RAG avec Strapi 5 et des bases vectorielles

Bâtissez un pipeline RAG automatisé avec Strapi 5. Synchronisez votre CMS headless avec vos bases vectorielles via des hooks, du chunking et des embeddings.

Par l’équipe Revisionary · · 6 min de lecture

Visualisation géométrique abstraite de nœuds interconnectés et de blocs de données superposés représentant la recherche vectorielle et la synchronisation de contenu headless.

Pour mettre en place un pipeline de génération augmentée par récupération (RAG ou Retrieval-Augmented Generation) avec Strapi 5, vous devez extraire le contenu publié de votre CMS headless, le découper en blocs sémantiques (chunks), générer des embeddings vectoriels et synchroniser ces vecteurs à chaque modification des entrées. Cette configuration permet aux grands modèles de langage (LLM) et aux moteurs de recherche sémantique de répondre à des questions en s'appuyant directement sur votre contenu éditorial, sans réentraînement manuel.

Exploiter un système RAG sur du contenu headless résout les problèmes d'hallucination, mais pose des défis de synchronisation. Lorsqu'un éditeur met à jour un article ou dépublie un guide, l'index vectoriel doit refléter cet état immédiatement. Voici comment concevoir, construire et maintenir un pipeline de synchronisation RAG automatisé entre Strapi 5 et une base de données vectorielle.

L'architecture d'un pipeline RAG piloté par le CMS

Une architecture RAG articulée autour de Strapi repose sur quatre étapes clés :

  1. Détection d'événements : écouter les actions de publication, de mise à jour et de suppression au sein du CMS.
  2. Transformation et chunking : nettoyer le balisage, analyser les composants structurés et diviser les documents en fenêtres de contexte cohérentes.
  3. Génération d'embeddings : convertir les fragments de texte en vecteurs de haute dimension à l'aide d'un modèle d'embedding.
  4. Stockage vectoriel et indexation des métadonnées : insérer ou supprimer des vecteurs accompagnés de métadonnées (comme l'ID de document, la locale, le slug et la catégorie) dans une base de données vectorielle telle que pgvector, Pinecone ou Qdrant.

Strapi 5 propose une API Document Service moderne qui gère nativement les brouillons, les états de publication et l'internationalisation, facilitant ainsi le suivi du cycle de vie des contenus.

Choisir entre webhooks et abonnements aux cycles de vie

Deux approches principales permettent de déclencher la synchronisation vectorielle depuis Strapi 5 : les webhooks HTTP ou les cycles de vie du Document Service côté serveur.

Approche 1 : les webhooks externes

Strapi intègre un moteur de webhooks accessible dans Settings → Webhooks. Lorsqu'un auteur publie ou dépublie une entrée, Strapi envoie un payload HTTP POST à un worker externe (comme une fonction AWS Lambda, un Cloudflare Worker ou un Route Handler Next.js). Ce dernier récupère le document complet, le découpe en chunks et met à jour le store vectoriel.

Les webhooks découplent les appels coûteux aux API d'embedding du cycle requête-réponse du CMS, évitant ainsi de ralentir le panneau d'administration de Strapi si l'API d'embedding subit des temps de latence.

Approche 2 : les cycles de vie du Document Service côté serveur

Si vous préférez conserver la logique au sein de votre codebase Strapi ou garantir une cohérence immédiate, vous pouvez vous abonner directement aux cycles de vie du Document Service en Node.js. Dans Strapi 5, les cycles de vie des documents s'exécutent lors des opérations documentaires (publish, update, delete, discardDraft).

Voici comment enregistrer un souscripteur dans src/index.js :

javascript
module.exports = {
  register() {},
  bootstrap({ strapi }) {
    strapi.documents.use(async (context, next) => {
      // Exécuter l'action CMS sous-jacente
      const result = await next();

      const targetUID = 'api::article.article';
      if (context.uid !== targetUID) {
        return result;
      }

      const { action, params } = context;

      // Gérer les événements de publication et de dépublication
      if (action === 'publish') {
        await strapi.service('api::article.rag').indexDocument(result);
      } else if (action === 'unpublish' || action === 'delete') {
        await strapi.service('api::article.rag').removeDocument(params.documentId);
      }

      return result;
    });
  },
};

L'utilisation d'un service personnalisé (api::article.rag) vous permet de gérer des files d'attente avec retries ou de déléguer les tâches d'indexation à des workers en arrière-plan comme BullMQ.

Découper les composants de contenu Strapi en chunks

Les bases de données vectorielles offrent de bien meilleurs résultats lorsque les segments de texte sont compacts et ciblés sur un sujet précis. Si vous passez un article entier de 3 000 mots dans un seul vecteur, les détails et faits spécifiques se retrouvent dilués dans l'embedding.

Dans Strapi, le contenu riche réside souvent au sein de Dynamic Zones ou de blocs structurés. Découper le contenu selon les délimitations des composants produit des chunks bien plus cohérents qu'un simple découpage arbitraire par nombre de caractères.

Préparer la représentation textuelle

Avant le chunking, convertissez les blocs structurés ou le Markdown en texte brut propre. Préservez la hiérarchie en préfixant les paragraphes enfants par leurs titres :

javascript
function extractArticleText(article) {
  const chunks = [];
  
  // Inclure le titre et le résumé comme contexte principal
  chunks.push(`Title: ${article.title}\nSummary: ${article.description || ''}`);

  // Parcourir les blocs de Dynamic Zone ou les champs de texte enrichi
  if (Array.isArray(article.content_blocks)) {
    for (const block of article.content_blocks) {
      if (block.__component === 'blocks.text-section') {
        chunks.push(`${block.heading ? block.heading + '\n' : ''}${block.body}`);
      } else if (block.__component === 'blocks.qa-item') {
        chunks.push(`Question: ${block.question}\nAnswer: ${block.answer}`);
      }
    }
  }

  return chunks;
}

Fenêtre glissante et dimensionnement en tokens

Pour les blocs de texte volumineux, appliquez un découpage par fenêtre glissante (sliding window). Visez 250 à 500 tokens par chunk, avec un chevauchement (overlap) de 10 % à 15 %. Ce chevauchement garantit qu'une phrase coupée entre deux fragments ne perde pas son contexte sémantique.

Générer les embeddings et stocker les vecteurs

Une fois les chunks prêts, soumettez-les à un modèle d'embedding (par exemple text-embedding-3-small ou des modèles auto-hébergés comme bge-large-en). Enregistrez les vecteurs obtenus avec des métadonnées qui vous aideront à filtrer les résultats lors de la recherche.

Voici une implémentation complète d'un service Strapi mettant à jour un store vectoriel externe :

javascript
module.exports = ({ strapi }) => ({
  async indexDocument(document) {
    const chunks = extractArticleText(document);
    
    for (let index = 0; index < chunks.length; index++) {
      const text = chunks[index];
      
      // Générer l'embedding vectoriel
      const embedding = await generateEmbedding(text);
      
      // Créer un identifiant vectoriel déterministe
      const vectorId = `${document.documentId}_chunk_${index}`;
      
      // Insérer ou mettre à jour dans le store vectoriel avec les métadonnées
      await vectorClient.upsert({
        id: vectorId,
        values: embedding,
        metadata: {
          documentId: document.documentId,
          chunkIndex: index,
          title: document.title,
          locale: document.locale,
          text: text,
          updatedAt: document.updatedAt,
        },
      });
    }
  },

  async removeDocument(documentId) {
    // Supprimer tous les vecteurs de chunks associés à ce documentId
    await vectorClient.deleteMany({
      filter: { documentId: { $eq: documentId } }
    });
  },
});

Interroger la base de données vectorielle depuis Next.js

Lorsqu'un utilisateur saisit une requête dans votre application front-end (un champ de recherche ou un chatbot, par exemple), votre Route Handler Next.js convertit cette requête en embedding et exécute une recherche par similarité dans votre index vectoriel.

javascript
// app/api/search/route.js dans Next.js
import { NextResponse } from 'next/server';

export async function POST(request) {
  const { query, locale = 'en' } = await request.json();

  // Convertir la chaîne de requête en vecteur
  const queryVector = await generateEmbedding(query);

  // Rechercher les 5 chunks les plus similaires correspondant à la locale
  const matches = await vectorClient.query({
    vector: queryVector,
    topK: 5,
    filter: { locale: { $eq: locale } },
    includeMetadata: true,
  });

  const context = matches.map((match) => match.metadata.text).join('\n\n---\n\n');

  return NextResponse.json({ results: matches, retrievedContext: context });
}

Le contexte récupéré est ensuite injecté dans votre template de prompt LLM :

text
You are a technical assistant. Answer the user's question using only the reference
context below. If the answer cannot be found in the context, state that you do
not know.

Context:
{{retrievedContext}}

Question: {{query}}

Gérer les révisions de contenu et les brouillons

Un piège classique des pipelines RAG headless consiste à indexer des contenus en brouillon avant qu'ils ne soient prêts pour la publication. Dans Strapi 5, les entrées coexistent sous forme de brouillon et d'état publié sous un même documentId.

Veillez à ce que votre logique de synchronisation vérifie rigoureusement le statut de publication. Si votre équipe éditoriale utilise un plugin comme Revisionary pour conserver l'historique complet des versions de vos contenus Strapi et comparer les modifications au fil du temps, votre pipeline vectoriel ne doit indexer que la version activement marquée comme publiée, en ignorant les itérations en brouillon et les instantanés intermédiaires.

Lorsqu'un éditeur dépublie un document ou restaure une révision antérieure, la routine de synchronisation doit purger les chunks existants et recalculer les embeddings du nouvel état restauré.

Gérer l'internationalisation et les locales

Si vous gérez un site Strapi multilingue, chaque locale représente une variante documentaire distincte partageant la même entrée logique ou regroupée sous des clés de locale. La similarité vectorielle peut varier d'une langue à l'autre :

  1. Modèles d'embeddings multilingues : privilégiez des modèles entraînés sur des représentations multilingues (comme text-embedding-3 ou multilingual-e5) si vos utilisateurs posent des questions dans une langue pour trouver du contenu rédigé dans une autre.
  2. Filtrage par métadonnées : si votre interface utilisateur restreint strictement la recherche à la langue active, stockez locale dans les métadonnées vectorielles et appliquez un filtre exact au moment de la requête. Cela évite d'afficher des extraits dans plusieurs langues dans les résultats de recherche.

Bonnes pratiques pour la maintenance d'un pipeline RAG

Pour garantir la fiabilité de votre pipeline en production, appliquez ces principes :

DomainePratique recommandée
Réindexation en masseCréez une commande CLI Strapi sur mesure (strapi rag:reindex) pour vider et reconstruire la base vectorielle lors d'un changement de modèle d'embedding ou de taille de chunk.
Rate limitingLors de la publication en masse de contenus, régulez les requêtes vers l'API d'embedding à l'aide d'une file d'attente en mémoire pour éviter les erreurs HTTP 429.
Gestion des métadonnéesStockez le texte brut du chunk directement dans les métadonnées vectorielles afin que votre front-end puisse composer le contexte du prompt sans requêtes supplémentaires vers Strapi.
Nettoyage des orphelinsRapprochez régulièrement la base vectorielle des enregistrements publiés dans Strapi pour éliminer les chunks orphelins dus à d'éventuelles coupures réseau.

Conclusion

Connecter Strapi 5 à une base de données vectorielle transforme votre CMS headless en une source de connaissances fiable et maîtrisée pour la recherche assistée par IA et les agents conversationnels. En tirant parti des hooks du Document Service, en découpant proprement les composants structurés et en appliquant des filtres rigoureux sur l'état de publication et les locales, vous garantissez que vos LLM répondent toujours à partir de contenus vérifiés et à jour.

Mots-clésstrapi 5iaragbases de données vectoriellesnext.js

L’équipe Revisionary

Les développeurs de Revisionary, un plugin d’historique des versions pour Strapi 5. Nous écrivons sur Strapi, le développement web, l’IA et la sécurité des applications.

Cet article a été rédigé avec l'aide d'une IA.

Tous les articles

IA

RAG versionné avec Strapi et Revisionary

Apprenez à créer un pipeline RAG qui interroge le contenu de Strapi tout en respectant l’historique des versions, grâce à Revisionary pour des réponses à jour et traçables.

· 5 min de lecture