Un moteur de recherche avec PostgreSQL
Pierre-Louis Le Portz
Développeur chez BAM
github: pleportz // twitter: @leportzp



- Un champ texte
- Un ou plusieurs mots clés recherchés
- Classement par pertinence


"tarte"
SELECT ...
recettes
recettes


Recherche textuelle
1. Moteur de recherche en PostgreSQL
2. Performance
3. Solutions alternatives
Approche naïve avec ILIKE
'Délicieuse tarte' ILIKE '%TARTE%'
Ce n'est pas robuste si l'utilisateur emploie :
- le pluriel
- une forme dérivée du mot
De plus c'est LENT !
'Tarte à la tomate' ILIKE '%tarte%'
'Délicieuse tarte' ILIKE '%tartes%'
'Tarte préparée avec amour' ILIKE '%amoureusement%'
SELECT * FROM recipe WHERE title ILIKE '%Tarte%';true
false
true
false
to_tsvector

to_tsquery

@@

ts_rank

Full-Text Search de PostgreSQL
to_tsquery
'Tarte & à & la & tomate'
'tart' & 'tomat'
to_tsquery('french', X)
'Tarte | à | la | tomate'
'tart' | 'tomat'
Transforme une chaîne de caractère en une text search query.
On s'affranchit :
- de la casse,
- des mots inutiles,
- des pluriels
Plus généralement, on peut matcher 2 mots qui ont la même racine.

'Tarte à la tomate'
.replace(/ /g, ' & ')
to_tsvector
'Amoureusement'
Transforme une chaîne de caractère en un text search vector.
Même comportement que to_tsquery concernant la casse, la racine des mots et les mots inutiles.
'Tarte à la tomate'
'tart':1 'tomat':4
'amour':1
'avec amour'
'amour':2
to_tsvector('french', 'Tarte à la tomate')


@@
l'opérateur de correspondance
to_tsquery('french', 'Tarte & poireau') @@ to_tsvector('french', 'Tarte à la tomate')
'tart':1 'tomat':4
to_tsquery('french', 'Tarte') @@ to_tsvector('french', 'Tarte à la tomate')
- Permet de savoir si une text search query correspond à un text search vector
- On peut utiliser des opérateurs comme & (AND), | (OR) et ! (NOT) dans la query
true
false
true
to_tsquery('french, 'Tarte | poireau') @@ to_tsvector('french', 'Tarte à la tomate')
'tart'
to_tsvector

to_tsquery

@@

SELECT title FROM recipe
WHERE to_tsquery('french', 'Tarte & à & la & tomate')
@@
to_tsvector('french', title);ts_query @@ ts_vector
ts_rank
- Compare une query et un vecteur pour renvoyer un indice de pertinence
- Dépend :
- de la fréquence de la query dans le vector
- des poids (A, B, C, ou D) attribués aux racines dans le vecteur
SELECT
ts_rank(
to_tsvector('french', 'Tarte à la tomate'),
to_tsquery('french', 'Tarte')
);
= 0.061
SELECT
ts_rank(
to_tsvector('french', 'Tarte à la tomate'),
to_tsquery('french', 'Tarte & à & la & tomate')
);
= 0.097
SELECT
ts_rank(
setweight(to_tsvector('french', 'Tarte à la tomate'), 'A'),
to_tsquery('french', 'Tarte & à & la & tomate')
);
= 0.934
En assemblant tout ...
SELECT * FROM recipe
WHERE
to_tsquery('french', 'Tarte & à & la & tomate') @@ to_tsvector('french', title)
ORDER BY
ts_rank(to_tsvector('french', title), to_tsquery('french', 'Tarte & à & la & tomate')) DESC;to_tsvector

to_tsquery

@@

ts_rank


Performance
Table "movie"
- title ~3 mots
- overview ~ 40 mots
10.000 films
Exemple
- title : "The Lion King"
- overview : "A young lion cub named Simba can't wait to be king. But his uncle craves the title for himself and will stop at nothing to get it."
On veut la liste des films dont le titre ou le résumé contient "eat"
Performance du ILIKE
SELECT title FROM movie
WHERE title ILIKE '%eat%' OR overview ILIKE '%eat%';Plein de résultats qui n'ont rien à voir avec "manger" et de plus :
- Cost 629
- Execution time : 118 ms
Performance du FTS - étape 1
SELECT title FROM movie
WHERE
to_tsquery('english', 'eat')
@@
(to_tsvector('english', title) || to_tsvector('english', overview))
ORDER BY
ts_rank(
setweight(to_tsvector('english', title), 'A')
|| setweight(to_tsvector('english', overview), 'B'),
to_tsquery('english', 'eat')
) DESC;Des résultats pertinents et ordonnés mais
- Cost 6562
- Execution time 542 ms

SELECT title FROM movie
WHERE text_search_vector @@ to_tsquery('english', 'eat')
ORDER BY ts_rank(text_search_vector, to_tsquery('english', 'eat')) DESC;Performance du FTS - étape 2
- Cost 131
- Execution time 0.2 ms
UPDATE movie
SET text_search_vector =
setweight(to_tsvector('english', coalesce(title, '')), 'A')
|| setweight(to_tsvector('english', coalesce(overview, '')), 'B');ALTER TABLE movie ADD COLUMN text_search_vector TSVECTOR;
Solutions alternatives

Algolia et Elastic Search :
Pour aller plus loin sans réinventer la roue





Conclusion
Le full-text search de PostgreSQL, c'est :
- performant
- rapide à mettre en place
- $ gratuit $
Vous n'avez pas forcément besoin de sortir l'artillerie lourde.
Avez-vous des questions ?
Ressources utiles :
- Mon repo avec toutes les expérimentations : https://github.com/pleportz/postgresql-sandbox
- Doc PostgreSQL sur les index à utiliser : https://www.postgresql.org/docs/9.1/textsearch-indexes.html
- "Postgres full-text search is Good Enough!" : http://rachbelaid.com/postgres-full-text-search-is-good-enough/
- Pour aller plus loin :
- les MATERIALIZED VIEWS : https://www.postgresql.org/docs/current/rules-materializedviews.html
- zombodb - Making Postgres and Elasticsearch work together : https://github.com/zombodb/zombodb
Pierre-Louis Le Portz
Développeur chez BAM
github: pleportz // twitter: @leportzp



Un moteur de recherche textuel avec PostgreSQL
By Pierre-Louis Le Portz
Un moteur de recherche textuel avec PostgreSQL
- 247