OpenSearch vs PostgreSQL — Recherche hybride sans douleur

Cet article vous a aidé ?

Vous n’avez pas prévu de gérer une « ingénierie de plateforme de recherche ». Vous vouliez juste que la barre de recherche de votre produit cesse de vous faire honte :
les utilisateurs ne trouvent pas des éléments dont ils savent qu’ils existent, la latence monte lors des mises à jour par lot, et chaque « petit ajustement de pertinence » tourne en weekend.

Le choix habituel semble simple mais est trompeur : « Peut-on garder la recherche dans PostgreSQL ? » versus « Avons-nous besoin d’OpenSearch ? »
La vraie réponse est presque toujours : « Utilisez les deux, mais seulement si vous savez avec rigueur où se trouve la vérité et comment vous opérez le pipeline. »

La décision que vous devez réellement prendre

« OpenSearch vs PostgreSQL » est un mauvais cadrage. Vous décidez en réalité :

  • Où se situe la source de vérité ? (Presque toujours PostgreSQL.)
  • Quels types de requêtes doivent être rapides ? Mot-clé, phrase, préfixe, flou, facettes, similarité vectorielle, classement hybride, géolocalisation, agrégations.
  • Quelle défaillance est acceptable ? « La recherche est obsolète pendant 5 minutes » est souvent acceptable. « Le paiement échoue » ne l’est pas.
  • Combien de réglages opérationnels votre équipe peut-elle assumer ? PostgreSQL peut faire de la recherche, mais dès que vous avez besoin d’un vrai réglage de pertinence et de récupération hybride à l’échelle, OpenSearch justifie son coût opérationnel.

Voici un conseil franc :

  • Utilisez uniquement PostgreSQL quand : le jeu de données est modeste, les motifs de requête sont simples, et la correction de la recherche compte plus que des améliorations de pertinence sophistiquées. Vous voulez moins de pièces en mouvement.
  • Utilisez uniquement OpenSearch quand : votre produit tolère la cohérence éventuelle et que la recherche est le produit. Vous garderez une base de données quelque part, mais vous n’allez pas prétendre que c’est le moteur de recherche.
  • Utilisez un hybride (PostgreSQL + OpenSearch) quand : vous avez besoin d’un classement riche, d’agrégations ou de recherche vectorielle à grande échelle, mais vous avez aussi besoin de vérité transactionnelle et d’audit raisonnable.

Une citation à garder sur un post-it quand on vous demande « juste un seul système » : « idée paraphrasée » — Werner Vogels (CTO d’Amazon) a longtemps défendu l’idée de concevoir pour la disponibilité en acceptant la cohérence éventuelle quand c’est acceptable.
La recherche est l’un des rares endroits où ce compromis est généralement rationnel.

Faits historiques qui comptent plus que les slides des fournisseurs

Ce ne sont pas des trivia. Ils expliquent pourquoi chaque outil se comporte comme il le fait sous la pression.

  1. La recherche full-text de PostgreSQL est dans le cœur depuis des décennies (tsvector/tsquery), et elle est optimisée d’abord pour les charges transactionnelles, la recherche ensuite.
  2. Le modèle d’index inversé de Lucene (la base d’Elasticsearch/OpenSearch) a été conçu pour une récupération et un scoring rapides, pas pour des mises à jour ACID par ligne.
  3. Les index GIN dans Postgres sont brillants pour l’appartenance à un ensemble (comme la présence d’un token) mais peuvent devenir lourds à maintenir quand vos champs texte changent constamment.
  4. L’histoire du fork d’Elasticsearch compte opérationnellement : OpenSearch existe à cause de changements de licence ; beaucoup d’équipes ont hérité d’habitudes opérationnelles Elasticsearch et les ont appliquées sans changement — parfois correctement, parfois désastreusement.
  5. Les moteurs de recherche traitent les mises à jour comme des opérations de « reindex » en interne : une « mise à jour de document » est souvent une suppression + ajout. Voilà pourquoi des petites mises à jour fréquentes créent du churn de segments et une pression de fusion.
  6. BM25 est devenu l’outil de pertinence par défaut pour de nombreuses piles modernes ; les fonctions de ranking de PostgreSQL sont utiles mais moins flexibles pour les workflows de tuning en production.
  7. La recherche vectorielle est devenue courante rapidement : pgvector et le kNN dans OpenSearch sont apparus parce que la « recherche par mots-clés » n’était plus suffisante une fois que les embeddings sont devenus peu coûteux.
  8. Les shards distribués sont une taxe, pas seulement une fonctionnalité : le scaling OpenSearch via des shards vous donne du débit, mais aussi des modes de panne (shards chauds, skew, tempêtes de relocation) qu’une instance Postgres unique n’invente pas.

Ce en quoi chaque système excelle (et échoue)

Recherche PostgreSQL : ce qu’elle maîtrise

  • Correction transactionnelle : les résultats de recherche reflètent les données commit quand vous interrogez la même base. Pas de décalage de synchronisation sauf si vous l’ajoutez.
  • Déploiement simple : un moteur, une chaîne de sauvegarde, un jeu de permissions.
  • Filtrage avec jointures : quand la « recherche » est surtout des filtres structurés avec un peu de correspondance textuelle, Postgres est souvent plus rapide et plus simple.
  • Audit et conformité : récupération point-in-time, archivage WAL, et contrôle d’accès strict sont des outils du quotidien.

