Skip to content

Tech: accélère les recherches et diminue la charge en base en utilisant les indexes#12918

Merged
LeSim merged 2 commits into
mainfrom
fix_search
Apr 8, 2026
Merged

Tech: accélère les recherches et diminue la charge en base en utilisant les indexes#12918
LeSim merged 2 commits into
mainfrom
fix_search

Conversation

@LeSim

@LeSim LeSim commented Apr 2, 2026

Copy link
Copy Markdown
Member

TODO:

  • regarder les droits pour créer une fonction dans la base pour la prod

avec une console rails, j'ai pu faire en prod un CREATE FUNCTION

close #12754

TL;DR : l'introduction de la recherche non accentué empêche postgresql d'utiliser son index accentué. On corrige en supprimant l'ancien index et en en recréant 1 sans accent.

Pour tester on utilise DossierSearchService.dossier_by_full_text(Dossier.all, 'toto').explain(:analyze)

avant la migration on avait une execution en 9,5s :

                                                                                     QUERY PLAN
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
 Gather Merge  (cost=3185289.80..3210768.59 rows=218765 width=2010) (actual time=9577.311..9588.616 rows=0.00 loops=1)
   Workers Planned: 2
   Workers Launched: 2
   Buffers: shared hit=12863 read=600594
   ->  Sort  (cost=3184289.77..3184517.65 rows=91152 width=2010) (actual time=9557.032..9557.033 rows=0.00 loops=3)
         Sort Key: (COALESCE(ts_rank(to_tsvector('french'::regconfig, unaccent((search_terms)::text)), to_tsquery('french'::regconfig, unaccent('toto:*'::text))), '0'::real)) DESC
         Sort Method: quicksort  Memory: 25kB
         Buffers: shared hit=12863 read=600594
         Worker 0:  Sort Method: quicksort  Memory: 25kB
         Worker 1:  Sort Method: quicksort  Memory: 25kB
         ->  Parallel Seq Scan on dossiers  (cost=0.00..3017887.68 rows=91152 width=2010) (actual time=9556.950..9556.951 rows=0.00 loops=3)
               Filter: (to_tsvector('french'::regconfig, unaccent((search_terms)::text)) @@ to_tsquery('french'::regconfig, unaccent('toto:*'::text)))
               Rows Removed by Filter: 3646089
               Buffers: shared hit=12785 read=600594
 Planning Time: 0.164 ms
 JIT:
   Functions: 12
   Options: Inlining true, Optimization true, Expressions true, Deforming true
   Timing: Generation 4.582 ms (Deform 3.308 ms), Inlining 174.105 ms, Optimization 528.966 ms, Emission 352.267 ms, Total 1059.920 ms
 Execution Time: 9589.890 ms
(20 rows)

apres, execution en 0,045 ms:

                                                                                  QUERY PLAN
