Platane a rejoint l'initiative France Num pour accompagner les TPE PME dans leur transformation numérique : diagnostics, formations et aides financières.
Construire un RAG en production sur PostgreSQL avec pgvector marche très bien, jusqu'au jour où les performances de recherche par similarité commencent à dériver. C'est le moment où la décision technique devient stratégique : faut-il rester sur pgvector et indexer sérieusement, ou migrer vers un vector store spécialisé comme Qdrant ?
La réponse, dans la plupart des cas, n'est pas celle que vendent les comparatifs marketing. La vraie question n'est pas "qui ranke le mieux sur ANN-Benchmarks ?" mais "qui s'opère le mieux quand on a déjà une stack PostgreSQL en production, des contraintes de souveraineté, des budgets serrés et zéro tolérance au downtime ?"
On revient ici sur un cas concret : un RAG juridique d'environ 1 To, 9 millions d'embeddings 1536 dimensions, hébergé sur un cluster Kubernetes Scaleway, où il a fallu construire un index HNSW de 50 Go en restant en ligne. Le tout en GitOps via Flux et CloudNativePG (CNPG). Voici les chiffres, les choix, et ce qu'on referait, ou pas.
Prérequis avant de commencer
Avant de planifier une opération de ce type, vérifie que ton contexte coche les cases suivantes. Si l'un d'eux manque, traite-le avant de toucher à l'index vectoriel.
Une base PostgreSQL avec pgvector ≥ 0.6 (parallel HNSW build), idéalement 0.8.2 pour le correctif CVE-2026-3172 sur les builds parallèles.
Un opérateur Postgres Kubernetes (CNPG, Crunchy Data, Zalando), pour piloter les rolling updates sans bricolage shell.
Des nodes "performance" disponibles à la demande dans ton catalogue cloud (chez Scaleway, le pool POP2 ou les instances HC) : tu n'as pas besoin de payer cette puissance H24, juste pendant le build.
Un PVC redimensionnable à chaud : l'index HNSW pèse en gros 1,5 à 2 fois la taille de la colonne vector, et le CREATE INDEX CONCURRENTLY consomme de l'espace temporaire en plus.
Une fenêtre d'observation contrôlée (pas un vendredi soir) pour surveiller la progression du build et intervenir si la mémoire dévie.
Un agent RAG qui tolère 1 à 3 secondes de retrieval : si ton produit attend du sub-50 ms, le post est valable mais la conclusion devient différente (cf. plus bas).
Quand pgvector suffit, quand passer à Qdrant
C'est le débat le plus mal posé du moment. Les benchmarks vendor des deux camps mesurent rarement la même chose, et le critère décisif n'apparaît jamais dans le tableau de comparaison.
Voici le seul cadre qui compte vraiment, à partir d'un échantillon réaliste de production.
Dimension
pgvector + HNSW
Qdrant dédié
Le BlogDes infos, des actus, du fun !
22/04/2026
pgvector en production : indexer un RAG d'1 To sans downtime
Construire un index HNSW pgvector sur 9 millions d'embeddings dans un cluster Postgres de prod, sans interrompre les utilisateurs. Retour d'expérience.
La lecture honnête de ce tableau : Qdrant gagne sur les tail latencies, Postgres gagne sur le throughput, et les deux gagnent sur des terrains où l'autre n'est même pas en compétition. Pour un agent RAG offscreen qui tourne en arrière-plan, où la latence de génération LLM coûte 1 à 3 secondes de toute façon, gagner 30 ms sur le retrieve est invisible. Les 95 % du budget latence sont consommés par l'embedding et le LLM, pas par la base (Aleksapolskyi, 2025).
À l'inverse, ajouter Qdrant à une stack déjà PostgreSQL, c'est un service de plus à monitorer, sauvegarder, mettre à jour, sécuriser, et c'est une couche de double-écriture entre le store métier (workspaces, ACL, billing) et le vector store. Dans 80 % des cas, ce coût opérationnel n'est pas remboursé par les 30 ms gagnés.
Pourquoi un index HNSW de 50 Go tourne sur 4 Go de shared_buffers
C'est la question qui revient à chaque review d'archi : "Comment ton index peut-il être douze fois plus gros que ta mémoire et fonctionner ?"
La réponse tient en deux mots : locality of reference. HNSW est un graphe multi-couches où chaque requête traverse en moyenne quelques centaines de nœuds, pas la totalité du graphe. Une fois qu'une requête a "chauffé" un sous-graphe pertinent à un workspace donné, les requêtes suivantes du même workspace touchent à peu près les mêmes pages, qui restent dans le cache.
Concrètement, sur un déploiement avec 4 Go de shared_buffers et un index de 50 Go :
Cold cache (premier hit sur un workspace pas requêté depuis longtemps) : 1,7 à 3,9 secondes par requête de similarité. Le graphe doit être ramené depuis le disque page par page.
Warm cache (requêtes répétées sur un workspace actif) : moins de 10 ms. Le sous-graphe est résident en page cache.
Distribution beaucoup plus prévisible qu'avant l'index, où certaines requêtes pouvaient prendre 8 minutes selon la taille du workspace. Écart-type divisé par plus de 10.
Cette logique tombe quand le pattern d'accès est "vraiment uniforme" sur tout le corpus, ce qui est rare en multi-tenant. La plupart des produits SaaS ont une distribution Pareto sur les workspaces actifs : 20 % des tenants génèrent 80 % des requêtes. Si c'est ton cas, tu peux rationaliser sur la mémoire sans sacrifier la latence steady state.
Construire un index HNSW sans downtime, en 4 étapes
Le piège majeur : un build HNSW sur des millions de vecteurs 1536-dim demande beaucoup de RAM et de CPU pendant plusieurs heures. C'est strictement incompatible avec les nodes "dev" Scaleway (les moins chers du catalogue) où tournent les pods steady state. Si tu lances le build sur ces nodes, soit tu OOM, soit le build "ne fait plus rentrer le graphe en mémoire" et passe en mode disque (issue pgvector #500 documente ce comportement) : la durée explose, parfois à plus de 24 h, et il n'aboutit jamais.
La stratégie en 4 étapes, chacune un commit dans le repo infra (rollback granulaire) :
Étape 1, le PVC. Tu commites une augmentation de spec.storage.size dans la Cluster CRD. Flux reconcile, CNPG demande à Kubernetes un resize en ligne du PVC. Aucun pod ne redémarre, aucune coupure, aucun risque. Garde une marge confortable : sur un index HNSW de 50 Go, prévois 80 Go de buffer parce que CREATE INDEX CONCURRENTLY maintient une copie temporaire et accumule du WAL pendant les heures de build.
Étape 2, le bump des resources. C'est le moment délicat. Tu ajoutes un nodeSelector (scaleway.com/pool: performance) avec les tolerations qui vont avec, et tu pousses les requests/limits CPU/RAM. CNPG déclenche un rolling switchover : il met à jour les replicas un par un, puis fait basculer le primary. La micro-window de failover dure environ 10 secondes, contrôlée par le primaryUpdateStrategy: unsupervised (documentation CNPG). Un pool de connexion type pgBouncer absorbe ce flicker sans erreur applicative.
Étape 3, le build. Tu te connectes au primary fraîchement provisionné sur le node performance et tu lances :
SET maintenance_work_mem ='64GB';SET max_parallel_maintenance_workers =16;CREATEINDEX CONCURRENTLY idx_chunks_embedding_hnsw
ON chunks USING hnsw (embedding vector_cosine_ops)WITH(m =16, ef_construction =200);
Le CONCURRENTLY est non négociable : il n'acquiert pas de lock écriture, donc l'app continue à insérer des nouveaux documents pendant le build. Le pendant : si le build échoue à mi-parcours (OOM, timeout, network glitch), tu te retrouves avec un index INVALID qu'il faut DROP puis recréer. Vérifie la progression avec pg_stat_progress_create_index toutes les 5 minutes.
Sur ce cas, build de 25 minutes pour 50 Go d'index sur 9 millions de vecteurs, sur un node POP2-HC-48C-96G. Pour comparaison, le même build aurait pris plus de 4 heures sur les nodes dev steady state, à supposer qu'il n'OOM pas.
Étape 4, le scale-down. Une fois l'index validé (un EXPLAIN ANALYZE sur quelques requêtes témoins suffit), tu commites le revert du nodeSelector vers le pool dev. Nouveau rolling switchover, nouvelle micro-window de 10 secondes. La DB retombe à sa config économique steady state, l'index reste là, les performances de search aussi.
Pourquoi on ne ferait jamais "on choisira plus tard" sur un index vectoriel
Voici le vrai post-mortem de cet article. La décision initiale assumée par l'équipe était : "On indexera quand on aura vu la charge réelle, pas avant." Sur le papier, c'est défendable : on ne sur-optimise pas, on attend les vraies métriques de prod, on choisit la bonne stratégie en connaissance de cause.
Dans la pratique, c'est exactement le scénario qui mène à devoir indexer dans l'urgence sur un produit déjà en charge. Tu n'as plus le luxe de tester un PoC Qdrant sur deux semaines. Tu n'as plus le temps d'évaluer pgvectorscale ni de comparer les paramètres m et ef_construction. Tu fais ce que tu peux avec ce que tu as, et tu vis avec.
L'anti-pattern à graver : un index n'est pas une optimisation, c'est un prérequis fonctionnel. Une recherche par similarité sans index sur un corpus qui dépasse le million de vecteurs n'est pas "lente", elle est cassée par construction. La latence linéaire vs la taille du workspace est instable par essence : un workspace tier 1 marche, un workspace tier 3 timeout à 8 minutes.
Le piège qu'on a vraiment vécu
Symptôme. Quelques heures après le démarrage du build sur un environnement de staging plus modeste (32 Go de RAM, maintenance_work_mem = 8GB, pas de pool performance disponible), le warning attendu est apparu :
NOTICE: hnsw graph no longer fits into maintenance_work_mem after 2840000 tuples
DETAIL: Building will take significantly more time.
HINT: Increase maintenance_work_mem to speed up builds.
À partir de là, la progression mesurée par pg_stat_progress_create_index ralentit non-linéairement. À 18 heures de build, le pourcentage stagnait autour de 41 %, avec une dérive inverse : chaque incrément prenait plus de temps que le précédent. Le pattern est documenté sur le thread pgvector #822.
Diagnostic. Quand le graphe HNSW dépasse maintenance_work_mem, pgvector bascule sur un build "on-disk" qui réécrit des portions du graphe en cours de construction. Plus l'index grossit, plus chaque insertion doit potentiellement toucher les couches déjà écrites. La complexité passe de O(N log N) à quelque chose de bien moins gracieux. Un build qui aurait pris 25 minutes en mémoire prend des dizaines d'heures et ne converge jamais dans un délai utile.
Fix. Avorter (DROP INDEX), recréer l'environnement avec un node performance et un maintenance_work_mem qui couvre la totalité estimée du graphe. Pour 9 millions de vecteurs 1536-dim avec m = 16, viser au moins 1,5 fois la taille finale de l'index est le seuil pratique. Sur 50 Go d'index final, ça veut dire au moins 64 Go de maintenance_work_mem, donc un node avec ≥ 96 Go de RAM. La règle empirique : si tu peux te permettre de ne pas chercher la limite inférieure, ne la cherche pas. Le coût d'une heure de POP2-HC-48C-96G est négligeable face au risque d'avoir à recommencer.
Souveraineté et résilience : les choix qui pèsent
Sur un domaine régulé (juridique, santé, finance), l'hébergement n'est pas un détail. Le RAG decrit ici tourne intégralement sur un cluster Kubernetes Scaleway en France (datacenters parisiens), avec un stream WAL en réplication temps réel vers OVH pour la résilience multi-fournisseurs. Aucune donnée ne sort de l'UE, ni en repos ni en transit.
Cette contrainte change la fenêtre d'optimisation : les options "passer sur RDS Aurora avec leur fork pgvector optimisé" ou "déployer Pinecone serverless" ne sont pas sur la table. Le choix se fait entre des outils qu'on peut héberger sur du Scaleway, donc en pratique : pgvector dans Postgres self-hosted, Qdrant en cluster auto-géré, ou Weaviate en cluster auto-géré. Et là, la lecture du tableau plus haut devient encore plus claire : le coût d'opérer un service distribué supplémentaire sur ton cluster K8s est très réel.
C'est aussi pour ça qu'on aime bien l'astuce du "bump-build-revert" : tourner steady state sur les nodes dev (les moins chers), et ne payer la puissance que pendant les opérations exceptionnelles. Sur 12 mois, ça représente un facteur de 4 à 6 sur la facture compute par rapport à un sizing sur le pic. C'est l'inverse d'un compromis : c'est exploiter une élasticité qu'on aurait bêtement ignorée en gardant des nodes premium à demeure.
L'agence Platane (https://platane.io) opère plusieurs plateformes IA et SaaS sur ce type d'architecture, dont Jef, l'assistant IA du Barreau de Bruxelles, un RAG juridique multi-tenant avec isolation totale par avocat, ou Astory, une plateforme financière sur le même cluster Kubernetes Scaleway en haute disponibilité avec stream WAL offshore vers OVH. La maîtrise du stack pgvector + CNPG + Flux dans ce contexte précis fait partie de notre expertise solutions IA.
FAQ pgvector en production
Q : Faut-il pgvectorscale plutôt que pgvector vanilla ?
R : Si ton index ne tient pas dans la RAM disponible et que tu ne veux pas payer une instance assez grosse pour que ce soit le cas, oui. Sur le benchmark de The Build (avril 2026), DiskANN est 9× plus rapide que HNSW à recall équivalent quand l'index dépasse la RAM. Sinon, pgvector HNSW reste plus simple opérationnellement.
Q : Quelle valeur pour m et ef_construction ?
R : m = 16 et ef_construction = 200 couvrent 80 % des cas. Monte m à 24-32 si la qualité de recall est critique (recherche legal, médical), au prix d'un index 1,5× plus gros et d'un build 1,3× plus long. Au runtime, joue sur hnsw.ef_search (par défaut 40) : monte à 100 si tu observes un déficit de recall.
Q : Comment monitorer la santé d'un index HNSW en prod ?
R : Trois requêtes à automatiser. (1) Taille de l'index vs RAM disponible : si l'index dépasse 70 % de la RAM utilisable, prépare un plan B. (2) Distribution des latences avec pg_stat_statements sur la requête de similarité : surveille p95 et p99, pas seulement la moyenne. (3) Recall mesuré périodiquement sur un set de requêtes témoins : si tu observes du EXPLAIN ANALYZE qui retourne moins de résultats que LIMIT, tu es en post-filter degradation, active iterative_scan.
Q : Quel shared_buffers viser pour un index de N Go ?
R : Empiriquement, 25 % de la RAM allouée au pod est un point de départ raisonnable. Si tu observes du cache thrashing (taux de hit pg_buffercache < 95 %), bump à 40 %. Au-delà, le retour sur investissement décroît rapidement, l'OS page cache fait déjà une grande partie du boulot.
Pour aller plus loin
L'opération décrite ici n'est pas un héroïsme, c'est l'application disciplinée de patterns matures : CREATE INDEX CONCURRENTLY, GitOps via Flux, rolling updates CNPG, élasticité de pool Kubernetes. Aucune brique exotique. Le mérite, s'il y en a un, est dans la séquence : décomposer en commits granulaires, mesurer à chaque étape, accepter qu'une opération de 2 heures se planifie comme une opération de 2 jours.
La vraie leçon n'est pas technique, elle est éditoriale : un produit RAG ne se conçoit pas comme un produit search, mais le moment où il faut indexer arrive quand même. Le seul choix qu'on regrette est celui qu'on ne fait pas : reporter l'indexation jusqu'à ce que le problème impose la solution. À l'inverse, indexer trop tôt avec un IVFFlat conservateur ne coûte rien et préserve toutes les options.