Recherche PostgreSQL : où ça fait mal

  • Le tuning de pertinence devient maladroit : on peut faire du ranking, des poids, des dictionnaires, des synonymes, mais le workflow est moins ergonomique que des outils dédiés.
  • Facettes et agrégations à grande échelle : oui, SQL peut agréger ; non, vous n’aimerez peut-être pas la latence sous forte cardinalité et de nombreuses requêtes concurrentes.
  • Récupération hybride (mot-clé + vecteur + rerank) : c’est possible, mais vos plans de requête deviennent des machines de Rube Goldberg.
  • Amplification des écritures : maintenance GIN plus mises à jour fréquentes plus réalités de vacuum peuvent devenir votre plafond de performance.

OpenSearch : ce qu’il maîtrise

  • Récupération full-text rapide : les index inversés sont conçus pour ça.
  • Facettes / agrégations : agrégations de termes, buckets de plage, et requêtes de type analytics sont des citoyens de première classe.
  • Outils de pertinence : analyseurs, synonymes, boosting, function scores et explicabilité sont la raison d’être du produit.
  • Scale horizontal : vous pouvez ajouter des nœuds et redistribuer les shards. Ce n’est pas gratuit, mais c’est possible.
  • Patrons vectoriels et hybrides : la recherche kNN et la récupération hybride sont désormais des patterns opérationnels courants.

OpenSearch : où ça fait mal

  • Complexité opérationnelle : tuning JVM, dimensionnement des shards, merge de segments, circuit breakers, pression sur le heap, coordination du cluster.
  • Cohérence éventuelle : intervalles de refresh, pipelines d’ingestion et retries signifient que le « maintenant » est un concept flou.
  • La réindexation est un mode de vie : les mappings changent, les analyseurs changent, les synonymes changent. Vous réindexerez. Prévoyez-le.

Blague n°1 : Un cluster de recherche, c’est comme des lapins domestiques — silencieux, mignons, et soudain vous en avez 400 et plus personne ne se rappelle comment s’en occuper.

Vérification réalité du moteur de stockage

PostgreSQL stocke des lignes. OpenSearch stocke des documents. Ça ressemble à du marketing, mais ça compte :

  • Mises à jour partielles sont bon marché en Postgres et souvent coûteuses en OpenSearch (update = reindex document).
  • Grands champs de texte peuvent gonfler TOAST de Postgres et augmenter les I/O ; dans OpenSearch ils augmentent la taille des segments et le coût des merges.
  • Compression et comportement de merge dans OpenSearch sont un processus de fond constant. Dans Postgres, vacuum et autovacuum sont vos processus de fond. Aucun n’est optionnel.

Architectures de recherche hybride qui ne vous pénalisent pas après coup

Pattern A : PostgreSQL est la vérité, OpenSearch est l’index optimisé en lecture

Classique. Vous écrivez dans Postgres. Vous streammez les changements vers OpenSearch. L’application interroge OpenSearch pour les IDs et le classement, puis récupère les lignes canoniques dans Postgres.
C’est ennuyeux. L’ennui scale.

  • Avantages : la correction vit en un seul endroit ; les pannes de recherche dégradent en douceur (« résultats obsolètes » ou « recherche limitée »).
  • Inconvénients : vous devez opérer un pipeline de sync et gérer des backfills.

Pattern B : Double lecture avec bascule par feature flag

Lors d’une migration du FTS de Postgres vers OpenSearch, vous faites tourner les deux en parallèle. Vous comparez résultats et latence.
Vous déployez par tenant, par fonctionnalité, ou par type de requête.

  • Avantages : migrations plus sûres ; permet l’A/B testing de pertinence.
  • Inconvénients : double coût de requête ; nécessite un cache et des limites de taux soignés.

Pattern C : Postgres pour les filtres + OpenSearch pour la récupération, puis rerank

Quand les contraintes structurées sont complexes (permissions, droits, fenêtres temporelles), Postgres peut calculer l’ensemble de candidats ou les contraintes.
OpenSearch récupère les candidats ; un reranker (éventuellement ML) réordonne.

  • Avantages : forte correction sur le contrôle d’accès ; garde le « qui voit quoi » en SQL, où les auditeurs vivent.
  • Inconvénients : peut devenir coûteux si les ensembles de candidats sont énormes ; nécessite une pagination soigneuse.

Stratégies de synchronisation : choisissez-en une et rendez-la ennuyeuse

La plupart des douleurs viennent du « on les gardera juste synchrones » sans précision. Choisissez un mécanisme réel :

  • Outbox transactionnelle : écrivez une ligne dans une table outbox dans la même transaction ; un worker publie vers OpenSearch. Haute correction, relecture simple.
  • Réplication logique / CDC : streamez les changements WAL et transformez-les en documents. Puissant, mais vous opérez alors un pipeline de données.
  • Reconstruction par lot nightly : acceptable pour une « recherche de catalogue » dans certains métiers ; inacceptable pour la sécurité/alertes/recherche sensible au temps.

La règle d’or : concevez pour la relecture. Si vous ne pouvez pas rembobiner et reconstruire l’index OpenSearch de façon déterministe, vous n’avez pas un système — vous avez une vibe.

Réalités opérationnelles : SLO, modes de panne et douleur du on-call

Définissez des SLO qui correspondent à l’architecture

