Vous avez déployé une recherche. Les utilisateurs l’ont adorée pendant une semaine. Puis sont arrivés les tickets « la recherche est lente », suivis de « les résultats de recherche sont erronés », et enfin le classique exécutif : « On peut pas juste la rendre genre Google ? »
C’est à ce moment que la finance pose la question opérationnellement la plus dangereuse : « Elasticsearch est-il moins cher que de le faire dans Postgres ? »
La réponse honnête est que « moins cher » dépend moins du prix affiché que de ce à quoi vous vous engagez pour les trois prochaines années : mouvement des données, hygiène d’index, domaines de défaillance et coût humain de gestion d’un système distribué supplémentaire.
Le cadre de décision : ce que vous achetez vraiment
La recherche full-text de PostgreSQL (FTS) et Elasticsearch ne sont pas des concurrents au sens « case à cocher des fonctionnalités ». Ce sont des concurrents au sens « combien d’alertes à 3 h du matin voulez-vous par trimestre ».
Les deux peuvent répondre « trouver les documents correspondant à ces termes, classés raisonnablement ». Les coûts divergent avec le temps parce que les modèles opérationnels divergent.
Ce qui reste bon marché dans Postgres
- Moins de composants en mouvement. Même système de sauvegarde, même stratégie HA, même stack d’observabilité, même calendrier de mises à jour.
- Consistance transactionnelle par défaut. Les résultats de recherche reflètent les écritures validées sans pipeline CDC ni double-écriture.
- Corpus petit à moyen avec requêtes prévisibles. Catalogues produits, tickets, notes, fiches CRM, documents internes.
- Équipes déjà compétentes sur Postgres. Réutiliser les compétences n’est pas un avantage flou ; c’est une ligne budgétaire.
Ce qui reste bon marché dans Elasticsearch
- Requêtes de pertinence complexes et type analytique. Facettes, agrégations, fuzziness, synonymes, pondération par champ, « did you mean » et expérimentations de classement multi-champs intensives.
- Fort fan-out de requêtes avec gains de cache. Si votre trafic de lecture écrase les écritures, ES peut être une solution de requêtes rentable—lorsqu’il est réglé correctement.
- Grand corpus de textes et recherche multi-tenant quand vous avez accepté la surcharge opérationnelle et dimensionné les shards intelligemment.
- Organisations déjà matures avec Elastic. Si vous avez une équipe plateforme et des playbooks éprouvés, le coût marginal diminue.
La question « moins cher » à long terme se réduit à trois questions
- Voulez-vous un ou deux systèmes de référence ? Les clusters de recherche sont rarement des systèmes de référence, ce qui implique pipelines d’ingestion, backfills et réconciliation.
- Pouvez-vous tolérer la consistance éventuelle dans les résultats de recherche ? Sinon, vous paierez quelque part—généralement en complexité.
- Qui prendra la responsabilité de la qualité de la pertinence ? Si c’est « personne », Postgres FTS l’emporte souvent en étant « assez bon » et stable.
Idée paraphrasée de Werner Vogels (CTO d’Amazon) : Tout échoue, tout le temps ; concevez les systèmes en supposant la défaillance.
Ce n’est pas de la poésie. C’est une prévision budgétaire.
Faits intéressants et courte histoire (parce que tout ça n’est pas apparu hier)
- FTS de PostgreSQL n’est pas nouveau. La recherche full-text centrale est arrivée dans PostgreSQL 8.3 (2008), en s’appuyant sur des modules tsearch antérieurs.
- Les index GIN ont changé la donne. Le support des Generalized Inverted Index (GIN) a rendu pratique les recherches token → ligne à grande échelle pour les données tsvector.
- Elasticsearch a surfé sur la vague Lucene. Lucene est antérieur à Elasticsearch d’une décennie ; ES a emballé Lucene en un service distribué avec API REST et fonctionnalités de cluster.
- « Near real-time » est littéral. Les systèmes basés sur Lucene rafraîchissent périodiquement les segments ; les documents nouvellement indexés deviennent recherchables après un refresh, pas instantanément au commit.
- Le classement de Postgres est ancré en RI. ts_rank/ts_rank_cd implémentent des idées classiques de recherche d’information ; ce n’est pas magique, mais ce n’est pas non plus une correspondance naïve de sous-chaînes.
- Le nombre de shards par défaut d’Elasticsearch a brûlé de nombreuses équipes. Les shards ne sont pas gratuits ; trop de shards augmentent la surcharge et le temps de récupération.
- Vacuum est un levier de coût. Le bloat et le churn d’index de Postgres peuvent transformer une « recherche bon marché » en « pourquoi notre disque est à 90 % plein ».
- Les changements de mapping peuvent être coûteux opérationnellement. Dans ES, beaucoup de changements de mapping exigent un reindex. Vous payez en IO et en temps.
- Les synonymes sont un problème organisationnel. Que ce soit Postgres ou ES, les listes de synonymes deviennent une politique produit—quelqu’un doit arbitrer les débats.
Blague #1 : La pertinence de la recherche, c’est comme le café de bureau—tout le monde a un avis, et personne ne veut entretenir la machine.
Modèle de coût à long terme : calcul, stockage, personnes et risque
Poste de coût 1 : calcul et mémoire
Postgres FTS consomme généralement du CPU pendant les requêtes (classement, filtrage) et lors des écritures (maintien des index GIN, triggers, colonnes générées). Il bénéficie aussi massivement du cache : les pages d’index chaudes en mémoire comptent.
Si vous exécutez Postgres avec « juste assez de RAM », vous découvrirez que les requêtes full-text excellent à transformer l’IO aléatoire en mode de vie.
Elasticsearch consomme de la mémoire pour le heap (métadonnées du cluster, lecteurs de segments, caches) et utilise fortement le cache de pages OS pour les segments Lucene. Sous-dimensionner le heap entraîne des drames GC ; sur-dimensionner le heap affame le cache OS. Dans les deux cas, vous aurez un tableur.
Poste de coût 2 : amplification du stockage
Le stockage est souvent l’endroit où les mathématiques à long terme basculent. Les index de recherche ne sont pas compacts. Ils sont une redondance délibérée pour accélérer les requêtes.
- Postgres : une copie des données (plus WAL), et un ou plusieurs index. FTS ajoute du stockage tsvector et un index GIN (ou GiST). Le bloat est le multiplicateur silencieux.
- Elasticsearch : une copie du source (sauf si vous la désactivez), structures d’index inversé, doc values pour les agrégations, et typiquement au moins une réplique. C’est 2× avant même d’y réfléchir. Les snapshots ajoutent une autre couche.
Si vous comparez une instance Postgres unique à un cluster ES avec répliques, vous ne comparez pas deux logiciels. Vous comparez la tolérance au risque.
Poste de coût 3 : mouvement des données et backfills
Postgres FTS : les données sont déjà là. Vous ajoutez une colonne, construisez un index, et c’est réglé—jusqu’à ce que vous changiez la configuration de tokenisation ou ajoutiez un champ nécessitant le recomputation des vecteurs.
Elasticsearch : il faut ingérer. Cela implique CDC (logical decoding), un pattern outbox, un pipeline streaming ou la double-écriture. Cela implique aussi des backfills. Les backfills surviennent au pire moment : après que vous dépendez de la recherche pour le chiffre d’affaires.
Poste de coût 4 : personnes et processus
Elasticsearch en production n’est pas « installer et rechercher ». C’est des politiques de cycle de vie d’index, dimensionnement des shards, intervalles de refresh, mappings, analyzers et des upgrades de cluster qui ne peuvent pas être traités comme une mise à jour de librairie.
Postgres FTS n’est pas gratuit non plus, mais vous payez une « taxe système nouveau » plus petite.
Le système le moins coûteux est celui que votre on-call peut expliquer sous pression. Si votre équipe n’a jamais fait un redémarrage rolling d’un cluster ES pendant que des shards se relocalisent et que le trafic utilisateur monte, vous n’avez pas fini de budgéter.
Poste de coût 5 : risque et rayon d’impact
Elasticsearch isole la charge de recherche de votre base de données primaire. Cela peut réduire le risque si les requêtes de recherche sont lourdes et imprévisibles.
Postgres garde tout ensemble : moins de systèmes, mais une plus grande probabilité qu’une mauvaise requête de recherche devienne un incident de base de données.
« Moins cher à long terme » n’est pas « la plus petite facture mensuelle ». C’est « le moins d’incidents multi-équipes et de week-ends d’urgence pour reindexer ».
Recherche full-text PostgreSQL : ce qu’elle fait bien, et ce qu’elle pénalise
Les points forts (et pourquoi ils restent forts)
Postgres FTS brille quand la recherche est un attribut de vos données transactionnelles, pas un produit séparé.
Vous pouvez garder votre modèle de données simple : une table, une colonne tsvector, un index GIN, et des requêtes avec to_tsquery ou plainto_tsquery.
- Consistance : Dans la même transaction, vous pouvez mettre à jour le contenu et le vecteur de recherche de manière atomique (colonnes générées ou triggers).
- Simplicité opérationnelle : Une sauvegarde, une restauration. Un endroit pour appliquer les politiques de sécurité. Un jeu de contrôles d’accès.
- Parfait pour la « recherche dans une appli » : où l’UX est surtout « taper des mots, obtenir des enregistrements », avec des besoins de classement légers.
Les punitions (où les coûts s’insinuent)
Postgres vous fera payer pour trois péchés : lignes surdimensionnées, fort churn d’écritures et modèles de requêtes non bornés.
- Amplification des écritures : Mettre à jour un tsvector et un index GIN peut être coûteux sous un fort volume d’écritures.
- Bloat : Les mises à jour/suppressions fréquentes sur des colonnes indexées peuvent gonfler les tables et les index GIN, augmentant l’IO et ralentissant VACUUM.
- Plafond de pertinence : Vous pouvez faire des pondérations, dictionnaires et configs, mais vous n’obtiendrez pas les outils ES pour synonymes, analyzers par champ à grande échelle et expérimentations de pertinence sans les développer.
- Pitfalls multi-tenant : Si vous faites « tenant_id AND tsquery » pour des milliers de tenants, vous aurez besoin d’index partiels, de partitionnement ou des deux.
Quand Postgres est le choix le moins coûteux à long terme
- Votre corpus de recherche est inférieur à quelques dizaines de millions de lignes, et vous pouvez garder les documents raisonnablement petits.
- Vous pouvez contraindre les requêtes (pas de jokers en tête, pas de requêtes « OR sur tout » qui explosent).
- Vous avez besoin de forte consistance et d’opérations simples plus que d’une pertinence de pointe.
- Votre équipe est déjà dimensionnée pour Postgres, pas pour la recherche distribuée.
Quand Postgres devient le choix coûteux
- Le trafic de recherche est suffisamment important pour concurrencer les requêtes OLTP et vous ne pouvez pas l’isoler avec des réplicas.
- Vous avez besoin de fortes facettes/agrégations sur de nombreux champs à faible latence.
- Le tuning de pertinence devient un différenciateur produit et vous avez besoin de boucles d’itération plus rapides que SQL plus code personnalisé.
Elasticsearch : ce qu’il fait bien, et ce qu’il pénalise
Les points forts
Elasticsearch est conçu pour être recherché. Il n’hésite pas à pré-calculer des structures d’index pour garder la latence des requêtes basse.
Il est aussi pensé pour le scale horizontal : ajouter des nœuds, rééquilibrer les shards, continuer. En pratique, « continuer » est là où vos runbooks justifient leur salaire.
- Fonctions de pertinence et UX : analyzers, token filters, synonymes, fuzziness, highlighting, pondération par champ, « more like this ».
- Agrégations : facettes, histogrammes, estimations de cardinalité et requêtes à saveur analytique que Postgres peut faire mais souvent avec un profil de coût différent.
- Isolation : Vous pouvez garder la charge de recherche loin de votre base transactionnelle.
- Histoire de montée en charge : Avec un dimensionnement correct des shards, ES peut étaler les lectures et le stockage sur plusieurs nœuds proprement.
Les punitions
Elasticsearch punit les équipes qui le traitent comme une boîte noire puis sont surprises quand il se comporte comme un système distribué.
Les shards sont l’endroit où les corps sont enterrés.
- Surcharge des shards : Trop de shards gaspille le heap, augmente les descripteurs de fichiers, ralentit les mises à jour de l’état du cluster et prolonge la récupération.
- Taxe de reindex : Les erreurs de mapping ou de analyzer nécessitent souvent un reindex complet. C’est du temps, de l’IO et un risque opérationnel.
- Consistance éventuelle : Vous devez gérer le lag d’ingestion, les intervalles de refresh et les tickets « pourquoi mon écriture n’est pas encore cherchable ».
- Chorégraphie des upgrades : Les mises à niveau rolling sont faisables, mais les versions, plugins et changements incompatibles demandent de la discipline.
- Couplage caché : Votre appli, pipeline d’ingestion, templates d’index, ILM et settings du cluster deviennent un gros organisme.
Blague #2 : Elasticsearch est simple jusqu’à ce que vous ayez besoin qu’il soit fiable—puis il devient un cours de systèmes distribués auquel vous n’êtes pas inscrit.
Quand Elasticsearch est le choix le moins coûteux à long terme
- La recherche est une fonctionnalité produit principale et vous devez itérer rapidement sur la pertinence.
- Vos patterns de requêtes incluent des agrégations/facettes sur de nombreux champs avec des exigences de faible latence.
- Votre jeu de données est suffisamment grand qu’un niveau de recherche dédié évite de frapper la base OLTP.
- Vous avez (ou financerez) la maturité opérationnelle : sizing, monitoring, ILM, snapshots et une restauration testée.
Quand Elasticsearch devient le choix coûteux
- Vous n’avez pas d’histoire d’ingestion propre et vous vous retrouvez à double-écrire avec des comportements incohérents.
- Vous exécutez trop de shards « au cas où » et payez heap et CPU pour toujours.
- Vous considérez le reindex comme un événement rare et le réalisez en pleine saison de pointe.
Patrons d’architecture qui vous évitent des ennuis
Patron A : « Postgres uniquement » avec contraintes sensées
Faites cela si la recherche est secondaire. Utilisez une colonne tsvector générée, un index GIN, et acceptez que vous construisiez une « bonne recherche interne », pas une entreprise de recherche.
Mettez des garde-fous dans votre API pour empêcher les utilisateurs de générer des requêtes pathologiques.
- Utilisez
websearch_to_tsquerypour les entrées utilisateur (meilleure UX, moins de surprises). - Utilisez des poids et un petit nombre de champs ; ne mettez pas un gros blob JSON entier dans un vecteur à moins d’assumer la conséquence.
- Considérez des réplicas en lecture pour isoler la charge de recherche.
Patron B : Postgres comme source de vérité + Elasticsearch comme projection
C’est le patron « adulte » courant : OLTP dans Postgres, recherche dans ES. Le coût est le pipeline.
Ne le faites que si vous pouvez répondre « comment reconstruire ES à partir de Postgres » avec confiance.
- Utilisez une table outbox et un consumer pour indexer les changements.
- Concevez des opérations d’indexation idempotentes.
- Planifiez les backfills et l’évolution du schéma (documents versionnés ou alias d’index).
Patron C : Recherche à deux niveaux — par défaut bon marché, avancé coûteux
Gardez la plupart des recherches dans Postgres. Routez seulement les requêtes avancées (facettes, correspondance floue, classement lourd) vers Elasticsearch.
Cela réduit la charge sur ES et garde le pipeline plus petit. Cela crée aussi « deux sources de vérité pour le comportement de recherche », soyez délibéré.
Règle dure : ne laissez pas la recherche devenir votre chemin d’écriture
Si les écritures côté utilisateur dépendent d’ES étant sain, vous avez transformé votre cluster de recherche en dépendance transactionnelle critique. C’est comme ça que « incident de recherche » devient « incident de revenu ».
Gardez le chemin d’écriture dans Postgres ; laissez ES prendre du retard plutôt que de bloquer.
Tâches pratiques (commandes), signification des sorties et décision à prendre
Ce sont les types de vérifications qui transforment « je pense que c’est lent » en « c’est lent parce que nous faisons X, et on peut le corriger par Y ».
Les commandes sont des exemples exécutables. Remplacez les noms de BD et chemins pour correspondre à votre environnement.
Task 1: Check Postgres index sizes (is FTS eating your disk?)
cr0x@server:~$ psql -d appdb -c "\di+ public.*"
List of relations
Schema | Name | Type | Owner | Table | Persistence | Access method | Size | Description
--------+--------------------+-------+----------+-------------+-------------+---------------+--------+-------------
public | documents_fts_gin | index | app | documents | permanent | gin | 12 GB |
public | documents_pkey | index | app | documents | permanent | btree | 2 GB |
(2 rows)
Ce que cela signifie : Votre index GIN est le plus gros. C’est normal—jusqu’à ce que ça ne le soit plus.
Décision : Si l’index FTS domine le disque, évaluez si vous indexez trop de champs, trop de texte, ou si vous souffrez du bloat lié au churn. Si le churn est élevé, planifiez une stratégie VACUUM/REINDEX.
Task 2: Inspect table and index bloat indicators (quick approximation)
cr0x@server:~$ psql -d appdb -c "SELECT relname, n_live_tup, n_dead_tup, last_vacuum, last_autovacuum FROM pg_stat_user_tables ORDER BY n_dead_tup DESC LIMIT 5;"
relname | n_live_tup | n_dead_tup | last_vacuum | last_autovacuum
------------+------------+------------+--------------------+-------------------------
documents | 42000000 | 9800000 | | 2025-12-29 03:12:01+00
events | 180000000 | 1200000 | 2025-12-28 01:02:11| 2025-12-29 02:44:09+00
(2 rows)
Ce que cela signifie : Les tuples morts sont élevés. L’autovacuum tourne, mais il peut ne pas suivre.
Décision : Ajustez autovacuum pour les tables chaudes ou réduisez le churn des colonnes text indexées. Si les tuples morts continuent de croître, attendez-vous à des cliffs de performance et des surprises disque.
Task 3: Validate your FTS query uses the GIN index (EXPLAIN ANALYZE)
cr0x@server:~$ psql -d appdb -c "EXPLAIN (ANALYZE, BUFFERS) SELECT id FROM documents WHERE fts @@ websearch_to_tsquery('english','backup policy');"
QUERY PLAN
---------------------------------------------------------------------------------------------------------------
Bitmap Heap Scan on documents (cost=1234.00..56789.00 rows=1200 width=8) (actual time=45.210..62.118 rows=980 loops=1)
Recheck Cond: (fts @@ websearch_to_tsquery('english'::regconfig, 'backup policy'::text))
Heap Blocks: exact=8412
Buffers: shared hit=120 read=8410
-> Bitmap Index Scan on documents_fts_gin (cost=0.00..1233.70 rows=1200 width=0) (actual time=40.901..40.902 rows=980 loops=1)
Index Cond: (fts @@ websearch_to_tsquery('english'::regconfig, 'backup policy'::text))
Buffers: shared hit=10 read=2100
Planning Time: 0.322 ms
Execution Time: 62.543 ms
(10 rows)
Ce que cela signifie : Il utilise l’index GIN, mais lit beaucoup de blocks heap depuis le disque.
Décision : Si les lectures dominent, ajoutez de la RAM (cache), réduisez la taille des résultats avec des filtres, ou envisagez une stratégie couvrante (stocker moins de grandes colonnes, utiliser TOAST judicieusement, envisager la dénormalisation pour le chemin de recherche uniquement).
Task 4: Check Postgres cache effectiveness (are you IO-bound?)
cr0x@server:~$ psql -d appdb -c "SELECT datname, blks_hit, blks_read, round(100.0*blks_hit/nullif(blks_hit+blks_read,0),2) AS hit_pct FROM pg_stat_database WHERE datname='appdb';"
datname | blks_hit | blks_read | hit_pct
--------+-----------+-----------+---------
appdb | 982341210 | 92341234 | 91.41
(1 row)
Ce que cela signifie : Un ratio de cache de 91 % est moyen, pas excellent pour une BD qui veut une faible latence. Pour des charges FTS intensives, on vise généralement plus haut.
Décision : Si le ratio chute pendant les pics de recherche, vous payez le prix du cloud-IO en latence. Envisagez plus de RAM, un meilleur indexage, ou basculer la charge de lecture hors du primaire (réplica ou ES).
Task 5: Find the slowest FTS queries (pg_stat_statements)
cr0x@server:~$ psql -d appdb -c "SELECT mean_exec_time, calls, rows, query FROM pg_stat_statements WHERE query ILIKE '%tsquery%' ORDER BY mean_exec_time DESC LIMIT 3;"
mean_exec_time | calls | rows | query
----------------+-------+------+-----------------------------------------------------------
812.44 | 1200 | 100 | SELECT ... WHERE fts @@ websearch_to_tsquery($1,$2) ORDER BY ...
244.10 | 8400 | 20 | SELECT ... WHERE fts @@ plainto_tsquery($1,$2) AND tenant_id=$3
(2 rows)
Ce que cela signifie : Vos recherches les plus lentes sont maintenant évidentes, pas mythiques.
Décision : Ajoutez des LIMIT, resserrez les filtres, ajustez la stratégie de classement/tri ou pré-calculer des champs de ranking. Si les requêtes lentes sont des « recherches globales sur tout », envisagez ES.
Task 6: Check autovacuum settings for a hot table (are you vacuuming too late?)
cr0x@server:~$ psql -d appdb -c "SELECT relname, reloptions FROM pg_class JOIN pg_namespace n ON n.oid=relnamespace WHERE n.nspname='public' AND relname='documents';"
relname | reloptions
-----------+---------------------------------------------
documents | {autovacuum_vacuum_scale_factor=0.02}
(1 row)
Ce que cela signifie : Quelqu’un a déjà baissé le scale factor du vacuum (bien).
Décision : Si le bloat est toujours élevé, augmentez le nombre d’autovacuum workers, ajustez les limites de coût, ou planifiez des REINDEX/pg_repack périodiques (avec contrôle de changement).
Task 7: Measure WAL volume (is search indexing inflating write costs?)
cr0x@server:~$ psql -d appdb -c "SELECT pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(),'0/0')) AS wal_since_boot;"
wal_since_boot
----------------
684 GB
(1 row)
Ce que cela signifie : Le volume WAL est énorme ; cela peut être normal pour un système chargé, ou un signe que votre texte indexé churn.
Décision : Si la croissance du WAL se corrèle avec les mises à jour de texte, réduisez la fréquence des mises à jour, évitez de réécrire des documents entiers, ou déplacez la projection de recherche vers ES.
Task 8: Check Elasticsearch cluster health (baseline triage)
cr0x@server:~$ curl -s http://localhost:9200/_cluster/health?pretty
{
"cluster_name" : "search-prod",
"status" : "yellow",
"timed_out" : false,
"number_of_nodes" : 6,
"number_of_data_nodes" : 4,
"active_primary_shards" : 128,
"active_shards" : 256,
"unassigned_shards" : 12
}
Ce que cela signifie : Yellow signifie que les primaires sont alloués mais pas les répliques. C’est une redondance réduite et cela peut devenir un problème de performance pendant une récupération.
Décision : Si des répliques non assignées persistent, corrigez les problèmes d’allocation avant d’augmenter le trafic. N’acceptez pas le jaune comme « OK » sans une dérogation au risque explicite.
Task 9: Count shards per node (are you paying the shard tax?)
cr0x@server:~$ curl -s http://localhost:9200/_cat/shards?v
index shard prirep state docs store ip node
docs-v7 0 p STARTED 912341 18gb 10.0.0.21 data-1
docs-v7 0 r STARTED 912341 18gb 10.0.0.22 data-2
docs-v7 1 p STARTED 901122 17gb 10.0.0.23 data-3
docs-v7 1 r STARTED 901122 17gb 10.0.0.24 data-4
...output truncated...
Ce que cela signifie : Vous voyez les tailles de store par shard et leur placement.
Décision : Si vous voyez des centaines ou milliers de petits shards (moins d’1 Go), consolidez (moins de shards primaires par index, politiques de rollover, reindex). Les shards sont une surcharge fixe ; payez-la une fois, pas pour toujours.
Task 10: Inspect JVM heap pressure (is GC your hidden latency?)
cr0x@server:~$ curl -s http://localhost:9200/_nodes/stats/jvm?pretty | head -n 25
{
"cluster_name" : "search-prod",
"nodes" : {
"q1w2e3" : {
"name" : "data-1",
"jvm" : {
"mem" : {
"heap_used_in_bytes" : 21474836480,
"heap_max_in_bytes" : 25769803776
},
"gc" : {
"collectors" : {
"young" : { "collection_count" : 124234, "collection_time_in_millis" : 982341 },
"old" : { "collection_count" : 231, "collection_time_in_millis" : 412341 }
}
}
Ce que cela signifie : L’utilisation du heap est ~83 % du max, et les compteurs GC old sont non négligeables.
Décision : Si le heap reste haut avec des GC old fréquents, réduisez le nombre de shards, ajustez les caches ou redimensionnez le heap (prudemment). « Ajouter du heap » n’est pas toujours la solution ; cela peut réduire le cache OS et nuire.
Task 11: Check indexing pressure via refresh and merge activity (are you IO-bound from writes?)
cr0x@server:~$ curl -s http://localhost:9200/_nodes/stats/indices/refresh,merges?pretty | head -n 40
{
"nodes" : {
"q1w2e3" : {
"indices" : {
"refresh" : {
"total" : 882341,
"total_time_in_millis" : 9123412
},
"merges" : {
"current" : 12,
"current_docs" : 402341,
"total_time_in_millis" : 22123412
}
Ce que cela signifie : Un temps de merge élevé et des merges en cours suggèrent une forte activité d’écriture/segment, souvent limitée par l’IO.
Décision : Si l’indexation concurrence la latence de recherche, ajustez les intervalles de refresh, limitez l’ingest, ou séparez des index hot ingest. N’« optimisez » pas en désactivant le refresh aveuglément ; vous déplacerez simplement la douleur.
Task 12: Verify replica and snapshot posture (what’s your restore story?)
cr0x@server:~$ curl -s http://localhost:9200/_cat/indices?v
health status index uuid pri rep docs.count store.size
yellow open docs-v7 xYz 16 1 18000000 320gb
Ce que cela signifie : Yellow plus rep=1 suggère que les répliques existent mais ne sont pas pleinement allouées (ou qu’il manque des nœuds).
Décision : Si vous comptez sur les répliques pour la HA, mettez le cluster en green ou réduisez le nombre de répliques en connaissance de cause. Assurez-vous aussi d’avoir des snapshots et d’avoir testé la restauration ; les répliques ne sont pas des sauvegardes.
Task 13: Measure ingest lag (is ES “behind” Postgres?)
cr0x@server:~$ psql -d appdb -c "SELECT now() - max(updated_at) AS db_freshness FROM documents;"
db_freshness
--------------
00:00:03.421
(1 row)
cr0x@server:~$ curl -s http://localhost:9200/docs-v7/_search -H 'Content-Type: application/json' -d '{"size":1,"sort":[{"updated_at":"desc"}],"_source":["updated_at"]}' | jq -r '.hits.hits[0]._source.updated_at'
2025-12-30T10:41:12Z
Ce que cela signifie : Si max(updated_at) de Postgres est plus récent que le timestamp le plus récent d’ES, votre pipeline accuse du retard.
Décision : Décidez si la consistance éventuelle est acceptable. Sinon, corrigez le débit d’ingestion, ajoutez du backpressure, ou routez les requêtes « critiques pour la fraîcheur » vers Postgres.
Task 14: Postgres vs ES latency sampling (stop guessing)
cr0x@server:~$ time psql -d appdb -c "SELECT count(*) FROM documents WHERE fts @@ websearch_to_tsquery('english','error budget');"
count
-------
1242
(1 row)
real 0m0.219s
user 0m0.010s
sys 0m0.005s
cr0x@server:~$ time curl -s http://localhost:9200/docs-v7/_search -H 'Content-Type: application/json' -d '{"query":{"match":{"body":"error budget"}},"size":0}' | jq '.took'
37
real 0m0.061s
user 0m0.012s
sys 0m0.004s
Ce que cela signifie : Le « took » d’ES est 37 ms et bout en bout ~61 ms ; Postgres est ~219 ms ici.
Décision : Si ES est systématiquement plus rapide et que votre pipeline est sain, ES peut être moins cher en terme d’expérience utilisateur—tout en coûtant plus en opérabilité. Si Postgres est « suffisamment rapide », n’achetez pas un cluster supplémentaire pour gagner 150 ms.
Task 15: Check Linux IO saturation (the universal villain)
cr0x@server:~$ iostat -xz 1 3
Linux 6.5.0 (db-1) 12/30/2025 _x86_64_ (16 CPU)
avg-cpu: %user %nice %system %iowait %steal %idle
12.1 0.0 4.3 18.7 0.0 64.9
Device r/s w/s rkB/s wkB/s await svctm %util
nvme0n1 820.0 410.0 81200 32100 14.2 0.9 98.7
Ce que cela signifie : %util ~99 % et await ~14 ms : le stockage est saturé.
Décision : Avant de réécrire des requêtes, résolvez l’IO : volumes plus rapides, plus de mémoire, moins de bloat, meilleur caching, ou déplacez la charge de recherche. Les disques saturés transforment des systèmes « acceptables » en usines à pages d’alerte.
Playbook de diagnostic rapide : trouver le goulot en quelques minutes
Première étape : décider si c’est du calcul, de l’IO ou de la coordination
- Vérifiez la latence de bout en bout. Metrics applicatives : p50/p95/p99 pour l’endpoint de recherche. Si le p99 est mauvais, concentrez-vous là.
- Vérifiez la saturation IO système.
iostat -xzsur les nœuds BD/recherche. Await élevé et %util sont votre signal « stop everything ». - Vérifiez CPU et mémoire. Si le CPU est saturé et l’IO est correct, vous êtes lié au calcul (classement, merges, GC, ou trop de parsing JSON).
Deuxième étape : isoler où le temps est passé
- Postgres : utilisez
EXPLAIN (ANALYZE, BUFFERS). Si les buffers montrent beaucoup de lectures, c’est IO/cache. Si c’est CPU, vous verrez un temps élevé avec principalement des hits. - Elasticsearch : comparez
tookvs temps client. Si « took » est bas mais le temps client haut, vous avez un problème réseau, LB, TLS ou file d’attente threadpool. - Pipeline : vérifiez le lag d’ingestion. « La recherche est erronée » signifie souvent « ES est en retard ».
Troisième étape : choisissez le levier le plus petit et sûr
- Contraignez les requêtes. Ajoutez LIMIT, ajoutez des filtres, retirez les fonctionnalités pathologiques.
- Corrigez bloat/shards. Vacuum/reindex (Postgres) ou consolidation de shards / tuning ILM (ES).
- Ajoutez du matériel seulement après. La RAM aide les deux systèmes, mais c’est le pansement le plus cher lorsque le design sous-jacent est cassé.
Trois mini-histoires d’entreprise du terrain
Incident causé par une mauvaise hypothèse : « La recherche est eventual consistent, mais les utilisateurs ne le remarqueront pas »
Une PME B2B a ajouté Elasticsearch pour améliorer la recherche dans les tickets clients. Ils ont branché un flux CDC depuis Postgres vers un service d’indexation et ont livré.
Les métriques initiales semblaient excellentes : p95 inférieur, équipe support plus heureuse, moins de pics sur la base.
Puis la première semaine d’audit de conformité est arrivée. Le support recherchait des tickets nouvellement créés et ne les voyait pas. Ils ont recréé des tickets, attaché des fichiers en double et escaladé vers l’ingénierie.
L’équipe d’ingénierie supposait que « refresh interval » était un petit réglage. Ils l’avaient augmenté pour réduire la charge d’indexation lors d’un précédent test de charge.
Pendant la semaine d’audit, le volume de tickets a monté en flèche. Le lag d’ingestion a augmenté, l’indexeur a pris du retard et l’intervalle de refresh a caché les nouveaux documents plus longtemps.
Le mode de défaillance n’était pas « Elasticsearch est down ». C’était pire : le système était up et renvoyait des résultats plausibles mais incorrects.
La correction fut opérationnelle, pas philosophique : resserrer les SLOs sur la fraîcheur, mesurer explicitement le lag, et router les vues « juste créées » vers Postgres pour une courte fenêtre.
Ils ont aussi ajouté une bannière « la recherche peut prendre jusqu’à N secondes pour refléter les changements » pour les workflows internes—une UX ennuyeuse et honnête qui a évité les doublons.
Optimisation qui s’est retournée contre eux : « Réduisons la charge Postgres en indexant tout d’un coup »
Une autre société s’est appuyée sur Postgres FTS pour une base de connaissances. Ça fonctionnait bien jusqu’à ce que le produit demande un classement plus riche.
Un ingénieur a décidé de pré-calculer un énorme tsvector incluant titre, corps, tags, commentaires et texte extrait de PDF—tout.
La latence des requêtes s’est améliorée au début car moins de joins et moins de calculs étaient nécessaires par requête. L’équipe a célébré et est passée à autre chose.
Deux mois plus tard, la base a commencé à croître plus vite que prévu. Autovacuum n’a plus suivi. L’IO a augmenté. Les réplicas de lecture ont commencé à prendre du retard.
L’« optimisation » a augmenté l’amplification des écritures. Toute modification d’une partie du document réécrivait une ligne plus grosse et mettait à jour un ensemble d’entrées GIN plus volumineux.
Le système n’a pas échoué bruyamment. Il a échoué lentement : p95 en hausse, timeouts occasionnels, puis un week-end d’ajustement d’autovacuum et d’extension disque.
Ils se sont remis en réduisant ce qui était indexé, en séparant les textes extraits rarement modifiés dans une table séparée et en ne mettant à jour son vecteur que lorsque ce texte changeait.
La leçon : dans Postgres, vous pouvez acheter de la vitesse de requête au prix des écritures. Si vous ne mesurez pas le coût des écritures, il vous facturera plus tard.
Pratique ennuyeuse mais correcte qui a sauvé la mise : « Rebuilders de recherche et restaurations testées »
Une fintech faisait tourner Postgres et Elasticsearch. La recherche n’était pas un système de référence, mais elle était exposée aux clients et liée aux revenus.
Leur équipe plateforme a insisté sur un « game day » trimestriel de reconstruction de recherche : supprimer un index, reconstruire depuis la source, mesurer le temps et valider les comptes et contrôles ponctuels.
Personne n’aimait ça. Ce n’était pas glamour, et ça ne produisait jamais de slide produit.
Ce que ça a produit, c’est de la confiance : ils savaient combien de temps prenait un reindex, ce que cela coûtait et où se logeaient les goulots.
Un jour, une erreur de mapping s’est glissée en production, provoquant un comportement étrange pour certaines requêtes. L’équipe n’a pas paniqué.
Ils ont avancé vers une nouvelle version d’index derrière un alias, réindexé en background et basculé le trafic après validation.
Les clients ont vu un léger flottement de pertinence pendant une courte période, pas une panne totale. L’entreprise n’a pas eu besoin d’une war room de douze personnes et d’un incident commander épuisé.
La pratique ennuyeuse a sauvé la mise, ce qui est le plus grand compliment en opérations.
Erreurs courantes : symptôme → cause racine → correction
1) « La recherche Postgres est devenue lente avec le temps »
Symptôme : la latence p95 augmente mois après mois ; l’utilisation du disque grimpe ; autovacuum tourne en permanence.
Cause racine : bloat des tables et index GIN dû au fort churn d’update/delete sur des champs text indexés ; autovacuum non adapté aux grandes tables.
Correction : Baisser les scale factors d’autovacuum sur les tables chaudes, augmenter les ressources de vacuum, réduire le churn d’updates et envisager REINDEX/pg_repack périodiques en fenêtres de maintenance.
2) « Elasticsearch est rapide en test mais lent en production »
Symptôme : les benchmarks labo sont excellents ; la production a des spikes de latence et des timeouts.
Cause racine : trop de shards, pression sur le heap et GC, ou merges en concurrence avec les requêtes sous charge réelle d’ingestion.
Correction : Réduire le nombre de shards, corriger le dimensionnement, ajuster l’intervalle de refresh et le débit d’ingest, vérifier l’équilibre heap/page cache et monitorer le backlog de merges.
3) « Les résultats de recherche n’incluent pas les données fraîches »
Symptôme : les utilisateurs ne trouvent pas les enregistrements nouvellement créés/actualisés, mais l’enregistrement existe dans Postgres.
Cause racine : lag du pipeline d’ingestion, intervalle de refresh trop long ou événements d’indexation échoués sans alerte.
Correction : Instrumenter le lag, alerter sur le backlog, ajouter des retries idempotents et fournir une stratégie de fraîcheur (read-your-writes via Postgres ou sessions collantes).
4) « La recherche a provoqué un incident base de données »
Symptôme : CPU et IO en pic sur le primaire ; endpoints transactionnels non liés ralentissent.
Cause racine : l’endpoint de recherche exécute des requêtes coûteuses sur le primaire sans LIMITs ; pas d’isolation de lecture ; contraintes de requête faibles.
Correction : Router la recherche vers des réplicas, faire respecter des budgets de requêtes, ajouter LIMIT, exiger des filtres et mettre en cache les requêtes courantes.
5) « On ne peut pas changer l’analyzer/mapping sans downtime »
Symptôme : tout changement de pertinence devient un projet de reindex effrayant.
Cause racine : pas de versioning d’index/alias ; pas de répétition des workflows de reindex.
Correction : Adoptez des indices versionnés avec alias, automatisez les pipelines de reindex et pratiquez régulièrement une reconstruction.
6) « Elasticsearch est green mais les requêtes time out quand même »
Symptôme : la santé du cluster est green ; les utilisateurs voient des timeouts intermittents.
Cause racine : saturation des threadpools, requêtes lentes ou surcharge de coordination (ex. agrégations lourdes) malgré une allocation de shards saine.
Correction : Identifier les requêtes lentes, ajouter des timeouts/coupe-circuits côté appli, réduire la cardinalité des agrégations et pré-calculer des champs.
7) « Le classement FTS de Postgres semble ‘incorrect’ »
Symptôme : les résultats contiennent des correspondances mais l’ordre semble faux aux yeux humains.
Cause racine : poids manquants, mauvaise config/dictionnaire de langue, ou mélange de champs sans structure.
Correction : Utilisez des vecteurs pondérés par champ, choisissez le regconfig correct, considérez les requêtes de phrase et validez avec un jeu de tests sélectionnés.
Listes de contrôle / plan étape par étape
Étapes : choisir Postgres FTS (et le garder bon marché)
- Définir les contraintes de requête. Décidez ce que les utilisateurs peuvent rechercher : champs, opérateurs, longueur max, tokens max.
- Modéliser le vecteur. Gardez-le intentionnel : titre + corps + quelques métadonnées, pondérés.
- Indexer avec GIN. Construisez l’index GIN et validez avec EXPLAIN qu’il est utilisé.
- Budgétiser le churn. Si vous mettez à jour les documents fréquemment, ajustez autovacuum tôt, pas après l’apparition du bloat.
- Protéger le primaire. Placez la recherche sur des réplicas quand le trafic augmente. L’incident de base de données le moins cher est celui que vous n’avez pas.
- Mesurer. Suivez latence des requêtes, lectures de buffers, tuples morts et croissance disque chaque mois.
Étapes : choisir Elasticsearch (et éviter les pièges coûteux)
- Concevoir d’abord le pipeline d’ingestion. Outbox/CDC, retries, idempotence, backfill.
- Définir le versioning d’index. Utilisez des alias pour reindexer sans downtime.
- Dimensionner les shards intentionnellement. Choisissez des tailles de shard qui gardent la récupération raisonnable et la surcharge faible ; évitez les shards minuscules.
- Planifier ILM/retention. Hot/warm/cold si nécessaire ; au minimum rollover et politiques de suppression.
- Fixer des SLOs pour la fraîcheur. Mesurez le lag, pas les impressions.
- Tester les restaurations. Les drills snapshot/restore ne sont pas optionnels si la recherche compte.
- Écrire des runbooks. Défaillance de nœud, cluster yellow/red, reindex, changement de mapping, réponse à la pression de heap.
Étapes : approche hybride (la plupart des équipes devraient commencer ici)
- Commencez avec Postgres FTS pour les flux cœur et apprenez les vrais patterns de requêtes.
- Instrumentez le comportement de recherche. Journalisez les requêtes (en toute sécurité), mesurez la latence, capturez les cas « pas de résultats ».
- Montez à Elasticsearch seulement pour les fonctionnalités qui l’exigent vraiment (facettes, flou, itération de pertinence intensive).
- Gardez « lire-vos-écritures » dans Postgres pour les éléments UI critiques de fraîcheur.
- Rendez ES reconstruisible. Si vous ne pouvez pas le reconstruire, vous ne le maîtrisez pas ; il vous maîtrise.
FAQ
1) La recherche full-text Postgres est-elle « suffisante » pour une recherche côté client ?
Souvent, oui—surtout pour des applications B2B où les utilisateurs savent ce qu’ils cherchent. Si vous avez besoin de fortes facettes, de correspondance floue, de synonymes à grande échelle et d’itération rapide de la pertinence, ES est généralement plus adapté.
2) Quel est le plus gros coût caché à long terme d’Elasticsearch ?
Le pipeline et la discipline opérationnelle : versioning d’index, sizing des shards, workflows de reindex et surveillance de la fraîcheur. Le cluster est la partie facile ; le garder correct est le coût.
3) Quel est le plus gros coût caché à long terme de Postgres FTS ?
L’amplification des écritures et le bloat. Si vous indexez de gros champs texte mutables et les mettez à jour souvent, votre index GIN et votre table peuvent croître et ralentir de façons qui ressemblent à une « dégradation mystérieuse ».
4) Les réplicas de lecture peuvent-ils rendre la recherche Postgres « aussi bonne » qu’un cluster de recherche ?
Les réplicas aident à isoler la charge, mais ils ne transforment pas Postgres en moteur de pertinence. Ils étendent l’échelle « bon marché et stable ». Ils n’apportent pas les analyzers et l’ergonomie d’agrégation à la ES.
5) Est-il sûr de double-écrire vers Postgres et Elasticsearch ?
Ça peut l’être, mais c’est rarement l’option la plus simple et sûre. La double-écriture introduit des problèmes de consistance lors de pannes partielles. Préférez outbox/CDC pour que Postgres reste l’autorité d’écriture et ES une projection.
6) Comment décider uniquement sur la taille des données ?
La taille des données prédit faiblement sans les patterns de requête et le taux de mises à jour. Des dizaines de millions de lignes peuvent être fines en Postgres FTS avec de bonnes contraintes et du matériel adapté. Quelques millions de docs peuvent être pénibles en ES si vous créez des milliers de shards minuscules.
7) Les répliques Elasticsearch sont-elles une sauvegarde ?
Non. Les répliques protègent contre la perte de nœud, pas contre les erreurs d’opérateur, les mauvais mappings, les suppressions accidentelles ou la corruption propagée. Vous avez toujours besoin de snapshots et de restaurations testées.
8) Et utiliser la recherche trigram de Postgres au lieu du full-text ?
Les trigrams sont excellents pour la correspondance de sous-chaîne et un fuzzy approximatif sur des champs courts (noms, identifiants) et peuvent compléter le FTS. Ils peuvent aussi être coûteux s’ils sont mal utilisés sur de gros blobs de texte. Utilisez le bon index pour la bonne question.
9) Si nous avons besoin à la fois de recherche et d’analytique ?
Si vous poussez des agrégations lourdes et des tableaux de bord dans ES, vous faites tourner une charge analytique sur un cluster de recherche. Ça peut fonctionner, mais cela augmente la contention des ressources. Séparez les préoccupations si cela commence à se combattre.
10) Comment garder les coûts à long terme prévisibles ?
Mesurez les moteurs : bloat/WAL de Postgres et nombre de shards/pression heap/lag d’ingest d’ES. Les coûts deviennent prévisibles quand les taux de croissance sont visibles et liés à des plans de capacité, pas à des incidents surprises.
Prochaines étapes pratiques
Si vous voulez la réponse la moins coûteuse à long terme pour la plupart des équipes produit : commencez avec Postgres FTS, disciplinez-le, et n’ajoutez Elasticsearch que lorsque vous pouvez nommer l’écart fonctionnel en une phrase.
« Parce que tout le monde utilise Elasticsearch » n’est pas un besoin ; c’est un mouvement limitant pour la carrière avec une facture mensuelle.
- Inventoriez vos vrais besoins. Facettes ? Flou ? Synonymes ? Multi-langue ? Contraintes de fraîcheur ?
- Exécutez les tâches ci-dessus sur votre système actuel. Obtenez tailles d’index, signaux de bloat, compteurs de shards, pression heap et lag d’ingest.
- Décidez quel coût vous payez : contention base de données (Postgres) ou pipeline + opérations cluster (ES).
- Si vous choisissez Postgres : appliquez des budgets de requête, isolez avec des réplicas, tunnez autovacuum et gardez les vecteurs petits et pertinents.
- Si vous choisissez Elasticsearch : construisez une projection reconstruisible avec alias, dimensionnement sensé des shards, SLOs de fraîcheur et procédures pratiquées de restore/reindex.
Une recherche peu coûteuse à long terme n’est pas une marque. Ce sont des habitudes : contraindre les requêtes, mesurer les bons compteurs et traiter la maintenance d’index comme une charge de production à part entière.