Aller au contenu
~/benhattab
Retour aux notes
·3 min de lecture

Pourquoi je garde mes embeddings dans Postgres

Sur Synka Sphere, j'ai choisi pgvector plutôt qu'une base vectorielle dédiée. Ce n'est pas une question de performance — c'est une question de cohérence et de nombre de systèmes à sauvegarder.

RAGPostgreSQLpgvectorArchitecture

Quand on construit du RAG, le réflexe est d'ajouter une base vectorielle : Qdrant, Pinecone, Weaviate. Elles sont excellentes, et j'ai utilisé Qdrant sur un pipeline OCR avec de très bons résultats.

Sur Synka Sphere, j'ai pourtant gardé les embeddings dans PostgreSQL avec pgvector. Voici le raisonnement, parce que la réponse n'est pas « c'est plus rapide » — ce ne l'est pas toujours.

Ce que coûte vraiment un second système

Une base vectorielle séparée, ce n'est pas juste une dépendance de plus dans le docker-compose. C'est :

  • une seconde sauvegarde à planifier, à chiffrer et surtout à tester en restauration ;
  • un second système à monitorer, à mettre à jour, à sécuriser ;
  • une cohérence à maintenir à la main entre deux stockages qui n'ont pas de transaction commune.

Ce dernier point est le vrai problème. Quand un document est supprimé dans Postgres, ses vecteurs doivent disparaître de l'index. Sans transaction partagée, il y a une fenêtre — courte, mais réelle — pendant laquelle les deux systèmes ne sont pas d'accord.

Sur un produit qui ingère les documents internes de ses clients, cette fenêtre a un nom : un document supprimé qui continue d'alimenter les réponses d'un agent.

L'isolation compte plus que la latence

Synka Sphere est multi-tenant. Les documents d'un workspace ne doivent jamais être atteignables depuis un autre — y compris dans la recherche sémantique.

Avec pgvector, cette contrainte s'exprime dans le même langage que le reste du domaine :

SELECT id, content, embedding <=> :query AS distance
FROM document_chunks
WHERE workspace_id = :workspace   -- même garantie que partout ailleurs
ORDER BY distance
LIMIT 8;

C'est une clause WHERE ordinaire, soumise aux mêmes scopes, aux mêmes policies et aux mêmes tests que n'importe quelle autre requête. Avec un système externe, l'isolation devient une convention de nommage de collections — une couche de plus où se tromper, sur exactement le sujet où l'on n'a pas le droit de se tromper.

Une transaction, pas deux écritures

Ingérer un document devient atomique :

DB::transaction(function () use ($document, $chunks) {
    $document->save();
    $document->chunks()->createMany($chunks);   // vecteurs compris
});

Si quoi que ce soit échoue, tout est annulé. Il n'existe pas d'état où le document est enregistré mais pas indexé, ni l'inverse. Avec deux systèmes, cette garantie ne s'obtient qu'en écrivant soi-même de la logique de compensation — c'est-à-dire en réimplémentant les transactions, moins bien.

Quand je changerai d'avis

Ce choix a une limite, et je préfère la nommer :

  • Au-delà de quelques millions de chunks, l'index HNSW de pgvector demande un réglage sérieux, et une base dédiée devient réellement plus performante.
  • Si la charge vectorielle et la charge relationnelle divergent, les faire cohabiter sur la même instance devient une contrainte plutôt qu'une simplification.
  • Si le filtrage par métadonnées devient très complexe, les moteurs spécialisés ont de meilleures primitives.

Aucun de ces seuils n'est atteint. Et une migration ultérieure vers Qdrant est un travail borné et prévisible : la logique métier ne bouge pas, seule la couche de récupération change.

Le principe général

L'architecture jeune ne se juge pas au pic de performance qu'elle atteint, mais au nombre de choses qui peuvent casser pendant qu'on cherche encore ses clients.

Ajoutez un système quand la douleur de ne pas l'avoir devient réelle — pas quand un article de blog vous dit que c'est la bonne pratique.

Une seule base à sauvegarder, une seule à restaurer, une seule à sécuriser. À ce stade, c'est ce qui compte le plus.