Un système hybride a au moins trois buckets de latence :

  • Lag d’ingestion : temps entre le commit Postgres et la recherche disponible dans OpenSearch.
  • Latence de requête : P50/P95/P99 pour les requêtes de recherche, plus le temps pour hydrater les résultats depuis Postgres.
  • Correction de fraîcheur : à quelle fréquence les utilisateurs voient des résultats obsolètes ou manquants pour des données nouvellement écrites.

Modes de panne OpenSearch à anticiper

  • Pression sur le heap provoque des storms GC, qui ressemblent à des pics de latence aléatoires jusqu’à ce que le cluster passe en jaune/rouge.
  • Shards chauds dus à un routage skewé font fondre un nœud pendant que les autres sont inactifs.
  • Pression de merge lors de forts taux de mise à jour provoque des pics IO et une inflation de la latence de requête.
  • Explosion des mappings (champs dynamiques) gonfle l’état du cluster et casse tout de façon très démocratique.

Modes de panne PostgreSQL à anticiper

  • Dette d’autovacuum menant à tables/index gonflés, requêtes lentes et finalement risques de wraparound des transaction IDs.
  • Gonflement GIN surtout pour des documents mis à jour fréquemment ; l’index grandit plus vite que votre patience.
  • Mauvais plans de requête dus à des statistiques obsolètes ou des problèmes du type parameter sniffing (plans génériques vs personnalisés).
  • Surprises de verrouillage issues de changements de schéma ou de mises à jour concurrentes lourdes.

Blague n°2 : La façon la plus rapide d’améliorer la latence de recherche est de renommer le tableau de bord de « P95 » en « P50 » et d’aller déjeuner.

Ce qu’il faut éviter comme un pager à 3 h du matin

  • Laisser OpenSearch devenir le système de référence pour tout ce dont la conformité se soucie.
  • Mappings dynamiques en production sauf si vous aimez découvrir des conflits « string vs number » en plein incident.
  • Réindexer sans marge de capacité. La réindexation n’est pas un hobby de fond ; c’est un test de charge que vous vous infligez.
  • Filtrage des permissions uniquement dans OpenSearch si votre modèle d’autorisation est complexe. Gardez les droits dans Postgres et traitez la recherche comme un moteur de suggestion.

Playbook de diagnostic rapide

Votre recherche est lente ou incorrecte. N’engagez pas une discussion architecturale dans Slack. Vérifiez le goulet d’étranglement en quelques minutes.

Premier point : est-ce un lag d’ingestion ou de la douleur au temps de requête ?

  • Symptôme : les utilisateurs ne trouvent pas les nouveaux éléments, mais les anciens fonctionnent → probablement un lag d’ingestion.
  • Symptôme : tout se recherche, mais c’est lent/peu fiable → probablement un problème au temps de requête (OpenSearch/PG) ou un goulot d’hydratation.

Deuxième point : isolez quel saut échoue

  1. App → OpenSearch : mesurez la latence brute des appels de recherche et le taux d’erreur.
  2. OpenSearch interne : CPU, heap, GC, pools de threads, rejets de queue, merges de segments.
  3. Hydratation → Postgres : SQL lent, saturation du pool de connexions, cache d’index manquant, verrouillage, I/O.
  4. Pipeline de sync : lag du consumer, files d’erreurs (dead-letter), retries, erreurs de requêtes bulk.

Troisième point : choisissez la mitigation la plus rapide et sûre

  • Lag d’ingestion : mettez en pause les mises à jour non critiques ; augmentez la taille des bulk avec prudence ; ajoutez des consumers au pipeline ; augmentez temporairement l’intervalle de refresh.
  • Surcharge de requêtes OpenSearch : réduisez la complexité des requêtes (moins d’agrégations), ajoutez du caching, ajustez le nombre de shards seulement avec un plan, ou routez temporairement des tenants lourds ailleurs.
  • Surcharge Postgres : limitez le fanout d’hydratation ; récupérez les IDs par lots ; ajoutez des index manquants ; augmentez le pool de connexions seulement si la DB peut le supporter.

Tâches pratiques : commandes, sorties et décisions

Ce sont des tâches réelles que vous pouvez exécuter lors du déploiement ou d’un incident. Chaque point inclut ce que la sortie signifie et quelle décision prendre ensuite.

1) Vérifier la santé du cluster OpenSearch et ce que « yellow » signifie réellement

cr0x@server:~$ curl -s localhost:9200/_cluster/health?pretty
{
  "cluster_name" : "search-prod",
  "status" : "yellow",
  "number_of_nodes" : 3,
  "active_primary_shards" : 48,
  "active_shards" : 48,
  "unassigned_shards" : 48
}

Signification : les primaires sont allouées, les réplicas ne le sont pas. Les requêtes peuvent fonctionner, mais vous n’avez aucune redondance et un risque lors du rééquilibrage.
Décision : ne lancez pas une réindexation ; corrigez d’abord l’allocation (disponibilité des nœuds, seuils de disque, limites de shards) ou réduisez temporairement le nombre de réplicas.

2) Trouver les raisons des shards non assignés avant de deviner

cr0x@server:~$ curl -s localhost:9200/_cat/shards?v
index             shard prirep state      docs store ip         node
products_v12      0     p      STARTED   9812  42mb  10.0.0.12  os-1
products_v12      0     r      UNASSIGNED

