28 points par xguru 2024-08-26 | 2 commentaires | Partager sur WhatsApp
  • Il est possible de construire dans Postgres un moteur de recherche hybride réunissant recherche sémantique, plein texte et floue
  • La recherche est une composante importante de nombreuses applications, mais il n’est pas facile de bien l’implémenter. En particulier dans les pipelines RAG, la qualité de la recherche peut déterminer le succès ou l’échec de l’ensemble du processus
  • La recherche sémantique est tendance, mais la recherche lexicale traditionnelle reste toujours la colonne vertébrale de la recherche
  • Les techniques sémantiques peuvent améliorer les résultats, mais elles fonctionnent le mieux sur la base d’une recherche textuelle robuste

Mettre en œuvre un moteur de recherche avec Postgres

  • Combinaison de trois techniques :
    • recherche plein texte avec tsvector
    • recherche sémantique avec pgvector
    • correspondance floue avec pg_trgm
  • Cette approche n’est peut-être pas la meilleure absolue dans tous les cas, mais constitue une excellente alternative à la mise en place d’un service de recherche séparé
  • Un point de départ solide que l’on peut implémenter et faire évoluer au sein d’une base de données Postgres existante
  • Pourquoi utiliser Postgres pour tout : Utilisez simplement Postgres partout, PostgreSQL suffit, Utilisez simplement Postgres

Implémenter la FTS et la recherche sémantique

  • Supabase propose une excellente documentation sur l’implémentation de la recherche hybride, qui servira de point de départ
  • En suivant ce guide, on implémente la FTS avec un index GIN et la recherche sémantique avec pgvector, également appelée bi-encoder dense retrieval
  • D’après l’expérience personnelle de l’auteur, choisir des embeddings en 1536 dimensions donne de bien meilleurs résultats
  • Les fonctions Supabase sont remplacées par des CTE et des requêtes, en préfixant les paramètres par $
  • Ici, les résultats sont fusionnés à l’aide du RRF (Reciprocal Ranked Fusion)
  • Cette méthode garantit que les éléments bien classés dans plusieurs listes obtiennent un bon rang dans la liste finale
  • Elle évite aussi qu’un élément très bien classé dans une liste mais mal classé dans d’autres n’obtienne une place trop élevée dans le classement final
  • Calculer le score avec le rang au dénominateur peut pénaliser les enregistrements mal classés
  • Points à noter
    • $rrf_k : pour éviter que le premier élément ne reçoive un score excessivement élevé, on ajoute souvent une constante k au dénominateur afin de lisser les scores
    • $ _weight : il est possible d’attribuer un poids à chaque méthode. C’est très utile pour ajuster les résultats

Implémenter la recherche floue

  • Les méthodes précédentes couvrent déjà beaucoup de cas, mais les fautes de frappe sur des entités nommées peuvent poser un problème immédiat
  • La recherche sémantique capte certaines similarités et atténue donc une partie de ces problèmes, mais elle a du mal avec les noms, les acronymes et d’autres textes qui ne sont pas sémantiquement proches
  • Pour y remédier, on introduit l’extension pg_trgm afin d’autoriser la recherche floue
    • Elle fonctionne avec des trigrammes. Les trigrammes décomposent les mots en séquences de 3 caractères, ce qui les rend utiles pour la recherche floue
    • Cela permet de faire correspondre des mots similaires même en présence de fautes de frappe ou de légères variantes
    • Par exemple, "hello" et "helo" partagent de nombreux trigrammes, ce qui facilite leur correspondance en recherche floue
  • On crée un nouvel index sur la colonne souhaitée, puis on l’ajoute à la requête de recherche globale
  • L’extension pg_trgm expose l’opérateur %, qui filtre les textes dont la similarité est supérieure à pg_trgm.similarity_threshold (0.3 par défaut)
  • Il existe aussi plusieurs autres opérateurs utiles

Affiner la recherche plein texte

  • Ajuster les poids de tsvector : dans un vrai document, il n’y a pas que le titre, mais aussi le contenu
  • Même s’il y a plusieurs colonnes, on ne conserve qu’une seule colonne d’embedding
  • L’auteur a constaté qu’en pratique, conserver title et body dans le même embedding ne faisait pas de grande différence de performance par rapport au maintien de plusieurs embeddings
  • Au final, title est censé être une représentation concise du corps du texte. Il est conseillé d’expérimenter selon les besoins
  • Le titre est censé être court et riche en mots-clés, tandis que le corps sera plus long et contiendra davantage de détails
  • Il faut donc ajuster la manière dont les colonnes de recherche plein texte sont pondérées entre elles
  • On peut accorder des priorités selon l’emplacement ou l’importance d’un mot dans le document
    • poids A : le plus important (ex. : titre, en-têtes). Valeur par défaut 1.0
    • poids B : important (ex. : début du document, résumé). Valeur par défaut 0.4
    • poids C : importance standard (ex. : texte principal). Valeur par défaut 0.2
    • poids D : le moins important (ex. : notes de bas de page, annotations). Valeur par défaut 0.1
  • En ajustant ces poids selon la structure du document et les besoins de l’application, on peut affiner la pertinence
  • Pourquoi donner plus de poids au titre
    • Parce que le titre exprime généralement de manière concise le sujet principal du document
    • Les utilisateurs ont tendance à parcourir d’abord les titres lors d’une recherche, donc une correspondance sur le titre est souvent plus proche de leur intention qu’une correspondance dans le corps du texte