----------------------------------------------------------------------------------------------------------------------------------------------------
 Sort  (cost=1989.70..1989.70 rows=1 width=2054) (actual time=0.014..0.014 rows=0.00 loops=1)
   Sort Key: (COALESCE(ts_rank(to_tsvector('french'::regconfig, immutable_unaccent((search_terms)::text)), '''toto'':*'::tsquery), '0'::real)) DESC
   Sort Method: quicksort  Memory: 25kB
   Buffers: shared hit=3
   ->  Bitmap Heap Scan on dossiers  (cost=1984.67..1989.69 rows=1 width=2054) (actual time=0.011..0.011 rows=0.00 loops=1)
         Recheck Cond: (to_tsvector('french'::regconfig, immutable_unaccent((search_terms)::text)) @@ '''toto'':*'::tsquery)
         Buffers: shared hit=3
         ->  Bitmap Index Scan on index_dossiers_on_search_terms  (cost=0.00..1984.67 rows=1 width=0) (actual time=0.007..0.007 rows=0.00 loops=1)
               Index Cond: (to_tsvector('french'::regconfig, immutable_unaccent((search_terms)::text)) @@ '''toto'':*'::tsquery)
               Index Searches: 1
               Buffers: shared hit=3
 Planning:
   Buffers: shared hit=1
 Planning Time: 0.210 ms
 Execution Time: 0.045 ms
(15 rows)

Annexes

C'est quoi cette histoire d'immutabilité ?
unaccent() de PostgreSQL est marquée STABLE, pas IMMUTABLE.

  - IMMUTABLE = "pour les mêmes arguments, retourne toujours le même résultat, quoi qu'il arrive" (pas de dépendance au contenu de la DB, à la locale, etc.)
  - STABLE = "même résultat au sein d'une même requête, mais pourrait changer entre requêtes"

  PostgreSQL exige que les fonctions dans un index sur expression soient IMMUTABLE, car l'index est calculé une fois puis stocké sur disque — il faut garantir que la valeur ne changera jamais.

  unaccent() est marquée STABLE parce qu'elle dépend d'un dictionnaire (unaccent.rules) qui pourrait théoriquement être modifié. En pratique, ce dictionnaire ne change jamais.

  La solution classique est de créer un wrapper :

  CREATE FUNCTION immutable_unaccent(text) RETURNS text AS $$
    SELECT public.unaccent('public.unaccent', $1)
  $$ LANGUAGE sql IMMUTABLE;

  On déclare cette fonction IMMUTABLE nous-mêmes, ce qui "promet" à PostgreSQL que le résultat est stable. C'est un pattern très répandu et documenté (PostgreSQL wiki, Stack Overflow, etc.) — c'est
  safe tant qu'on ne modifie pas le dictionnaire unaccent.rules, ce que personne ne fait en pratique.

la reponse de Tom Lane : attention unaccent dépent de fichier externe unaccent.rules qui peut facilement être changé, la fonction unaccent ne peut être considéré comme immutable.

Une autre reponse qui explique comment le faire qd mm.

Si on demande a claude de regarder les modifications sur le fichier unaccent.rules on obtient :

  │ 2026-03-20 │ Update Unicode 17.0.0                            │
  │ 2024-07-05 │ Ajout de redirections de codepoints              │
  │ 2023-09-20 │ Support des caractères traduits entre guillemets │
  │ 2022-03-10 │ Re-update CLDR 39                                │
  │ 2021-04-08 │ Update CLDR 39                                   │
  │ 2020-04-24 │ Update Unicode 13.0.0 / CLDR 37                  │
  │ 2019-02-01 │ Ajout de caractères combinants                   │
  │ 2019-01-10 │ Update CLDR 34                                   │
  │ 2018-09-01 │ Ajout de caractères grecs                        │
  │ 2017-08-16 │ Ajout de lettres vietnamiennes                   │

  Donc le fichier est modifié régulièrement (environ 1 fois/an), principalement pour ajouter de nouveaux caractères Unicode. Mais ces changements sont :
  - des ajouts de nouveaux mappings (nouveaux caractères → ASCII), pas des modifications de mappings existants
  - livrés uniquement lors d'une mise à jour majeure de PostgreSQL

  En pratique, pour notre cas (recherche full-text en français), les caractères accentués français (é→e, è→e, ç→c...) n'ont pas changé depuis la création du fichier. Le wrapper immutable_unaccent reste
   donc safe.

Au pire du pire, si ca foire ... on peut faire un réindex.

C'est quoi ce fichier db_functions.rake ?

Arf, c'est con mais rails ne sait pas sérialiser les fonctions customs dans le schema.rb. Or le schema.rb est utilisé comme source de vérité pour reconstruire la db pour les tests, ou a chaque fois qu'on fait un schema.load.

Pour recréer qd mm cette fonction, on place un hook pour que db_functions:create_functions soit appelé à chaque fois qu'on load le schéma. Et dans cette fonction, on réapplique la partie de la migration qui crée la fonction qu'on veut.

L'autre solution aurait été de passé du schema.rb à une structure.sql aussi supporté par rails mais qui me semble moins lisible et peut nécessaire pour l'instant.

@LeSim
LeSim force-pushed the fix_search branch 8 times, most recently from 4c485d2 to b8a0864 Compare April 3, 2026 13:31
@tchak

tchak commented Apr 7, 2026

Copy link
Copy Markdown
Member

Par curiosité, la technique de "custom FTS configuration" semble sympa. Y a-t-il une raison pour laquelle tu as décidé de ne pas l'utiliser ou pourquoi ça n'a pas marché ?

@LeSim
LeSim force-pushed the fix_search branch 2 times, most recently from 22a7c24 to 3284f40 Compare April 7, 2026 13:58
@LeSim

LeSim commented Apr 7, 2026

Copy link
Copy Markdown
Member Author

@tchak bien vu, je ne connaissais pas, j'ai suivi ta recommendation

@LeSim
LeSim enabled auto-merge April 8, 2026 12:03
@LeSim
LeSim added this pull request to the merge queue Apr 8, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Apr 8, 2026
@LeSim
LeSim added this pull request to the merge queue Apr 8, 2026
Merged via the queue into main with commit b1f711b Apr 8, 2026
29 of 35 checks passed
@LeSim
LeSim deleted the fix_search branch April 8, 2026 12:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ETQ Tech, je veux m'assurer que les index de search sont utilisés

2 participants