Signification : le shard réplique ne peut pas être placé.
Décision : inspectez allocation explain ; si c’est un watermark disque, libérez de l’espace ou ajoutez des nœuds ; si c’est la limite de shards, réduisez les shards ou augmentez les limites délibérément.

3) Expliquer l’allocation des shards avec les propres mots du cluster

cr0x@server:~$ curl -s -H 'Content-Type: application/json' localhost:9200/_cluster/allocation/explain?pretty -d '{"index":"products_v12","shard":0,"primary":false}'
{
  "index" : "products_v12",
  "shard" : 0,
  "primary" : false,
  "current_state" : "unassigned",
  "can_allocate" : "no",
  "allocate_explanation" : "cannot allocate because allocation is not permitted to any of the nodes",
  "node_allocation_decisions" : [
    {
      "node_name" : "os-2",
      "node_decision" : "no",
      "deciders" : [
        {
          "decider" : "disk_threshold",
          "decision" : "NO",
          "explanation" : "the node is above the high watermark"
        }
      ]
    }
  ]
}

Signification : les seuils de disque bloquent l’allocation des réplicas.
Décision : libérez de l’espace disque, ajoutez du stockage, ou déplacez des shards ; ne « désactivez » pas simplement les watermarks sauf si vous aimez les indices en lecture seule subitement.

4) Vérifier la pression des ressources d’un nœud OpenSearch depuis l’extérieur (Linux)

cr0x@server:~$ ps -o pid,cmd,%cpu,%mem --sort=-%mem | head
  PID CMD                         %CPU %MEM
 2178 /usr/share/opensearch/jdk   312.0 48.6
 1042 /usr/bin/node app-server     64.2  3.1

Signification : le JVM d’OpenSearch consomme beaucoup de mémoire et du CPU.
Décision : vérifiez heap vs RSS ; si RSS est énorme, vous pourriez swapper ou le page cache est sous pression. Étape suivante : vérifier GC et utilisation du heap.

5) Inspecter l’utilisation du heap JVM via nodes stats

cr0x@server:~$ curl -s localhost:9200/_nodes/stats/jvm?pretty | head -n 25
{
  "nodes" : {
    "n1" : {
      "jvm" : {
        "mem" : {
          "heap_used_in_bytes" : 15234567890,
          "heap_max_in_bytes" : 17179869184
        },
        "gc" : {
          "collectors" : {
            "old" : {
              "collection_count" : 92,
              "collection_time_in_millis" : 184322
            }
          }
        }
      }
    }
  }
}

Signification : le heap est utilisé à ~88% avec un temps GC significatif sur la génération old.
Décision : réduisez la charge de requêtes/agrégations, vérifiez l’utilisation de fielddata, et envisagez le tuning du heap seulement après avoir confirmé que ce n’est pas un problème de mapping/agrégation.

6) Détecter les rejets de thread pool (un tueur silencieux)

cr0x@server:~$ curl -s localhost:9200/_nodes/stats/thread_pool?pretty | grep -A3 rejected | head
          "search" : {
            "threads" : 13,
            "queue" : 1000,
            "rejected" : 421

Signification : les requêtes de recherche sont rejetées parce que la queue est pleine.
Décision : ralentissez les clients (circuit breaker), réduisez le fanout, baissez les agrégations coûteuses, ou ajoutez de la capacité. N’augmentez pas seulement la taille de la queue en l’appelant « correction ».

7) Vérifier la pression segments/merge (pourquoi les mises à jour ressemblent au papier de verre)

cr0x@server:~$ curl -s localhost:9200/_nodes/stats/indices/segments,merge?pretty | head -n 35
{
  "nodes" : {
    "n1" : {
      "indices" : {
        "segments" : {
          "count" : 2345,
          "memory_in_bytes" : 987654321
        },
        "merge" : {
          "current" : 12,
          "current_docs" : 1900000,
          "total_throttled_time_in_millis" : 842112
        }
      }
    }
  }
}

Signification : de nombreux segments et merges, avec du temps de throttling qui s’accumule.
Décision : ajustez l’intervalle de refresh, la stratégie de bulk ingest, et les patterns de mise à jour. Envisagez d’extraire les champs qui changent souvent du document ou d’utiliser la dénormalisation plus prudemment.

8) Vérifier les settings d’index qui affectent la fraîcheur et le coût d’ingestion

cr0x@server:~$ curl -s localhost:9200/products_v12/_settings?pretty | head -n 40
{
  "products_v12" : {
    "settings" : {
      "index" : {
        "refresh_interval" : "1s",
        "number_of_shards" : "12",
        "number_of_replicas" : "1"
      }
    }
  }
}

Signification : un intervalle de refresh à 1s est excellent pour la fraîcheur, coûteux pour l’ingestion.
Décision : si l’ingestion accuse du retard, augmentez temporairement refresh_interval (par ex. 10s–30s) et communiquez les attentes de fraîcheur ; revenez en arrière après résorption du backlog.

9) Vérifier rapidement le bloat et la santé du vacuum PostgreSQL

cr0x@server:~$ psql -d app -c "SELECT relname,n_live_tup,n_dead_tup,round(100.0*n_dead_tup/nullif(n_live_tup+n_dead_tup,0),2) AS dead_pct FROM pg_stat_user_tables ORDER BY n_dead_tup DESC LIMIT 5;"
  relname   | n_live_tup | n_dead_tup | dead_pct
