25. Annexe: Solr, un moteur de recherche¶
Avertissement au lecteur
Ce chapitre était auparavant dispensé dans le cours de Recherche d’information. Les systèmes ont évolué et le cours repose maintenant sur Elasticsearch. Cette présentation de Solr, basée sur une version un peu ancienne, peut permettre de comparer Elasticsearch et Solr, de comprendre quels sont les invariants du travail de concepteur de moteur de recherche.
Nous allons continuer notre découverte des systèmes d’indexation avec le moteur de recherche Solr, un des outils Open Source les plus répandus (avec Sphinx et ElasticSearch, sur lequel nous reviendrons ultérieurement pour la simplicité de son modèle de passage à l’échelle par distribution). Solr est (comme ElasticSearch) basé sur les index du système Lucene. Pour l’instant nous allons installer Solr, indexer nos collections et les interroger. Dans un second temps nous regarderons en interne comment fonctionne le système.
S1: installation, mise en route¶
Supports complémentaires:
Dans cette première session nous installons Solr et testons quelques fonctionnalités sur les exemples fournis.
Installation¶
L’installation est très simple: vous avez juste besoin d’une version récente de
Java (1.7 au moins) sur votre ordinateur. Récupérez la version 4.10.2 de Solr
depuis le site http://archive.apache.org/dist/lucene/solr/ (fichier
solr-4.10.2.zip, 149 Mo), décompressez l’archive quelque part (appelons ce
quelque part soldir
). Rendez-vous dans soldir/example
et exécutez la
commande:
java -jar start.jar
Cela devrait vous permettre d’avoir une collection par défaut.
Solr est une application java contenue dans un serveur Web, sur le port 8983. Vous devriez, avec votre navigateur, pouvoir accéder à l’URL http://localhost:8983/solr/ et obtenir l’affichage de la figure L’interface d’administration de Solr. Félicitations: Solr est installé et prêt à l’emploi.
Note
Comme pour tous les systèmes présentés dans ce cours, nous ne donnons
pas la procédure d’installation en production. Notre but est de découvrir
les principes et fonctionnalités d’un moteur de recherche, pas de devenir un expert
en administration. Nous nous limiterons à la configuration pré-définie dans le
sous-répertoire example
. Elle exécute Solr comme une servlet contenue dans le serveur Jetty.
Insertion de documents¶
Solr fournit des services REST (le format d’échange étant XML ou JSON) pour
insérer des documents, effectuer des requêtes, etc. Le service qui se charge
de mettre à jour un index se situe à l’URL http://localhost:8983/solr/update (si
Solr est sur votre machine locale bien sûr). La requête HTTP pour transmettre un
document XML à indexer s’attend à recevoir un document de type
Content-type:application/xml
, avec un codage binaire pour éviter
d’interpréter un codage utf-8 comme de l’ASCII. Bref, voici la commande curl
pour transmettre un fichier item.xml
.
curl http://localhost:8983/solr/update --data-binary @item.xml -H 'Content-type:application/xml'
Important
Curl est un utilitaire qui permet de ransmettre des requêtes HTTP à un service depuis la ligne de commande. Pour la présentation de REST et de Curl, voir le chapitre Interrogation de bases NoSQL.
Solr fournit quelques documents exemples dans exampledocs
, ainsi que deux utilitaires pour
les indexer.
post.jar
est un programme java avec diverses options. L’utilisation la plus simple est l’insertion de documents XML locaux:java -jar post.jar *.xml
Vous devriez obtenir des messages de confirmation de l’insertion (c’est plus clair avec le programme java). Vous pouvez voir les documents à l’url http://localhost:8983/solr/gettingstarted/browse.
Reportez-vous maintenant à l’interface Web, et sélectionnez le core Collection1
dans
la barre de navigation gauche. Vous devriez obtenir l’affichage de la figure Consultation de la Collection1 montrant des documents indexés.,
et la confirmation que des documents ont été indexés.
Note
La notion de core correspond, dans ElasticSearch, à la notion d’index (ce qui est quand même plus clair).
Avant de passer à la section suivante, jetez un œil au contenu (et à la structure) des documents XML que vous venez d’indexer. En voici un, abrégé.
<add>
<doc>
<field name="id">SOLR1000</field>
<field name="name">Solr, the Enterprise Search Server</field>
<field name="manu">Apache Software Foundation</field>
<field name="cat">software</field>
<field name="cat">search</field>
<field name="features">Advanced Full-Text Search Capabilities using Lucene</field>
<field name="features">Optimized for High Volume Web Traffic</field>
<field name="price">0</field>
<field name="popularity">10</field>
<field name="inStock">true</field>
<field name="incubationdate_dt">2006-01-17T00:00:00.000Z</field>
</doc>
</add>
Le document est constitué de champs (fields), chaque champ étant indexé séparément. Le nom
du champ est indiqué par l’attribut name
. Pour que ces champs soit acceptés par Solr,
il faut qu’ils aient été déclarés au préalable dans le schéma de l’index. Par exemple,
dans le schéma utilisé par Solr pour cet exemple initial, le champ
price
est déclaré comme numérique, et le champ features
comme chaîne
de caractères. On retrouve le même principe que dans ElasticSearch:
l’indexation se fait sur des documents structurés contenant les données
de la base sur lesquelles ont souhaite effectuer les recherches. Le fait
d’imposer un schéma pour ces documents (ce qu’ElasticSearch ne fait pas pas défaut)
est caractéristique
d’une approche où on privilégie le contrôle et les contraintes sur la rapidité
de mise en œuvre.
Important
Dans ce qui suit nous allons parler des documents Solr pour désigner la liste des champs transmise à l’index, et les distinguer des documents applicatifs contenus dans notre base de données documentaire.
Solr peut être vu comme une base de données spécialisée dans la recherche d’information: on y insère des documents (Solr), conformes à un schéma, et on peut rechercher des documents (Solr). Peut-on utiliser Solr comme base documentaire? Pas tout à fait: nous y revenons plus loin.
Interroger un index avec Solr¶
Comme pour les insertions, Solr dispose d’une interface REST pour rechercher des documents.
Essayez par exemple l’URL suivante (avec curl
, ou simplement avec votre navigateur):
http://localhost:8983/solr/select?q=video
La réponse est formattée en XML. On peut aussi demander un retour en JSON:
http://localhost:8983/solr/select?q=video&wt=json&indent=yes
Et, enfin, il est sans doute plus agréable d’utiliser l’interface d’administration qui permet d’entrer des requêtes et de jouer sur tous les paramètres.
Regardez la réponse transmise par Solr. Elle comprend un en-tête donnant quelques propriétés sur l’exécution (notamment le temps de réponse, ici très court) et la liste des documents (Solr) dont Solr a considéré qu’ils satisfaisaient les critères.
Ces documents Solr sont constitués des valeurs de champs
insérés dans l’index. Le résultat d’une recherche est donc le document Solr
initialement
inséré. Souvent, cela ne suffit pas, et il faut, à partir du document
Solr, accéder au document applicatif (et complet) stocké dans la base documentaire. Parmi la
liste des champs, on en trouve presque toujours un nommé id
qui contient
la valeur de la clé d’accès au document dans la base documentaire. Il reste
donc à faire un get
sur cette dernière.
Important
Les champs ramenés par une requête sont en fait ceux déclarés par le schéma comme étant stockés. Voir plus loin.
Notez que certains de ces champs (cat
et features
par exemple) sont multivalués (ils
sont représentés par des tableaux en JSON). Reportez-vous à l’exemple du document Solr en XML
donné ci-dessus: nous avons bien indiqué plusieurs valeurs pour, par exemple, le champ cat
.
L’index en a tenu compte.
Solr étant un moteur de recherche, c’est dans la recherche qu’il excelle. Il existe donc de nombreuses et puissantes options pour aller au delà de la simple recherche par mot-clé illustrée jusqu’à présent. En voici un échantillon (vous pouvez aussi vous reportez à la section consacrée à ElasticSearch).
Tout d’abord, on peut effectuer une « projection » à la SQL pour ne conserver que certains champs.
Le paramètre correspondant est fl
(comme field list). Passez par
exemple ce paramètre avec la valeur id,name
:
http://localhost:8983/solr/select?q=video&fl=id,name
Une autre manière d’exploiter la structuration en champs est d’indiquer explicitement dans quel champ on effectue une recherche. On préfixe pour cela la valeur du mot-clé avec le nom du champ. Soit, par exemple:
http://localhost:8983/solr/select?q=name:black
On peut paginer le résultat avec les paramètres start
et rows
:
http://localhost:8983/solr/select?q=video&start=1&rows=10
Les fonctionnalités de recherche sont proches de celles déjà vues pour ElasticSearch, et pour cause: les deux systèmes s’appuient sur Lucene et son langage de base. En revanche, il n’y a pas d’équivalent sous Solr du langage DSL d’ElasticSearch.
Exercices¶
Exercice: mise en route de Solr
Vous devez simplement installer Solr et tester les commandes montrées dans la session qui précède. Il s’agit à peu près du tutorial Solr standard tel qu’il se trouve au début de la documentation officielle. Vous avez le droit de jouer avec l’interface d’interrogation pour voir quels documents sont recherchés en réponse à une requête.
S2: construisons notre moteur de recherche¶
Supports complémentaires:
Nous avons donc notre base des films dans MongoDB. Nous allons indexer
cette base avec Solr. Un index dans Solr est dénommé « core ». L’index
que nous avons utilisé jusqu’à présent est configuré par
défaut pour s’appeler collection1
. Nous allons configurer
un autre index nommé movies
.
Placez-vous dans le répertoire soldir/example/solr
. C’est là que se trouve
le sous-répertoire collection1
avec sa configuration et les données
de l’index. Commençons par le copier (l’option -R
sous Unix indique une copie
récursive).
cp -R collection1 movies
cd movies
Maintenant, dans movies
, vous devriez avoir:
un fichier
core.properties
; éditez-le et changez la propriéténame
enmovies
;un répertoire
data
qui contiendra les données de l’index; videz-le avecrm -Rf data/*et enfin un répertoire
conf
qui contient la configuration de l’index, dont le schéma; c’est là que nous allons travailler.cd conf
Pour indexer notre base nous allons donner à Solr des documents Solr que vous pouvez récupérer à partir du Webscope. La seule différence avec la représentation JSON complète est que les artistes sont représentés simplement par leur nom et prénom. Voici un exemple, qui nous donne donc les champs à indexer.
{
"_id": "movie:57",
"title": "Jackie Brown",
"year": "1997",
"genre": "crime",
"summary": "Jackie Brown, hôtesse de l'air, arrondit ses fins de mois ()",
"country": "USA",
"director": "Quentin Tarantino",
"actors": ["Robert De Niro", "Pam Grier", "Bridget Fonda","Michael Keaton","Samuel Jackson"]
}
Commençons par définir le schéma correspondant à ce type de document.
Le schéma de l’index¶
Le schéma de l’index donne la liste de tous les champs d’un document Solr
et peut comprendre pour chacun de très nombreuses options parmi lesquelles:
le type du champ (numérique, entier),
la possibilité de calculer la valeur d’un champ à partir d’un autre,
des traitements divers à appliquer à la valeur du champ, etc. Nous allons
en étudier certaines mais pour l’instant nous commencerons avec le minimum.
Voici le squelette du fichier schema.xml
(qui, rappelons-le,
se trouve dans movies/conf
et que vous allez devoir compléter).
<?xml version="1.0" encoding="UTF-8" ?>
<schema name="example" version="1.5">
<!-- Liste des champs de l'index -->
<fields>
<field name="_id" type="string" indexed="true" stored="true"
required="true" />
<field name="title" type="string" indexed="true" stored="true"
required="true" />
<field name="summary" type="text" indexed="true" stored="false"
required="false" />
<!-- A compléter -->
<!-- Un champ dans lequel on concatène les autres pour une recherche "plein-texte" -->
<field name="text" type="text" indexed="true" stored="false"
multiValued="true" />
<copyField source="summary" dest="text" />
<copyField source="title" dest="text" />
<!-- Un champ "technique" requis par Solr/Lucene -->
<field name="_version_" type="long" indexed="true" stored="true" />
</fields>
<!-- La clé d'accès à un document dans l'index -->
<uniqueKey>_id</uniqueKey>
<!-- Configuration des types de champ -->
<types>
<fieldType name="string" class="solr.StrField" />
<fieldType name="int" class="solr.IntField" />
<fieldType name="long" class="solr.LongField" />
<fieldType name="text" class="solr.TextField">
<analyzer>
<tokenizer class="solr.StandardTokenizerFactory" />
<filter class="solr.LowerCaseFilterFactory" />
</analyzer>
</fieldType>
</types>
</schema>
Il comprend trois parties:
la liste des champs, dans l’élément
fields
, complétée par l’indication du champ de recherche par défaut;le champ qui identifie le document Solr, dans l’élément
uniqueKey
;la liste des types de champ, dans l’élément
types
.
Note
Pour des besoins internes, tout schéma doit contenir un champ _version_
défini comme ci-dessus.
Définition des types et de la clé¶
Chaque type utilisé dans le schéma d’un index doit apparaître
dans un des éléments fieldType
du fichier schema.xml
.
Nous n’allons pas passer beaucoup de temps sur les types de champ pour l’instant. Disons brièvement
que Solr fournit tout un ensemble de types pré-définis qui suffisent pour
les besoins courants, qu’un type Solr correspond à une classe Java (le
type est en fait spécifié par la classe), et qu’on peut associer
des options à un type. Solr impose et contrôle le typage, ce que l’on
peut voir comme un avantage ou un inconvénient (y réfléchir!).
Les options indiquent d’éventuels traitements à appliquer
à chaque valeur du type avant son insertion dans l’index. Dans notre
schéma ci-dessus, nous avons par exemple un type text
pour lequel
nous définissons un « analyseur » StandardTokenizerFactory
qui va
se charger de « découper » le texte en « tokens » pour une recherche plein-texte.
C’est un élément très important de l’indexation qui mérite qu’on y consacre
du temps et de la réflexion, ce que nous ferons dans la prochaine section.
Retenez pour l’instant
que cette spécification permet d’indexer chaque mot d’un texte
et donc d’effectuer une recherche sur n’importe quelle combinaison de mots.
L’élément uniqueKey
permet de rechercher un document dans l’index par sa clé. C’est
une opération qui s’avère le plus souvent indispensable, ne serait-ce que pour savoir qu’un
document est indexé. Même si l’élément n’est pas obligatoire à proprement parler,
le définir systématiquement semble une bonne pratique.
Définition des champs¶
Examinons la définition d’un champ de l’index, en prenant l’exemple de l’identifiant.
<field name="_id" type="string" indexed="true"
stored="true" required="true" multiValued="false" />
Les attributs de l’élément XML caractérisent le champ. Le nom et le type sont les informations de base. Ensuite, on peut trouver toutes sortes d’attributs. La plupart, ayant une valeur par défaut, sont optionnels. C’est le cas des suivants, mais nous les avons fait figurer en raison de leur importance.
indexed
indique simplement que le champ peut être utilisé dans une recherche;
stored
indique que la valeur du champ est stockée dans l’index, et qu’il est donc possible de récupérer cette valeur comme résultat d’une recherche, sans avoir besoin de retourner à la base principale; en d’autres termes,stored
permet de traiter l’index aussi comme une base de données;
required
indique que le champ est obligatoire;enfin,
multivalued
vauttrue
pour les champs ayant plusieurs valeurs, soit, concrètement, un tableau en JSON; c’est le cas par exemple pour le nom des acteurs.
Il faut comprendre l’impact des champs indexed
et stored
dont
toutes les combinaisons sont possibles.
indexed='true'
,stored='false'
: on pourra interroger le champ, mais il faudra accéder au document principal dans la base documentaire si on veut sa valeur;
indexed='true'
,stored='true'
: on pourra interroger le champ, et accéder à sa valeur dans l’index;
indexed='false'
,stored='true'
: on ne peut pas interroger le champ, mais on peut récupérer sa valeur dans l’index;
indexed='false'
,stored='false'
: n’a pas de sens à priori; le seul intérêt est d’ignorer le champ s’il est fourni dans le document Solr.
Important
Vous vous posez peut-être la question: comment peut-on indexer un champ sans le stocker? Et bien c’est notamment le cas pour les textes qui sont décomposés en termes, chaque terme étant ensuite indexé indépendamment sous la forme d’une liste inversée. Il est alors impossible de reconstituer le texte avec les données de l’index, d’où l’intérêt de conserver ce dernier dans son intégralité, à part.
Bien entendu, le choix est une question de compromis: pour simplifier, stocker une valeur prend plus d’espace que l’indexer. Dans la situation la plus extrême, on dupliquerait la base documentaire en stockant chaque document aussi dans l’index. Un stockage plus important implique de moins bonnes performances.
Note
Y a-t-il une valeur par défaut pour les options ci-dessus? Et bien
ce n’est pas très clair, les valeurs par défaut de indexed
et stored
par exemple sont héritées du type du champ (par exemple TextField
),
et au niveau du type on ne sait pas toujours comment c’est défini. Il semble
donc préférable de toujours les mettre explicitement, ce qui a aussi l’avantage
d’être lisible.
Notre squelette de schéma comprend également un champ « calculé », le champ text
.
Les instructions copyField
indiquent qu’au moment de l’insertion d’un document,
on va « copier » certains champs dans celui-ci. Pourquoi? Pour deux motivations
principales:
le type du champ « destination » correspond à un mode particulier d’indexation, éventuellement différent et complémentaire de celui du champ « origine »; par exemple le contenu d’un titre est indexé comme une chaîne de caractères dans le champ
title
, et comme un texte « tokenisé » quand on le copie dans le champtext
;si toutes les occurrences de chaînes de caractères sont concaténées et placées dans un même champ, on obtient, en prenant ce champ pour cible, une recherche plein-texte globale, une fonctionnalité souvent utile.
La spécification dans notre schéma est à compléter selon votre goût.
Recharger le schéma¶
Après tout changement de schéma, il faut recharger l’index. Pour recharger un index, à partir de l’interface d’administration, utilisez l’option Reload après avoir sélectionné le core, comme montré sur la figure Rechargement d’un index après changement de schéma.
Voici finalement la commande de chargement (en supposant que vous avez appelé solr_doc.json
l’extrait de la base Webscope).
curl 'http://localhost:8983/solr/movies/update/json?commit=true' \
--data-binary @solr_doc.json -H 'Content-type:application/json'
Quand le chargement est effectué avec succès, vous devriez pouvoir consulter les statistiques sur votre nouvel index avec l’interface Solr, et exécuter des requêtes.
Attention, si l’index existant
ne correspond pas au nouveau schéma, le rechargement échouera. C’est une des grosses
différences entre Solr (ou un index en général)
et une base de données: il est difficile de faire évoluer un schéma.
Si le nouveau schéma est incompatible avec l’ancien, la seule solution est de reconstruire
l’index. Voici les commandes pour supprimer tous les documents de l’index movies
(une commande de destruction, suivie d’une commande de validation).
curl http://localhost:8983/solr/movies/update \\
--data '<delete><query>*:*</query></delete>' -H 'Content-type:text/xml; charset=utf-8'
curl http://localhost:8983/solr/movies/update \\
--data '<commit/>' -H 'Content-type:text/xml; charset=utf-8'
Vous pouvez alors modifier le schéma et recharger la base.
Consulter l’index¶
Continuons notre survol des fonctionnalités de Solr avec un aperçu de la principale: la recherche de documents selon certains critères. Solr est un moteur de recherche offrant des possibilités très sophistiquées et nous allons nous contenter des options de base dans ce qui suit. La manière d’effectuer une recherche (et d’obtenir des résultats) varie en fonction de nombreux facteurs, dont:
la syntaxe de la requête, qui va du très structuré (requête Booléenne sur des champs) au très informel (une liste de mots-clés);
le classement du résultat: une des grandes différences avec une recherche dans une base de données (relationnelle en particulier) est qu’un moteur de recherche comme Solr renvoie les documents considérés comme pertinents (notion à définir), ce qui va au-delà de ceux qui correspondent exactement aux critères de recherche.
Dans tout ce qui suit, nous allons utiliser l’interface query
de l’application Web
d’administration de Solr, accessible rappelons-le à http://localhost:8983/solr.
C’est
un moyen simple de tester l’interrogation. Dans le cadre d’une application complète,
on accède à un Solr grâce à l’interface de programmation que nous ne présentons pas ici.
Le principal paramètre que nous allons utiliser est simplement nommé q
pour query. Par défaut,
il est proposé dans l’interface avec la valeur *.*
ce qui permet de ramener tous les documents
de l’index.
Les autres paramètres sont, en bref:
fq
: pour filter query, un moyen d’interroger non pas l’index entier mais un résultat pré-calculé et stocké en cache;
sort
, pour trier le résultat;
start
etrow
, les paramètres classiques de pagination du résultat;
fl
pour field list, indique la liste des champs (stockés) à inclure dans le résultat;
df
, le champ à interroger si non spécifié dans la requête (la valeur par défaut est indiqué dans la configuration et vaut en principetext
, le champ dans lequel nous avons concaténé toutes nos chaînes de caractères);enfin, on trouve la liste des query parsers disponibles; un query parser correspond à une syntaxe d’interrogation particulière.
Voici quelques exemples d’utilisation de ces paramètres: vous êtes invités à les tester dans votre environnement.
avec
sort=title asc
: tri du résultat sur le titre, en ordre ascendant;avec
fl=title, year
, restriction des champs dans les documents du résultat;avec
start=10, rows=9
, récupération des documents classés entre les positions 10 à 19;avec
q=Alien
, vous devriez retrouver le document Solr correspondant au filmAlien
;avec
q=Alien
maisdf=summary
, vous ne devriez rien trouver;avec
q=title:Vertigo
,df=text
, vous devriez retrouver le film Vertigo;mais avec
q=title:vertigo
(en minuscules), on ne trouve rien. Solr serait-il sensible à la casse?… non car en cherchant
scottie
, on retrouve bien Vertigo, alors que Scottie apparaît avec une majuscule dans le résumé du film; si vous ne comprenez pas pourquoi relisez-attentivement le début du chapitre, et notamment la partie sur le schéma.
Vous remarquerez que le document résultat ne montre que ceux qui ont été définis dans
le schéma comme « stockés ». Aussi étrange que cela paraisse (à première vue) les autres champs sont
utiles pour la recherche, mais on ne peut pas récupérer leur valeur. En revanche, il est possible
d’obtenir des informations calculées par Solr, sous forme de (pseudo-)champ. C’est
le cas par exemple pour le score, qui évalue la pertinence d’un document pour une recherche.
Dans le paramètre fl
, donnez par exemple la liste title, year, score
. Faites
une recherche sur un mot-clé (par exemple, fin
) et vérifiez que le score détermine le
classement des films trouvés.
Par ce moyen, on peut en particulier obtenir des informations sur le pourquoi du score et
du classement: ajoutez l’expression [explain style=nl]
dans la liste des champs
fl
et appréciez les détails affichés. Un de nos objectifs à l’avenir est de comprendre
tout ce que cela veut dire.
Exercices¶
Exercice: construire l’index sur les films
Vous devez finir la configuration du schéma, en choisissant pour chaque champ les valeurs d’option appropriées (ne stockez pas le résumé, potentiellement volumineux, par exemple). Faites attention aux champs qui sont absents ou n’ont pas de valeurs (parfois, pas d’acteur par exemple). Rechargez l’index quand vous avez terminé.
Correction
Le fichier est ici.
S3: L’analyse de documents avec Solr¶
Supports complémentaires:
Voyons maintenant comment on analyse le contenu des documents à indexer. En présence d’un document textuel un tant soit peu complexe, on ne peut pas se contenter de découper plus ou moins arbitrairement en mots sans se poser quelques questions et appliquer un pré-traitement du texte. Les effets de ce pré-traitement doivent être compris et maîtrisé: ils influent directement sur la précision et le rappel. Quelques exemples simples pour s’en convaincre:
si on cherche les documents contenant le mot « loup », on s’attend à trouver ceux contenant « loups », « Loup », « louve »; si on ne normalise pas en supprimant les majuscules et les pluriels, on va dégrader le rappel; le cas de « loup / louve » illustre une transformation qui dépend de la langue et nécessite une analyse approfondie du contenu;
inversement, si on normalise « cote », « côte », « côté » en supprimant les accents, on va unifier des mots dont le sens est différent, et on va diminuer la précision.
Il faut donc trouver un bon équilibre, aucune solution n’étant parfaite. C’est de l’art et du réglage… Pour résumer, l’analyse se compose de plusieurs phases:
Tokenization: découpage du texte en « termes ».
Normalisation: identification de toutes les variantes d’écritures d’un même terme et choix d’une règle de normalisation (que faire des majuscules? acronymes? apostrophes? accents?).
Stemming (« racinisation »): rendre la racine des mots pour éviter le biais des variations autour d’un même sens (auditer, auditeur, audition, etc.)
Stop words (« mots vides »), quels mots garder pour représenter le contenu sémantique du document?
Ce qui suit est une brève introduction, essentiellement destinée à comprendre les outils prêts à l’emploi que nous utiliserons ensuite.
Tokenisation et normalisation¶
Un tokenizer prend en entrée un texte (une chaîne de caractères) et produit une séquence de tokens. Il effectue donc un traitement purement lexical, consistant typiquement à éliminer les espaces blancs, la ponctuation, les liaisons, etc., et à identifier les « mots ». Des transformations peuvent également intervenir (suppression des accents par exemple, ou normalisation des acronymes - U.S.A. devient USA).
La tokenization est très fortement dépendante de la langue. Une fois la langue identifiée, on divise le texte en tokens (« mots »). ce n’est pas du tout aussi facile qu’on le dirait!
Dans certaines langues (Chinois, Japonais), les mots ne sont pas séparés par des espaces.
Certaines langues s’écrivent de droite à gauche, de haut en bas.
Que faire (et de manière cohérente) des acronymes, élisions, nombres, unités, URL, email, etc.
Que faire des mots composés: les séparer en tokens ou les regrouper en un seul? Par exemple:
# Anglais: hostname, host-name et host name, … # Français: Le Mans, aujourd’hui, pomme de terre, … # Allemand: Levensversicherungsgesellschaftsangestellter (employé d’une société d’assurance vie).
Pour les majuscules et la ponctuation, une solution simple est de normaliser systématiquement (minuscules, pas de ponctuation). Ce qui donnerait le résultat suivant pour notre petit jeu de données.
\(d_1\):
le loup est dans la bergerie
\(d_2\):
le loup et les trois petits cochons
\(d_3\):
les moutons sont dans la bergerie
\(d_4\):
spider cochon spider cochon il peut marcher au plafond
\(d_5\):
un loup a mangé un mouton les autres loups sont restés dans la bergerie
\(d_6\):
il y a trois moutons dans le pré et un mouton dans la gueule du loup
\(d_7\):
le cochon est à 12 euros le kilo le mouton à 10 euros kilo
\(d_8\):
les trois petits loups et le grand méchant cochon
Stemming (racine), lemmatization¶
La racinisation consiste à confondre toutes les formes d’un même mot, ou de mots apparentés, en une seule racine. Le stemming morphologique retire les pluriels, marque de genre, conjugaisons, modes, etc. Le stemming lexical fond les termes proches lexicalement: « politique, politicien, police (?) » ou « université, universel, univers (?) ». Ici, le choix influe clairement sur la précision et le rappel (plus d’unification favorise le rappel au détriment de la précision).
La racinisation est très dépendante de la langue et peut nécessiter une analyse linguistique complexe. En anglais, geese est le pluriel de goose, mice de mouse; les formes masculin / féminin en français n’ont parfois rien à voir (« loup / louve ») mais aussi (« cheval / jument »: parle-t-on de la même chose?) Quelques exemples célèbres montrent les difficultés d’interprétation:
« Les poules du couvent couvent »: où est le verbe, où est le substantif?
« La petite brise la glace »: idem.
Voici un résultat possible de la racinisation pour nos documents.
\(d_1\):
le loup etre dans la bergerie
\(d_2\):
le loup et les trois petit cochon
\(d_3\):
les mouton etre dans la bergerie
\(d_4\):
spider cochon spider cochon il pouvoir marcher au plafond
\(d_5\):
un loup avoir manger un mouton les autre loup etre rester dans la bergerie
\(d_6\):
il y avoir trois mouton dans le pre et un mouton dans la gueule du loup
\(d_7\):
le cochon etre a 12 euro le kilo le mouton a 10 euro kilo
\(d_8\):
les trois petit loup et le grand mechant cochon
Il existe des procédures spécialisées pour chaque langue. En anglais, le « Potter stemming » est le plus connu.
Mots vides et autres filtres¶
Un des filtres les plus courants consiste à retire les mots porteurs d’une information faible (« stop words » ou « mots vides ») afin de limiter le stockage.
Les articles: le, le, ce, etc.
Les verbes « fonctionnels »: être, avoir, faire, etc.
Les conjonctions: et, ou, etc.
et ainsi de suite. [
Le choix est délicat car, d’une part, ne pas supprimer les mots vides augmente l’espace de stockage nécessaire (et ce d’autant plus que la liste associée à un mot très fréquent est très longue), d’autre part les éliminer peut diminuer la pertinence des recherches (« pomme de terre », « Let it be », « Stade de France »).
Parmi les autres filtres, citons en vrac:
Majuscules / minuscules*. On peut tout mettre en minuscules, mais Lyonnaise des Eaux, Société Générale, Windows, etc.
Acronymes. CAT = cat ou Caterpillar Inc.? M.A.A.F ou MAAF ou Mutuelle … ?
Dates, chiffres. Monday 24, August, 1572 – 24/08/1572 – 24 août 1572; 10000 ou 10,000.00 ou 10,000.00
Dans tous les cas, les même règles de transformation s’appliquent aux documents ET à la requête. Voici, au final, pour chaque document la liste des tokens après application de quelques règles simples.
\(d_1\):
loup etre bergerie
\(d_2\):
loup trois petit cochon
\(d_3\):
mouton etre bergerie
\(d_4\):
spider cochon spider cochon pouvoir marcher plafond
\(d_5\):
loup avoir manger mouton autre loup etre rester bergerie
\(d_6\):
avoir trois mouton pre mouton gueule loup
\(d_7\):
cochon etre douze euro kilo mouton dix euro kilo
\(d_8\):
trois petit loup grand mechant cochon
Les analyseurs Solr¶
Un moteur de recherche comme
Solr permet d’associer à chaque type de champ la spécification d’une analyse à appliquer
à chaque valeur indexée du champ. Si vous reprenez le squelette de schéma fourni
pour notre collection movies, vous pouvez noter la définition d’un analyzer
pour les champs de type text
.
<fieldType name="text" class="solr.TextField">
<analyzer>
<tokenizer class="solr.StandardTokenizerFactory" />
<filter class="solr.LowerCaseFilterFactory" />
</analyzer>
</fieldType>
Ce simple exemple montre les trois concepts qui interviennent dans la définition d’un analyseur:
le tokenizer effectue le traitement lexical consistant à transformer le texte en un ensemble de tokens ;
les filtres (filter) examinent les tokens un par un et décident de les conserver, de les remplacer par un ou plusieurs autres;
enfin, l’analyseur est une chaîne de traitement (pipeline) constituée de tokeniseurs et de filtres; on peut constituer ainsi des processus de transformation du texte arbitrairement complexes.
Un filtre prend une séquence de tokens et produit une autre séquence de tokens. Chaque token considéré par un filtre peut être conservé tel quel, éliminé, ou remplacé par un ou plusieurs tokens. Par exemple:
le filtre
LowerCaseFilterFactory
met tous les tokens en minuscules;le filtre
SynonymFilterFactory
remplace un token par la liste de ses synonymes;le filtre
StopFilterFactory
élimine les stop words.le filtre
PorterStemFilterFactory
applique le stemming selon la méthode Porter pour les mots anglais.
En règle générale, le même analyzer doit être appliqué aux documents et à la requête,
pour des raisons évidentes de cohérence (si, par exemple, on retire les accents dans les documents
mais pas dans la requête, la recherche ne marchera pas ou très mal). Dans de rares cas
où des traitements différents devraient être appliqués, Solr permet la spécification
de deux analyseurs. Voici, à titre d’exemple (notez l’attribut XML type
),
une spécification qui donnerait des résultats désastreux !
<fieldType name="nametext" class="solr.TextField">
<analyzer type="index">
<tokenizer class="solr.StandardTokenizerFactory"/>
<filter class="solr.LowerCaseFilterFactory"/>
</analyzer>
<analyzer type="query">
<tokenizer class="solr.StandardTokenizerFactory"/>
<filter class="solr.UpperCaseFilterFactory"/>
</analyzer>
</fieldType>
Nous laissons donc cette option de côté. En résumé, la spécification d’un analyseur comprend un tokenizer qui transforme un flux de caractères en une séquence de « mots » (appelés tokens), suivi d’une liste de filtres qui effectuent chacun des transformations de mots selon certaines règles. Voyons maintenant dans le détail ces étapes.
Tester votre configuration¶
L’analyseur Solr pour un index (un core) se configure dans le fichier schema.xml
. Pour vérifier
que cette configuration convient, Solr fournit un outil extrêmement utile: un formulaire
Web dans lequel on peut saisir du texte et consulter l’analyse appliquée à ce texte et son résultat.
Accédez à l’interface Web de Solr (http://localhost:8983/solr), choisissez le core movies
dans la barre de menu à gauche, puis le choix analysis
. Vous obtenez une interface
semblable à celle de la figure Test d’un analyseur avec l’interface de Solr.
Nous utilisons cet utilitaire de la manière suivante:
dans le champ field value (index), on saisit un texte à analyser;
dans le menu déroulant au dessous, on choisit un des champs du schéma de l’index; c’est donc l’analyseur de ce champ qui sera appliqué.
Quand on lance l’analyse, Solr montre les différentes étapes (tokenizer, filtres) et leurs
résultats. On peut donc visualiser tout ce qui se passe quand on fournit, dans un
document Solr, une valeur à indexer. Dans l’exemple illustré par la figure, on voit
que le texte est l'histoire d'une année
. Avec notre configuration de l’analyseur
les élisions (l”) sont conservées, ainsi que les accents. Il apparaît donc que si l’on
cherche histoire
dans l’index, rien de sera ramené. De même, si on la malchance
d’utiliser un clavier sans accent, on ne pourra pas rechercher annee
(en tout cas
l’association avec année
ne sera pas trouvée). Il est probable que l’on
souhaiterait ne pas indexer d'une
qui apporte peu d’information sur
le sens de l’expression indexée.
Notre analyseur ne fait donc pas l’affaire (pour le français). À vous de jouer!
Note
Puisque nous appliquons le même analyseur à la création de l’index et à la requête,
l’analyse appliquée à une valeur entrée dans field value (query)
donnerait des résultats identiques.
L’interface présentée ci-dessus rend très facile la mise au point d’un analyseur. Les étapes sont les suivantes:
test d’un texte court pour vérifier si une transformation souhaitée (élimination des accents par exemple) est obtenue ou non;
si l’analyseur doit être modifié, on édite le fichier
schema.xml
et on change la configuration du champ indexé;il faut recharger le core avec la fonction disponible dans l’interface Solr (Core admin);
et on recommence autant de fois que nécessaire.
Exercices¶
Le but de ces exercices est de configurer notre index movies pour que les
textes (en français) placés dans le champ text
soient correctement analysés. Il s’agit pour l’essentiel de trouver
et d’appliquer le bon tokenizer ou le bon filtre. À vous de chercher sur le Web ou la
documentation Solr: c’est un bon moyen d’explorer au-delà du strict exercice proposé.
Exercice: traitement du texte
Réglez l’analyseur pour que les transformations suivantes soient effectuées.
suppression des élisions (l”, d”, jusqu”, etc.);
suppression des accents (plusieurs solutions: les étudier);
enfin ajouter un filtre de racinisation (stemming) adapté au français.
Au final, faites des tests avec le résumé de quelques films pour voir le résultat.
Correction
Le filtre à ajouter pour les élisions.
<filter class="solr.ElisionFilterFactory" />
Filtre pour les accents (attention, cette solution transforme tout caractère ASCII « étendu » en l’un des caractères ASCII compris entre les codes 0 et 127. Le « ç » par exemple devient « c ».
<filter class="solr.ASCIIFoldingFilterFactory"/>
Exercice: élimination des stop words
Maintenant ajoutez à votre analyseur un filtre éliminant les stop words. Vous trouverez facilement sur le Ouaibe des fichiers de stop words prêts à l’emploi. Tous les articles, prépositions, articles, etc., devraient ne pas être indexés.
Correction
Le filtre à ajouter pour les mots vides (en utilisant le fichier fourni avec Solr!).
<filter class="solr.StopFilterFactory" ignoreCase="true"
words="lang/stopwords_fr.txt" format="snowball" />
Exercice: ajout de synonymes (optionnel)
Finalement, testez l’option de gestion des synonymes. Solr fournit un filtre des synonymes
(SynonymFilter
)
qui s’appuie sur un fichier contenant, sur chaque ligne, une liste de synonymes. Par exemple:
Télévision, Télévisions, TV, TVs
C’est à vous de créer ce fichier et de le référencer quand vous placer le filtre des synonymes dans l’analyseur. Il faut ensuite réfléchir à quelques options:
Pratique-t-on par expansion ou par réduction? Dans le cas de l’expansion, chaque mot est « étendu » à l’ensemble des synonymes. Cela signifie qu’un document indexé avec
télévision
le sera aussi avec TV, ce qui augmente d’autant la taille de l’index. Dans le cas d’une réduction, tous les synonymes sont indexés sous un unique terme-représentant.Faut-il traiter de la même manière l’analyseur pour l’index et pour les requêtes? Réflechissez à ce qui peut se passer dans les divers cas de figure…
Enfin, faut-il traiter les synonymes avant ou après la racinisation?
Vous trouverez bien entendu des ressources sur le Web à ce sujet. Testez, validez, trouvez la bonne ou la mauvaise méthode: apprenez!
S4: Classement avec Solr¶
Pour conclure en pratique ce chapitre sur le classement avec la similarité cosinus basée sur le tf.idf, nous allons effectuer une mise en pratique avec Solr. L’objectif est d’obtenir l’explication du classement effectué par Solr pour une requête donnée, et de vérifier que nous comprenons ce qui se passe.
Mise en place de la base¶
Pour bien interpréter les calculs, nous prenons nos 4 documents sur les loups et les
moutons (vous pouvez prendre ceux sur les cochons, ou tout exemple qui vous convient mieux).
Vous allez créer un nouvel index (core) dans Solr, nommé testTfIdf
, en suivant les mêmes instructions
que celles décrites dans le chapitre Annexe: Solr, un moteur de recherche pour la mise en place de l’index
sur les films.
Le schéma est simple: les documents ont simplement un id et un champ
contenu
.
<?xml version="1.0" encoding="UTF-8" ?>
<schema name="example" version="1.5">
<!-- Liste des champs de l'index -->
<fields>
<field name="_id" type="string" indexed="true" stored="true"
required="true" />
<field name="contenu" type="text" indexed="true" stored="true"
required="false" />
<!-- Un champ dans lequel on concatène les autres pour une recherche "plein-texte" -->
<field name="text" type="text" indexed="true" stored="false"
multiValued="true" />
<copyField source="contenu" dest="text" />
<!-- Un champ "technique" requis par Solr/Lucene -->
<field name="_version_" type="long" indexed="true" stored="true" />
</fields>
<!-- La clé d'accès à un document dans l'index -->
<uniqueKey>_id</uniqueKey>
<!-- Configuration des types de champ -->
<types>
<fieldType name="string" class="solr.StrField" />
<fieldType name="int" class="solr.IntField" />
<fieldType name="long" class="solr.LongField" />
<fieldType name="text" class="solr.TextField">
<analyzer>
<tokenizer class="solr.StandardTokenizerFactory" />
<!-- Normalisations diverses -->
<filter class="solr.LowerCaseFilterFactory" />
<filter class="solr.ElisionFilterFactory"/>
<filter class="solr.PatternReplaceFilterFactory"
pattern="^(\p{Punct}*)(.*?)(\p{Punct}*)$" replacement="$2"/>
<filter class="solr.SnowballPorterFilterFactory"
language="French" protected="protwords.txt"/>
</analyzer>
</fieldType>
</types>
</schema>
Rechargez votre core, et insérez le document suivant, nommé loups.json
(ou un autre, mais c’est à vous de le créer).
[
{
"_id" : "A",
"contenu" : "Le loup est dans la bergerie"
} ,
{
"_id" : "B",
"contenu" : "Les moutons sont dans la bergerie"
},
{
"_id" : "C",
"contenu" : "Un loup a mangé un mouton, les autres loups sont restés dans la bergerie."
},
{
"_id" : "D",
"contenu" : "Il y a trois moutons dans le pré, et un mouton dans la gueule du loup."
}
]
Pour rappel, voici la commande de chargement.
curl 'http://localhost:8983/solr/testTfIdf/update/json?commit=true'
--data-binary @loups.json -H 'Content-type:application/json'
Vous devriez pouvoir accéder à votre core avec l’interface Web, et effectuer des
requêtes comme contenu:loup
pour rechercher tous les documents qui parlent
de loup(s).
Comprendre le classement Solr¶
Solr nous fournit des informations sur le classement d’une requête si on ajoute le
paramètre debugQuery=on
. Avec l’interface Web, il suffit de cocher une case. Par ailleurs,
il est préférable pour consulter les informations de choisir le format de sortie XML.
La figure Afficher les explications de Solr sur le classement montre la fenêtre d’interrogation avec les deux options
(xml
et debugQuery
) appropriées.
Le résultat contient alors un élément XML debug
qui vous donne des informations
sur la requête et sur le classement. Voici le début de ce document:
<lst name="debug">
<str name="rawquerystring">contenu:loup</str>
<str name="querystring">contenu:loup</str>
<str name="parsedquery">contenu:loup</str>
<str name="parsedquery_toString">contenu:loup</str>
<lst name="explain">
<str name="A">
0.375 = (MATCH) weight(contenu:loup in 0) [DefaultSimilarity], result of:
0.375 = fieldWeight in 0, product of:
1.0 = tf(freq=1.0), with freq of:
1.0 = termFreq=1.0
1.0 = idf(docFreq=3, maxDocs=4)
0.375 = fieldNorm(doc=0)
</str>
</lst_name>
C’est ici que votre sens de l’interprétation intervient: sur la base du cours, vous devriez
repérer des informations comme tf
, idf
, fieldNorm
, et en déduire le calcul
du score (weight) obtenu par Solr. Attention: Solr (Lucene en fait) applique des formules
légèrement différentes de celles que nous avons données. Par exemple, l’idf est calculé avec
- Vous en saurez plus en consultant les explications de la page
https://lucene.apache.org/core/4_0_0/core/org/apache/lucene/search/similarities/TFIDFSimilarity.html.
La documentation « officielle », par forécement très claire.
http://www.openjems.com/solr-lucene-score-tutorial/
Un bon tutorial qui explique avec exemples comment le calcul est fait en pratique (avec ajustements, arrondis, etc., d’où une certaine difficulté à s’y retrouver)
Regroupements (clustering)¶
La similarité cosinus peut être utilisée non seulement pour mesurer la pertinence d’un document par rapport à une requête, mais aussi la proximité des documents entre eux. Deux documents sont d’autant plus proches que l’angle qu’ils forment est petit.
Sur la base de cette mesure de proximité, il est possible de développer un algorithme de regroupement (clustering). Solr fournit une intégration du moteur de regroupement Carrot (http://project.carrot2.org/). Pour avoir un aperçu de cette option, lancez Solr avec la commande suivante.
java -Dsolr.clustering.enabled=true -jar start.jar
Puis, dans l’interface Web, prenez la collection collection1
fournie
comme exemple de base avec Solr, dans laquelle vous avez chargé les quelques
documents XML du répertoire exampledocs
. Le clustering de Solr est configuré
par défaut pour s’appliquer au schéma de ces documents.
Dans l’interface d’administration, lancez une requête *.*
après avoir
remplacé /select
par /clustering
dans le champ Request-handler
. Vous obtenez
une liste de groupes (clusters) dont voici un échantillon.
{"clusters": [
{
"labels": [
"iPod"
],
"score": 1.3174612693376382,
"docs": [
"F8V7067-APL-KIT",
"IW-02",
"MA147LL/A"
]
},
{
"labels": [
"Hard Drive"
],
"score": 3.781542178548597,
"docs": [
"SP2514N",
"6H500F0"
]
}
]
}
Entrer dans les détails de l’algorithme et de sa configuration nous amènerait un peu trop loin, mais cela peut faire l’objet d’un projet pour le cours. Vous trouverez un point de départ ici: https://cwiki.apache.org/confluence/display/solr/Result+Clustering.
Exercices¶
Exercice Ex-S4-1: pesons les loups et les moutons avec Solr
Reprenez les requêtes suivantes:
\(q_1\). « loup et pré »
\(q_2\). « loup et mouton »
\(q_3\). « bergerie »
\(q_4\). « gueule du loup »
consultez le résultat donné par Solr pour le classement, et tentez de l’expliquer.
Au fait, quel est le poids des termes de la requête? Par défaut, il sont tous égaux à 1, et donc considérés comme d’importance égale. Vous pouvez changer cela dans la syntaxe de la requête. Par exemple, la requête suivante accorde le même poids aux loups et aux cochons:
loup cochon
Si vous êtes surtout intéressés par les documents parlant de cochons, et accessoirement de loups, vous pouvez indiquer un poids plus important (que 1) pour vos cochons:
loup cochon^2
Vous êtes invités à tester cette option et son influence sur le classement.
Quiz¶
Répondez aux questions suivantes pour vérifier que vous avez bien compris le cours.
On exprime une requête avec un seul terme. Est-ce que l’idf du terme a une importance pour le classement?
Quel est l’idf d’un terme qui apparaît dans chaque document de la collection (c’est le moment de regarder une table des logarithmes si vous ne l’avez pas en tête) ?
Quel est l’impact de la base du logarithme de l’idf sur le classement ?
Je soumets une requête \(t_1 t_2 \cdots t_n\). Quel est le poids de chaque terme dans le vecteur représentant cette requête? La normalisation de ce vecteur est elle importante pour le classement (justifier)?
Je soumets une requête composée de deux termes: « tératologie » et « document ». Lequel a le plus d’importance pour le classement, et comment est-ce pris en compte par le moteur de recherche?
Dans la figure euclidiandistance, qu’obtient-on en projetant un vecteur sur chaque axe?
Je soumets une requête composée de deux termes: \(t_1\) et \(t_2\). Chaque document contient soit \(t_1\), soit \(t_2\) mais jamais les deux ensemble. De plus il y a autant de documents avec \(t_1\) que de documents avec \(t_2\). Quels sont les valeurs possibles des poids calculés par le moteur de recherche?
Même question si la requête est simplement \(t_1\).
Un document peut-il obtenir un classement élevé même s’il ne contient pas tous les termes de la requête? Donner un exemple ou un contre-exemple (le plus simple possible).