- Mise en place d’une architecture de moteur de recherche fonctionnant sans service externe à partir d’une base de données existante, en se concentrant sur la tokenisation, la pondération et le scoring
- L’idée centrale consiste à tokeniser tout le texte puis à le stocker, puis à faire correspondre les tokens de la même manière au moment de la recherche afin de calculer la pertinence
- Une combinaison de tokenizers Word, Prefix et N-Gram permet de gérer à la fois les correspondances exactes, les correspondances partielles et les fautes de frappe, chaque tokenizer ayant son propre poids
- Grâce à un système de pondération et à un algorithme de scoring basé sur SQL, la longueur des documents, la diversité des tokens et la qualité moyenne sont évaluées ensemble
- La structure offre une grande extensibilité et une forte transparence, ce qui permet d’ajouter librement de nouveaux tokenizers ou types de documents, d’ajuster les poids et de modifier le scoring
Pourquoi construire son propre moteur de recherche
- Les services externes comme Elasticsearch ou Algolia sont puissants, mais impliquent une API complexe à apprendre et une charge de gestion d’infrastructure
- Lorsqu’on a simplement besoin d’une fonction de recherche qui s’intègre à la base de données existante et reste facile à déboguer, il est utile de la construire soi-même
- L’objectif est un moteur de recherche simple qui renvoie des résultats pertinents sans dépendance externe
Concept clé : tokenisation et mise en correspondance
- Le principe de base consiste à tokeniser tout le texte puis à le stocker, puis à générer les tokens de la même manière au moment de la recherche pour les faire correspondre
- Lors de la phase d’indexation, le contenu est découpé en unités de tokens puis stocké avec des poids
- Lors de la phase de recherche, la requête est tokenisée de la même façon afin de trouver les tokens correspondants et de calculer un score
- Lors de la phase de scoring, les poids stockés sont utilisés pour produire un score de pertinence
Conception du schéma de base de données
- Deux tables sont utilisées :
index_tokens et index_entries
index_tokens : stocke les tokens uniques et les poids par tokenizer
index_entries : relie les tokens aux documents et stocke le score final en intégrant les poids du champ, du document et du tokenizer
- Formule de calcul du poids final :
field_weight × tokenizer_weight × ceil(sqrt(token_length))
- Des index sont définis pour la consultation des documents, la recherche de tokens, les requêtes par champ et le filtrage par poids
Système de tokenisation
- WordTokenizer : séparation par mots, suppression des mots courts, pour les correspondances exactes (poids 20)
- PrefixTokenizer : génération de préfixes de mots, pour l’autocomplétion et les correspondances partielles (poids 5)
- NGramsTokenizer : génération de combinaisons de caractères de longueur fixe, pour gérer les fautes de frappe et les correspondances partielles (poids 1)
- Tous les tokenizers appliquent en commun la mise en minuscules, la suppression des caractères spéciaux et la normalisation des espaces
Système de pondération
- Poids du champ : reflète l’importance du titre, du corps, des mots-clés, etc.
- Poids du tokenizer : Word > Prefix > N-Gram
- Poids du document : calculé en combinant les deux éléments précédents et la longueur du token
- La fonction
ceil(sqrt()) est utilisée pour atténuer l’influence des tokens longs ; au besoin, elle peut être remplacée par une fonction logarithmique ou linéaire
Service d’indexation
- Seuls les documents qui implémentent
IndexableDocumentInterface peuvent être indexés
- Lors de la création ou de la modification d’un document, l’indexation est effectuée via un écouteur d’événements ou des commandes (
app:index-document, app:reindex-documents)
- Procédure :
- suppression de l’ancien index puis génération de nouveaux tokens
- exécution de tous les tokenizers sur chaque champ
- vérification de l’existence du token puis création si nécessaire (
findOrCreateToken)
- insertion par lot (batch insert) dans
index_entries avec les poids calculés
- La structure permet d’éviter les doublons, d’améliorer les performances et de gérer les mises à jour
Service de recherche
- La requête est traitée avec les mêmes tokenizers afin d’obtenir le même ensemble de tokens que lors de l’indexation
- Après suppression des doublons, les tokens sont triés par longueur décroissante (les plus longs d’abord) et limités à 300 au maximum
- Une requête SQL joint les tokens et les documents, puis calcule et trie le score de pertinence
- Le résultat est renvoyé sous la forme
SearchResult(documentId, score)
Algorithme de scoring
- Score de base :
SUM(sd.weight)
- Correction de diversité des tokens :
LOG(1 + COUNT(DISTINCT token_id))
- Correction du poids moyen :
LOG(1 + AVG(weight))
- Pénalité de longueur du document :
1 / (1 + LOG(1 + token_count))
- Normalisation : division par le score maximal pour ramener le résultat dans l’intervalle 0~1
- Un filtre de poids minimal du token (
st2.weight >= ?) permet d’éliminer les correspondances insignifiantes à faible poids
Traitement des résultats
- Les résultats de recherche sont renvoyés sous forme d’ID de document et de score, puis convertis en documents réels via le repository
- La fonction
FIELD() est utilisée pour conserver l’ordre des résultats de recherche lors de la récupération des documents
Extensibilité du système
- Un nouveau tokenizer peut être ajouté en implémentant
TokenizerInterface
- Un nouveau type de document peut être enregistré en implémentant
IndexableDocumentInterface
- Les poids et la logique de scoring peuvent être ajustés simplement en modifiant le SQL
Conclusion
- Cette structure constitue un moteur de recherche simple mais réellement fonctionnel, offrant des performances suffisantes sans infrastructure externe
- Ses atouts sont une logique claire, un contrôle complet et un débogage facile
- Elle souligne qu’un code que l’on comprend et contrôle directement peut avoir plus de valeur qu’un système complexe
1 commentaires
Avis Hacker News
L’idée de base de la recherche est simple et le domaine est intéressant.
Mais les vraies difficultés commencent quand il faut gérer de gros volumes de données et traiter des requêtes ambiguës.
Une approche fondée sur un SGBD peut convenir à l’échelle d’un petit site web, mais elle atteint vite ses limites à l’échelle de la Wikipédia anglophone.
Pour débuter, le SeIRP e-book est une excellente ressource gratuite.
Le fait qu’il n’existe pas de bonne réponse évidente rend cela particulièrement délicat.
Google affiche parfois des publicités comme étant les « résultats les plus pertinents », ce qui fait de Marginalia Search un bon contre-exemple.
Je me demande si vous avez déjà consulté les articles du TREC.
Un moteur de recherche doit lutter en permanence contre des acteurs adverses qui cherchent des revenus publicitaires.
Cela devient un jeu du chat et de la souris sans fin, où il faut changer sans cesse les métriques de qualité pour éviter qu’elles soient détournées.
J’aimerais savoir quelle taille de corpus on pourrait interroger avec une base d’environ 5 secondes par requête et 12 requêtes par minute.
Par exemple, il est difficile de décider si l’article wiki sur Gilligan’s Island ou un blog de fan est un « meilleur » résultat.
Si on ajoute à cela la manipulation du classement ou le keyword stuffing, le défi devient bien plus complexe qu’un simple problème de passage à l’échelle.
La recherche est vraiment difficile.
Même des entreprises aussi riches en ressources et en savoir-faire qu’Apple, Microsoft ou OpenAI ont une qualité de recherche médiocre.
Ce n’est pas qu’un problème technique.
Pour améliorer la qualité de recherche, il faut ajuster finement les paramètres de classement, et ce genre de travail se planifie mal dans des cadres de gestion comme les sprints ou Jira.
C’est au fond un domaine qui exige confiance et autonomie pour les développeurs.
Elles investissent des milliards dans les modèles d’IA, tandis que la web app ou la recherche passent au second plan, d’où ce résultat.
J’ai travaillé il y a une dizaine d’années avec un collègue en doctorat sur la conception de moteurs de recherche.
Il parlait avec énormément de passion de l’intégration entre recherche et base de données, et j’ai beaucoup appris grâce à lui.
J’aimerais un jour explorer en profondeur les entrailles d’Apache Solr et de Lucene.
À l’époque, il n’existait pas de solution de recherche open source, donc il fallait la construire soi-même.
La leçon tirée de cette expérience est : ne construisez pas votre propre moteur de recherche.
D’innombrables personnes ont passé des années sur ce problème, et si on le fait soi-même, on tombe dans un enfer de maintenance sans fin.
Dès que commencent les demandes du genre « ajoutez une correction orthographique » ou « l’an prochain, ajoutons aussi une taxonomie », c’est terminé.
J’avais vraiment adoré le cours de David Evans, professeur à la Virginia University, sur la construction d’un moteur de recherche.
Construire soi-même un « moteur de recherche classique » était un projet très amusant.
Voir le lien du cours et la playlist YouTube.
Les moteurs de recherche que j’utilise souvent ont le défaut d’ignorer les sigles ou mots de 2 à 3 lettres.
Quand on cherche un mot court comme « mp3 » ou « PHP » et qu’il est supprimé, c’est vraiment pénible.
J’ai lu Programming Collective Intelligence de Toby Segaran, et cela m’a donné des idées sur la recherche, les recommandations et les classifieurs, entre autres.
C’était un article intéressant.
Je me demande jusqu’où va le niveau d’optimisation des tokenizers utilisés par les moteurs de recherche populaires.
Je me demande à quel point ce système serait scalable.
Elasticsearch montre des performances assez impressionnantes, même au-delà de l’échelle recommandée.
Un moteur de recherche textuel simple n’est pas difficile à créer.
Mais créer un bon moteur de recherche est une toute autre histoire.
Implémenter simplement un algorithme comme BM25 ne suffit pas.
La plupart des entreprises que j’ai conseillées utilisaient au départ leur propre solution, puis ont fini par migrer vers Elasticsearch ou Opensearch.
Une implémentation maison paraît simple au début, mais avec le temps elle se complique à cause des problèmes de ranking et de dégradation des performances.
Des symptômes comme « c’est lent » ou « les résultats n’ont aucun sens » reviennent sans cesse.
Elasticsearch résout ce type de problème depuis déjà 10 ans, et c’est aujourd’hui bien plus mature.
On entend aussi que « la configuration est difficile », mais de nos jours la plupart des choses sont auto-configurées, et il existe de nombreux services managés.
C’est même plus facile à manier que Postgres.
Au final, l’essentiel est d’optimiser le mapping de l’index.
Certains disent « on n’a pas besoin de ces fonctions avancées », mais en pratique la qualité de la recherche est directement liée à la survie du business.
Si l’on veut une vraie bonne recherche, il faut au bout du compte accepter cette complexité.
Des alternatives émergentes comme SeekStorm, souvent mentionnées récemment sur HN, ont l’air intéressantes, mais je n’ai pas encore vu de cas de production réels.
En particulier, le conseil de désactiver le dynamic mapping et d’empêcher l’indexation des champs inutiles m’a semblé utile.
Si je ne me trompe pas, c’est un projet plus ancien que Lucene.