Dans cet article, je vous propose une première astuce pour avoir de meilleures performances FTS avec Postgres.
Contrairement à ElasticSearch, Postgres n'optimise pas automatiquement votre schéma. La philosophie est que l'optimisation dépend de l'utilisation de la base : fréquence d'écriture, prédicats dans les requêtes, jointures, etc. À vous d'anticiper l'utilisation de votre base pour concevoir le schéma le plus performant.
Comme vu dans mon introduction à FTS dans
Postgres, il faut lemmatiser un texte dans un
tsvector
pour la recherche plein texte. Lemmatiser est coûteux en cycles CPU,
on doit pouvoir optimiser ça.
Reprenons notre exemple naïf:
$ docker run --detach --rm --publish 5432:5432 postgres:10-alpine
$ psql -h localhost -U postgres
psql (10.1)
Saisissez « help » pour l'aide.
postgres=# CREATE TABLE textes AS VALUES
('Chuck Norris a gagné la guerre du Golf, en 18 trous.'),
('Google, c''est le seul endroit où tu peux taper Chuck Norris...'),
('Chuck Norris mange ses oranges tout rond: Chuck Norris fait pas de quartier.');
SELECT 3
postgres=#
À première vue, on pourrait se dire : il suffit d'indexer
to_tsvector('french', column1)
. Malheureusement, c'est une fausse piste. Cette
solution stocke le choix de la configuration dans l'index. Par exemple, lorsque
nous ajouterons des citations en anglais, nous indexerons
to_tsvector('french', column1)
sur des textes anglais. Cela chargera l'index
pour rien.
Et que se passe-t-il si on change la configuration ? L'index est maintenu à
l'insertion ou à la mise-à-jour de la table, et non de la configuration. Il
faudrait tout réindexer en une fois avec
REINDEX
. Ça
ne donne pas envie.
En fait, on ne va pas utiliser l'index comme cache de to_tsvector()
. Une
meilleure solution est de mettre en cache le tsvector
dans une colonne dédiée.
postgres=# ALTER TABLE textes ADD COLUMN texte_vector tsvector;
ALTER TABLE
postgres=# UPDATE textes SET texte_vector = to_tsvector('french', column1) WHERE texte_vector IS NULL;
UPDATE 3
postgres=#
Notez que cette requête UPDATE
peut être réutilisée à la demande pour
réindexer la table. On pourra réindexer par tranche, sans arrêter le service.
On peut maintenant réécrire notre requête FTS en:
postgres=# SELECT column1 FROM textes WHERE plainto_tsquery('french', 'gagner') @@ texte_vector;
column1
------------------------------------------------------
Chuck Norris a gagné la guerre du Golf, en 18 trous.
(1 ligne)
postgres=#
Ok ça fonctionne sans surprise. Comparons ce que nous dit le planificateur pour les deux variantes:
postgres=# EXPLAIN SELECT column1 FROM textes WHERE plainto_tsquery('french', 'gagner') @@ to_tsvector('french', column1);
QUERY PLAN
------------------------------------------------------------------------------
Seq Scan on textes (cost=0.00..241.00 rows=4 width=32)
Filter: ('''gagn'''::tsquery @@ to_tsvector('french'::regconfig, column1))
(2 lignes)
postgres=# EXPLAIN SELECT column1 FROM textes WHERE plainto_tsquery('french', 'gagner') @@ texte_vector;
QUERY PLAN
--------------------------------------------------------
Seq Scan on textes (cost=0.00..21.00 rows=4 width=32)
Filter: ('''gagn'''::tsquery @@ texte_vector)
(2 lignes)
postgres=#
Le coût passe de 241 à 21... pas mal ! Un EXPLAIN
ne vaut pas un benchmark,
mais ça donne une idée du coût de to_tsvector
.
Conclusion
Cette colonne cache réduit sensiblement le coût d'une recherche dans la table sans mener la maintenance de la configuration dans une impasse.
À retenir donc, c'est que stocker un tsvector
dans une table est une bonne
idée. Même si à première vue on ne voudrait pas copier la donnée.
Deuxième intérêt, un index ne tracera que ce qui est réellement recherché. Nous
verrons plus tard comment indexer les tsvector
pour plus de performances.