------------+------------+------------+---------
products    |   12400123 |    3120099 |   20.11
events      |    9021123 |    2019921 |   18.29

Signification : les tuples morts sont élevés ; le vacuum peut être en retard.
Décision : vérifiez les paramètres d’autovacuum, les transactions longues, et l’I/O. Envisagez un VACUUM manuel (pas FULL) pendant une fenêtre d’activité réduite si vous êtes en difficulté.

10) Confirmer si Postgres n’a pas l’index que vous pensez avoir

cr0x@server:~$ psql -d app -c "\d+ products"
                                                  Table "public.products"
   Column   |           Type           | Collation | Nullable | Default | Storage  | Stats target | Description
------------+--------------------------+-----------+----------+---------+----------+--------------+-------------
 id         | bigint                   |           | not null |         | plain    |              |
 title      | text                     |           | not null |         | extended |              |
 body       | text                     |           |          |         | extended |              |
 search_tsv | tsvector                 |           |          |         | extended |              |
Indexes:
    "products_pkey" PRIMARY KEY, btree (id)

Signification : pas d’index GIN sur search_tsv.
Décision : ajoutez le bon index avant de blâmer le hardware. Vérifiez aussi comment search_tsv est maintenu (trigger vs colonne générée vs batch).

11) Expliquer correctement une requête de recherche Postgres lente

cr0x@server:~$ psql -d app -c "EXPLAIN (ANALYZE,BUFFERS) SELECT id FROM products WHERE search_tsv @@ plainto_tsquery('english','wireless headphones') ORDER BY ts_rank(search_tsv, plainto_tsquery('english','wireless headphones')) DESC LIMIT 20;"
                                                         QUERY PLAN
----------------------------------------------------------------------------------------------------------------------------
 Limit  (cost=0.43..152.33 rows=20 width=8) (actual time=812.221..812.260 rows=20 loops=1)
   Buffers: shared hit=120 read=9812
   ->  Index Scan using products_search_tsv_gin on products  (cost=0.43..9821.33 rows=1290 width=8) (actual time=812.219..812.248 rows=20 loops=1)
         Index Cond: (search_tsv @@ plainto_tsquery('english'::regconfig, 'wireless headphones'::text))
 Planning Time: 2.012 ms
 Execution Time: 812.312 ms

Signification : de nombreuses lectures disque (read=9812) dominent ; l’index existe mais les données ne sont pas en cache.
Décision : envisagez de « chauffer » le cache pour les requêtes chaudes, ajoutez de la RAM, réduisez le working set, ou déplacez cette charge sur OpenSearch si la concurrence doit croître.

12) Surveiller les requêtes lentes Postgres en temps réel

cr0x@server:~$ psql -d app -c "SELECT pid,now()-query_start AS age,wait_event_type,wait_event,substr(query,1,80) AS q FROM pg_stat_activity WHERE state='active' ORDER BY age DESC LIMIT 8;"
 pid  |   age   | wait_event_type |  wait_event   |                                        q
------+---------+-----------------+---------------+--------------------------------------------------------------------------------
 8123 | 00:01:12| IO              | DataFileRead  | SELECT id FROM products WHERE search_tsv @@ plainto_tsquery('english','wirel...
 8344 | 00:00:44| Lock            | tuple         | UPDATE products SET body = $1 WHERE id = $2

Signification : attentes I/O et verrous au niveau ligne sont présents.
Décision : séparez les hotspots lecture/écriture ; envisagez de déplacer les écritures lourdes hors des tables soutenant la recherche, ou découplez l’indexation du chemin OLTP.

13) Valider le lag de réplication (si l’hydratation lit une réplique)

cr0x@server:~$ psql -d app -c "SELECT application_name,state,write_lag,flush_lag,replay_lag FROM pg_stat_replication;"
 application_name |   state   | write_lag | flush_lag | replay_lag
------------------+-----------+-----------+-----------+-----------
 app-read-replica  | streaming | 00:00:00  | 00:00:01  | 00:00:08

Signification : le replay lag de la réplique est de 8 secondes.
Décision : si les utilisateurs se plaignent de « éléments manquants après création », vérifiez si l’hydratation lit des réplicas. Routez les lectures de fraîcheur critique vers le primaire ou acceptez explicitement la staleness.

14) Vérifier le backlog du pipeline de sync (table outbox)

cr0x@server:~$ psql -d app -c "SELECT count(*) AS pending, min(created_at) AS oldest FROM search_outbox WHERE processed_at IS NULL;"
 pending |         oldest
---------+------------------------
  184221 | 2026-02-04 08:11:03+00

Signification : vous avez un large backlog et il est ancien.
Décision : mettez en pause les producteurs non essentiels, scalez les consumers, et vérifiez les erreurs bulk d’OpenSearch. Si le backlog augmente en charge normale, vous avez dimensionné le pipeline sur l’optimisme, pas la réalité.

15) Vérifier les erreurs d’ingestion bulk OpenSearch (ne faites pas confiance au « 200 OK »)

cr0x@server:~$ curl -s -H 'Content-Type: application/json' localhost:9200/_bulk -d $'{"index":{"_index":"products_v12","_id":"42"}}\n{"title":"x","price":"not-a-number"}\n' | head
{"took":7,"errors":true,"items":[{"index":{"_index":"products_v12","_id":"42","status":400,"error":{"type":"mapper_parsing_exception","reason":"failed to parse field [price] of type [float]"}}}]}

