Moteur de recherche et Drupal : Pertinence des résultats et boosting SolR

Photo @chdugue - Photo illustrant à la gare Saint Charles, l'affichage des premiers résultats.
Seconde partie de notre plongée dans la Search API Solr de Drupal. Nous abordons ici le traitement et les configurations relatives à la pertinence des résultats ainsi que le "boosting" SolR.

Pertinence et score

L’algorithme de notation de Solr est connu sous le nom de modèle tf.idf (une mesure statistique qui permet d’évaluer l'importance d'un mot relativement à un document appartenant à une collection).

Lucene combine le modèle booléen avec le modèle d'espace vectoriel : 

  • Modèle booléen de la recherche d'information (BM)
    La recherche est basée sur le fait que les documents contiennent ou non les termes de la requête. Ces quatre facteurs sont réunis dans une formule de notation.
  • Modèle d'espace vectoriel (VSM)
    C’est un modèle algébrique permettant de représenter des documents textuels comme des vecteurs d'identifiants (tels que des termes d'index). Il est utilisé dans le filtrage de l'information, la recherche d'information, l'indexation et les classements par pertinence.

Lucene combine les deux modèles - les documents approuvés par le BM sont attribués une note par le VSM.

Ce modèle de notation implique un certain nombre de facteurs de notation, dont :
r = requête, d = document et t = terme de recherche

  • tf(t dans d) : Fréquence des termes. La fréquence à laquelle un terme apparaît dans un document. Pour une requête de recherche, plus la fréquence des termes est élevée, plus le score du document est élevé.
  • idf(t) : Fréquence inverse des documents. Plus un terme est rare dans tous les documents de l'index, plus sa contribution au score est élevée.
  • coord(r, d) : Facteur de coordination. Plus il y a de termes de la requête dans un document, plus son score est élevé.
  • fieldNorm : Longueur du champ. Plus un champ contient de mots, plus son score est faible. Ce facteur pénalise les documents dont les champs sont plus longs.
  • queryNorm(r) : facteur de normalisation utilisé pour rendre les scores comparables entre les requêtes. Ce facteur n'affecte pas le classement des documents (puisque tous les documents classés sont multipliés par le même facteur), mais tente simplement de rendre comparables les scores de différentes requêtes (ou même de différents index).

Ces facteurs et modèles nous emmènent à la fonction pratique utilisée par SOLR :
score(r, d) = coord(r, d) * queryNorm(r) * Σ [ tf(t dans d) · idf(t)² · t.getBoost() · norm(t,d) ]
Les scores sont également toujours normalisés de sorte qu'ils se situent entre 0 et 1,0.