Ajustements selon la longueur

  • En lisant la documentation de ts_rank_cd, on découvre qu’il existe un paramètre de normalisation
    • Les deux fonctions de classement utilisent une option entière normalization qui indique si, et comment, la longueur du document doit influencer le rang. Comme cette option entière contrôle plusieurs comportements, il s’agit d’un masque de bits : on peut spécifier un ou plusieurs comportements avec | (par ex. 2|4).

  • Ces différentes options permettent notamment de :
    • corriger les biais liés à la longueur des documents
    • équilibrer la pertinence sur des ensembles de documents variés
    • ajuster les résultats de classement pour une représentation plus cohérente
  • De bons résultats peuvent être obtenus avec 0 (pas de normalisation) pour le titre et 1 (longueur logarithmique du document) pour le corps
  • Là encore, il est recommandé d’expérimenter différentes options pour trouver celles qui conviennent le mieux au cas d’usage

Réordonner avec un cross-encoder

  • De nombreux systèmes de recherche fonctionnent en deux étapes
  • Autrement dit, on récupère d’abord les N premiers résultats avec un bi-encoder, puis on les rerank avec un cross-encoder en les comparant à la requête de recherche
    • bi-encoder : rapide, donc adapté à la recherche dans un grand nombre de documents
    • cross-encoder
      • plus lent mais plus performant, donc adapté au reranking des résultats récupérés
      • traite ensemble la requête et le document, ce qui permet une compréhension plus fine de leur relation
      • offre ainsi une meilleure précision de classement au prix d’un coût de calcul plus élevé et d’une moins bonne scalabilité
  • Il existe divers outils pour cela
  • L’un des meilleurs est Rerank de Cohere
  • Une autre possibilité consiste à le construire soi-même avec le GPT d’OpenAI
  • Les cross-encoders peuvent améliorer la précision des résultats de recherche en comprenant mieux la relation entre la requête et le document
  • Mais leur coût de calcul est élevé, ce qui limite leur scalabilité
  • C’est pourquoi une approche en deux étapes est efficace : utiliser un bi-encoder pour la recherche initiale, puis n’appliquer le cross-encoder qu’au petit nombre de documents récupérés

Quand chercher une solution alternative

  • PostgreSQL est un bon choix pour de nombreux scénarios de recherche, mais il n’est pas sans limites
  • L’absence d’algorithmes avancés comme BM25 peut se faire sentir lorsqu’il faut gérer des documents de longueurs très différentes
  • La recherche plein texte de PostgreSQL repose sur TF-IDF, ce qui peut poser problème avec des documents très longs et des termes rares dans de grandes collections
  • Avant de chercher une solution alternative, il faut absolument mesurer. Cela n’en vaut peut-être pas la peine

Conclusion

  • Cet article couvre de nombreux sujets, de la recherche plein texte de base à des techniques avancées comme la correspondance floue, la recherche sémantique et le boost des résultats
  • En exploitant les puissantes capacités de Postgres, il est possible de créer un moteur de recherche robuste et flexible adapté à des besoins spécifiques
  • Postgres n’est peut-être pas l’outil auquel on pense en premier pour la recherche, mais il peut mener très loin
  • Clés d’une excellente expérience de recherche
    • itération continue et ajustements fins
    • il ne faut pas hésiter à utiliser les techniques de débogage évoquées pour comprendre les performances de la recherche, puis à ajuster les poids et paramètres en fonction des retours et du comportement des utilisateurs
  • PostgreSQL peut manquer de fonctionnalités de recherche avancées, mais il reste dans la plupart des cas assez puissant pour construire un moteur de recherche efficace
  • Avant d’envisager une solution alternative, il est préférable d’exploiter au maximum les capacités de Postgres et d’en mesurer les performances ; si cela reste insuffisant, on pourra alors considérer une autre solution

2 commentaires

 
eajrezz 2024-08-27

Je me demande si la recherche en coréen fonctionne bien aussi.

 
xguru 2024-08-26

Le sujet du Weekly aujourd’hui portait aussi sur Postgres, et encore une fois, c’est Postgres. On dirait bien qu’il y a beaucoup d’articles qui sortent, clairement à la hauteur de sa popularité haha.
Pour BM25, voir ci-dessous.

pg_bm25 - Une extension de recherche full-text pour Postgres offrant une qualité du niveau d’Elastic
ParadeDB - PostgreSQL for Search