Signification : l’API bulk a accepté la requête mais a échoué sur un item à cause d’un conflit de mapping.
Décision : envoyez les items échoués vers une voie dead-letter ; corrigez le mapping et réindexez. Si vous ignorez cela, votre index deviendra silencieusement incomplet.

16) Confirmer les mappings de champs OpenSearch pour éviter les « surprises de mapping dynamique »

cr0x@server:~$ curl -s localhost:9200/products_v12/_mapping?pretty | head -n 40
{
  "products_v12" : {
    "mappings" : {
      "properties" : {
        "title" : { "type" : "text" },
        "price" : { "type" : "float" }
      }
    }
  }
}

Signification : price est un float ; l’ingestion doit le respecter.
Décision : verrouillez les mappings et validez les données côté producteur. « Laissez OpenSearch deviner » marche jusqu’à ce que ça ne marche plus, et ça ne casse jamais politiquement.

Mini-récits du monde de l’entreprise (anonymisés)

Mini-récit 1 : L’incident causé par une mauvaise hypothèse

Une entreprise B2B de taille moyenne a déplacé la « recherche globale » du FTS PostgreSQL vers OpenSearch pour supporter les facettes et des requêtes préfixes plus rapides.
Ils ont fait la migration comme on le fait souvent : nouveau service, nouvel index, pipeline CDC, et un tableau de bord triomphant montrant la latence en baisse.

Puis les tickets de support sont arrivés : « J’ai créé un enregistrement et la recherche ne le trouve pas. » Le manager technique a supposé une erreur utilisateur ou un cache.
L’équipe a doublé la fréquence de refresh d’OpenSearch à 500ms et a considéré le problème réglé. Les tickets ont continué.

La mauvaise hypothèse était subtile : ils pensaient que le pipeline CDC était « quasi temps réel » parce qu’il traitait les messages rapidement quand la queue était vide.
Pendant les heures de pointe, le consumer outbox accusait un retard de plusieurs minutes. Les intervalles de refresh ne corrigent pas les documents manquants qui n’ont pas encore été indexés.

La correction n’était pas sophistiquée. Ils ont ajouté du monitoring de lag, scalé les consumers, et rendu l’UI honnête : « Les nouveaux éléments peuvent mettre jusqu’à N minutes à apparaître. »
Pour une poignée de workflows où la fraîcheur importait, l’application interrogeait directement PostgreSQL pendant une courte fenêtre « juste créé ».

La leçon : l’intervalle de refresh n’est pas une garantie de cohérence. C’est un contrôle de visibilité après l’ingestion. Votre pipeline est maintenant la frontière de cohérence.

Mini-récit 2 : L’optimisation qui s’est retournée contre eux

Une autre organisation avait un marketplace multi-tenant. La recherche « allait bien » jusqu’à ce qu’ils ajoutent la recherche sémantique avec embeddings.
Quelqu’un a proposé une optimisation : tout mettre dans un unique index OpenSearch avec mapping dynamique agressif, et router par tenant ID pour la localité.
Ça fonctionnait parfaitement en staging. Staging est un endroit magique où les données ne se disputent jamais.

En production, le comportement des tenants a divergé. Un tenant a envoyé des documents avec un objet « metadata » contenant des centaines de clés variant par enregistrement.
Le mapping dynamique a créé des champs comme si on payait au champ. L’état du cluster a gonflé, l’utilisation du heap a grimpé, et les redémarrages en rolling ont commencé à échouer parce que les nœuds mettaient trop de temps à rejoindre.

Ils ont essayé d’« optimiser » en augmentant le heap. Cela a acheté du temps, puis accru les pauses GC.
Pendant ce temps, le débogage de la pertinence est devenu impossible car les besoins d’analyse de différents tenants entraient en collision dans un même mapping.

La correction finale : passer à un schéma contrôlé. Aplatir les metadata dans un seul champ keyword quand nécessaire, et whitelist explicitement les clés indexables.
Certains tenants à fort volume ont obtenu des indices dédiés avec des politiques de cycle de vie séparées. Le système est devenu un peu moins élégant et beaucoup plus fiable.

La leçon : mapping dynamique + variabilité multi-tenant est un bug de scalabilité, pas une fonctionnalité. Vous payez la flexibilité par la stabilité du cluster.

Mini-récit 3 : La pratique ennuyeuse qui a sauvé la mise

Une entreprise liée aux paiements utilisait PostgreSQL comme vérité et OpenSearch pour la recherche. Ils avaient une habitude qui semblait excessive : chaque version d’index était immuable.
products_v10, products_v11, products_v12. L’application interrogeait un alias appelé products_current.

Un développeur devait ajouter des synonymes et changer des analyseurs pour améliorer la pertinence. Cela requiert la réindexation.
Au lieu de « mettre à jour l’index en place », ils ont construit products_v13 en parallèle, validé les comptes de documents, exécuté des comparaisons de requêtes, puis basculé atomiquement l’alias.

Lors du déploiement, ils ont découvert un bug d’ingestion : le champ price arrivait parfois sous la forme « N/A ». Le nouvel index rejetait ces documents. L’ancien index servait toujours le trafic.
Ils ont corrigé le producteur, rejoué les entrées outbox échouées, reconstruit v13, et basculé l’alias à nouveau. Les utilisateurs ont à peine remarqué autre chose qu’une pertinence qui s’améliorait progressivement.