norm(t,d) encapsule quelques facteurs de boost et de longueur (au moment de l'indexation) :

  • Boost du document - défini en appelant doc.setBoost() avant d'ajouter le document à l'index.
  • Field boost - défini en appelant field.setBoost() avant d'ajouter le champ à un document.
  • lengthNorm - calculé lorsque le document est ajouté à l'index en fonction du nombre de tokens de ce champ dans le document, de sorte que les champs courts contribuent davantage au score. La norme de longueur est calculée par la classe Similarity en vigueur lors de l'indexation.

La méthode computeNorm(java.lang.String, org.apache.lucene.index.FieldInvertState) est responsable de la combinaison de tous ces facteurs en une seule valeur flottante.

Lorsqu'un document est ajouté à l'index, tous les facteurs ci-dessus sont multipliés. Si le document possède plusieurs champs portant le même nom, tous leurs boosts sont multipliés ensemble.

Qu'est-ce que le boosting ?

t.getBoost() est un boost de recherche du terme t dans la requête q tel que spécifié dans le texte de la requête (voir syntaxe de la requête), ou tel que défini par les appels de l'application à setBoost(). Notez qu'il n'y a pas vraiment d'API directe pour accéder au boost d'un terme dans une requête multi-termes, mais plutôt que les termes multiples sont représentés dans une requête comme des objets TermQuery multiples, et donc le boost d'un terme dans la requête est accessible en appelant la sous-requête getBoost().

Le boosting est utilisé pour modifier le score des documents récupérés lors d'une recherche. Il existe un boosting au moment de l'indexation et un boosting au moment de la recherche ou de la requête des documents. Le boosting au moment de l'indexation est utilisé pour booster les champs des documents de façon permanente. Une fois qu'un document est boosté pendant la période d'indexation, le boost est stocké avec le document. Par conséquent, une fois la recherche terminée et lors du calcul de la pertinence, le boost stocké est pris en compte. Le boosting au moment de la recherche ou de la requête est dynamique. Certains champs peuvent être renforcés dans la requête, ce qui permet à certains documents d'obtenir un score de pertinence plus élevé que d'autres. Par exemple, nous pouvons améliorer le score des livres en ajoutant le paramètre cat:book^4 dans la requête. Ce renforcement rendra le score des livres relativement plus élevé que celui des autres éléments de l'index.

Lors de l'indexation, les utilisateurs peuvent spécifier que certains documents sont plus importants que d'autres, en leur attribuant un boost. Pour cela, le score de chaque document est également multiplié par sa valeur de boost doc-boost(d).

La recherche dans Solr est basée sur des champs, donc chaque terme de la requête s'applique à un seul champ, la normalisation de la longueur du document est basée sur la longueur du champ en question, et en plus du boost du document, il existe également des boosts pour les champs du document.

Le même champ peut être ajouté plusieurs fois à un document pendant l'indexation, et donc le boost de ce champ est la multiplication des boosts des ajouts séparés (ou parties) de ce champ dans le document.

Au moment de la recherche, les utilisateurs peuvent spécifier des boosts pour chaque requête, sous-requête et chaque terme de la requête. Ainsi, la contribution d'un terme de la requête au score d'un document est multipliée par le boost de ce terme de la requête query-boost(q).

Un document peut correspondre à une requête multi-termes sans contenir tous les termes de cette requête (ceci est correct pour certaines des requêtes), et les utilisateurs peuvent récompenser davantage les documents correspondant à plus de termes de la requête par un facteur de coordination, qui est généralement plus grand lorsque plus de termes sont mis en correspondance : facteur de coordination(q,d).

Le boosting donne une plus grande pertinence à un ensemble de documents par rapport aux autres. Le facteur de renforcement est multiplié par le score de pertinence. Si vous avez un score de pertinence de base de 1.5 et que vous avez un facteur d'amplification de 1, le score de pertinence reste à 1,5 ; si vous choisissez d'amplifier de 2, le score de pertinence passe à 3 (1.5 * 2 = 3). Il est important de noter que si vous avez sélectionné 0 comme boost, le score de pertinence sera annulé : 1.5 * 0 = 0.

Les boosts au moment de la requête permettent de spécifier quels termes/clauses sont "plus importants". Plus le facteur d'amplification est élevé, plus le terme sera pertinent, et donc plus les scores du document correspondant seront élevés.

Une technique typique de boosting consiste à attribuer des boosts plus élevés aux correspondances de titres qu'aux correspondances de contenu du corps du texte :
(title:foo OR title:bar)^1.5 (body:foo OR body:bar)

Vous devez examiner attentivement la sortie de l'explication pour déterminer les poids de boost appropriés.
La documentation officielle sur la syntaxe de l'analyseur de requêtes se trouve ici : http://lucene.apache.org/java/3_5_0/queryparsersyntax.html
La syntaxe des requêtes n'a pas changé de manière significative depuis Lucene 1.3 (elle est maintenant 3.5.0).

Boosting par date

Il est parfois souhaitable de hiérarchiser la liste des résultats en tenant compte de la date, sans négliger les principaux paramètres de recherche qui ont été spécifiés.

Scénarios où cela est utile :

  • Lorsqu'il n'y a pas de paramètres de recherche concurrents et qu'il est important que les résultats soient listés en fonction de la date (par exemple, aucun terme saisi, tous les résultats listés).
  • Lorsqu'il est utile de considérer les éléments de manière plus favorable lorsqu'ils ont une date plus récente (par exemple, dans les listes d'articles d'actualité).

Dans Drupal, c’est un processeur qui gère cette fonctionnalité (cf ci-après - Processeurs Drupal). Pour aller plus loin, dans cet article on explique comment mettre en place des requêtes personnalisées pour faire varier cette fonctionnalité : https://www.solrtutorial.com/boost-documents-by-age.html.

Qu'est-ce que la recherche partielle ? (Partial search)

Si l'utilisateur saisit "ballon" comme requête, le moteur de recherche considérera un document comme correspondant s'il contient "volleyball" ou "beachvolleyball", etc. Le terme recherché doit donc être contenu dans le mot.

Indexation

Afin de rendre les données disponibles pour la recherche, nous devons les enregistrer dans l'index. 

Dans la page de configuration de l’index, on peut trouver aussi des options d’indexation :    

  • Lecture seule (Read-only) : Autoriser seulement à lire les données indexées déjà sans donner la possibilité de les changer sur le serveur.
  • Indexer les éléments immédiatement : Les nouveaux éléments sont indexés et mis à jour immédiatement. Si vous indexez un grand nombre d'éléments, vous voudrez peut-être laisser cette option désactivée et économiser les temps d'attente. Si le contenu à indéxer ne nécessite pas d’être afficher pour les utilisateurs à l’instant exact de sa publication, il est conseillé de ne pas activer cette option. Autrement, pour les contenus à publier immédiatement, comme par exemple pour des brèves et des communiqués de presse, cette option pourra être utilisée mais de préférence en créant un autre index spécifique pour ce type de contenu.
  • Suivi des changements dans les entités référencées : Mettre les éléments à réindexer automatiquement en file d'attente si l'une des valeurs des champs indexés à partir des entités qu'ils référencent est modifiée. L'activation de ce paramètre peut entraîner des problèmes de performances sur les sites lors de l'enregistrement de certains types d'entités. Cependant, lorsque ce paramètre est désactivé, les champs des entités référencées peuvent devenir obsolètes dans l'index.
  • Sélectionnez la taille du lot Cron : Il s'agit du nombre d'éléments à indexer pour chaque exécution de Cron. Plus le nombre est conséquent, plus la ressource sera consommée.

Après avoir créé et configuré l’index, on doit y ajouter des champs : /admin/config/search/search-api/index/nom_index/fields

Dans cette page, on peut ajouter des champs créés dans Drupal et appartenant aux types de contenu choisis auparavant dans la section “Sources de données et types de contenu”. Il faut ajouter tous les champs dont on a besoin pour créer notre page recherche (champs à afficher, champs à utiliser dans les tris, champs à utiliser dans des conditions...). 

Dans la colonne “Type”, on peut trouver les types de données disponibles provenant de Solr (élaborés dans un paragraphe précédent). À préciser qu’afin d’utiliser le boost, il faut choisir la valeur “texte intégral”. En pratique, il est conseillé de le faire sur les champs contenant les données les plus importantes comme : 

  • Titre x 13.0 ;
  • Body x 5.0 ;
  • Les champs texte des paragraphes x 5.0 ;
  • Autres champs textes importants x 1.0.

Une recherche partielle est effectuée sur ce type de champ.

Pour les champs date de Drupal on choisit le type ”date” et pour tout champ qui n’est pas intéressant pour le calcul du score de la pertinence, il est préférable de choisir le type “storage-only” (images, médias, statut de publication, type de contenu...) Ces derniers seront disponibles à utilisation dans la vue de la page de recherche mais ne feront pas partie des champs analysés dans l’index. 

Processeurs Drupal

Les processeurs configurent les données au moment de l'indexation et de la recherche. Le serveur Solr a déjà des processeurs de pré et post-traitements de données intégrés et actifs. Mais on peut aussi ajouter/activer d’autres liés à la structure de données de Drupal depuis le back office celui-ci (/admin/config/search/search-api/index/nom_index/processors).

Parmi les plus importants :

  • Contrôle d’accès : Ajoute des contrôles d'accès au contenu pour les nœuds et les commentaires. En d’autres termes, ça prend en considération les permissions d’accès aux entités quand c’est activé.
  • État de l'entité (entity status) : Exclure les utilisateurs inactifs et les entités non publiées (qui ont un état "non publié") d'être indexés.
  • Boost des dates récentes : Favoriser les documents les plus récents et pénaliser les documents plus anciens.
  • Surligner (highlight) : Mettre en évidence ou surligner les résultats des champs retournés.
  • Boost spécifique par type de contenu Drupal (Type-specific boosting) : Il sert à donner un score plus haut à des types de contenu que d’autres.

L’ordre des processeurs est important dans le traitement des résultats. Il existe trois phases de traitement :

  • Phase de prétraitement avant indexation ;
  • Phase de prétraitement avant l’envoi de la requête ;
  • Phase de posttraitement de la requete.

Pour chaque processeur activé, on pourra choisir dans quelle phase (et dans quel ordre dans la même phase) le placer. Par exemple, le processeur “Surligner” doit toujours passer en dernier puisqu’il intervient sur le contenu même et modifie la donnée (champs et attributs HTML). Finalement, quelques processeurs peuvent être configurés dans la section “Paramètres du processeur”.

Autocomplétion

Avec le module Search API Autocomplete, on peut configurer des suggestions de résultats pour les champs de saisie manuelle dans la recherche (recherche texte libre). Ça peut être activé par vue de recherche crée (/admin/config/search/search-api/index/nom_index/autocomplete).

Les suggestions peuvent être affichées en utilisant les modes d’affichage de Drupal et configurés par type de contenu ou par champ. Il suffit donc de créer un mode d’affichage dans Drupal pour le type de contenu à afficher dans les suggestions, configurer son template puis le choisir dans la page de configuration de l’autocomplete.

Exemple de moteur de recherche
Sur https://www.cgt.fr/, nous avons mis en place une recherche basée sur SOLR.

  • Version Drupal : 9.3.22 ;
  • Version SOLR : 6.6.6 ;
  • Version module : 4.2.7.

Page résultats de recherche pour le terme “salaires”
 
IMAGE

Caches et performance

Rappelons d'abord rapidement ce qu'est la mémoire cache. Les ordinateurs ont une mémoire cache qui stocke temporairement les données les plus fréquemment utilisées. C'est un excellent moyen pour réutiliser ces dernières, car le processus de récupération est très rapide. Les ordinateurs ont également une mémoire, mais il est plus coûteux d'y récupérer des données. Cependant, la taille de la mémoire cache est limitée et il doit y avoir un moyen de gérer les données qui doivent être retirées de la mémoire cache afin de stocker de nouvelles données. 

Un exemple d’algorithme de gestion de cache, LRU (Least Recently Used) -  Il s'agit d'un algorithme de remplacement du cache qui supprime les données les moins récemment utilisées afin de libérer de l'espace pour les nouvelles données.

Si vous disposez d'une application qui affiche des images .png au chargement de la page. Vous souhaitez les stocker dans votre cache pour que les images se chargent rapidement pour vos utilisateurs. Maintenant, votre utilisateur interagit avec votre page et ces actions déclenchent le chargement de nouvelles images à l'écran. Vous voulez toujours lui offrir un chargement rapide des images, mais vous manquez d'espace dans votre cache. L'ancien est remplacé par le nouveau ! C'est là que vous pouvez utiliser un algorithme de remplacement de cache pour supprimer une ancienne image de votre cache au profit d'une nouvelle.

FilterCache

Le cache de filtre est utilisé par les filtres. Il permet de contrôler la façon dont les requêtes de filtre sont traitées afin de maximiser les performances. Le principal avantage de FilterCache est que lorsqu'une nouvelle recherche est lancée, ses caches peuvent être pré-remplis en utilisant les données des caches de l'ancienne recherche. Ainsi, cela aidera certainement à maximiser les performances. 

Exemple de configuration :

<filterCache               
 class="solr.FastLRUCache" 
 size="512"                
 initialSize="512"         
 autowarmCount="0"         
/>      

QueryResultCache 

Le cache des résultats de la requête contient les résultats des recherches précédentes : des listes ordonnées d'ID des résultats basées sur une requête, un tri et la gamme de résultats demandés.
Exemple de configuration :

<queryResultCache     
 class="solr.LRUCache"
  size="512"          
 initialSize="512"    
 autowarmCount="0"    
/>   

DocumentCache

Ce cache contient les objets des résultats (les champs stockés pour chaque résultat). En d’autres termes, il contient les détails du QueryResultCache.
Exemple de configuration :

<documentCache         
 class="solr.LRUCache" 
 size="512"            
 initialSize="512"     
 autowarmCount="0"     
/>

Les deux derniers caches vont souvent ensemble. Il donne de meilleures performances dans les scénarios où il y a principalement des cas d'utilisation en lecture seule. Prenons l'exemple d'une page article. Un article peut contenir des informations et une section commentaires. Dans le cas des informations, c’est logique d’activer ces caches sur ces champs car les lectures de la base de données dans ce cas sont beaucoup plus nombreuses que les écritures. 

Mais si, principalement, les cas d'utilisation sont en écriture seule, il est préférable de désactiver ces deux caches car à chaque rechargement des données, ces caches sont vidés et n'auront pas beaucoup d'effet sur les performances. Ainsi, en gardant à l'esprit l'exemple de l’article mentionné ci-dessus, il est conseillé de désactiver ces caches pour le champ commentaires.

Nous espérons avoir éclairci certaines notions. Nous espérons surtout que cet article vous sera utile. L'utilisation de Solr et de Drupal, ensemble, nous a toujours permis de couvrir l'intégralité des besoins de nos clients et de leurs utilisateurs en matière de recherches avancées sur leur site Drupal. Pourvu que ça dure !

Références
Lucidworks.com - Scaling Lucene and Solr
bluedrop.fr - Astuce de code pour grouper des labels différents dans le tri proposé par le module facets de Drupal
bluedrop.fr - Plus loin dans search API SolR pour Drupal - la rencontre de la racinisation et de la lemmatisation
Medium - Configuring Solr for Optimum Performance
Opensenselabs.com - HowTo: Use Apache Solr with Drupal 8
SolR Apache - https://solr.apache.org/features.html
Ostraining.com - How to use Search API Solr Search in Drupal 8
Slideshare - Using Search API, Search API Solr and Facets in Drupal 8
Sematext.com - Getting Started with Apache Solr
Medium - What is an LRU Cache?
Solr Tutorial - https://www.solrtutorial.com/
Lucene Apache - Class Similarity
Lucene Apache - Class TFIDFSimilarity