La leçon : versions d’index immuables + cutover par alias ne sont pas glamour, mais elles transforment une réindexation en routine plutôt qu’en crise.

Erreurs courantes : symptômes → cause racine → correction

1) « La recherche est lente seulement quand on ajoute des agrégations »

  • Symptômes : P95 explose sur les requêtes à facettes ; CPU monte ; rejets du thread pool recherche OpenSearch.
  • Cause racine : agrégations terms haute cardinalité sur des champs non analysés, trop de shards, ou heap insuffisant pour les structures d’agrégation.
  • Correction : réduisez la cardinalité des agrégations, utilisez des champs keyword, ajoutez du contexte filter, précalculez les facettes, ou séparez les indices par tenant/catégorie. Validez avec les stats des thread pools et le heap.

2) « Les nouveaux enregistrements n’apparaissent pas, mais OpenSearch est vert »

  • Symptômes : données fraîches manquantes ; santé du cluster OK ; pas d’erreurs de requête.
  • Cause racine : lag d’ingestion, erreurs bulk, accumulation de dead-letter, ou intervalle de refresh trop élevé pour les attentes.
  • Correction : monitorer le lag outbox/CDC, inspecter les réponses bulk pour erreurs, implémenter la relecture. Définir et communiquer des SLO de fraîcheur.

3) « Postgres FTS était rapide, puis il a ralenti au fil des mois »

  • Symptômes : latence qui augmente ; lectures I/O élevées ; tables et index GIN qui grandissent disproportionnellement.
  • Cause racine : bloat dû aux mises à jour/suppressions + dette d’autovacuum ; croissance de la pending list GIN ; transactions longues bloquant le vacuum.
  • Correction : tuner autovacuum, scinder les transactions longues, envisager REINDEX ou reconstruire l’index GIN pendant une maintenance, et maintenir efficacement search_tsv.

4) « Le redémarrage d’un nœud OpenSearch provoque une cascade de timeouts »

  • Symptômes : restart en rolling cause jaune/rouge ; requêtes timeout ; relocations de shards saturent réseau/disque.
  • Cause racine : trop grand nombre de shards, marge disque insuffisante, throttles de relocation mal configurés, ou réplicas indisponibles.
  • Correction : réduire le nombre de shards (shards plus gros, moins nombreux), assurer une marge disque, orchestrer les redémarrages en étapes, et maintenir des réplicas sains.

5) « On a réglé la pertinence et maintenant les résultats sont faux »

  • Symptômes : correspondances exactes enterrées ; termes de marque ignorés ; changements de classement imprévisibles.
  • Cause racine : changements d’analyseur sans réindexation, synonymes appliqués au mauvais moment, mismatches de mapping multi-field.
  • Correction : versionnez les analyseurs et les indices, testez avec explain, maintenez des requêtes « golden », et déployez avec des flips d’alias et des runs de comparaison.

6) « Notre requête hybride duplique ou manque des résultats lors de la pagination »

  • Symptômes : utilisateurs voient des éléments répétés à la page 2 ; éléments manquants ; ordre incohérent.
  • Cause racine : clés de tri instables entre OpenSearch et l’hydratation Postgres, utilisation de la pagination par offset sous écritures concurrentes.
  • Correction : utilisez search_after ou la pagination par curseur dans OpenSearch ; hydratez par IDs stables ; maintenez un tri secondaire déterministe (par ex. _score + id).

Listes de contrôle / plan pas-à-pas

Pas-à-pas : choisir Postgres-only vs OpenSearch vs hybride

  1. Notez les types de requête : mot-clé, phrase, préfixe, flou, facettes, geo, vecteur, classement hybride.
  2. Notez les exigences de fraîcheur : « secondes », « minutes », « horaire ». Soyez explicite.
  3. Définissez le comportement en cas de panne : que se passe-t-il si la recherche est down ou obsolète ?
  4. Estimez le churn des données : les documents sont-ils souvent mis à jour ou surtout append-only ?
  5. Prototypiez la requête la plus difficile dans les deux systèmes et mesurez le P95 sous concurrence, pas seulement un timing utilisateur unique.
  6. Choisissez l’architecture la plus simple qui respecte les SLO. Pas la plus cool.

Pas-à-pas : construire un pipeline hybride sûr

  1. Gardez Postgres comme vérité : ne laissez pas OpenSearch devenir la seule copie des champs critiques.
  2. Implémentez une table outbox avec un curseur durable et support de relecture.
  3. Rendez l’indexation idempotente : ID de document dérivé de la clé primaire ; les mises à jour écrasent.
  4. Utilisez l’indexation bulk avec backoff et un mécanisme dead-letter.
  5. Versionnez les indices et utilisez des alias pour le cutover.
  6. Monitorer le lag : âge du backlog outbox, taux d’erreur bulk, rejets de thread pool OpenSearch, lag de réplication Postgres si pertinent.
  7. Faites un load test ingestion + requête ensemble : le cluster qui indexe bien peut très mal répondre quand les merges démarrent.

Pas-à-pas : réindexer sans drame

  1. Créer une nouvelle version d’index avec mappings et settings explicites.
  2. Backfiller depuis Postgres par batches contrôlés.
  3. Double-écrire les nouveaux changements (ou rejouer l’outbox) dans l’ancien et le nouvel index.
  4. Valider les comptes de documents, des requêtes échantillons, et les logs d’erreurs bulk.
  5. Basculer l’alias atomiquement vers le nouvel index.
  6. Conserver l’ancien index pour rollback jusqu’à ce que la confiance soit acquise ; puis supprimer.

FAQ

1) PostgreSQL peut-il fournir une recherche « suffisamment bonne » pour un catalogue produit ?

Oui, si vos besoins sont surtout une recherche approximativement exacte par mot-clé plus des filtres, et si votre scale/churn sont raisonnables.
Quand vous avez besoin de nombreuses facettes, de correspondance floue, de synonymes à grande échelle, ou d’un classement hybride vecteur+mot-clé, OpenSearch devient le choix sensé.

2) Si j’ajoute pgvector, ai-je encore besoin d’OpenSearch ?

Parfois non. pgvector est excellent quand vous voulez les embeddings proches de vos données transactionnelles et pouvez tolérer le coût de requête.
Mais OpenSearch a tendance à gagner quand vous avez besoin de pertinence combinée, d’agrégations, et de performance de récupération sous forte concurrence.

3) Pourquoi ne pas tout stocker dans OpenSearch et supprimer Postgres ?

Parce que vous réinventeriez les garanties transactionnelles avec du scotch. OpenSearch n’est pas une base OLTP.
Gardez la vérité dans Postgres (ou une autre base) sauf si le domaine tolère vraiment la cohérence éventuelle pour tout ce qui vous importe.

4) Quel est le plus grand piège opérationnel avec OpenSearch ?

L’explosion des shards et des mappings non contrôlés. Trop de shards créent une surcharge de coordination ; les champs dynamiques gonflent l’état du cluster et la mémoire.
Les deux échouent lentement, puis tous en même temps.

5) Quel est le plus grand piège opérationnel avec une recherche uniquement Postgres ?

La gestion du vacuum et du bloat, surtout avec des index GIN sur du texte fréquemment mis à jour.
Postgres peut le faire, mais vous devez traiter le vacuum comme une préoccupation de production de première classe, pas comme une fée qui s’en occupe.

6) Comment rendre la recherche hybride suffisamment cohérente pour les utilisateurs ?

Définissez des SLO de fraîcheur, mesurez le lag du pipeline, et fournissez des fallback gracieux.
Pour les workflows nécessitant une visibilité immédiate, interrogez Postgres directement pour les enregistrements récemment créés ou affichez un état « en cours d’indexation ».

7) Comment paginer dans un système hybride ?

Privilégiez la pagination de type curseur d’OpenSearch (search_after) avec un tri stable, puis hydratez ces IDs dans Postgres via une seule requête par lot.
Évitez la pagination par offset sur des jeux de données changeants sauf si vous aimez les doublons.

8) Ai-je besoin d’un index OpenSearch séparé par tenant ?

Pas toujours. Des indices par tenant peuvent faire exploser le nombre de shards. Un index partagé avec routage peut fonctionner, mais seulement avec un contrôle strict du schéma.
Les tenants à fort volume ou à forte variabilité justifient souvent des indices dédiés.

9) Comment savoir si je dois augmenter refresh_interval d’OpenSearch ?

Si l’ingestion est à la traîne, les merges throttlent, et les utilisateurs peuvent tolérer de la staleness, augmentez-le temporairement.
Si la promesse produit est « recherche instantanée », l’augmenter transfère simplement la plainte d’ops vers le support.

10) Les résultats de recherche doivent-ils toujours être hydratés depuis Postgres ?

Souvent oui pour la correction et le contrôle d’accès, mais pas toujours pour la performance. Beaucoup de systèmes stockent assez de champs dénormalisés dans OpenSearch pour l’affichage.
La règle : si un champ doit être correct et à jour, hydratez-le ou vérifiez-le contre Postgres.

Prochaines étapes que vous pouvez faire cette semaine

  • Décidez la source de vérité : écrivez-la. Si ce n’est pas Postgres, soyez explicite sur ce que vous risquez.
  • Ajoutez des métriques de lag pour votre pipeline d’indexation (âge du backlog outbox, débit des consumers, taux d’erreur bulk).
  • Exécutez le playbook de diagnostic rapide une fois un jour calme, pas pendant un incident. Capturez des baselines.
  • Versionnez votre index et implémentez le cutover par alias si ce n’est pas fait. C’est la différence entre « changement » et « incident ».
  • Choisissez trois requêtes golden et suivez-les en CI ou via un job programmé avec latence et vérifications du premier résultat.
  • Réduisez les surprises : désactivez le mapping dynamique quand possible, mappez explicitement les champs, et validez les types de données côté producteur.

Si vous bloquez pour décider : commencez par Postgres-only si vous pouvez atteindre les objectifs de pertinence et de latence aujourd’hui. Passez à l’hybride quand vous pouvez nommer les patterns de requête spécifiques qui l’exigent.
N’adoptez pas OpenSearch juste parce que quelqu’un veut des synonymes. Adoptez-le parce que votre système a besoin d’un moteur de recherche, pas parce que vous voulez un autre cluster à surveiller.

← Précédent
Durcir Windows pour serveurs de lab à domicile : modifications minimales, gains maximaux
Suivant →
Meilleures cartes mères pour des groupes IOMMU propres : quoi vérifier avant d’acheter

Laisser un commentaire