La recherche Full Text avec Solr

Configurer un moteur de recherche performant à l’aide d’Apache Lucene/Solr et Apache Tomcat

Guillaume Rossolini

Apache Lucene est un moteur d’indexation de texte permettant d’effectuer des recherches en langage naturel à l’aide de diverses manipulations automatiques du texte. Le texte indexé est enregistré sous de multiples représentations, de même pour le texte recherché, et les résultats de recherche sont déterminés suite à la comparaison de ces variantes.

Apache Solr étend le principe de Lucene en facilitant l’administration (interface RESTful) et en ajoutant des fonctionnalités : filtres de recherche, manipulation des résultats, etc.

Dans ce tutoriel, vous apprendrez à mettre en place un serveur Tomcat avec plusieurs schémas Solr.

Cet article est destiné aux programmeurs ayant déjà mis en place au moins un moteur de recherche sur un site ou dans une application. Si vous n’avez encore jamais mis en place de moteur de recherche ou si vous n’avez remarqué aucun problème en les utilisant, vous risquez de ne pas comprendre certaines subtilités, voire l’intérêt d’utiliser Solr.

Commentez ♪

- Titre : La recherche Full Text avec Solr

- Auteur : Guillaume Rossolini

- Parution : 10 mars 2010

- Mise à jour : 19 avril 2012

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2010 Guillaume Rossolini. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. Introduction

- I-A. La recherche intuitive

- I-B. La recherche Full-Text

- I-B-1. Le concept

- I-B-2. La recherche par lexèmes

- I-B-2-a. Introduction

- I-B-2-b. Racinisation

- I-B-2-c. Flexion

- I-B-3. La recherche phonétique

- I-B-3-a. Introduction

- I-B-3-b. Soundex

- I-B-3-c. Metaphone et Double Metaphone

- I-B-3-d. Autres algorithmes

- I-B-3-e. Exemples

- I-B-4. Synthèse des traitements linguistiques

- I-B-5. Les moteurs full-text indépendants

- I-B-6. Fonctionnalité full-text dans les SGBD

- I-C. Remerciements

- II. Fonctionnement de Solr

- II-A. Le concept

- II-B. Serveur hôte (Tomcat)

- II-C. Manipulation d’un index

- II-C-1. Consultation

- II-C-2. Maintenance

- II-C-3. Administration

- II-D. Format de la réponse

- III. Installation et configuration des applications

- III-A. Prérequis techniques

- III-B. Installer Tomcat

- III-B-1. Installer Tomcat sous Microsoft Windows

- III-B-2. Installer Tomcat sous Linux

- III-C. Ajouter la webapp Solr à Tomcat

- III-C-1. Installer Solr sous Microsoft Windows

- III-C-2. Installer Solr sous Linux

- III-C-3. Configuration de Tomcat pour utiliser Solr

- III-D. Ajouter des noyaux à Solr

- III-D-1. En théorie

- III-D-2. Configuration minimale d’un noyau

- III-D-3. En pratique : installer le noyau d’exemple

- IV. La structure des documents du noyau : schema.xml

- IV-A. Les types de données du document : /schema/types

- IV-A-1. Introduction

- IV-A-2. Les attributs disponibles pour chaque type de données

- IV-A-3. Les types simples

- IV-A-4. Les types complexes

- IV-A-4-a. Description d’un type complexe

- IV-A-4-b. Filtrer les caractères : les CharFilterFactories

- IV-A-4-c. Décomposer la chaîne : les TokenizerFactories

- IV-A-4-d. Manipuler les termes : les TokenFilterFactories

- IV-A-5. Exemple de déclaration de types

- IV-B. Les champs du document : /schema/fields

- IV-B-1. Introduction

- IV-B-2. Les attributs disponibles pour chaque champ de données

- IV-B-3. Exemple de déclaration de champs

- IV-C. Finaliser le schéma

- IV-D. Tester le schéma

- V. Maintenance des données d’un index

- V-A. Configuration nécessaire

- V-B. Ajout et modification de données

- V-C. Suppression de données

- V-D. Validation de mises à jour

- V-E. Annulation de mises à jour

- V-F. Soumettre le document de maintenance

- V-G. Exemple

- VI. Interroger un index de données

- VI-A. Préparation

- VI-B. Surcharge de paramètres

- VI-C. Composant solr.StandardRequestHandler

- VI-C-1. Paramètre « q »

- VI-C-2. Paramètre « fl »

- VI-C-3. Paramètre « sort »

- VI-C-4. Paramètres « start » et « rows »

- VI-C-5. Paramètre « fq »

- VI-C-6. Autres paramètres

- VII. L’API du noyau : solrconfig.xml

- VII-A. Directives de configuration du noyau

- VII-B. Inclusion de classes Java

- VII-C. Pré configurer une requête grâce à un requestHandler

- VIII. Un schéma d’exemple : index de livres au format poche

- VIII-A. Cahier des charges

- VIII-B. solrconfig.xml (composants)

- VIII-C. schema.xml

- VIII-D. Envoi des données au noyau

- VIII-E. Interrogation du noyau

- VIII-E-1. Conseils pour bien suivre le déroulement de cet exemple

- VIII-E-2. Recherche à partir d’une chaîne saisie par l’utilisateur

- VIII-E-2-a. Découpage de la chaîne saisie : solr.SearchHandler

- VIII-E-2-b. Explication de la présence des résultats : solr.HighlightComponent

- VIII-E-2-c. Explication du score des résultats : solr.DebugComponent

- VIII-E-2-d. Filtres automatiques sur les résultats : solr.FacetComponent

- VIII-E-2-d-i. Fonctionnement général

- VIII-E-2-d-ii. Les requêtes-facettes : facet_query

- VIII-E-2-d-iii. Les champs facettes : facet_field

- VIII-E-2-d-iv. Les facettes sur un type date : facet_date

- VIII-E-2-d-v. Les facettes par intervalles : facet_range

- VIII-E-2-d-vi. Exemple complet

- VIII-E-2-e. Statistiques sur le jeu de résultats : solr.StatsComponent

- VIII-E-3. Similitude de documents : MoreLikeThis

- VIII-F. solrconfig.xml (request handlers)

- IX. Conclusion générale

I - Introduction

I-A - La recherche intuitive

Dans mon expérience, la plupart des sites Internet utilisent une base de données relationnelle (type MySQL, PostgreSQL, Microsoft SQL Server ou autre) pour conserver leurs données. Ces données sont donc enregistrées de manière très structurée, mais elles ne sont accessibles que par le langage SQL.

L’avantage de cette solution est que l’on peut aisément retrouver un enregistrement lié à partir d’un autre : quel est le dernier article écrit dans chaque catégorie, quel est l’âge de la personne dont je suis en train de consulter le profil, etc. Retrouver une donnée élémentaire (un champ) à partir d’une autre donnée élémentaire (un identifiant) est une tâche que SQL accomplit à merveille.

Cependant, le comportement naturel d’un visiteur sur un site Web n’est pas toujours de passer par les menus très hiérarchiques et successifs mis à sa disposition. Prenons l’exemple de Google : combien connaissez-vous d’internautes qui ne prennent plus la peine de configurer des marque-pages dans leur navigateur, préférant chercher leurs sites favoris sur Google chaque fois qu’ils en ont besoin ? Eh oui, une très grande proportion des internautes préfère utiliser un moteur de recherche efficace et rapide à une navigation plus efficace, mais segmentée. C’est le concept de recherche en langage naturel qui séduit ces internautes.

Il convient de rappeler l’une des grandes faiblesses de SQL : sa pauvreté en matière de permutations de texte (instructions LIKE et REGEXP). Que se passe-t-il lorsqu’un visiteur fait une faute de frappe dans votre moteur de recherche à base de SQL, sans doute n’obtient-il pas de résultats ou pas les résultats attendus ? Ou lorsqu’il recherche un nom propre, a-t-il correctement placé chaque accent, trait d’union ou apostrophe ? Ou bien avez-vous calculé vous-même toutes les permutations possibles et les avez-vous gardées dans votre base de données ? Pensez-vous que vote visiteur, frustré par la faiblesse de votre moteur de recherche, ne va pas très rapidement s’en lasser ? Après avoir effectué la même recherche sur Google, ce même visiteur ne va-t-il pas rapidement se retrouver sur un site concurrent ?

Pire, que se passe-t-il si le contenu lui-même de votre site n’est pas écrit et corrigé à la perfection ? Une proportion de vos documents est vraisemblablement exclue de vos résultats de recherche.

Certes, les moteurs de recherche tels Google fonctionnent bien, mais ce sont aussi des visites qui ne sont pas effectuées sur notre site et, par conséquent, nous ne pouvons pas les inclure dans des études des habitudes de comportement. Ainsi, il serait utile de proposer à nos visiteurs un système de recherche qui se rapproche de celui proposé par les grands moteurs de recherche.

Dans cet article, nous verrons qu’un tel système n’est pas réservé à quelques multinationales. En effet, il existe des solutions open source et gratuites qui répondent à notre besoin : nous présenterons ici Lucene au travers de son extension, Solr.

I-B - La recherche Full-Text

I-B-1 - Le concept

Nous partons de l’observation suivante : il est parfaitement illusoire de croire qu’il est possible d’obliger les utilisateurs d’un moteur de recherche à saisir leurs termes sous une forme déterminée (au masculin singulier, en minuscules, etc.), car ce n’est pas intuitif. Ainsi, notre moteur de recherche doit être capable d’analyser les termes saisis par l’utilisateur et de trouver des correspondances dans le texte indexé.

La recherche full text est le principe d’opérer des traitements (l’indexation) sur du texte afin d’y retrouver plus facilement des mots. Ce procédé consiste à effectuer une suite de manipulations sur chacun des mots du texte afin d’augmenter la pertinence des recherches.

En règle générale, les interfaces de recherche full text permettent de segmenter le texte en mots (tokens) selon diverses règles (espaces, ponctuation, changement de casse, etc.). Les manipulations possibles sur ces mots sont diverses : modifier la casse, remplacer les lettres accentuées, etc. Outre ces traitements simples (et qui ne sont pas toujours judicieux), il existe deux procédés plus complexes, mais qui donnent de meilleurs résultats : la recherche par radicaux (lexèmes) et la recherche par consonances (phonétique).

I-B-2 - La recherche par lexèmes

I-B-2-a - Introduction

Je ne suis linguiste qu’à mes heures perdues et je ne maîtrise pas à la perfection le vocabulaire que je vais utiliser dans ce chapitre. Je prie le lecteur de bien vouloir faire preuve d’indulgence à mon égard. Je corrigerai cet article si des imperfections sont relevées.

Le wiki de Solr décrit trois méthodes principales de recherche par lexèmes. La racinisation (Porter ou KStem) permet de retrouver la forme élémentaire du mot (son lexème). À l’inverse, la flexion détermine toutes les variantes du mot (déclinaisons, conjugaisons, ajout d’affixes ou modifications du radical). Enfin, le projet Hunspell permet d’effectuer divers traitements sur les mots : correction orthographique, racinisation, flexion, etc. en tenant compte des règles grammaticales de la langue spécifiée.

En contexte linguistique, il me semble que cette comparaison est inexacte dans la mesure où la flexion ne donne pas des lexèmes, mais des mots. Cependant, cette simplification sera suffisante pour la mise en application qui nous intéresse ici.

Ce mode de recherche est très efficace pour trouver les correspondances des mots faisant partie du vocabulaire d’une langue, mais sa pertinence est bien plus faible pour les noms propres, les néologismes et les autres mots n’en faisant pas partie.

I-B-2-b - Racinisation

Un lexème est la forme élémentaire de chaque mot, c’est en quelque sorte sa racine linguistique. C’est sa forme, réduite au point de ne pas avoir d’existence dans la langue écrite ou orale, mais qui sert de point de départ pour toutes les formes dérivées de ce mot. Le procédé permettant d’obtenir le lexème d’un mot s’appelle la racinisation, ou stemming en anglais.

Le lexème est le point de départ pour constituer les lemmes, qui sont eux-mêmes le point de départ pour constituer les mots. La racinisation est un procédé complexe et il est différent dans chaque langue, c’est pourquoi il n’existe pas d’algorithme informatique pour toutes les langues. De même, les algorithmes existants ne sont pas implémentés dans tous les langages informatiques. Ainsi, le langage informatique convenant le mieux à vos choix techniques (Java, .NET, PHP, etc.) n’a peut-être pas d’implémentation d’un algorithme de racinisation pour les langues qui vous intéressent. Un projet, cependant, facilite leur implémentation : Snowball. Il présente une liste d’exemples pour chaque algorithme, notamment en français.

Voici une liste de mots que nous utiliserons dans cette introduction (les traitements ont été effectués à l’aide de Solr 1.4) :

Tableau 1 : Racinisation (mots de la langue française)

Mot

Lexème anglais

Lexème français

Lexème espagnol

développa

développa

développ

developp

développable

développ

développ

developp

développaient

développai

développ

developpaient

développante

développant

développ

developp

développement

développ

développ

developpement

développer

développ

développ

developp

développeur

développeur

développeur

developpeur

développeuse

développeus

développ

developpeus

développons

développon

développon

developpons

enveloppe

envelopp

envelopp

envelopp

Nous pouvons aisément voir que la plupart des mots ont un lexème français commun : « développ ». Ainsi, une recherche sur l’un de ces mots pourrait permettre de sortir des résultats contenant la plupart des autres mots. D’un point de vue utilisateur, c’est effectivement souhaitable.

Il convient de rappeler que les lexèmes présentés ici sont déterminés par un programme informatique. Un être humain connaissant la langue française réduirait probablement toutes les conjugaisons de « développer » en « développ » ; de même, le mot « développeur » serait sans doute réduit au même lexème que pour « développeuse », c’est-à-dire « développ ».

Afin de mieux comprendre en quoi la racinisation n’est pas adaptée aux noms propres, voici la racinisation des exemples de Frédéric Brouard (cf. la partie phonétique ci-dessous) :

Tableau 2 : Racinisation (noms propres)

Mot

Lexème anglais

Lexème français

Lexème espagnol

MARTIN

martin

martin

martin

BERNARD

bernard

bernard

bernard

FAURE

faur

faur

faur

PEREZ

perez

per

perez

GROS

gros

gros

gros

CHAPUIS

chapui

chapuis

chapuis

BOYER

boyer

boi

boy

GAUTHIER

gauthier

gauthi

gauthi

REY

rey

rey

rey

BARTHELEMY

barthelemi

barthelemy

barthelemy

HENRY

henri

henry

henry

MOULIN

moulin

moulin

moulin

ROUSSEAU

rousseau

rousseau

rousseau

Nous pouvons voir ici le point faible de la racinisation pour les noms propres. La plupart des mots de ce dernier tableau ne sont pas réduits. Ceux qui ont été réduits ont généralement été confondus avec un verbe dont l’algorithme a cru reconnaître une forme conjuguée ou à l’infinitif et qu’il a réduit à son lexème supposé (mais incorrect).

I-B-2-c - Flexion

La flexion consiste à dériver un mot afin d’obtenir toutes ses représentations possibles : déclinaisons grammaticales (genre et nombre), conjugaisons, préfixes, suffixes et modifications du radical. C’est l’approche inverse de la racinisation, mais l’effet en est souvent proche.

En informatique, plusieurs programmes et bibliothèques permettent de fléchir un mot : ispell (d’adoption historiquement plus large), aspell/pspell (remplacement d’ispell), MySpell/Hunspell (créés par OpenOffice.org et utilisés par Mozilla), etc. Leur nombre croissant, un projet connexe a vu le jour : Enchant, qui propose une API commune et qui permet donc d’utiliser n’importe lequel de ces programmes avec les mêmes appels de fonctions.

L’inconvénient majeur de la méthode par flexion est qu’elle ne donne de bons résultats que si l’on part d’un mot équivalent au lexème (mot non fléchi). Par exemple, si dans le texte se trouve le mot « développaient » et qu’un utilisateur cherche le mot « développement », une méthode par flexion est incapable de les faire correspondre. En revanche, une méthode par racinisation permet de retrouver le lexème commun à ces deux mots : « développ ».

Voici la flexion des exemples cités pour la racinisation :

Tableau 3 : Exemples de flexion avec OpenOffice.org 3.2.1 Writer

Lexème ou mot

Flexions en français

développ

développa, développe, développé

développaien

développaient, développante, développable, développement, développable

développeme

développement, développeuse, développe, développante

développon

développons, développante, développée, développé, développable

À l’évidence, OpenOffice Writer ne se limite pas à de la flexion : il fait aussi une mesure de racinisation. Cela donne un exemple biaisé. Il est même possible qu’OOo effectue également des traitements supplémentaires, car il me proposait également « envelopper ». Néanmoins, cette liste d’exemples vous donne une idée de ce que permet d’obtenir le procédé de flexion.

I-B-3 - La recherche phonétique

I-B-3-a - Introduction

La phonétique consiste à représenter la manière dont un mot se prononce. Certains dictionnaires proposent une représentation phonétique des mots, et l’enseignement des langues vivantes requiert habituellement l’accoutumance à ces mêmes signes.

En informatique, la recherche informatique ne consiste pas à représenter les mots avec des signes spéciaux comme en phonétique, mais à trouver une représentation simplifiée à base de caractères ASCII, généralement en majuscules. Des exemples de telles représentations sont présentés dans l’article L’art des Soundex par Frédéric Brouard.

Avec un ensemble de résultats naturellement large, ce mode de recherche est très pratique pour complémenter la recherche par lexèmes. Utilisé seul, en revanche, il risque de donner un trop grand nombre de « faux positifs ».

I-B-3-b - Soundex

Soundex est un algorithme décrit en 1918 par Robert C. Russell and Margaret K. Odell. Il permet de décrire la prononciation des mots anglais au moyen de lettres et de chiffres.

Soundex est prévu exclusivement pour la langue anglaise et, même dans cette langue, il a des limites.

Dans les exemples présentés ici, nous supprimons tous les accents avant de calculer les codes Soundex.

I-B-3-c - Metaphone et Double Metaphone

Les algorithmes Metaphone (1990) et Double Metaphone (2000), tous deux par Lawrence Philips, permettent une meilleure représentation des mots pour la langue anglaise. Le Double Metaphone permet en principe de mieux identifier le contexte d’utilisation de chaque mot, et ce dans diverses langues (notamment le français).

Pour ma part, je ne suis pas persuadé qu’il soit possible de déterminer la prononciation d’un mot, et donc sa représentation informatique, sans savoir dans quelle langue (voire même dans quel dialecte local) il est utilisé. Toutefois, Double Metaphone semble être l’algorithme bénéficiant de la plus vaste adoption de nos jours.

I-B-3-d - Autres algorithmes

J’ai mentionné plus haut l’article de Frédéric Brouard sur L’art des Soundex : l’auteur y propose une alternative adaptée à la langue française, qu’il a baptisée Phonex et dont il propose l’implémentation dans plusieurs langages informatiques. Je laisse au lecteur le soin de déterminer s’il est judicieux de l’utiliser.

I-B-3-e - Exemples

Voici les équivalents phonétiques de nos exemples de racinisation et de flexion (les traitements ont été effectués à l’aide de Solr 1.4) :

Tableau 4 : Traitements phonétiques (mots de la langue française)

Mot

Soundex

Refined Soundex

Metaphone

Double Metaphone

développa

D141

D60207010

TFLP

TFLP

développable

D141

D60207010170

TFLP

TFLP

développaient

D141

D6020701086

TFLP

TFLP

développante

D141

D60207010860

TFLP

TFLP

développement

D141

D602070108086

TFLP

TFLP

développer

D141

D602070109

TFLP

TFLP

développeur

D141

D602070109

TFLP

TFLP

développeuse

D141

D6020701030

TFLP

TFLP

développons

D141

D6020701083

TFLP

TFLP

enveloppe

E514

E08207010

ENFL

ANFL

Pour comparaison, voici les exemples de Frédéric Brouard :

Tableau 5 : Traitements phonétiques (noms propres)

Mot

Soundex

Refined Soundex

Metaphone

Double Metaphone

MARTIN

M635

M809608

MRTN

MRTN

BERNARD

B656

B1098096

BRNR

PRNR

FAURE

F600

F2090

FR

FR

PEREZ

P620

P10905

PRS

PRS

GROS

G620

G4903

KRS

KRS

CHAPUIS

C120

C30103

KPS

XPS

BOYER

B600

B109

BYR

PR

GAUTHIER

G360

G40609

K0R

K0, KTR

REY

R000

R90

R

R

BARTHELEMY

B634

B109607080

BR0L

PR0L, PRTL

HENRY

H560

H0890

HNR

HNR

MOULIN

M450

M80708

MLN

MLN

ROUSSEAU

R200

R9030

RS

RS

I-B-4 - Synthèse des traitements linguistiques

Voici une synthèse permettant d’identifier quel traitement linguistique est le plus efficace dans chaque situation :

Tableau 6 : Traitements linguistiques (mots de la langue française)

Mot

Lexème anglais

Lexème français

Lexème espagnol

Soundex

Refined Soundex

Metaphone

Double Metaphone

développa

développa

développ

developp

D141

D60207010

TFLP

TFLP

développable

développ

développ

developp

D141

D60207010170

TFLP

TFLP

développaient

développai

développ

developpaient

D141

D6020701086

TFLP

TFLP

développante

développant

développ

developp

D141

D60207010860

TFLP

TFLP

développement

développ

développ

developpement

D141

D602070108086

TFLP

TFLP

développer

développ

développ

developp

D141

D602070109

TFLP

TFLP

développeur

développeur

développeur

developpeur

D141

D602070109

TFLP

TFLP

développeuse

développeus

développ

developpeus

D141

D6020701030

TFLP

TFLP

développons

développon

développon

developpons

D141

D6020701083

TFLP

TFLP

enveloppe

envelopp

envelopp

envelopp

E514

E08207010

ENFL

ANFL

Tableau 7 : Traitements linguistiques (collisions)

Mot

Lexème anglais

Lexème français

Lexème espagnol

Soundex

Refined Soundex

Metaphone

Double Metaphone

accident (en, fr)

accid

accident

accident

A235

A0306086

AKST

AKST

accede (en)

acced

acced

acced

A230

A03060

AKST

AKST

accéder (fr)

accéder

acced

acced

A236

A030609

AKKT

AKTR

acid (en)

acid

acid

acid

A230

A0306

AST

AST

acide (fr)

acid

acid

acid

A230

A03060

AST

AST

Tableau 8 : Traitements linguistiques (noms propres)

Mot

Lexème anglais

Lexème français

Lexème espagnol

Soundex

Refined Soundex

Metaphone

Double Metaphone

MARTIN

martin

martin

martin

M635

M809608

MRTN

MRTN

BERNARD

bernard

bernard

bernard

B656

B1098096

BRNR

PRNR

FAURE

faur

faur

faur

F600

F2090

FR

FR

PEREZ

perez

per

perez

P620

P10905

PRS

PRS

GROS

gros

gros

gros

G620

G4903

KRS

KRS

CHAPUIS

chapui

chapuis

chapuis

C120

C30103

KPS

XPS

BOYER

boyer

boi

boy

B600

B109

BYR

PR

GAUTHIER

gauthier

gauthi

gauthi

G360

G40609

K0R

K0, KTR

REY

rey

rey

rey

R000

R90

R

R

BARTHELEMY

barthelemi

barthelemy

barthelemy

B634

B109607080

BR0L

PR0L, PRTL

HENRY

henri

henry

henry

H560

H0890

HNR

HNR

MOULIN

moulin

moulin

moulin

M450

M80708

MLN

MLN

ROUSSEAU

rousseau

rousseau

rousseau

R200

R9030

RS

RS

Il apparaît que les algorithmes phonétiques sont intéressants pour les noms propres, surtout dans la mesure où ces types de mots peuvent revêtir différentes orthographes pour une même prononciation. Les mots faisant partie d’une langue, pour leur part, sont aisément retrouvés par la racinisation. En revanche, appliqués aux mots faisant partie d’une langue, les traitements phonétiques donnent beaucoup de faux positifs. Enfin, appliquée aux noms propres, la racinisation donne trop peu de résultats pour être utile.

I-B-5 - Les moteurs full-text indépendants

Fondamentalement, Solr est un moteur de recherche Full Text (ou texte plein en français). Nous pourrions donc utiliser un moteur open source qui s’intègre plus facilement à notre SGBD, par exemple Sphinx pour les moteurs MySQL et PostgreSQL. Malheureusement, la plupart des moteurs full text (du moins ceux sur lesquels je me suis renseigné) sont faibles pour la racinisation (ou stemming en anglais) de la langue française, c’est-à-dire retrouver la racine de chaque mot et s’en servir pour effectuer les comparaisons.

Par exemple, toutes les conjugaisons d’un verbe sont en fait des variantes du lexème de ce verbe, et une recherche sur le mot « développer » devrait également renvoyer des documents sur « développez », « développe », « développons », etc. Il en va de même pour l’accord du genre et du nombre et pour toute une série de déclinaisons possibles.

Solr implémente différents algorithmes de racinisation et pour un nombre de langues parfois supérieur aux autres moteurs. Malgré le nom du filtre utilisé par Solr (SnowballPorterFilter) suggérant l’exclusivité de l’algorithme Porter, il permet en réalité d’utiliser différents algorithmes. Il y en a notamment trois pour la langue anglaise : Porter, Porter2 et Levins. Un plugin permet également d’utiliser KStem, mais des conflits de licence empêchent ce plugin d’être distribué avec Solr, il doit donc être compilé par l’utilisateur.

Nous avons vu précédemment que, si elles sont utilisées seules, les méthodes de recherche par lexèmes ou phonétique ne sont pas suffisantes. Sans l’une, il est impossible de chercher des noms propres. Sans l’autre, les résultats contiennent trop de faux positifs et ils omettent les variantes grammaticales des mots.

Voyons ci-dessous quelles stratégies d’analyse linguistique sont proposées par les moteurs full-text open source énumérés par Wikipédia à l’heure de l’écriture de ces lignes :

Tableau 9 : Les moteurs full-text open source et leurs fonctionnalités d’analyse linguistique

Moteur

Recherche par lexèmes

Recherche phonétique

Correction

DataparkSearch (Licence GNU GPL)

Par flexion (ispell)

Non

Oui (aspell)

Ferret (Licence MIT)

Multilingue (cf. liste)

Non

Non

htdig (GNU GPL)

Oui (ispell)

Soundex, Double Metaphone

Oui (ispell)

mnoGoSearch (Licence GNU GPL ou propriétaire)

Par flexion (ispell)

Soundex

Non

Swish-e (Licence GNU GPL)

Oui

Soundex, Double Metaphone

Non

Xapian (Licence GNU GPL)

Multilingue (cf. liste)

Non

Non

Sphinx (Licence GNU GPL)

Anglais et russe

Via le SGBD

Non

Apache (Lucene) Solr (Licence Apache)

Multilingue (cf. liste), par flexion ou par racinisation

Soundex, Double Metaphone

Oui

Partant de ces observations et en nous rappelant le constant précédent (nous avons besoin à la fois de la recherche par lexèmes et phonétique), le seul moteur adapté à nos besoins est : Apache Solr.

I-B-6 - Fonctionnalité full-text dans les SGBD

Certains SGBD ont des fonctionnalités full-text suivant la norme ISO/IEC 13249-2 du langage SQL. Si vous souhaitez les essayer, voici quelques liens :

Microsoft SQL Server Express :

- La version complète de Microsoft SQL Server 2008 R2 Express (attention, la version habituelle ne contient pas le moteur full-text) ;

- La base de données d’exemple AdventureWorks 2008R2 ;

- En cas de problème lors de l’installation, voir les prérequis techniques.

Oracle Express :

- Oracle 10g Express Release 2 (10.2) ;

- La documentation.

PostgreSQL :

- PostgreSQL 9 ;

- La documentation.

MySQL Community Server

- MySQL Community Server 5.1 ou 5.5 ;

- La documentation pour MySQL 5.1 ;

- La documentation pour MySQL 5.5.

Si tous ces moteurs ont des fonctionnalités full-text, pourquoi choisir Solr ? L’une des raisons est que Solr offre bien plus de fonctionnalités que ce mode de recherche paramétrable : il permet également de renvoyer des filtres (facettes), des statistiques, un ensemble de documents similaires à un document donné, des corrections d’orthographe, etc. De plus, un index Solr peut être utilisé comme source de données pour diverses applications.

I-C - Remerciements

Comme pour la rédaction de tout article, j’ai un certain nombre de personnes à remercier pour leur soutien ou pour leurs corrections : Elsa T., Pierre R., tvibes, nils nicolas.

II - Fonctionnement de Solr

II-A - Le concept

Un noyau (core en anglais) Solr est décrit par un schéma (schema.xml) et un fichier de configuration (solrconfig.xml). Le fichier schema.xml définit les types et les champs de données, tandis que le fichier solrconfig.xml définit les manières d’utiliser ce schéma (c’est-à-dire l’API du noyau). Nous détaillerons plus loin ces deux fichiers.

Chaque schéma est prévu pour enregistrer un ensemble de documents. Chaque document est un ensemble de champs à valeur unique ou à valeurs multiples. En effet, un champ peut avoir plusieurs valeurs si son attribut multiValued a la valeur true. À noter qu’un index Solr (c’est-à-dire un couple schema.xml + solrconfig.xml) est orienté vers un unique format de document. Si on définit un schéma pour le document « produit », il ne convient pas d’y enregistrer également des documents « fournisseur ». Pour cela, il faudra prévoir un deuxième index.

Un schéma est prévu pour retrouver un document à partir de diverses informations. Il est fréquent de dupliquer les informations texte secondaires dans chaque document, contrairement aux habitudes des bases relationnelles d’utiliser des clefs étrangères. Par exemple, le nom du fournisseur et celui de chaque client est recopié dans chaque document « produit » afin de pouvoir retrouver un produit grâce à ces noms. En revanche, un index Solr n’est pas prévu pour retrouver les informations dont un document dépend ; par exemple, à partir d’un document « produit », il n’est pas aisé de demander à Solr de renvoyer une liste structurée de clients.

Préparer un noyau Solr nécessite une configuration assez lourde, très orientée vers la méthodologie empirique. D’après mes essais, il est en revanche assez aisé de construire un document à partir de données SQL (c’est une forme de dé normalisation). J’insiste sur un point : vous ne pourrez pas produire un schéma efficace si vous n’effectuez pas dessus un grand nombre de tests et si vous ne le faites pas évoluer en fonction de la manière dont il est effectivement utilisé.

Le schéma définit obligatoirement un champ qui sert d’identifiant unique aux documents : uniqueKey.

Deux termes seront utilisés tour à tour dans cet article : « index » et « noyau ». Pour un noyau simple, ces deux termes désignent sensiblement la même chose, mais ils s’appliquent à des contextes sémantiques différents : tandis que le terme noyau désigne l’ensemble configuration + données, index désigne uniquement les données contenues dans le noyau. Pour être précis, nous verrons plus loin qu’il peut y avoir plusieurs « index » par « noyau » ; cependant, par souci de simplicité ou par abus de langage, un mot est souvent employé pour l’autre.

II-B - Serveur hôte (Tomcat)

Je ne m’étendrai pas sur le principe fondamental des serveurs Web. Un guide rapide d’installation de Tomcat est présenté au chapitre suivant.

Par défaut, Tomcat utilise le port 8080 en HTTP, c’est donc celui que nous utiliserons tout au long de cet article. Dans le vocabulaire de Tomcat, Solr est une « webapp ». Par souci de simplicité pour cet article, la webapp s’appelle « solr » à la racine du serveur. L’URL de Solr est ainsi :
http://localhost:8080/solr/, ce qui vous laisse l’opportunité d’installer d’autres applications à côté de Solr sur votre Tomcat. Et, croyez-moi, vous en aurez envie tôt ou tard.

La variable « solr/home » dans Tomcat fait référence au répertoire contenant :

- le fichier WAR de Solr (c’est la webapp à proprement parler), qui par défaut s’appelle « apache-solr-3.3.0.war » ;

- le fichier « solr.xml » de configuration de notre webapp ;

- un répertoire « cores » contenant la configuration et les données de nos divers index (ce nom de dossier est arbitraire, il est défini dans le fichier solr.xml que nous détaillerons dans le chapitre d’installation du serveur).

Si nous avons trois index « cores/produits », « cores/fournisseurs » et « cores/clients », nous obtenons les URL : http://localhost:8080/solr/produits/, http://localhost:8080/solr/fournisseurs/ et http://localhost:8080/solr/clients/.

Par la suite, j’utiliserai l’URL http://localhost:8080/solr/CORENAME/ pour évoquer la racine de votre index Solr.

II-C - Manipulation d’un index

Il existe différents handlers (SolrRequestHandler) pour prendre en charge une requête dans Solr.

Voici les plus importants dans cet article :

- XmlUpdateRequestHandler : Modifier l’index à partir de commandes et de documents formatés en XML ;

- SearchHandler : Rechercher dans l’index (anciennement StandardRequestHandler, mais, depuis Solr 1.3, les deux classes sont identiques).

II-C-1 - Consultation

Un index Solr s’interroge par l’URL http://localhost:8080/solr/CORENAME/select?q=MOTS+RECHERCHÉS appelée avec la méthode HTTP GET.

La syntaxe d’interrogation, à savoir ici le contenu du paramètre « q », est une extension de celle de Lucene avec quelques différences. SearchHandler peut utiliser un ou plusieurs composants (SearchComponents) qui permettent d’en étendre les fonctionnalités. Chacun d’eux ajoute divers paramètres à ceux de SearchHandler. Voici les composants les plus courants :

Tableau 10 : Composants courants de SearchHandler

Nom de classe Java

Nom dans le XML

Description

QueryComponent

query

Paramètres de recherche

FacetComponent

facet

Catégoriser et filtrer les résultats de recherche

MoreLikeThisComponent

mlt

Proposer des documents similaires aux résultats de recherche

StatsComponent

stats

Statistiques sur les champs numériques des résultats de recherche

DebugComponent

debug

Outils pour déboguer les résultats de recherche

À titre d’exemple, voici les paramètres les plus utiles pour un SearchHandler avec un QueryComponent :

- q : chaîne recherchée

- q.op : opérateur par défaut (« AND » ou « OR »)

- df : champ de recherche utilisé s’il n’est pas précisé dans « q » (default field)

- version : le jeu de résultats aura la structure dictée par ce numéro (évite qu’une mise à jour du WAR ne change la structure des résultats de recherche)

- start : les résultats sont renvoyés à partir de ce décalage numérique (offset)

- rows : le nombre maximum de résultats à renvoyer

- fl : la liste des champs à renvoyer pour chaque résultat (field list)

Puisque les requêtes sont écrites dans des URL, les valeurs ainsi que les opérateurs Solr doivent être échappés. Par exemple, le signe + utilisé dans des calculs sur un champ date doit être encodé en %2B. Votre langage de programmation dispose sans doute de fonctions pour vous y aider.

II-C-2 - Maintenance

Un index Solr est maintenu par l’envoi de commandes XML avec la méthode HTTP POST à l’URL http://localhost:8080/solr/CORENAME/update

Ces commandes dépendent du nœud XML de premier niveau (« add » ou « delete ») :

- <add><doc>...</doc></add> : Ajouter ou modifier un document ;

- <delete><query>...</query></delete> : Supprimer un groupe de documents correspondant à une requête Solr ;

- <commit/> : Valider les dernières modifications ;

- <optimize/> : Valider les dernières modifications et opérer certaines optimisations ;

- <rollback/> : Annuler les dernières modifications.

Pour ceux qui connaissent CRUD, la commande XML update permet d’effectuer toutes les opérations de mise à jour de l’index : Create, Update et Delete. En particulier, la commande add indique à Solr que ce document doit être indexé. S’il existe déjà, il est mis à jour ; sinon, il est créé.

Solr répond toujours par un code HTTP adéquat. Par exemple, si la mise à jour a réussi, Solr renvoie un code 200. En revanche, si la mise à jour a échoué, Solr renvoie un code 500 en cas de problème de configuration du serveur, ou 400 si le document envoyé en POST n’est pas correct. Cela permet d’identifier facilement si la mise à jour doit être suivie d’une commande pour valider ou pour annuler les dernières modifications.

Si votre noyau contient plusieurs index, la maintenance s’effectue uniquement sur l’index de données. Les autres index se mettent à jour automatiquement d’après les règles définies dans la configuration du noyau.

II-C-3 - Administration

Chaque noyau Solr dispose d’une interface minimaliste d’administration. Attention, elle ne permet pas de modifier le schéma, la configuration ou le contenu, mais d’en consulter les valeurs de configuration. Elle permet également d’effectuer des requêtes de sélection simples ou avancées, ou encore d’analyser le comportement de tel champ ou de telle requête. Cette interface est accessible à l’URL : http://localhost:8080/solr/CORENAME/admin/

Il existe plusieurs URL utiles dans l’admin :

- http://localhost:8080/solr/CORENAME/admin/file : les fichiers du dossier « conf » de ce noyau ;

- http://localhost:8080/solr/CORENAME/admin/luke : quelques statistiques sur ce noyau (Lucene Index Toolbox) ;

- http://localhost:8080/solr/CORENAME/admin/ping : déterminer si ce noyau est en ligne ;

- http://localhost:8080/solr/CORENAME/admin/plugins : informations sur les plugins chargés pour ce noyau ;

- http://localhost:8080/solr/CORENAME/admin/properties : lire quelques statistiques sur ce noyau ;

- http://localhost:8080/solr/CORENAME/admin/system : informations système relatives à ce noyau ;

- http://localhost:8080/solr/CORENAME/admin/threads : les threads lancés par Tomcat pour ce noyau ;

Si vous souhaitez construire un tableau de bord et qu’appeler toutes ces URL à la suite vous pose problème, rappelez-vous que l’URL suivante est elle-même au format XML : http://localhost:8080/solr/CORENAME/admin/stats.jsp. Votre navigateur utilise une feuille de style XSL pour afficher quelque chose de présentable et lisible, mais il s’agit bien d’un document au format XML (et donc lisible par programmation).

Le composant CoreAdmin, accessible à l’adresse http://localhost:8080/solr/admin/cores, est également une source d’informations synthétiques sur un ou plusieurs noyaux. Ce composant est activé par le fichier de configuration de la webapp Solr que nous verrons au chapitre III-C-3. Il accepte le paramètre « wt » que nous allons voir ci-dessous.

Pour suivre cet article, vous allez avoir besoin de modifier fréquemment la configuration des noyaux. Plutôt que de redémarrer Tomcat pour valider ces modifications, je vous recommande d’utiliser l’action « RELOAD » du composant CoreAdmin : http://localhost:8080/solr/admin/cores?core=CORENAME&action=reload

II-D - Format de la réponse

Solr construit habituellement les réponses HTTP au format XML. Cependant, il est possible de spécifier un format de sortie alternatif à l’aide du paramètre « wt ».

Exemples :

- objets JavaScript : http://localhost:8080/solr/CORENAME/admin/ping?wt=json ;

- array PHP sérialisé : http://localhost:8080/solr/CORENAME/admin/ping?wt=phps ;

- javabin (utilisé par Solr lui-même pour la réplication) : http://localhost:8080/solr/CORENAME/admin/ping?wt=javabin ;

- etc.

La liste des formats par défaut est ici : http://wiki.apache.org/solr/QueryResponseWriter#List_of_Writers_Available

III - Installation et configuration des applications

III-A - Prérequis techniques

Si vous suivez cet article sur une machine Microsoft Windows, vous devez savoir installer des logiciels. Si vous avez un firewall sur votre machine, vous devez savoir le configurer pour permettre à Tomcat d’utiliser le réseau.

Si vous suivez cet article sur une machine Linux, vous devez être familier avec les lignes de commandes et un outil d’installation de paquets (apt-get ou autre). La compilation des logiciels n’est pas utile dans cet article.

Lucene est à la fois un logiciel d’indexation écrit en langage Java et un format de fichier. Il existe diverses implémentations dans des langages différents (je pense notamment à Zend_Search_Lucene pour Zend Framework), mais le principe général est d’obtenir un fichier d’index compatible Lucene. L’un des inconvénients de Lucene est le couplage fort avec sa partie logicielle. Il est par exemple difficile d’utiliser un index Lucene pour alimenter les recherches d’une entreprise entière, à cause de l’hétérogénéité de son parc informatique.

Chaque implémentation a ses conditions techniques. Pour utiliser la version Java de Lucene, une machine virtuelle Java (JVM) est nécessaire ; pour en utiliser la version Zend Framework, il faut disposer de PHP.

Pour sa part, Solr utilise le format d’index de Lucene, mais la partie logicielle, écrite uniquement en langage Java, s’appuie sur un moteur de servlets/JSP comme Tomcat ou Jetty (voir une liste complète sur la rubrique Java). Cela permet d’installer Solr sur un serveur et de l’interroger à distance (API RESTful) depuis tout système capable d’émettre des requêtes HTTP. On obtient alors une architecture client-serveur où le client peut prendre la forme d’un site web, d’une application desktop, d’un terminal mobile, etc. Afin d’utiliser Solr, il faut donc disposer d’un serveur Tomcat ou Jetty, qui ont eux-mêmes besoin d’une JVM ; de plus, il faut prévoir d’ouvrir un port du système d’exploitation (ou plusieurs, suivant le cas). Par conséquent, cela peut générer des inquiétudes de sécurité et la décision ne doit pas être prise à la légère.

Les exemples qui suivent traitent uniquement de Solr (version 3.3) installé sur un serveur Tomcat (version 6.0). Contrairement au tutoriel officiel, le fonctionnement de Solr sur Jetty ne sera pas traité ici.

Vous devez installer une JVM (au choix) :

- Java Development Kit ;

- OpenJDK.

Afin d’éviter la confusion causée par les allers et retours entre les mots Solr et Lucene, j’utiliserai désormais « Solr » même si certains paragraphes sont également vrais pour « Lucene ».

III-B - Installer Tomcat

Je présenterai ici les étapes minimales pour installer Tomcat sous les deux systèmes que je connais à peu près : Linux et Microsoft Windows.

III-B-1 - Installer Tomcat sous Microsoft Windows

Télécharger Tomcat : http://tomcat.apache.org/download-60.cgi

L’Apache Software Foundation fournit un installeur très facile à utiliser : « 32-bit/64-bit Windows Service Installer », je vous le recommande plutôt que les ZIP. L’installation s’effectue donc sans problème. Si vous avez Microsoft Windows Vista ou 7 avec UAC, vous aurez quelques messages de confirmation à valider en tant qu’administrateur. À l’issue de l’installation, pensez à vérifier que l’utilisateur système utilisé par Tomcat a le droit d’écrire dans les dossiers installés, particulièrement dans « work » et dans les logs.

À noter que l’installeur de l’ASF ne nous oblige pas à manipuler les variables d’environnement de notre système, contrairement à ce que certains articles peuvent laisser croire.

Après installation, ouvrez la page d’accueil de Tomcat avec votre navigateur pour vérifier que tout fonctionne comme prévu : http://localhost:8080

Vous pouvez arrêter et démarrer Tomcat à l’aide de l’application « Monitor Tomcat » dont le raccourci se trouve dans le menu démarrer. Si vous avez Microsoft Windows Vista ou 7 avec UAC, vous devrez le lancer en tant qu’administrateur.

Vous pouvez utiliser Junction, dont j’ai déjà parlé dans un autre article, pour déplacer le répertoire « conf » hors du disque système à la manière des liens symboliques d’Unix/Linux.

Dans la suite de l’article, je vais utiliser la variable $CATALINA_HOME pour faire référence au dossier d’installation de Tomcat.

III-B-2 - Installer Tomcat sous Linux

Avant d’installer des applications à l’aide d’apt-get, pensez à lancer apt-get update. Cela vous assurera d’obtenir les dernières versions disponibles pour votre système.

Le chemin d’installation dépend de votre distribution et de vos habitudes. Le wiki de Solr préconise /opt/tomcat mais, pour ma part, je vais vous proposer /usr/local/lib/tomcat. Vous pouvez compiler vous-mêmes Tomcat, mais ce n’est pas nécessaire, dans la mesure où l’Apache Software Foundation propose les binaires en téléchargement. Afin de ne pas confondre les lecteurs moins expérimentés, je ne traiterai donc pas la compilation ici.

Comme nous l’avons mentionné précédemment, vous aurez besoin d’une machine virtuelle Java : soit celle d’Oracle, soit celle d’OpenJDK. La licence de celle d’Oracle est peut-être restrictive pour vous, tandis qu’OpenJDK est peut-être moins optimal techniquement (les opinions à ce sujet sont diverses et variées). À vous de voir laquelle vous inspire le plus confiance. Lancez l’une de ces deux commandes :

apt-get install sun-java6-jre

apt-get install openjdk-6-jre

Lancer la commande suivante pour disposer de la variable d’environnement JAVA_HOME (modifiez le chemin selon votre installation) :

JAVA_HOME=/usr/lib/jvm/java-6-openjdk/jre

Ajouter la ligne suivante au fichier /,etc/profile afin de rendre cette variable disponible à chaque ouverture de session (modifiez le chemin selon votre installation) :

export JAVA_HOME=/usr/lib/jvm/java-6-openjdk/jre

Nous allons éviter de lancer Tomcat en tant qu’utilisateur root. Pour cela, on peut utiliser sudo. Si vous ne l’avez pas encore, c’est le moment de l’installer :

apt-get install sudo

Création de l’utilisateur système qui exécutera Tomcat. Cette ligne peut être adaptée suivant votre système et vos habitudes, par exemple ici j’ai mis le groupe 33 qui correspond à www-data, créé lorsque j’ai installé Apache httpd. Nous ne donnons pas de shell à cet utilisateur, il ne nous est donc pas demandé de mot de passe.

useradd tomcat -m -d /home/tomcat -g 33 -s /bin/false -c "Apache Tomcat server"

Installation de Tomcat :

# Télécharger puis extraire Tomcat
cd /usr/local/src
wget http://mirror.ibcp.fr/pub/apache//tomcat/tomcat-6/v6.0.29/bin/apache-tomcat-6.0.29.tar.gz
tar -xzf apache-tomcat-6.0.29.tar.gz
mv apache-tomcat-6.0.29 /usr/local/lib/tomcat

# Lien pour retrouver la conf de Tomcat à côté des autres dans /,etc
ln -s /usr/local/lib/tomcat/conf /,etc/tomcat

# Droits d’accès pour l’utilisateur système
chown -R tomcat:www-data /usr/local/lib/tomcat
chown tomcat:www-data /,etc/tomcat

Si le lien direct de cet exemple pour télécharger Tomcat ne fonctionne pas, rendez-vous sur http://www.apache.org/dyn/closer.cgi/tomcat/ et trouvez-en un autre.

Créer le script de démarrage /,etc/init.d/tomcat :

#!/bin/bash
#
# tomcat
#
# chkconfig:
# description:  Start up the Tomcat servlet engine.

CATALINA_HOME=/usr/local/lib/tomcat

case "$1" in
start)
if [ -f $CATALINA_HOME/bin/catalina.sh ];
then
echo $"Starting Tomcat"
sudo -u tomcat $CATALINA_HOME/bin/catalina.sh start
fi
;;

stop)
if [ -f $CATALINA_HOME/bin/catalina.sh ];
then
echo $"Stopping Tomcat"
sudo -u tomcat $CATALINA_HOME/bin/catalina.sh stop
fi
;;

restart)
if [ -f $CATALINA_HOME/bin/catalina.sh ];
then
echo $"Stopping Tomcat"
sudo -u tomcat $CATALINA_HOME/bin/catalina.sh stop
echo $"Starting Tomcat"
sudo -u tomcat $CATALINA_HOME/bin/catalina.sh start
fi
;;

*)
echo $"Usage: $0 {start|stop|restart}"
exit 1
;;
esac

Modifier les droits d’exécution du script de démarrage :

chmod a+x /,etc/init.d/tomcat

Vérifier que tout fonctionne en trois commandes :

1° commande : Démarrer Tomcat

/etc/init.d/tomcat start

Affichage attendu :

Starting Tomcat
Using CATALINA_BASE:   /usr/local/lib/tomcat
Using CATALINA_HOME:   /usr/local/lib/tomcat
Using CATALINA_TMPDIR: /usr/local/lib/tomcat/temp
Using JRE_HOME:        /usr
Using CLASSPATH:       /usr/local/lib/tomcat/bin/bootstrap.jar

2° commande : Liste des programmes en exécution, avec leur utilisateur

ps -eo user,pid,fname | grep tomcat

Affichage attendu (le pid varie) :

tomcat 8313 java

3° commande : Liste des sockets réseau ouverts

netstat -ap | grep java

Affichage attendu (la ligne importante est celle affichant "http-alt", qui correspond à 8080) :

tcp        0      0 localhost.localdom:8005 *:*                     LISTEN      10462/java
tcp        0      0 *:8009                  *:*                     LISTEN      10462/java
tcp        0      0 *:http-alt              *:*                     LISTEN      10462/java

Ouvrez la page d’accueil de Tomcat avec un navigateur :
http://localhost:8080

Après avoir constaté que tout fonctionne, assurons-nous que le système sache démarrer Tomcat automatiquement, puis arrêtons Tomcat afin de le configurer (Solr + sécurité) :

update-rc.d tomcat defaults
/etc/init.d/tomcat stop

Dans la suite de l’article, je vais utiliser la variable $CATALINA_HOME pour faire référence au dossier d’installation de Tomcat.

III-C - Ajouter la webapp Solr à Tomcat

III-C-1 - Installer Solr sous Microsoft Windows

Télécharger Solr : http://www.apache.org/dyn/closer.cgi/lucene/solr/

Copier le fichier « dist\apache-solr-3.3.0.war », situé dans cette archive, vers un dossier « Solr » où vous souhaitez conserver les fichiers de Solr, par exemple dans « Mes Documents\Solr ». Dans la mesure où ce dossier contiendra à la fois l’application Solr ainsi que les fichiers de configuration et les données de chaque index, il est préférable de choisir un dossier hors du disque système. De nouveau, vous avez la possibilité d’utiliser Sysinternals Junction pour obtenir un jeu de liens symboliques.

Créer un dossier « cores » à côté du fichier WAR.

Le dossier que vous avez choisi constituera la variable solr/home pour Tomcat, et nous y ferons référence en tant que $SOLR_HOME dans la suite de l’article.

Passer le chapitre « Installer Solr sous Linux » et continuer avec le chapitre suivant.

III-C-2 - Installer Solr sous Linux

# Préparer les répertoires
mkdir /home/tomcat/solr
mkdir /home/tomcat/solr/cores

# Télécharger puis extraire Solr
cd /usr/local/src
wget http://www.sharethecoupon.com/apache/lucene/solr/3.3.0/apache-solr-3.3.0.tgz
tar -xzf apache-solr-3.3.0.tgz
cp apache-solr-3.3.0/dist/apache-solr-3.3.0.war /home/tomcat/solr/

# Droits d’accès pour l’utilisateur système
chown -R tomcat:www-data /home/tomcat/solr

Si le lien direct de cet exemple pour télécharger Solr ne fonctionne pas, rendez-vous sur http://www.apache.org/dyn/closer.cgi/lucene/solr/ et trouvez-en un autre.

Ajouter au fichier /,etc/profile :

export LOGGING_CONFIG=/home/tomcat/solr/logging.properties

Le dossier que vous avez choisi constituera la variable solr/home pour Tomcat, et nous y ferons référence en tant que $SOLR_HOME dans la suite de l’article.

III-C-3 - Configuration de Tomcat pour utiliser Solr

Nous avons installé Tomcat dans $CATALINA_HOME et le fichier WAR de Solr dans $SOLR_HOME, puis nous nous sommes assurés que l’utilisateur de Tomcat puisse lire et écrire dans ces deux dossiers. Tomcat n’est pas encore lancé.

Nous allons modifier le fichier de configuration le plus haut niveau de Tomcat afin de réduire les adresses IP pouvant interroger Tomcat. Pour cela, ouvrir $CATALINA_HOME/conf/server.xml avec un éditeur de texte, trouver le nœud <Host> correspondant à localhost et ajouter cette ligne avant le tag </Host> :

<Valve className="org.apache.catalina.valves.RemoteAddrValve" allow="127.0.0.1"/>

Une restriction RemoteAddrValve peut être placée à divers endroits de la configuration de Tomcat. Ma suggestion ci-dessus s’applique à l’ensemble de localhost, mais pas à l’ensemble de Tomcat, elle est donc plus globale que seulement Solr, mais elle n’est pas absolue. À vous de trouver le(s) bon(s) endroit(s) pour appliquer cette restriction selon votre cas. Vous pouvez mettre plusieurs adresses IP en les séparant par des virgules, par exemple : « 127.0.0.1,10.0.0.1,192.168.1.1 ».

Le conseil de restriction ci-dessus vous empêchera d’accéder à Tomcat si vous n’utilisez pas la bonne URL. Par exemple, selon votre système, utiliser « http://localhost:8080/ » peut donner une page d’erreur alors que « http://127.0.0.1:8080/ » fonctionne, dans la mesure où « localhost » n’est pas strictement équivalent à « 127.0.0.1 ». Pour faire simple, n’utilisez RemoteAddrValve que si vous avez besoin de protéger votre Tomcat !

Nous allons créer le fichier permettant à Tomcat d’identifier la nouvelle webapp, Solr. Tomcat organise ses fichiers de configuration grâce à la directive Host name="..." du fichier $CATALINA_HOME/conf/web.xml en les plaçant sous $CATALINA_HOME/conf/Catalina/... : par exemple, une directive Host name="localhost" correspond au dossier $CATALINA_HOME/conf/Catalina/localhost. En fait, ce dossier localhost existe déjà sur votre système et il contient un ou deux fichiers XML, notamment manager.xml. Ainsi, afin d’indiquer à Solr que le serveur « localhost » doit charger la webapp « solr », il faut créer un fichier $CATALINA_HOME/conf/Catalina/localhost/solr.xml, accessible depuis : http://localhost:8080/solr/

Ce fichier est lu par Tomcat, qui a accès au système de fichiers, les chemins sont donc ceux du système de fichiers :

$CATALINA_HOME/conf/Catalina/localhost/solr.xml

<?xml version="1.0" encoding="utf-8"?>
<Context docBase="/home/tomcat/solr/apache-solr-3.3.0.war" crossContext="true" cachingAllowed="true">
<Environment name="solr/home" type="java.lang.String" value="/home/tomcat/solr" override="true" />
</Context>

Pensez à modifier <Context docBase et <Environment value selon vos valeurs (il s’agit de chemins d’accès absolus vers $SOLR_HOME sur le système).

Puis nous allons créer le fichier de configuration de la webapp Solr. Les valeurs <cores adminPath et <core name sont des URL relatives à http://localhost:8080/solr/. Ce fichier n’est pas lu directement par Tomcat, mais plutôt par Solr. Ainsi, les valeurs <core instanceDir sont des chemins d’accès (du système de fichiers) relatifs à $SOLR_HOME :

$SOLR_HOME/solr.xml

<?xml version="1.0" encoding="utf-8"?>
<solr persistent="false">
<cores adminPath="/admin/cores">
<core name="clients" instanceDir="cores/clients" />
<core name="fournisseurs" instanceDir="cores/fournisseurs" />
<core name="produits" instanceDir="cores/produits" />
</cores>
</solr>

Le fichier $SOLR_HOME/solr.xml permet également d’utiliser le composant CoreAdmin que nous avons évoqué au chapitre II-C-3.

III-D - Ajouter des noyaux à Solr

III-D-1 - En théorie

Un noyau Solr utilise plusieurs répertoires :

- $SOLR_HOME/cores/CORENAME/conf

- $SOLR_HOME/cores/CORENAME/data

- $SOLR_HOME/cores/CORENAME/spell

- $SOLR_HOME/cores/CORENAME/…

Seul le répertoire « conf » est obligatoire. Les autres répertoires sont utilisés pour emmagasiner les différents index : data pour les données, spell pour le correcteur orthographique, etc. Ces répertoires supplémentaires sont créés automatiquement par Solr lorsque Tomcat démarre, il n’est donc pas nécessaire de les créer vous-même.

Ainsi, dans notre exemple, nous avons besoin de créer les répertoires suivants :

- $SOLR_HOME/cores/clients/conf

- $SOLR_HOME/cores/fournisseurs/conf

- $SOLR_HOME/cores/produits/conf

Comme nous l’avons évoqué précédemment, un noyau est composé au minimum de deux fichiers : schema.xml décrit les types de données de l’index de données et solrconfig.xml décrit l’API (principe RESTful) permettant d’utiliser le noyau. Dans un premier temps, nous allons utiliser deux fichiers minimalistes afin de démarrer notre application Solr. Cela nous permettra de nous familiariser avec son interface d’administration avant de commencer à envoyer des commandes XML :

$SOLR_HOME/cores/produits/conf/schema.xml

<?xml version="1.0" encoding="UTF-8"?>
<schema>
</schema>

$SOLR_HOME/cores/produits/conf/solrconfig.xml

<?xml version="1.0" encoding="UTF-8" ?>
<config>
</config>

Un noyau peut fonctionner avec les fichiers ci-dessus, mais il est préférable de leur donner un minimum de contenu, ne serait-ce que pour indiquer l’emplacement des données de l’index de données :

$SOLR_HOME/cores/produits/conf/schema.xml

<?xml version="1.0" encoding="UTF-8"?>
<schema name="Tutoriel Solr - Clients" version="1.2">
</schema>

$SOLR_HOME/cores/produits/conf/solrconfig.xml (remplacer "/PATH/TO/SOLR/HOME" par votre chemin système)

<?xml version="1.0" encoding="UTF-8" ?>
<config>
<abortOnConfigurationError>true</abortOnConfigurationError>
<dataDir>/PATH/TO/SOLR/HOME/cores/produits/data</dataDir>
</config>

Si vous avez plusieurs environnements de travail, par exemple DEV + TEST + PROD, il peut être utile de séparer les valeurs des fichiers XML, en particulier dans le cas de solrconfig. C’est possible en créant un fichier « solrcore.properties » dans le répertoire « conf ». De cette manière, ce qui change entre les différents environnements est centralisé dans un fichier INI assez court. La transition d’un environnement à l’autre est ainsi bien plus simple que de rechercher les valeurs dans plusieurs longs fichiers XML (et à coup sûr en oublier la moitié) :

$SOLR_HOME/cores/produits/conf/schema.xml

<?xml version="1.0" encoding="UTF-8"?>
<schema name="${solr.schema.name}" version="1.2">
</schema>

$SOLR_HOME/cores/produits/conf/solrconfig.xml

<?xml version="1.0" encoding="UTF-8" ?>
<config>
<abortOnConfigurationError>${solr.config.abortOnConfigurationError}</abortOnConfigurationError>
<dataDir>${solr.config.dataDir}</dataDir>
</config>

$SOLR_HOME/cores/produits/conf/solrcore.properties (remplacer "/PATH/TO/SOLR/HOME" par votre chemin système)

solr.config.abortOnConfigurationError = true
solr.config.dataDir = /PATH/TO/SOLR/HOME/cores/produits/data
solr.schema.name = Tutoriel Solr - Produits

Ici, j’ai choisi de reprendre les noms des nœuds XML comme noms de variables dans le fichier solrcore.properties, mais ce n’est pas obligatoire. Le préfixe « solr. » est lui aussi facultatif, je l’utilise pour être assuré d’éviter les conflits de noms avec des variables existantes. Ce fichier aurait aussi bien pu contenir les variables abort_on_error, data_path et schema_name, qui auraient été appelées respectivement ${abort_on_error}, ${data_path} et ${schema_name} dans le XML.

En l’état, nos noyaux Solr se lancent, mais ils ne sont pas fonctionnels. C’est-à-dire que la webapp démarre sans problème, mais, les noyaux n’ayant ni schéma ni API, on ne peut rien en faire. Nous allons les configurer dans la suite de cet article.

III-D-2 - Configuration minimale d’un noyau

Afin de pouvoir utiliser les exemples de cet article avec notre propre noyau, nous allons prendre un peu d’avance sur le chapitre dédié à la configuration de l’API de notre application Solr, qui arrivera en fin d’article. Nous allons ajouter deux segments de code permettant de charger la page d’administration et d’exécuter des requêtes simples :

$SOLR_HOME/cores/produits/conf/solrconfig.xml

<?xml version="1.0" encoding="UTF-8" ?>
<config>
<abortOnConfigurationError>${solr.config.abortOnConfigurationError}</abortOnConfigurationError>
<dataDir>${solr.config.dataDir}</dataDir>

<requestHandler name="standard" class="solr.SearchHandler" default="true"/>
<requestHandler name="/admin" class="solr.admin.AdminHandlers" />
</config>

III-D-3 - En pratique : installer le noyau d’exemple

Comme nous l’avons vu précédemment, le fichier téléchargé de Solr contient le fichier WAR que nous avons utilisé pour configurer la webapp Solr dans Tomcat. Nous allons maintenant copier le noyau d’exemple fourni dans la même archive.

Cette archive contient deux types d’exemples : le dossier « solr » à la racine contient une configuration complète donnant une vue d’ensemble de toutes les fonctionnalités disponibles ; le dossier « multicore », également à la racine, contient deux noyaux pour démontrer principalement la fonctionnalité dont nous avons déjà parlé : installer plusieurs noyaux côte à côte dans la même webapp. Dans la mesure où cette fonctionnalité « multicore » a déjà été configurée précédemment dans ce tutoriel, nous allons utiliser l’exemple complet.

Ainsi, copiez vers votre dossier des noyaux le dossier « example/solr » de l’archive et renommez-le en « exemple » :

- $SOLR_HOME/cores/clients

- $SOLR_HOME/cores/exemple

- $SOLR_HOME/cores/fournisseurs

- $SOLR_HOME/cores/produits

Dans la mesure où ce noyau est prévu pour fonctionner seul, sa configuration ne tient pas compte des noyaux multiples. Sans aller jusqu’à tout modifier, nous allons nous contenter d’une légère adaptation pour que les données de son index ne soient pas enregistrées n’importe où. Dans le fichier $SOLR_HOME/cores/exemple/conf/sorconfig.xml, modifiez le nœud <dataDir> de la manière suivante :

<dataDir>${solr.data.dir:./solr/cores/exemple/data}</dataDir>

Chaque fois que vous ajoutez un noyau à Solr, pensez à l’activer dans le fichier solr.xml puis à redémarrer Tomcat :

$SOLR_HOME/solr.xml

<?xml version="1.0" encoding="utf-8"?>
<solr persistent="false">
<cores adminPath="/admin/cores">
<core name="clients" instanceDir="cores/clients" />
<core name="exemple" instanceDir="cores/exemple" />
<core name="fournisseurs" instanceDir="cores/fournisseurs" />
<core name="produits" instanceDir="cores/produits" />
</cores>
</solr>

Une fois Tomcat redémarré, vous devriez constater que Solr a créé un dossier « data » dans chacun des noyaux, et notamment dans « exemple ». Avant de continuer la lecture de ce tutoriel, vérifier dans un navigateur que tous les noyaux sont bien présents :

Image 1 : Admin des noyaux

Les noyaux affichés dans votre navigateur sont prêts à être utilisés. Cependant, ils ne contiennent encore aucun document. Il est donc inutile d’essayer de lancer des recherches pour le moment. Si vous savez envoyer un fichier avec la méthode HTTP POST à un serveur web, vous pouvez le faire dès à présent pour remplir le noyau d’exemple à l’aide des fichiers XML et du fichier CSV présents dans le dossier « example/exampledocs » de l’archive de Solr. Si vous savez les utiliser, ce même dossier contient deux fichiers post.sh et post.jar pour vous aider dans cette tâche. Sinon, vous pouvez continuer la lecture de cet article, nous apprendrons à mettre à jour un index au chapitre V-G.

Voici comment utiliser le script donné au chapitre V-G pour remplir votre noyau « exemple » :

Exemple d’utilisation en lignes de commandes (Linux et MacOS) :

./send.php exemple string "<delete><query>*:*</query></delete>"
./send.php exemple file hd.xml
./send.php exemple file ipod_other.xml
./send.php exemple file ipod_video.xml
./send.php exemple file mem.xml
./send.php exemple file monitor.xml
./send.php exemple file monitor2.xml
./send.php exemple file mp500.xml
./send.php exemple file payload.xml
./send.php exemple file sd500.xml
./send.php exemple file solr.xml
./send.php exemple file vidcard.xml
./send.php exemple string "<optimize waitSearcher='true'/>"

Exemple d’utilisation en lignes de commandes (Windows) :

php.exe send.php exemple string "<delete><query>*:*</query></delete>"
php.exe send.php exemple file hd.xml
php.exe send.php exemple file ipod_other.xml
php.exe send.php exemple file ipod_video.xml
php.exe send.php exemple file mem.xml
php.exe send.php exemple file monitor.xml
php.exe send.php exemple file monitor2.xml
php.exe send.php exemple file mp500.xml
php.exe send.php exemple file payload.xml
php.exe send.php exemple file sd500.xml
php.exe send.php exemple file solr.xml
php.exe send.php exemple file vidcard.xml
php.exe send.php exemple string "<optimize waitSearcher='true'/>"

IV - La structure des documents du noyau : schema.xml

Cette page sert de référence au reste de l’article. Si vous n’avez pas la patience de la lire de bout en bout à la première lecture, ce qui serait tout à fait normal, n’hésitez pas à passer à la suite pour le moment. Vous pourrez revenir sur cette page lorsque vous aurez besoin de configurer votre schéma ou votre API.

À tout moment, vous pouvez regarder le fichier example/solr/conf/schema.xml de l’archive téléchargée. Ce fichier est très complet et il contient de nombreux commentaires pour vous aider dans vos prises de décisions.

Le fichier schema.xml est constitué de plusieurs nœuds de second niveau :

- un nœud types

- un nœud fields

- un champ uniqueKey

- un champ defaultSearchField

- un champ solrQueryParser

- un ou plusieurs champs copyField

IV-A - Les types de données du document : /schema/types

IV-A-1 - Introduction

Chaque fieldType est décrit au moins par deux attributs name et class. Le premier permet de faire référence à ce type dans le reste du fichier XML ; l’autre décrit la classe Java utilisée pour construire le type en mémoire.

Les champs de notre document devront être définis soit avec un type simple, soit avec un type complexe. Les types dits « simples » sont pour des données brutes, c’est-à-dire des chaînes sans signification sémantique, des valeurs numériques ou des dates. Les types dits « complexes » sont prévus pour décomposer la valeur et pour l’analyser de manière très précise. Nous allons commencer par les types simples, que tout développeur a rencontrés maintes fois dans ses projets, puis nous verrons les types complexes, qui constituent la majorité de la valeur ajoutée de Solr.

IV-A-2 - Les attributs disponibles pour chaque type de données

La plupart des attributs suivants sont disponibles à la fois dans les définitions fieldType et field. L’expérience vous dira à quel endroit chaque attribut est le plus utile.

Tableau 11 : Attributs d’un nœud fieldType

Nom

Description

Valeurs

Obligatoire

Valeur par défaut

name

Nom du type

chaîne

oui

n/a

class

Nom de la classe Java

chaîne

oui

n/a

indexed

Mettre à vrai si ce type doit être disponible pour la recherche ou le tri

false|true

non

false

stored

Mettre à vrai si ce type peut être renvoyé par un requestHandler

false|true

non

false

compressed

Mettre à vrai si le type doit être compressé (gzip) ; utile uniquement pour StrField et TextField

false|true

non

false

compressThreshold

Longueur minimale de la valeur pour activer la compression ; utile uniquement si compressed est activé

entier

non

0

multiValued

Mettre à vrai si ce type peut contenir plusieurs valeurs par document

false|true

non

false

precisionStep

Une valeur faible améliore le temps de recherche
Une valeur élevée améliore le temps d’indexation et le volume de stockage

entier

non

Selon classe Java

omitNorms

Mettre à vrai si vous avez besoin de pouvoir trier sur ce type

false|true

non

false

omitTermFreqAndPositions

Mettre à vrai si vous n’avez besoin ni de la fréquence des termes ni de leur position ni de leur pondération (payload)

false|true

non

false

positionIncrementGap

Mettre à 100 pour les types de texte multivalué, ou bien à 0 pour le reste

entier

non

0

termVectors

Mettre à vrai si vous utilisez HighlightComponent, MoreLikeThisComponent ou TermVectorComponent
Inutile si le champ n’est pas de classe TextField

false|true

non

false

termPositions

Mettre à vrai si vous utilisez termVectors

false|true

non

false

termOffsets

Mettre à vrai si vous utilisez termVectors

false|true

non

false

sortMissingFirst

Pousser les documents n’ayant pas de valeur vers le début de la liste
Utile uniquement pour les types dont la représentation interne de la valeur est une chaîne (StrField, BoolField, DateField et les Sortable*)

false|true

non

false

sortMissingLast

Pousser les documents n’ayant pas de valeur vers la fin de la liste
Utile uniquement pour les types dont la représentation interne de la valeur est une chaîne (StrField, BoolField, DateField et les Sortable*)

false|true

non

false

default

Selon le fieldType du champ

non

vide

Vous trouverez ci-dessous une liste des attributs recommandés ou obligatoires pour chaque cas d’utilisation (1). Une étoile bleue (*) marque les valeurs recommandées, mais non obligatoires.

Tableau 12 : Attributs recommandés ou obligatoires pour chaque type de données

Cas d’utilisation \ Attribut

indexed

stored

multiValued

omitNorms

termVectors

termPositions

termOffsets

Recherche dans le champ

true

Récupérer le contenu du champ

true

Utiliser comme uniqueKey

true

false

Trier sur le champ

true

false

true *

Utiliser la pondération de champ (field boost)

false

La pondération de document (document boost) affecte la recherche sur ce champ

false

Récupérer la surbrillance (HighlightComponent)

true *

true

true *

true *

true *

Récupérer les facettes (FacetComponent)

true

Ajouter des valeurs multiples tout en conservant l’ordre

true

La longueur de la valeur affecte le score du document

false

Récupérer les résultats similaires (MoreLikeThisComponent)

true *

true *

TermVectorComponent : Fréquence des termes

true

TermVectorComponent : Fréquence du document

true

TermVectorComponent : tf * idf

true

TermVectorComponent : Position des termes

true

true

true

TermVectorComponent : Décalage des termes

true

true

true

Certains cas d’utilisation sont incompatibles. Il est par exemple impossible de trier les résultats sur un champ pouvant avoir plusieurs valeurs, dans la mesure où son attribut multiValued devrait avoir à la fois les valeurs true et false.

IV-A-3 - Les types simples

Tableau 13 : Principaux types simples proposés par Solr

Classe

Nom

Optimisations

Description

solr.StrField

string

non indexé

Type chaîne générique

solr.BoolField

boolean

Type booléen, peut prendre pour valeurs « true », « false », « 0 » ou « 1 »

solr.BinaryField

binary

Type de données dont les valeurs doivent être encodées en base64

solr.TrieIntField

tint

recherche, tri

Type numérique « int »

solr.TrieFloatField

tfloat

recherche, tri

Type numérique « float »

solr.TrieLongField

tlong

recherche, tri

Type numérique « long »

solr.TrieDoubleField

tdouble

recherche, tri

Type numérique « double »

solr.TrieDateField

tdate

recherche, tri

Type date selon le format ISO 8601 (cf. la RFC 3339 ou le W3C) avec pour zone obligatoire « Z » (UTC), par exemple : « 1995-12-31T23:59:59Z »

solr.RandomSortField

random

Type pour faciliter les tris aléatoires

Je ne présente ci-dessus que les types utiles avec la version de Solr choisie. En effet, les versions antérieures de schema.xml avaient plusieurs versions de chaque type de base. Ces types continuent d’exister afin de permettre la compatibilité avec d’anciens schémas, mais, pour un schéma nouvellement défini, ils sont obsolètes.

À titre d’exemple, voici les trois versions du type entier (int). Remarquons l’utilisation d’un préfixe selon le principe de la notation hongroise (cf. notation hongroise Apps) afin de décrire le contexte de chaque type : pint, sint, tint.

Tableau 14 : Versions du type entier dans Solr

Classe

Nom

Surnom

Description

solr.IntField

pint

plain int

aucune optimisation, lent pour les recherches et les tris

solr.SortableIntField

sint

sortable int

représentation du nombre sous forme de texte, ce qui permet le tri, mais n’est pas optimisé pour les requêtes

solr.TrieIntField

tint

trie int

le nombre est indexé et manipulé avant d’être enregistré dans l’index, ce qui permet d’appliquer des optimisations pour le tri et la recherche tout en améliorant les performances des requêtes

Le seul avantage des types « plain » sur les autres est un gain de place pour l’index, mais ce n’est habituellement pas l’argument qui oriente le projet vers Solr ; de plus, dans la mesure où ces valeurs sont stockées telles quelles par Solr, elles ne sont pas utilisables pour des tris naturels.

Les types « sortable » sont meilleurs que les types « plain », car ils représentent les valeurs sous forme d’une chaîne, manipulée de manière à permettre les tris naturels.

Enfin, les types « trie » sont mieux optimisés que les types « sortable » pour les requêtes d’intervalles, car Solr passe par des analyses et des filtres pour indexer ces valeurs (plutôt que de les représenter sous forme de texte).

Dans la mesure où nous n’avons pas besoin de conserver la compatibilité avec un ancien index, nous ne choisirons pas les types « plain » pour lesquels nous avons une alternative : un type « sortable » chaque fois que nous aurons besoin de sortMissingLast ou sortMissingFirst, ou « trie » le reste du temps. Nous allons donc ajouter les déclarations suivantes dans le nœud <types></types> de notre fichier schema.xml (ces déclarations ont peu de chances de différer d’un noyau à l’autre) :

/schema/types

<!-- Types de base -->
<fieldType name="string" class="solr.StrField"
sortMissingLast="true" omitNorms="true"/>

<fieldType name="boolean" class="solr.BoolField"
sortMissingLast="true" omitNorms="true"/>

<fieldtype name="binary" class="solr.BinaryField"/>

<fieldType name="random" class="solr.RandomSortField"
indexed="true" />

<!-- Types numériques permettant d’utiliser sortMissingLast ou sortMissingFirst -->
<fieldType name="sint" class="solr.SortableIntField"
sortMissingLast="true" omitNorms="true"/>

<fieldType name="sfloat" class="solr.SortableFloatField"
sortMissingLast="true" omitNorms="true"/>

<fieldType name="slong" class="solr.SortableLongField"
sortMissingLast="true" omitNorms="true"/>

<fieldType name="sdouble" class="solr.SortableDoubleField"
sortMissingLast="true" omitNorms="true"/>

<!-- Types numériques optimisés pour les requêtes par intervalles -->
<fieldType name="tint" class="solr.TrieIntField"
precisionStep="8" omitNorms="true" positionIncrementGap="0"/>

<fieldType name="tfloat" class="solr.TrieFloatField"
precisionStep="8" omitNorms="true" positionIncrementGap="0"/>

<fieldType name="tlong" class="solr.TrieLongField"
precisionStep="8" omitNorms="true" positionIncrementGap="0"/>

<fieldType name="tdouble" class="solr.TrieDoubleField"
precisionStep="8" omitNorms="true" positionIncrementGap="0"/>

<fieldType name="tdate" class="solr.TrieDateField"
precisionStep="6" omitNorms="true" positionIncrementGap="0"/>

Les champs de type StrField ne sont pas analysés par Solr, ils sont stockés tels quels. Aucune optimisation n’est apportée sur ces valeurs pour le tri, la recherche ou les requêtes par intervalles. Ainsi, il ne faut pas utiliser ce type de données pour les champs sur lesquels on souhaite effectuer des tris ou des recherches.

Les noms de classes commençant par « solr » sont directement liés aux classes Java du paquetage org.apache.solr.schema (même si le fichier schema.xml par défaut indique org.apache.solr.analysis : il s’agit sans doute d’une erreur). Par exemple, un type solr.StrField instancie des objets de la classe Java org.apache.solr.schema.StrField. N’ayez crainte, il n’est pas nécessaire de connaître l’API Java pour configurer un noyau Solr performant. Je ne donne ce lien qu’à titre indicatif.

Il ne faut pas confondre les types de données que nous manipulons en tant qu’utilisateurs de la webapp (StrField, BoolField,etc.) et la « représentation interne » que j’évoque par moments.

IV-A-4 - Les types complexes

IV-A-4-a - Description d’un type complexe

Les types complexes ne sont ni présents ni configurés par défaut dans Solr. Bien que les exemples présentent parfois des types complexes génériques, leur utilité est très limitée, car ils ne sont pas adaptés à votre application ou à votre noyau. C’est à vous, en tant que créateur du noyau, de définir les besoins de votre application et de configurer les types Solr permettant d’y répondre. Ce sont ces types configurables qui surpassent de très loin les fonctionnalités offertes par les opérateurs LIKE et REGEXP du langage SQL ainsi que, dans une moindre mesure, la plupart des autres moteurs full-text. C’est pour les utiliser que vous vous êtes tourné(e) vers Solr.

Un type complexe est configuré à partir du type solr.TextField auquel on ajoute un ou deux analyseurs (<analyzer></analyzer>), eux-mêmes composés d’une suite de <tokenizer/> et de <filter/>.

Comme nous l’avons évoqué plus haut, les types complexes sont la raison principale qui oriente un projet vers l’intégration de Solr. En effet, ce sont des types dont les données seront analysées, manipulées et comparées à un niveau dont la profondeur est largement supérieure à ce que SQL peut faire avec des opérateurs comme LIKE et REGEXP, voire même souvent avec les modules Full-Text des moteurs SGBD, sans pour autant perdre en efficacité (temps de réponse). Voici un exemple permettant aux requêtes « Wi-Fi » et « wi fi » de trouver les documents contenant le mot « Wi-Fi », sans aucun code de manipulation de la requête dans l’application Web :

<fieldType name="text" class="solr.TextField" positionIncrementGap="100">
<analyzer type="index">
<tokenizer class="solr.WhitespaceTokenizerFactory"/>
<filter class="solr.WordDelimiterFilterFactory"
generateWordParts="1" generateNumberParts="1"
catenateWords="1" catenateNumbers="1" catenateAll="0"
splitOnCaseChange="1"/>
<filter class="solr.LowerCaseFilterFactory"/>
</analyzer>
<analyzer type="query">
<tokenizer class="solr.WhitespaceTokenizerFactory"/>
<filter class="solr.WordDelimiterFilterFactory"
generateWordParts="1" generateNumberParts="1"
catenateWords="0" catenateNumbers="0" catenateAll="0"
splitOnCaseChange="1"/>
<filter class="solr.LowerCaseFilterFactory"/>
</analyzer>
</fieldType>

Nous pouvons voir dans cet exemple que Solr peut se comporter différemment selon qu’il rencontre le même mot dans un document mis à l’index ou dans une requête utilisateur. Nous pourrons voir plus en détail comment Solr manipule le texte lorsque notre premier noyau sera totalement configuré, mais voici un avant-goût :

Texte ajouté à l’index :

1. La chaîne est décomposée en sous-chaînes à l’aide des espaces ;

2. Chaque sous-chaîne est à son tour décomposée à l’aide des séparateurs de mots, des nombres et du changement de casse (« Wi-Fi » devient à la fois « Wi-Fi » et « Wi » + « Fi ») ;

3. Chaque élément est enfin mis en minuscules (« Wi-Fi », « wi » et « fi »).

Texte de requête :

1. La chaîne est décomposée en sous-chaînes à l’aide des espaces ;

2. Chaque sous-chaîne est à son tour décomposée à l’aide des séparateurs de mots, des nombres et du changement de casse (« wi fi » devient « wi » + « fi ») ;

3. Chaque élément est enfin mis en minuscules (« wi » et « fi »).

Revenons à la composition de fieldType pour un type complexe. Un nœud fieldType peut contenir jusqu’à deux nœuds analyzer. Ces derniers peuvent contenir un nombre quelconque de nœuds charFilter (choisis parmi les CharFilterFactories) suivis d’un nœud tokenizer (unique, obligatoire et choisi parmi les TokenizerFactories) et enfin d’un nombre quelconque de nœuds filter (choisis parmi les TokenFilterFactories).

En bref, un analyseur se compose de la manière suivante (cardinalités entre parenthèses) :

1. CharFilterFactories (0,*)

2. TokenizerFactories (1, 1)

3. TokenFilterFactories (0, *)

Si un nœud fieldType n’a aucun nœud analyzer, c’est un type simple. S’il a exactement deux nœuds analyzer, alors l’un d’eux doit être de type « index » et l’autre de type « query », comme le montre l’exemple précédent. En revanche, s’il n’a qu’un seul nœud analyzer, ce dernier ne doit pas porter d’attribut type,car il s’applique aux deux types d’analyseurs pour ce fieldType (les requêtes et l’indexation se feront selon les mêmes règles).

Les tableaux qui suivent ne prétendent pas donner une liste exhaustive des classes disponibles. Ce sont les plus utilisées, mais Solr en propose davantage et vous avez toujours la possibilité d’écrire vos propres plugins.

IV-A-4-b - Filtrer les caractères : les CharFilterFactories

Si vous en avez besoin, la documentation officielle de cette section est sur le wiki de Solr et la liste complète des classes fournies par Solr est dans la documentation Java.

Toute classe ne figurant pas dans src/java/org/apache/solr/analysis/*CharFilterFactory.java de l’archive téléchargée, n’est pas disponible dans votre version de Solr.

Tableau 15 : Les CharFilterFactories de Solr

Classe

Effet

Paramètres

solr.MappingCharFilterFactory

Remplacer des caractères d’après les correspondances décrites dans un fichier texte

mapping=« fichier.ext »

solr.HTMLStripCharFilterFactory

Supprimer les balises HTML

Si vous souhaitez utiliser une classe solr.MappingCharFilterFactory, alors vous devrez créer un fichier texte dans le dossier « conf » du noyau. Cette technique a souvent été utilisée pour remplacer les lettres accentuées par leur équivalent non accentué grâce au fichier mapping-ISOLatin1Accent.txt (2). Cependant, Solr propose une meilleure solution : le filtre solr.ASCIIFoldingFilterFactory (cf. plus bas).

Afin d’être parfaitement clair : <charFilter class="solr.MappingCharFilterFactory" n’est pas la bonne solution au problème des accents. La bonne solution est <filter class="solr.ASCIIFoldingFilterFactory", que nous verrons dans le paragraphe sur les TokenFilterFactories.

Si vous avez besoin de la classe solr.MappingCharFilterFactory malgré tout, alors voici la syntaxe du fichier texte :

# Syntaxe (la source ne peut pas être vide, la cible peut être vide) :
#   "source" => "cible"

# Exemples :
#   "À" => "A"
#   "\u00C0" => "A"
#   "\u00C0" => "\u0041"
#   "ß" => "ss"
#   "\t" => " "
#   "\n" => ""

Ainsi, vous pouvez remplacer une chaîne « source » non vide par une chaîne « cible », vide ou non. Puisque tout est encodé en UTF-8 dans Solr, vous devez préciser les valeurs UTF-8 des caractères sous leur forme hexadécimale : c’est l’une des raisons qui rendent l’alternative solr.ASCIIFoldingFilterFactory attrayante.

Exemple de déclaration (à placer au début d’un nœud "analyzer") :

<charFilter class="solr.HTMLStripCharFilterFactory"/>

Il existe aussi les classes solr.BaseCharFilterFactory et solr.PatternReplaceCharFilterFactory, mais, n’ayant pas trouvé leur liste de paramètres, je ne suis pas parvenu à les faire fonctionner.

IV-A-4-c - Décomposer la chaîne : les TokenizerFactories

Si vous en avez besoin, la documentation officielle de cette section est sur le wiki de Solr et la liste complète des classes fournies par Solr est dans la documentation Java.

Toute classe ne figurant pas dans src/java/org/apache/solr/analysis/*TokenizerFactory.java de l’archive téléchargée, n’est pas disponible dans votre version de Solr.

Tableau 16 : Les TokenizerFactories de Solr

Classe

Effet

Paramètres

solr.LetterTokenizerFactory

Découpe la chaîne d’après les lettres non contiguës

solr.WhitespaceTokenizerFactory

Découpe la chaîne d’après les espaces

solr.LowerCaseTokenizerFactory

Découpe la chaîne d’après les espaces puis passe les termes en minuscules

solr.StandardTokenizerFactory

Découpe la chaîne de manière intelligente et naturelle, tout en gardant une trace du type des termes isolés (acronymes, apostrophes, etc.) afin de permettre aux filter d’effectuer des traitements spécialisés
À utiliser avec un StandardFilter (cf. plus loin)
Sauf si vous avez besoin d’un filter incompatible ou si vous l’appliquez à une langue trop éloignée de l’anglais, c’est généralement le meilleur choix pour le tokenizer

solr.HTMLStripWhitespaceTokenizerFactory

Supprime le HTML puis se comporte comme un WhitespaceTokenizer

solr.HTMLStripStandardTokenizerFactory

Supprime le HTML puis se comporte comme un StandardTokenizer

solr.PatternTokenizerFactory

Découpe la chaîne selon une expression rationnelle

pattern=« regexp »

À mon humble avis, certaines de ces classes font du travail qui ne leur incombe pas. Par exemple, les classes HTMLStrip*Tokenizer font double emploi avec HTMLStripCharFilter, ainsi que LowerCaseTokenizer avec LowerCaseFilter (que nous verrons plus loin). Ma recommandation est de choisir un tokenizer pour segmenter la chaîne et de laisser les transformations aux charFilter et aux filter.

Exemple de déclaration (à placer après les nœuds "charFilter" dans un nœud "analyzer") :

<tokenizer class="solr.StandardTokenizerFactory"/>

Nous avons dit qu’un nœud analyzer doit contenir un nœud tokenizer, ni plus ni moins, or il peut arriver que vous ayez besoin d’un analyzer sans qu’aucun tokenizer ne modifie la chaîne. Dans ce cas, utilisez la classe « solr.KeywordTokenizerFactory », elle n’a aucun effet et elle rendra valide votre analyzer.

IV-A-4-d - Manipuler les termes : les TokenFilterFactories

Si vous en avez besoin, la documentation officielle de cette section est sur le wiki de Solr et la liste complète des classes fournies par Solr est dans la documentation Java.

Toute classe ne figurant pas dans src/java/org/apache/solr/analysis/*TokenFilterFactory.java de l’archive téléchargée, n’est pas disponible dans votre version de Solr.

Tableau 17 : Les TokenFilterFactories de Solr

Classe

Effet

Paramètres

solr.StandardFilterFactory

Si vous avez utilisé solr.StandardTokenizer ou un équivalent, ce filtre permet de supprimer les points des acronymes et les 's de la fin des mots (qui, en anglais, correspond à un possessif ou à un pluriel)

solr.LowerCaseFilterFactory

Passe les termes en minuscules

solr.TrimFilterFactory

Supprime les espaces en début et fin des termes

solr.StopFilterFactory

Supprime les termes les plus communs de la langue anglaise
Si le paramètre words est précisé, alors le fichier est utilisé à la place des mots pré configurés

words=« stopwords_en.txt »(3)
words=« stopwords_fr.txt »(4)
ignoreCase=« false|true »

solr.CommonGramsFilterFactory

Combine les « mots communs » en termes
Par exemple, les termes « cet » et « été » font partie des mots communs de la langue française et, pris individuellement, solr.StopFilterFactory permet de les exclure de la recherche ; cependant, la chaîne « cet été » est une recherche légitime : le filtre solr.CommonGramsFilterFactory crée le terme combiné « cet_été » afin de le rendre disponible à la recherche
On configure habituellement ce filtre avec la même liste de termes que pour solr.StopFilterFactory

words=« stopwords_en.txt »
words=« stopwords_fr.txt »
ignoreCase=« false|true »

solr.EdgeNGramFilterFactory

Créé une liste de termes à partir de la lettre numéro minGramSize jusqu’à maxGramSize de chaque terme, par exemple pour minGramSize="2" et side="front" :
Nigerian => « ni », « nig », « nige », « niger », « nigeri », « nigeria », « nigeria », « nigerian »

minGramSize=""
maxGramSize=""
side=« back|front »

solr.KeepWordFilterFactory

L’inverse de solr.StopWordsFilterFactory, mais les paramètres et le format de fichier sont identiques

words=« keepwords_en.txt »
words=« keepwords_fr.txt »
ignoreCase=« false|true »

solr.LengthFilterFactory

Supprime les termes dont la taille n’est pas comprise entre min et max

min=""
max=""

solr.WordDelimiterFilterFactory

Découpe les termes en sous-termes suivant certaines règles : contiguïté des lettres, changement de casse, changement lettre-chiffre dans un terme et supprime le « s » à la fin des termes
Les paramètres peuvent être combinés de n’importe quelle manière, cela génère des termes supplémentaires sans créer de conflit
Ce filtre illustre l’intérêt d’avoir un analyseur différent pour l’indexation et pour les requêtes
Attention, les tokenizers qui opèrent trop de modifications sur la chaîne limitent le champ d’action de ce filtre

splitOnCaseChange=« 0|1 »
splitOnNumerics=« 0|1 »
stemEnglishPossessive=« 0|1 »
generateWordParts=« 0|1 »
generateNumberParts=« 0|1 »
catenateWords=« 0|1 »
catenateNumbers=« 0|1 »
catenateAll=« 0|1 »
preserveOriginal=« 0|1 »
types=« wdftypes_en.txt »(5)

solr.SynonymFilterFactory

Gestion des synonymes (mots simples ou mots composés)
Le format du fichier de synonymes est détaillé après ce tableau

synonyms=« synonyms_fr.txt »
ignoreCase=« false|true »
expand=« false|true »

solr.RemoveDuplicatesTokenFilterFactory

Supprime les doublons successifs
Ce filtre est plus efficace s’il est utilisé après tous les filtres pouvant générer de nouveaux termes

solr.ISOLatin1AccentFilterFactory

Deprecated : cf. solr.ASCIIFoldingFilterFactory

solr.ASCIIFoldingFilterFactory

Convertir les caractères accentués en équivalent « ASCII 127 » lorsque c’est possible ; plus d’informations dans la documentation
Ce filtre est à préférer aux autres méthodes permettant d’enlever les accents

solr.ShingleFilterFactory

Un shingle est une combinaison de termes formant un nouveau terme

maxShingleSize=« 2 »
minShingleSize=« 2 »
outputUnigrams=« false|true »
tokenSeparator=«   »

solr.PositionFilterFactory

Permet d’optimiser certains cas particuliers en manipulant la position des termes dans le flux
Ce filtre est conseillé pour un analyseur de type="query",mais pas pour type="index"

positionIncrement=« 0 »

solr.ReversedWildcardFilterFactory

Permet d’optimiser les recherches préfixées ou ayant une wildcard (* ou ?) vers le début de la chaîne
Le paramètre withOriginal permet de conserver l’ordre original en plus de l’ordre inversé
Le paramètre maxPosAsterisk définit la position maximale de l’astérisque pour déclencher le filtre
Le paramètre maxPosQuestion définit la position maximale du point d’interrogation pour déclencher le filtre
Le paramètre maxFractionAsterisk déclenche le filtre si la position de l’astérisque est inférieure à cette fraction du terme
Le paramètre minTrailing définit le nombre minimum de caractères dans un terme (après la dernière wildcard) pour déclencher le filtre ; plus efficace si > 1
Ce filtre est conseillé pour un analyseur de type="index",mais pas pour type="query"

withOriginal=« false|true »
maxPosAsterisk=« 3 »
maxPosQuestion=« 2 »
maxFractionAsterisk=« 0.33 »
minTrailing=« 2 »

solr.PhoneticFilterFactory

Créé une représentation codée de la prononciation phonétique des termes
Le paramètre maxCodeLength n’est disponible que pour les encodages Metaphone et DoubleMetaphone
Le paramètre inject détermine si les jetons originaux sont remplacés ou ajoutés au flux
L’implémentation DoubleMetaphone de ce filtre ne permet pas d’obtenir l’alternative prévue par l’algorithme Double Metaphone original : pour cela, il faut utiliser solr.DoubleMetaphoneFilterFactory

encoder=« Metaphone »
encoder=« DoubleMetaphone »
encoder=« Soundex »
encoder=« RefinedSoundex »
inject=« false|true »
maxCodeLength=""

solr.DoubleMetaphoneFilterFactory

Créé une représentation codée de la prononciation phonétique (Double Metaphone) des termes

inject=« false|true »
maxCodeLength=""

solr.SnowballPorterFilterFactory

Racinisation (cf. l’introduction de cet article pour une définition) optimisée pour différentes langues
La liste complète est disponible sur le wiki de Solr
L’algorithme Snowball suppose que le texte est en minuscules

language=« French »
language=« German2 »
language=« Spanish »
…

Comme vous pouvez vous en douter, la plupart de ces filtres sont prévus pour la langue anglaise. Il vous incombe de trouver la version localisée des fichiers dont vous avez besoin, car Solr ne les fournit pas tous.

Le fichier utilisé par solr.SynonymFilterFactory suit le format suivant :

# Les lignes vides et les lignes commençant par dièse sont des commentaires
# Les séquences équivalentes de termes sont séparées par des virgules

# Les règles explicites prennent chaque séquence de termes du côté gauche de "=>"
# et les remplacent par chacune des alternatives du côté droit
# Ces types de règles ignorent le paramètre "expand" du schéma
# Exemples :
i-pod, i pod => ipod
sea biscuit, sea biscit => seabiscuit

# Les règles implicites ne prennent pas de "=>"
# En ce cas, le comportement est défini par le paramètre "expand" du schéma
# Cela permet d’utiliser le même fichier de synonymes pour différentes stratégies
# Exemples :
ipod, i-pod, i pod
foozball, foosball
universe, cosmos

# Si "expand" est à VRAI, alors la règle "ipod, i-pod, i pod" est équivalente à :
ipod, i-pod, i pod => ipod, i-pod, i pod
# Si "expand" est à FAUX, alors la règle "ipod, i-pod, i pod" est équivalente à :
ipod, i-pod, i pod => ipod

# Les règles multiples sont combinées
# Exemple :
foo => foo bar
foo => baz
# Devient :
foo => foo bar, baz

La recommandation officielle pour la gestion des synonymes (mono ou multi termes) est de définir expand="true" lorsque ce filtre est appliqué à un analyseur type="index",mais pas pour un analyseur type="query". Pour plus d’informations à ce sujet, veuillez lire la page Wiki de Solr sur ce filtre.

Exemple de déclaration (à placer après le nœud "tokenizer" dans un nœud "analyzer") :

<filter class="solr.SnowballPorterFilterFactory" language="French"/>
<filter class="solr.RemoveDuplicatesTokenFilterFactory"/>

Prenez garde à l’ordre de déclaration des classes dans votre analyseur ainsi qu’aux dépendances et aux incompatibilités entre classes. Je n’expliquerai pas toutes les subtilités dans cet article, vous allez devoir expérimenter par vous-mêmes.

Voici quelques exemples d’incohérences logiques dans un même analyseur :

- Utiliser un StandardFilter sans StandardTokenizer ;

- Utiliser un WordDelimiterFilter avec un tokenizer qui modifie davantage la chaîne que ne le ferait un WhitespaceTokenizer ;

- Utiliser un CommonGramsFilter sans avoir de StopFilter.

IV-A-5 - Exemple de déclaration de types

Ne prenez l’exemple ci-dessous qu’à des fins d’études, ne l’utilisez pas tel quel dans votre application finale.

Si nous avons la traduction en français et en anglais dans notre base de données, nous pourrions avoir les types dédiés suivants :

- Affichage : le seul traitement effectué sur la chaîne est un nettoyage cosmétique, ce type est donc adapté pour toutes les langues ;

- Auto complétion : les seuls traitements effectués sur la chaîne sont un nettoyage cosmétique et une segmentation en « n-grams », ce type est donc adapté pour toutes les langues ;

- Recherche en anglais : optimisation (index + requêtes) pour la recherche en langue anglaise ;

- Recherche en français : optimisation (index + requêtes) pour la recherche en langue française.

Cela se traduit par le nœud XML types suivant :

/schema/types

<!-- Type texte pour l’affichage : très peu de traitements -->
<fieldType name="rawText" class="solr.TextField">
<analyzer>
<charFilter class="solr.HTMLStripCharFilterFactory"/>
<tokenizer class="solr.KeywordTokenizerFactory"/>
</analyzer>
</fieldType>

<!-- Type texte pour l’auto complétion -->
<!-- Le filtre EdgeNGram n’est adapté qu’à l’analyzer "index" -->
<fieldType name="grammedText" class="solr.TextField" positionIncrementGap="100">
<analyzer type="index">
<charFilter class="solr.HTMLStripCharFilterFactory"/>
<tokenizer class="solr.WhitespaceTokenizerFactory"/>
<filter class="solr.ASCIIFoldingFilterFactory"/>
<filter class="solr.LowerCaseFilterFactory"/>
<filter class="solr.EdgeNGramFilterFactory" minGramSize="2" maxGramSize="10"/>
</analyzer>
<analyzer type="query">
<charFilter class="solr.HTMLStripCharFilterFactory"/>
<tokenizer class="solr.WhitespaceTokenizerFactory"/>
<filter class="solr.ASCIIFoldingFilterFactory"/>
<filter class="solr.LowerCaseFilterFactory"/>
</analyzer>
</fieldType>

<!-- Type texte pour la recherche, avec traitements spécifiques à l’anglais -->
<fieldType name="englishText" class="solr.TextField" positionIncrementGap="100">
<analyzer>
<charFilter class="solr.HTMLStripCharFilterFactory"/>
<tokenizer class="solr.StandardTokenizerFactory"/>
<filter class="solr.LowerCaseFilterFactory"/>
<filter class="solr.SnowballPorterFilterFactory" language="English"/>
</analyzer>
</fieldType>

<!-- Type texte pour la recherche, avec traitements spécifiques au français -->
<fieldType name="frenchText" class="solr.TextField" positionIncrementGap="100">
<analyzer>
<charFilter class="solr.HTMLStripCharFilterFactory"/>
<tokenizer class="solr.WhitespaceTokenizerFactory"/>
<filter class="solr.LowerCaseFilterFactory"/>
<filter class="solr.SnowballPorterFilterFactory" language="French"/>
</analyzer>
</fieldType>

IV-B - Les champs du document : /schema/fields

IV-B-1 - Introduction

Après avoir défini les types de champs nécessaires à votre noyau, vous pouvez passer à la définition des champs utilisant ces types de données. C’est ce que nous allons voir ici.

Cette partie dépend majoritairement des besoins de votre application. Nous allons voir quelques exemples, mais c’est à vous de construire les champs de données adaptés à votre cas d’utilisation.

Déclaration minimale d’un nœud "field"

<field name="uuid" type="string"/>

Il faut penser à créer un champ unique de type string pour chaque noyau. Pour ma part, j’utilise généralement un hash (sha1, md5 ou autre) de la clef primaire de ma table source (SQL) et j’appelle ce champ « uuid » (cela facilite les traitements avec certains plugins de Solr) :

<field name="uuid" type="string" stored="true" required="true"/>

Solr permet de copier la valeur d’un champ vers d’autres champs (nous verrons à la fin de cette section comment nous y prendre). Cela permet de faciliter l’import de données vers Solr puisqu’un <doc> contient les informations extraites directement de la source de données (par exemple votre base de données) sans se soucier de combien de variantes de chaque valeur le noyau a besoin (à l’identique, en minuscules, sans espaces, sans HTML, phonétique, etc.). Ainsi, la plupart des champs de votre schéma seront sans doute définis en plusieurs déclinaisons sans que cela ne complique d’aucune manière le procédé d’import de données vers votre noyau.

Une stratégie courante est de définir plusieurs versions de chaque champ de données : une copie exacte de l’original (dans chaque langue) et une copie supplémentaire pour chaque cas d’utilisation.

Une autre stratégie courante, et de surcroît compatible avec la précédente, est de définir un champ permettant de rassembler toutes les valeurs « texte » du document afin de faciliter les recherches.

IV-B-2 - Les attributs disponibles pour chaque champ de données

Comme nous l’avons dit précédemment, les attributs d’un nœud field sont les mêmes que pour un nœud fieldType. C’est dû au fait que la classe Java utilisée pour instancier l’objet field est la même que celle utilisée pour instancier l’objet fieldType ; elle en prend donc les propriétés et les méthodes. C’est pour cette raison qu’il est possible de définir une surcharge pour chacun des attributs au niveau du field par rapport aux valeurs définies pour les mêmes attributs au niveau du fieldType, qui peuvent elles-mêmes surcharger les valeurs par défaut de Solr pour cette classe Java.

IV-B-3 - Exemple de déclaration de champs

Les types (nœuds fieldType) montrés plus haut en exemple sont à répartir de la manière suivante dans les champs (nœuds field) de notre schéma :

Tableau 18 : Les champs de notre schéma

Type

Champ

rawText

productNameEnDisplay

rawText

productNameFrDisplay

grammedText

productNameEnAutoComplete

grammedText

productNameFrAutoComplete

englishText

productNameEnSearch

frenchText

productNameFrSearch

rawText

manufacturerNameDisplay

grammedText

manufacturerNameAutoComplete

englishText

manufacturerNameEnSearch

frenchText

manufacturerNameFrSearch

Ainsi, nous pouvons effectuer nos recherches sur le champ productNameFrSearch ou productNameEnSearch suivant la langue de notre utilisateur, et afficher en réponse le nom canonique du produit (non altéré) dans la même langue. Voici les définitions de champs pour notre schéma :

/schema/fields

<field name="uuid" type="string" stored="true" required="true"/>

<field name="productNameEnDisplay" type="rawText"/>
<field name="productNameFrDisplay" type="rawText"/>
<field name="productNameEnAutoComplete" type="grammedText"/>
<field name="productNameFrAutoComplete" type="grammedText"/>
<field name="productNameEnSearch" type="englishText"/>
<field name="productNameFrSearch" type="frenchText"/>

<!-- Un nom de constructeur n’est pas traduisible
il n’y a donc pas de version internationalisée -->
<field name="manufacturerNameDisplay" type="rawText"/>
<field name="manufacturerNameAutoComplete" type="grammedText"/>
<!-- Un utilisateur peut cependant chercher
un nom de constructeur dans sa langue favorite -->
<field name="manufacturerNameEnSearch" type="englishText"/>
<field name="manufacturerNameFrSearch" type="frenchText"/>

<field name="allEnAutoComplete" type="grammedText"/>
<field name="allFrAutoComplete" type="grammedText"/>
<field name="allEnSearch" type="englishText" multiValued="true"/>
<field name="allFrSearch" type="frenchText" multiValued="true"/>

IV-C - Finaliser le schéma

Il nous reste deux éléments à configurer : les valeurs par défaut et les copies de champs.

/schema

<!-- Valeurs par défaut -->
<uniqueKey>uuid</uniqueKey>
<defaultSearchField>allFrSearch</defaultSearchField>

<!-- Les versions localisées du nom de produit sont copiées
dans les champs localisés -->
<copyField source="productNameEnDisplay" dest="productNameEnAutoComplete"/>
<copyField source="productNameFrDisplay" dest="productNameFrAutoComplete"/>
<copyField source="productNameEnDisplay" dest="productNameEnSearch"/>
<copyField source="productNameFrDisplay" dest="productNameFrSearch"/>

<!-- La version internationale du nom de constructeur est copiée
dans les champs, localisés ou non -->
<copyField source="manufacturerNameDisplay" dest="manufacturerNameAutoComplete"/>
<copyField source="manufacturerNameDisplay" dest="manufacturerNameEnSearch"/>
<copyField source="manufacturerNameDisplay" dest="manufacturerNameFrSearch"/>

<!-- Les versions localisées et internationales des noms sont copiées
dans les champs, localisés ou non -->
<copyField source="productNameEnDisplay" dest="allEnSearch"/>
<copyField source="productNameFrDisplay" dest="allFrSearch"/>
<copyField source="manufacturerNameDisplay" dest="allEnSearch"/>
<copyField source="manufacturerNameDisplay" dest="allFrSearch"/>

Le nœud defaultSearchField n’est pas obligatoire, mais il permet de simplifier les tests. Par ailleurs, sachez que le fichier solrconfig.xml permet de définir cette valeur pour chaque requestHandler. Nous aborderons cela dans un chapitre ultérieur.

Une application typique peut utiliser un champ de différentes manières : recherche, affichage, tri, facettes, surbrillance, etc. Sachant que les meilleures combinaisons d’attributs pour ces différents cas d’utilisation sont différentes, il est préférable de définir plusieurs versions de chaque champ (chacune optimisée pour un cas d’utilisation) et d’avoir recours à copyField pour faciliter la population de l’index.

IV-D - Tester le schéma

Solr dispose d’un outil bien pratique pour vérifier que nos types fonctionnent comme prévu : l’analyseur de champs (field analysis). Le lien se trouve dans l’interface web d’administration de votre noyau ou bien directement via l’URL suivante :
http://localhost:8080/solr/CORENAME/admin/analysis.jsp

Image 2 : Analyseur de types

Le principe est de sélectionner « type » dans la liste déroulante, puis de saisir le nom du type à tester dans la première case de saisie. Les deux autres cases de saisie permettent de simuler une valeur indexée par ce type et une valeur cherchée par un utilisateur pour ce type. Prenons pour exemple notre type « frenchText » destiné aux recherches. Le synopsis de cet article servira de texte indexé, et les mots « manipuler du texte » feront office de termes recherchés. Pour le moment, la seule option qui nous intéresse est « highlight matches » afin de savoir si Solr a pu trouver notre mot dans le texte indexé :

Image 3 : Analyse simple - 1° partie

Image 4 : Analyse simple - 2° partie

Nous voyons ici que Solr effectue trois manipulations sur notre texte de recherche (il y a trois lignes sous « Query Analyzer ») ainsi que sur notre texte indexé (il y a trois lignes sous « Index Analyzer »). De plus, Solr nous montre en gris les correspondances de valeurs qu’il a pu trouver dans le texte indexé.

Pour un premier résultat, c’est assez engageant, car nous avons effectivement pu trouver tous les termes recherchés. Cependant, un problème apparent dans la copie d’écran ci-dessus est que l’une des occurrences du mot « texte » ne figure pas dans les cases en gris, ce qui signifie que Solr ne l’a pas correctement identifiée. Solr a pu faire correspondre trois occurrences du mot « texte » dans le paragraphe indexé, au lieu des quatre occurrences que nous attendions. Notre configuration actuelle est donc un bon début, mais elle n’est pas suffisamment efficace. Nous allons utiliser les options de l’outil Field Analysis pour déterminer la cause du problème.

Activons maintenant l’option « Field value (Query) / verbose output » :

Image 5 : Analyse détaillée

Nous comprenons maintenant que les manipulations de l’index et de la recherche sont dues aux classes Java chargées dans notre fieldType : solr.HTMLStripCharFilterFactory, solr.WhitespaceTokenizerFactory, solr.LowerCaseFilterFactory et solr.SnowballPorterFilterFactory. Après chacune de ces classes, de nouvelles variantes de l’index et de la chaîne de recherche sont affichées dans l’analyseur. Sans l’option « verbose », seuls les résultats du tokenizer et des filter sont affichés ; l’option « verbose » permet d’afficher également le résultat du charFilter.

Si nous activons maintenant l’option « Field value (Index) / verbose output », nous voyons le nom du filter duquel le terme « texte. » (en position 23 dans « manipulations automatiques du texte. ») ne sort pas comme les autres occurrences du mot « texte » : il s’agit du filter « SnowballPorter » en charge de la racinisation, probablement à cause du point résiduel à la fin du mot :

Image 6 : Analyse détaillée

Si notre supposition concernant la cause du problème est correcte, alors le coupable est probablement le tokenizer. Rappelons-nous que nous avons choisi la classe solr.WhitespaceTokenizerFactory pour découper le type frenchText, or elle n’est peut-être pas adaptée. Essayons de remplacer cette classe par un tokenizer plus agressif, par exemple solr.StandardTokenizerFactory :

type frenchText de départ (schema.xml)

<fieldType name="frenchText" class="solr.TextField" positionIncrementGap="100">
<analyzer>
<charFilter class="solr.HTMLStripCharFilterFactory"/>
<tokenizer class="solr.WhitespaceTokenizerFactory"/>
<filter class="solr.LowerCaseFilterFactory"/>
<filter class="solr.SnowballPorterFilterFactory" language="French"/>
</analyzer>
</fieldType>

type frenchText modifié (schema.xml)

<fieldType name="frenchText" class="solr.TextField" positionIncrementGap="100">
<analyzer>
<charFilter class="solr.HTMLStripCharFilterFactory"/>
<tokenizer class="solr.StandardTokenizerFactory"/>
<filter class="solr.LowerCaseFilterFactory"/>
<filter class="solr.SnowballPorterFilterFactory" language="French"/>
</analyzer>
</fieldType>

Afin que la nouvelle configuration soit prise en compte, pensez à redémarrer Tomcat ou à charger l’URL suivante :
http://localhost:8080/solr/admin/cores?core=CORENAME&action=reload

Relançons maintenant le même test :

Image 7 : Analyse détaillée

Cette fois, le texte est mieux découpé, car la ponctuation n’est plus comprise dans les termes. Dans Solr, un tokenizer associe une typologie à chaque terme : avec solr.WhitespaceTokenizerFactory, cette typologie était simplement « word » ; avec solr.StandardTokenizerFactory, cela peut être « ALPHANUM », « ACRONYM », « APOSTROPHE », etc. Cette typologie est utile si vous avez choisi un dérivé de StandardFilter dans au moins l’un des filter utilisés pour ce fieldType (ce qui n’est pas notre cas ici).

Je pense que cet exemple suffit pour illustrer la démarche d’optimisation du schéma :

1. testez vos types avec des requêtes légitimes de vos utilisateurs pour voir le comportement de Solr ;

2. modifiez votre schéma en conséquence ;

3. redémarrez Tomcat pour prendre en compte la nouvelle configuration ;

4. retour au #1 jusqu’à obtention d’un schéma satisfaisant (il y aura toujours des cas particuliers pour faire mentir votre schéma).

V - Maintenance des données d’un index

Les données d’index Solr peuvent être mises à jour avec un document au format CSV ou XML. D’après le fichier solrconfig.xml d’exemple, les formats javabin et JSON sont aussi disponibles, mais la documentation à ce sujet est moins abondante et je n’ai pas vérifié moi-même. Quel que soit son format, le document doit être soumis à Solr via la méthode HTTP POST. Dans les paragraphes suivants, nous allons voir comment le document doit être structuré et de quelle manière nous pouvons l’envoyer à Solr.

Le format XML a ma préférence sur le format CSV, car il est plus facile à relire que ce dernier, surtout dans le cas des champs multivalués. Avec un document CSV, les choses se compliquent dès que le texte à indexer contient le caractère de séparation de champ ou un saut de ligne (ce qui est une situation plutôt courante).

V-A - Configuration nécessaire

Afin de pouvoir envoyer des mises à jour de l’index au noyau, il nous faut configurer le noyau avec le requestHandler que nous utiliserons :

Tous les formats (solrconfig.xml)

<requestHandler name="/update" class="solr.XmlUpdateRequestHandler" />
<requestHandler name="/update/javabin" class="solr.BinaryUpdateRequestHandler" />
<requestHandler name="/update/csv" class="solr.CSVRequestHandler" startup="lazy" />
<requestHandler name="/update/json" class="solr.JsonUpdateRequestHandler" startup="lazy" />

Dans la mesure où il est peu raisonnable de développer et de maintenir plusieurs manières de mettre un index à jour, vous n’aurez probablement besoin que d’un seul requestHandler « update ». Ainsi, vous pouvez déclarer uniquement le handler de base « /update » et choisir sa classe Java en fonction de vos besoins :

Format XML (solrconfig.xml)

<requestHandler name="/update" class="solr.XmlUpdateRequestHandler" />

Format javabin (solrconfig.xml)

<requestHandler name="/update" class="solr.BinaryUpdateRequestHandler" />

Format CSV (solrconfig.xml)

<requestHandler name="/update" class="solr.CSVRequestHandler" startup="lazy" />

Format JSON (solrconfig.xml)

<requestHandler name="/update" class="solr.JsonUpdateRequestHandler" startup="lazy" />

V-B - Ajout et modification de données

Le document XML est construit de la manière suivante :

<add>
<doc>
<field name=""></field>
<field name=""></field>
<field name=""></field>
</doc>
<doc>
<field name=""></field>
<field name=""></field>
<field name=""></field>
</doc>
</add>

Des exemples plus complets sont distribués avec Solr (6).

Chaque nœud <doc> correspond à un document de l’index. Il contient l’ensemble des champs définis dans le fichier schema.xml pour ce noyau.

Les champs définis avec l’attribut required="true" dans le schéma doivent obligatoirement figurer pour chaque nœud <doc>. Un même champ peut apparaître plusieurs fois dans le même nœud <doc> s’il s’agit d’un champ multivalué, c’est-à-dire défini avec l’attribut multivalued="true" dans le schéma. Les autres champs ne peuvent apparaître qu’une seule fois pour chaque nœud <doc>.

Comme nous l’avons vu dans le chapitre précédent, la contrainte d’unicité d’un index est le champ défini comme <uniqueKey> dans le schéma. Pour Solr, il n’y a pas de distinction entre les opérations « create » et « update » des documents : lorsque le requestHandler de mise à jour est appelé pour une certaine uniqueKey, Solr s’occupe de créer le document correspondant s’il n’existait pas encore, ou de le modifier s’il existait déjà.

Voici les attributs optionnels pour chaque nœud XML du document de mise à jour :

Tableau 19 : Attributs optionnels du document de mise à jour selon son type

Nœud

Attribut

Valeurs

Description

add

overwrite

true | false

Remplacer les documents ayant la même uniqueKey
Défaut à « false »

add

commitWithin

millisecondes

Valider le document après ce délai

doc

boost

float

Augmenter la pertinence de ce document complet pour la recherche (si omitNorms="false" dans le schéma)
Défaut à « 1.0 »

field

boost

float

Augmenter la pertinence de ce champ pour la recherche (si omitNorms="false" dans le schéma)
Défaut à « 1.0 »

Lors de la configuration des champs de l’index par l’intermédiaire du fichier schema.xml, les champs déclarés comme « dest » de <copyField/> ne doivent pas figurer dans les mises à jour. Par exemple, pour le schéma donné dans cet article, nous pouvons résumer la liste des champs par le tableau suivant :

Tableau 20 : Les champs à inclure dans le document de mise à jour

Champ

schema:copyField

schema:field[required]

Présence dans doc. de màj

uuid

non défini

oui

oui, obligatoire

productNameEnDisplay

source

non

oui, facultatif

productNameFrDisplay

source

non

oui, facultatif

productNameEnAutoComplete

dest

non

non

productNameFrAutoComplete

dest

non

non

productNameEnSearch

dest

non

non

productNameFrSearch

dest

non

non

manufacturerNameDisplay

source

non

oui, facultatif

manufacturerNameAutoComplete

dest

non

non

manufacturerNameEnSearch

dest

non

non

manufacturerNameFrSearch

dest

non

non

allEnSearch

dest

non

non

allFrSearch

dest

non

non

Cela pourrait donner le document XML suivant :

<add>
<doc>
<field name="uuid"></field>
<field name="productNameEnDisplay"></field>
<field name="productNameFrDisplay"></field>
<field name="manufacturerNameDisplay"></field>
</doc>
</add>

Les champs suivants de l’index sont remplis par l’intermédiaire de copyField :

- productNameEnAutoComplete

- productNameFrAutoComplete

- productNameEnSearch

- productNameFrSearch

- manufacturerNameAutoComplete

- manufacturerNameEnSearch

- manufacturerNameFrSearch

- allEnSearch

- allFrSearch

V-C - Suppression de données

La suppression de documents d’un index se fait au moyen d’un document delete contenant une combinaison de nœuds id et query :

Supprimer tous les documents de l’index

<delete>
<query>*:*</query>
</delete>

Supprimer l’ID 05991

<delete>
<id>05991</id>
</delete>

Supprimer les ID 05991 et 06000 ainsi que les bureaux Bridgewater et Osaka

<delete>
<id>05991</id>
<id>06000</id>
<query>office:Bridgewater</query>
<query>office:Osaka</query>
</delete>

La syntaxe du nœud query pour la suppression de données est la même que pour la recherche avec un searchHandler. Nous aborderons cela plus loin dans l’article.

Le nom du nœud id est figé, il ne peut pas prendre le nom d’un champ du schéma. Solr s’attend à trouver le contenu de ce nœud dans le champ défini comme uniqueKey dans le schéma. Pour le schéma présenté dans cet article, les deux requêtes suivantes sont équivalentes :

<delete>
<id>aaaa</id>
</delete>

<delete>
<query>uuid:aaaa</query>
</delete>

V-D - Validation de mises à jour

La validation des mises à jour se fait par les commandes suivantes :

Valider les modifications

<commit/>

Valider les modifications et optimiser l’index

<optimize/>

Voici les attributs optionnels du document de mise à jour :

Tableau 21 : Attributs optionnels des commandes de validation des mises à jour

Nœud

Attribut

Valeurs

Description

commit et optimize

waitFlush

true | false

Bloquer jusqu’à ce que les modifications de l’index soient écrites sur le disque
Défaut à « true »

commit et optimize

waitSearcher

true | false

Bloquer jusqu’à ce qu’un nouveau chercheur soit ouvert et enregistré comme le chercheur principal, rendant ainsi les modifications visibles
Défaut à « true »

commit

expungeDeletes

true | false

Ignorer les suppressions
Défaut à « false »

optimize

maxSegments

int

Optimiser jusqu’à ce nombre de segments individuels
Défaut à « 1 »

V-E - Annulation de mises à jour

La commande rollback permet d’annuler toutes les commandes add et delete depuis la dernière commande commit ou optimize :

<rollback/>

V-F - Soumettre le document de maintenance

L’envoi d’un document de maintenance à Solr se fait via la méthode HTTP POST au requestHandler « update » de notre noyau, par exemple :
http://localhost:8080/solr/CORENAME/update

V-G - Exemple

Préparons un XML à envoyer :

add1.xml

<add>
<doc>
<field name="uuid">f2446de21f011a769ec62b4c4ecb96d2</field>
<field name="productNameEnDisplay">Solr, the enterprise search server</field>
<field name="productNameFrDisplay">Solr, le serveur de recherche pour entreprises</field>
<field name="manufacturerNameDisplay">Apache Software Foundation</field>
</doc>
</add>

Envoyons ce XML à l’aide de cURL :

Envoi avec cURL

curl http://localhost:8080/solr/CORENAME/update -H "Content-Type: text/xml" --data-binary "<delete><query>*:*</query></delete>"
curl http://localhost:8080/solr/CORENAME/update -H "Content-Type: text/xml" --data-binary "INSERER_ICI_TOUT_LE_XML"
curl http://localhost:8080/solr/CORENAME/update -H "Content-Type: text/xml" --data-binary "<optimize waitSearcher='false'/>"

cURL est le logiciel habituellement utilisé pour cette démonstration, mais vous n’êtes pas obligés d’y avoir recours. Vous pouvez utiliser le programme de votre choix, voire développer un petit script ou programme avec le langage de votre préférence.

Le code suivant est un exemple de script PHP pouvant fonctionner avec une installation PHP par défaut (ni le programme cURL ni l’extension php_curl ne sont nécessaires) :

send.php

#!/usr/bin/php5
<?php

if(PHP_SAPI != 'cli')
{
die('error: not sent via CLI'.PHP_EOL);
}

if($argc != 4 or !in_array($argv[2], array('file', 'string')))
{
die('usage is: '.basename(__FILE__).' CORENAME "file"|"string" FILENAME|CONTENTS'.PHP_EOL);
}

list(, $core_name, $content_type, $content) = $argv;
$url = sprintf('http://localhost:8080/solr/%s/update', $core_name);
$data = $content_type == 'string' ? $content : file_get_contents($content);

$params = array
(
'http' => array (
'method' => 'POST'
, 'header' => 'Content-Type: text/xml'
, 'content' => $data
, 'timeout' => 5
)
);

$context = stream_context_create($params);
$result = file_get_contents($url, FALSE, $context);

var_dump($result);
echo 'done'.PHP_EOL;

Exemple d’utilisation en lignes de commandes (Linux et MacOS) :

./send.php produits string "<delete><query>*:*</query></delete>"
./send.php produits file add1.xml
./send.php produits string "<optimize waitSearcher='true'/>"

Exemple d’utilisation en lignes de commandes (Windows) :

php.exe send.php produits string "<delete><query>*:*</query></delete>"
php.exe send.php produits file add1.xml
php.exe send.php produits string "<optimize waitSearcher='true'/>"

Pour vérifier que les modifications ont bien été effectuées, vous pouvez charger la page suivante :

http://localhost:8080/solr/admin/cores?core=CORENAME&action=status

Image 8 : Mise à jour réussie

Afin de vérifier plus précisément ce qui a été fait par Solr, notamment que les directives copyField ont réussi, voici une requête simple sur l’index :
http://localhost:8080/solr/produits/select?q=uuid:f2446de21f011a769ec62b4c4ecb96d2

Image 9 : Vérifier que les données sont dans l’index

Rappelons les champs envoyés à Solr :

- uuid

- productNameEnDisplay

- productNameFrDisplay

- manufacturerNameDisplay

La capture d’écran ci-dessus montre que Solr a bel et bien rempli tous les champs de l’index d’après les règles copyField. Comme prévu, certains champs de l’index contiennent plusieurs valeurs.

VI - Interroger un index de données

VI-A - Préparation

Dans le chapitre III-E, nous avons défini une configuration de solrconfig.xml permettant d’effectuer des requêtes :

<requestHandler name="standard" class="solr.SearchHandler" default="true"/>

Dans ce chapitre, nous allons avoir besoin de quelques informations de débogage. Pour les activer, modifiez les fichiers de votre noyau de la manière suivante :

$SOLR_HOME/cores/produits/conf/solrcore.properties

solr.output.echoHandler = true
solr.output.echoParams = explicit

$SOLR_HOME/cores/produits/conf/solrconfig.xml

<requestHandler name="standard" class="solr.SearchHandler" default="true">
<lst name="defaults">
<str name="echoHandler">${solr.output.echoHandler}</str>
<str name="echoParams">${solr.output.echoParams}</str>
</lst>
</requestHandler>

« echoHandler » peut prendre les valeurs « false » ou « true ».

« echoParams » peut prendre les valeurs « none », « explicit » ou « all ».

Pensez à recharger la configuration de Solr après avoir modifié le fichier solrconfig.xml. Pour rappel, cela peut être fait soit en redémarrant Tomcat soit en chargeant l’URL suivante :
http://localhost:8080/solr/admin/cores?core=CORENAME&action=reload

Cette modification ajoute deux éléments « handler » et « params » dans le responseHeader de la réponse. Par exemple, pour la requête effectuée à la fin du chapitre précédent, la réponse de Solr devient :

Image 10 : Paramètres explicites

Un troisième paramètre de débogage peut être ajouté : debugQuery. Il peut prendre les valeurs « false » ou « true ». Il ajoute des informations bien plus détaillées sur la manière dont Solr décompose les requêtes. Sauf pour parfaire votre éducation personnelle ou si vous pensez avoir identifié un bug dans Solr, vous ne devriez pas en avoir besoin.

VI-B - Surcharge de paramètres

Ce que j’appelle surcharge de paramètres est la possibilité de modifier la valeur d’un paramètre à tous les niveaux. Lorsque vous effectuez une requête Solr, les paramètres envoyés par HTTP GET sont évalués de la manière suivante :

1. Si le requestHandler a défini le paramètre dans la liste des « invariants », alors sa valeur est lue depuis solrconfig.xml ;

2. Sinon, il est déduit dans l’ordre suivant :

1. présence dans l’URL ;

2. définition dans les « defaults » du requestHandler ;

3. définition dans la déclaration du nom du champ dans schema.xml ;

4. définition dans la déclaration du nom du type du champ dans schema.xml ;

5. valeur par défaut du type du champ (valeur interne de Solr ou de Lucene).

Cela devient pratique dès que vous identifiez un paramètre qui revient très souvent à la même valeur. Puisque tous les composants de Solr peuvent être activés par un paramètre dans l’URL, il est très facile de prototyper une application. Par la suite, nous verrons comment utiliser avantageusement tout cela.

VI-C - Composant solr.StandardRequestHandler

Ce composant est très générique. Il permet d’effectuer des requêtes sur l’index et de récupérer les résultats. La majorité des autres composants réutilisent ses paramètres ou ajoutent les leurs.

Paramètres courants :

- q : « query », c’est le paramètre qui contient la chaîne recherchée, qui peut être appliquée à un champ précis ou au champ par défaut ;

- fl : liste de champs renvoyés ;

- sort : tri des résultats (potentiellement sur plusieurs champs) ;

- start : borne de départ pour limiter le jeu de résultats - permet la pagination des résultats ;

- rows : nombre maximum de résultats renvoyés ;

- fq : « filter query », similaire à la clause WHERE du langage SQL - peut contenir plusieurs conditions séparées des mots clefs AND ou OR, ou bien peut apparaître plusieurs fois.

VI-C-1 - Paramètre « q »

Ce paramètre a deux usages :

- soit en chaîne libre, c’est-à-dire une recherche sur le champ par défaut : « q=solr » ;

- soit en champ spécifique : « q=productNameEnSearch:solr ».

Il est possible de filtrer les résultats de la requête en utilisant la variante « q=champ:valeur » et les opérateurs, par exemple : « q=productNameEnSearch:solr AND manufacturerNameEnSearch:apache », mais nous verrons plus loin qu’il y a d’autres méthodes pour filtrer les résultats.

Il est également possible d’utiliser des couples de parenthèses pour appliquer les opérateurs, par exemple : « q=manufacturerNameEnSearch:apache AND (productNameFrSearch:recherche OR productNameEnSearch:search) ».

VI-C-2 - Paramètre « fl »

Il permet de ne renvoyer qu’un sous-ensemble des champs définis dans schema.xml pour ce document. S’il n’est pas renseigné, Solr considère qu’il faut renvoyer tous les champs (excepté score, car il ne fait pas partie du document).

Pour définir la liste des champs à renvoyer, il faut séparer les noms de champs par une virgule : « ?q=solr&fl=score,allEnSearch,allFrSearch » ou « ?q=solr&fl=score,* ».

VI-C-3 - Paramètre « sort »

Le paramètre « sort » s’applique de la manière suivante : sort=<fieldName> <direction>[,<fieldName> <direction>]...

La direction est soit « asc », soit « desc ».

Solr ne peut trier les résultats que dans les conditions suivantes :

- il s’agit du pseudo champ « score » ;

- il s’agit d’une fonction ne renvoyant qu’un seul résultat par document ;

- il s’agit d’un champ qui est à la fois indexé (indexed="true") et non multivalué (multiValued="false"), pourvu que son fieldType soit non tokenisé ou que son fieldType/analyzer/tokenizer utilise KeywordTokenizer.

Comme on peut le voir, effectuer un tri sur un champ de type texte est sujet à diverses contraintes. Si vous vous trouvez dans cette situation, la solution est généralement d’utiliser copyField pour avoir plusieurs variantes du champ, dont une soit compatible avec le paramètre sort.

VI-C-4 - Paramètres « start » et « rows »

Ces paramètres sont similaires aux clauses SQL LIMIT, OFFSET et TOP proposées par certains SGBD.

« start » permet de limiter le jeu de résultats par décalage du début. Par exemple, si ma requête « ?q=solr » me renvoie 10 résultats, je peux demander « ?q=solr&start=3 » pour que le jeu de résultats en contienne 4 de moins. Comme souvent en informatique, ce paramètre commence à la valeur zéro.

« rows » permet de limiter le jeu de résultats par un retour prématuré. Par exemple, si ma requête « ?q=solr » me renvoie 10 résultats, je peux demander « ?q=solr&rows=6 » pour ne récupérer que les 6 premiers.

VI-C-5 - Paramètre « fq »

Il permet de filtrer le jeu de résultats et, bien qu’il puisse sembler redondant, il évite de devoir construire un paramètre « q » compliqué.

Contrairement au paramètre « q », il est possible de préciser plusieurs fois le paramètre « fq » dans la même requête : « ?q=solr&fq=productNameFrSearch:recherche OR productNameEnSearch:search ».

Une différence majeure avec le paramètre « q » est que le paramètre « fq » n’influence pas le score des résultats.

Comparez : http://localhost:8080/solr/produits/select?q=solr%20AND%20%28productNameFrSearch:recherche%20OR%20productNameEnSearch:search%29&fl=score,uuid

Image 11 : Paramètre « q »

Avec : http://localhost:8080/solr/produits/select?q=solr&fq=productNameFrSearch:recherche%20OR%20productNameEnSearch:search&fl=score,uuid

Image 12 : Paramètre « fq »

VI-C-6 - Autres paramètres

Je vous laisse découvrir les autres paramètres de SearchHandler et de CommonQueryParameters, puis les paramètres de chacun des composants que vous allez utiliser.

VII - L’API du noyau : solrconfig.xml

Si le fichier schema.xml regroupe l’intelligence d’un noyau Solr, le fichier solrconfig.xml, quant à lui, regroupe sa configuration. On y trouvera des directives pour localiser les fichiers des classes Java à utiliser, le comportement par défaut de certaines classes, ainsi que la description de l’API de notre noyau.

Cette page sert de référence au reste de l’article. Si vous n’avez pas la patience de la lire de bout en bout à la première lecture, ce qui serait tout à fait normal, n’hésitez pas à passer à la suite pour le moment. Vous pourrez revenir sur cette page lorsque vous aurez besoin de configurer votre schéma ou votre API.

À tout moment, vous pouvez regarder le fichier example/solr/conf/solrconfig.xml de l’archive téléchargée. Ce fichier est très complet et il contient de nombreux commentaires pour vous aider dans vos prises de décisions.

VII-A - Directives de configuration du noyau

Nous avons déjà vu ces directives dans un chapitre précédent : il s’agit d’abortOnConfigurationError pour indiquer à Solr s’il doit poursuivre ou arrêter le chargement du noyau lorsqu’une erreur est rencontrée, ainsi que de dataDir pour localiser le répertoire des fichiers de l’index principal.

<abortOnConfigurationError>${solr.config.abortOnConfigurationError}</abortOnConfigurationError>
<dataDir>${solr.config.dataDir}</dataDir>

VII-B - Inclusion de classes Java

Dans la mesure où cet article ne détaillera pas les fonctionnalités de plugins de Solr, nous ne nous étendrons pas sur toutes les subtilités de cette directive. Sachez qu’elle existe et qu’elle permet de charger des classes supplémentaires dans Solr.

<lib dir="../../contrib/extraction/lib" />

VII-C - Pré configurer une requête grâce à un requestHandler

Dans le chapitre « VI. Interroger un index de données », nous avons vu que nous pouvons combiner toute une liste de paramètres pour interroger notre index Solr. Malheureusement, les URL pour interroger Solr deviennent très rapidement longues, très longues et difficiles à relire ou à déboguer.

À cet effet, ainsi que pour vous permettre de brancher facilement un système ACL sur vos URL Tomcat, Solr permet de définir des « request handlers » avec un nom arbitraire. Ces request handlers sont configurables avec des valeurs par défaut ou avec des valeurs fixes, selon vos besoins.

Habituellement, on définit au moins les request handlers suivants : « standard », « /update », « /admin » et « /admin/ping ». Ils sont réservés par Solr afin de permettre un minimum d’interactions avec l’index.

Exemple de request handlers par défaut :

<requestHandler name="standard" class="solr.SearchHandler" default="true">
<lst name="defaults">
<str name="echoHandler">${solr.output.echoHandler}</str>
<str name="echoParams">${solr.output.echoParams}</str>
<str name="debugQuery">${solr.output.debugQuery}</str>
</lst>
</requestHandler>

<requestHandler name="/update" class="solr.XmlUpdateRequestHandler" />
<requestHandler name="/admin" class="solr.admin.AdminHandlers" />

<requestHandler name="/admin/ping" class="solr.PingRequestHandler">
<lst name="defaults">
<str name="qt">standard</str>
<str name="q">solrpingquery</str>
<str name="echoParams">none</str>
</lst>
</requestHandler>

Ensuite, à vous de voir quels sont les points communs entre vos requêtes les plus fréquentes. Vous pouvez définir des request handlers pour les regrouper par type d’utilisation.

Par exemple, dans le cas d’un site de produits :

- home : sélection d’après des filtres arbitraires, pas besoin de calculer des filtres ou des suggestions, pas de saisie utilisateur…

- browse : navigation d’après quelques paramètres arbitraires, besoin de calculer des filtres, pas besoin de suggestions, pas de saisie utilisateur…

- search : navigation d’après une saisie utilisateur, besoin de calculer des filtres, besoin de suggestions…

Les composants utilisés par chaque requestHandler sont définis dans des tableaux de chaînes représentant les noms des composants (<arr name="..."><str>...</str><str>...</str></arr>). Il y a deux manières de déclarer ces tableaux : soit un unique tableau « components » avec la liste complète des composants à autoriser dans ce requestHandler, soit deux listes « first-components » et « last-components » qui s’ajoutent aux composants par défaut d’un requestHandler de Solr.

Composants par défaut d’un requestHandler :

<arr name="components">
<str>query</str>
<str>facet</str>
<str>mlt</str>
<str>highlight</str>
<str>stats</str>
<str>debug</str>
</arr>

Dans l’exemple ci-dessus, vous pouvez voir que les paramètres sont définis dans une liste appelée « defaults » (<lst name="defaults">...</lst>). Toutes les valeurs définies ici remplacent les valeurs définies dans schema.xml, et à plus forte raison les valeurs par défaut de Solr.

Dans Solr, un requestHandler se compose des listes suivantes :

- defaults : ces valeurs peuvent être remplacées au niveau de la requête ;

- invariants : ces valeurs sont considérées comme finales, elles ne peuvent pas être surchargées au niveau de la requête ;

- appends : ces valeurs sont ajoutées quoi qu’il en soit aux requêtes faites sur ce requestHandler.

La liste « appends » permet d’ajouter des valeurs à un paramètre multivalué chaque fois qu’il est utilisé dans la requête. Par exemple, si vous avez un champ booléen « inStock » dans votre schéma, vous pouvez le mettre dans la liste « appends » sous la forme « <str name="fq">inStock:true</str> » pour être assuré de n’afficher que des produits en stock.

De son côté, la liste « invariants » restreint l’usage de certains paramètres. Par exemple, vous pouvez bloquer la liste de champs renvoyés (fl) ou les filtres (facet.field, facet.query et facet.date).

Dans le fichier solrconfig.xml, les nœuds <requestHandler> sont positionnés à l’intérieur du nœud <config>.

VIII - Un schéma d’exemple : index de livres au format poche

VIII-A - Cahier des charges

Notre index permettra de rechercher des livres en anglais ou en français d’après les données « texte » suivantes : titre, nom de série, synopsis, nom d’auteur.

Les autres valeurs dont nous disposons permettront de définir des filtres pour préciser la recherche ou bien d’afficher des informations élémentaires (image, lien).

Notre application proposera les situations suivantes :

- affichage des détails d’un livre ;

- auto complétion ;

- recherche avec traitements spécifiques à l’Anglais, surlignement des résultats et proposition de corrections ;

- recherche avec traitements spécifiques au Français, surlignement des résultats et proposition de corrections.

Information légale :

Pour préparer cet exemple, je me suis appuyé sur les écrits d’Ilona Andrews. Leurs travaux sont protégés par le droit d’auteur (loi américaine). J’ai obtenu l’autorisation d’utiliser les extraits présentés ici. Si vous souhaitez les utiliser dans vos propres publications, vous devrez faire de même.

VIII-B - solrconfig.xml (composants)

Fichier solrconfig.xml de départ :

<?xml version="1.0" encoding="UTF-8" ?>
<config>
<abortOnConfigurationError>${config.abortOnConfigurationError}</abortOnConfigurationError>
<dataDir>${config.dataDir}</dataDir>

<requestHandler name="standard" class="solr.SearchHandler" default="true"/>
</config>

D’après notre cahier des charges, nous allons devoir dresser la liste des composants non standards que nous allons utiliser, et plus particulièrement ceux qui ont un impact sur notre Schéma.

affichage des détails d’un livre :

- solr.SearchHandler et solr.MoreLikeThisHandler peuvent convenir, et ils sont chargés par défaut ;

- aucun composant supplémentaire n’est requis.

auto complétion lors de la recherche :

- les composants les mieux adaptés pour cela sont solr.TermsComponent et Suggester (une extension de solr.SpellCheckComponent), or ils ne sont pas chargés par défaut ;

- malheureusement, les implémentations actuelles de Suggester utilisent un dictionnaire en mémoire (non conservé sur disque) qu’il faut penser à reconstruire après chaque reload du noyau ;

- notre choix se portera sur le composant le plus simple à utiliser : solr.TermsComponent.

recherche avec propositions de corrections :

- le composant solr.SpellCheckComponent est requis ;

- les champs concernés doivent subir très peu de traitements lors de l’analyse.

recherche avec surlignement des résultats :

- le composant solr.HighlightComponent est requis ;

- les champs concernés dans schema.xml ont besoin des valeurs « stored », « termVectors », « termPositions » et « termOffsets » définies à « true ».

Ajout des composants requis par notre application :

<searchComponent name="terms" class="solr.TermsComponent"/>

<searchComponent name="spellCheck" class="solr.SpellCheckComponent">
<lst name="spellchecker">
<str name="name">spellcheckEn</str>
<str name="field">lwAllEn</str>
<str name="queryAnalyzerFieldType">lowerText</str>
<str name="spellcheckIndexDir">./spellcheckEn</str>
<str name="buildOnOptimize">${config.dictionaries.buildOnOptimize}</str>
</lst>
<lst name="spellchecker">
<str name="name">spellcheckFr</str>
<str name="field">lwAllFr</str>
<str name="queryAnalyzerFieldType">lowerText</str>
<str name="spellcheckIndexDir">./spellcheckFr</str>
<str name="buildOnOptimize">${config.dictionaries.buildOnOptimize}</str>
</lst>
</searchComponent>

Personnalisation facultative de solr.HighlightComponent :

<searchComponent name="highlight" class="solr.HighlightComponent">
<highlighting>
<formatter name="html" class="solr.highlight.HtmlFormatter" default="true">
<lst name="defaults">
<str name="hl.simple.pre"><![CDATA[[hl]]]></str>
<str name="hl.simple.post"><![CDATA[[/hl]]]></str>
</lst>
</formatter>
<fragmentsBuilder name="colored" default="true" class="solr.highlight.ScoreOrderFragmentsBuilder">
<lst name="defaults">
<str name="hl.tag.pre"><![CDATA[
[hl=yellow],[hl=lawgreen],[hl=aquamarine]
,[hl=magenta],[hl=palegreen],[hl=coral]
,[hl=wheat],[hl=khaki],[hl=lime],[hl=deepskyblue]
]]></str>
<str name="hl.tag.post"><![CDATA[[/hl]]]></str>
</lst>
</fragmentsBuilder>
</highlighting>
</searchComponent>

Le composant spellCheck a une dépendance sur certains éléments de schema.xml : les champs « lwAllEn » et « lwAllFr », ainsi que le type « lowerText » qui leur est appliqué. Afin que les fonctionnalités de correction orthographique soient efficaces, ce type de données doit subir peu de traitements au moment de l’analyse. Idéalement, il faudrait nous contenter d’un tokenizer permettant d’identifier les mots, et de filtres pour supprimer la ponctuation et pour normaliser le texte. La réduction (stemming) et les autres manipulations complexes du texte ne sont pas souhaitables ici.

Dans le fichier solrconfig.xml, les nœuds <searchComponent> sont positionnés à l’intérieur du nœud <config>.

VIII-C - schema.xml

Les situations gérées par notre application supposent certains types et certains attributs.

Nous allons créer les types suivants :

- rawText : prévu pour l’affichage ;

- lowerText : pour le bon fonctionnement la correction orthographique ;

- grammedText : prévu pour l’auto complétion ;

- stemmedEnglishText : prévu pour la recherche en langue anglaise ;

- stemmedFrenchText : prévu pour la recherche en langue française.

Dictionnaire des données et variantes de chaque champ :

champ

type (variantes)

stored

indexed

required

multiValued

isbn13

string

true

true

createdAt

tdate

true

true

true

copyrightYear

tint

true

true

true

nbPages

tint

true

true

nbInSeries

tint

true

true

urlEn

string

true

true

urlFr

string

true

true

coverImageEn

string

true

true

coverImageFr

string

true

true

titleEn

rawText (grammedText, stemmedEnglishText)

true

true

titleFr

rawText (grammedText, stemmedFrenchText)

true

true

seriesEn

rawText (grammedText, stemmedEnglishText)

true

true

seriesFr

rawText (grammedText, stemmedFrenchText)

true

true

synopsisEn

rawText (grammedText, stemmedEnglishText)

true

true

true

synopsisFr

rawText (grammedText, stemmedFrenchText)

true

true

true

authorNames

rawText (stemmedEnglishText, stemmedFrenchText)

true

true

true

true

originalCoverArtBy

rawText

true

true

true

originalCoverDesignBy

rawText

true

true

true

originalInteriorTextDesignBy

rawText

true

true

true

editors

rawText

true

true

true

true

allEn

rawText (grammedText, stemmedEnglishText)

true

allFr

rawText (grammedText, stemmedFrenchText)

true

Fichier final : conf/schema.xml

Une fois le fichier schema.xml en place, vous pouvez recharger votre noyau afin de vérifier que tout s’est bien passé.

http://localhost:8080/solr/admin/cores?core=livres&action=reload

Un "status" de zéro est OK :

<?xml version="1.0" encoding="UTF-8"?>
<response>
<lst name="responseHeader">
<int name="status">0</int>
<int name="QTime">490</int>
</lst>
</response>

VIII-D - Envoi des données au noyau

Pour remplir notre noyau avec des données, nous allons préparer un gabarit du document XML final. Il nous restera alors à y copier/coller les données.

Structure XML pour remplir notre index :

<add>
<doc>
<field name="isbn13"></field>
<field name="copyrightYear"></field>
<field name="nbPages"></field>
<field name="nbInSeries"></field>
<field name="urlEn"></field>
<field name="urlEn"></field>
<field name="urlFr"></field>
<field name="urlFr"></field>
<field name="coverImageEn"></field>
<field name="coverImageFr"></field>
<field name="titleEn"></field>
<field name="titleFr"></field>
<field name="authorNames"></field>
<field name="authorNames">/field>
<field name="seriesEn"></field>
<field name="seriesFr"></field>
<field name="editors"></field>
<field name="synopsisEn"></field>
<field name="synopsisEn"></field>
<field name="synopsisFr"></field>
<field name="originalCoverArtBy"></field>
<field name="originalCoverDesignBy"></field>
<field name="originalInteriorTextDesignBy"></field>
</doc>
</add>

Fichier final : livres.xml

Une fois le fichier XML complet, vous pouvez l’envoyer à Solr par le moyen que vous avez choisi (cURL, PHP, etc.). Pour cela, référez-vous au chapitre V. Maintenance des données d’un index. Après envoi de votre document, attendez quelques secondes que Solr termine ses traitements (sauf si waitSearcher était activé, en ce cas vous n’avez pas besoin d’attendre) puis vous pourrez commencer à interroger le noyau.

Si Solr répond par un code HTTP 400, c’est probablement parce que votre document XML ne correspond pas au schéma de données. La raison exacte de l’erreur est précisée dans la balise <h1> de la réponse HTTP, rédigée au format HTML.

VIII-E - Interrogation du noyau

Voici une requête avec un mot volontairement incomplet, permettant de démontrer les capacités de racinisation de notre application. Le résultat n’a pas été identifié par une recherche partielle sur le mot « dominates », quoique cela aurait pu être le cas si nous avions utilisé l’un des champs gr* (grammed) pour la recherche, mais bien parce que le lemme (stem) de ce mot est « domin ».

Image 13 : Démonstration de recherche avec highlighting + spellcheck

Nous allons préparer deux types de requêtes : la recherche de documents à partir d’une chaîne saisie par l’internaute, puis l’affichage d’un document à partir de son ID.

L’exercice consiste à déterminer les champs affectés par la query string, les facettes à renvoyer, quels champs utiliser pour le highlighting, etc.

Pour le prototypage de nos quatre requestHandlers spécifiques (recherche + affichage, chacun dans deux langues), je vous propose la configuration suivante du requestHandler par défaut :

solrconfig.xml

...
<requestHandler name="standard" class="solr.SearchHandler" default="true">
<lst name="defaults">
<bool name="indent">on</bool>
<str name="echoParams">explicit</str>
<str name="wt">xml</str><!-- Vous pouvez le modifier à json, php... -->
</lst>
</requestHandler>
...

Pour des raisons pratiques d’affichage de cet article, je vais découper les requêtes HTTP en plusieurs lignes (un paramètre par ligne). Bien entendu, pour les exécuter, vous devez les coller en une seule ligne dans votre navigateur :

Voyons maintenant une recherche minimaliste que nous allons améliorer peu à peu (req-1.0) :

Requête :

http://localhost:8080/solr/livres/select?
fl=score%20isbn13%20copyrightYear%20nbPages%20nbInSeries%20authorNames%20titleEn%20seriesEn
&sort=score%20desc
&q=stAllEn:magic

Résultat :

<?xml version="1.0" encoding="UTF-8"?>
<response>
<lst name="responseHeader">
<int name="status">0</int>
<int name="QTime">0</int>
<lst name="params">
<str name="fl">score isbn13 copyrightYear nbPages nbInSeries authorNames titleEn seriesEn</str>
<str name="sort">score desc</str>
<str name="q">stAllEn:magic</str>
</lst>
</lst>
<result name="response" numFound="7" start="0" maxScore="0.12109267">
<doc>
<float name="score">0.12109267</float>
<arr name="authorNames"><str>Ilona Andrews</str><str>Gordon, Ilona</str><str>Gordon, Andrew</str></arr>
<int name="copyrightYear">2007</int>
<str name="isbn13">978-0441014897</str>
<int name="nbInSeries">1</int>
<int name="nbPages">260</int>
<str name="seriesEn">Kate Daniels</str>
<str name="titleEn">Magic Bites</str>
</doc>
<doc>
<float name="score">0.11724747</float>
<arr name="authorNames"><str>Ilona Andrews</str><str>Gordon, Ilona</str><str>Gordon, Andrew</str></arr>
<int name="copyrightYear">2008</int>
<str name="isbn13">978-0441015832</str>
<int name="nbInSeries">2</int>
<int name="nbPages">260</int>
<str name="seriesEn">Kate Daniels</str>
<str name="titleEn">Magic Burns</str>
</doc>
<doc>
<float name="score">0.11487859</float>
<arr name="authorNames"><str>Ilona Andrews</str><str>Gordon, Ilona</str><str>Gordon, Andrew</str></arr>
<int name="copyrightYear">2009</int>
<str name="isbn13">978-0441017027</str>
<int name="nbInSeries">3</int>
<int name="nbPages">310</int>
<str name="seriesEn">Kate Daniels</str>
<str name="titleEn">Magic Strikes</str>
</doc>
<doc>
<float name="score">0.108308576</float>
<arr name="authorNames"><str>Ilona Andrews</str><str>Gordon, Ilona</str><str>Gordon, Andrew</str></arr>
<int name="copyrightYear">2009</int>
<str name="isbn13">978-0441017805</str>
<int name="nbInSeries">1</int>
<int name="nbPages">326</int>
<str name="seriesEn">The Edge</str>
<str name="titleEn">On The Edge</str>
</doc>
<doc>
<float name="score">0.076585725</float>
<arr name="authorNames"><str>Ilona Andrews</str><str>Gordon, Ilona</str><str>Gordon, Andrew</str></arr>
<int name="copyrightYear">2010</int>
<str name="isbn13">978-0441019458</str>
<int name="nbInSeries">2</int>
<int name="nbPages">462</int>
<str name="seriesEn">The Edge</str>
<str name="titleEn">Bayou Moon</str>
</doc>
<doc>
<float name="score">0.06769286</float>
<arr name="authorNames"><str>Ilona Andrews</str><str>Gordon, Ilona</str><str>Gordon, Andrew</str></arr>
<int name="copyrightYear">2010</int>
<str name="isbn13">978-0441018529</str>
<int name="nbInSeries">4</int>
<int name="nbPages">366</int>
<str name="seriesEn">Kate Daniels</str>
<str name="titleEn">Magic Bleeds</str>
</doc>
<doc>
<float name="score">0.06769286</float>
<arr name="authorNames"><str>Ilona Andrews</str><str>Gordon, Ilona</str><str>Gordon, Andrew</str></arr>
<int name="copyrightYear">2011</int>
<str name="isbn13">978-0441020423</str>
<int name="nbInSeries">5</int>
<int name="nbPages">308</int>
<str name="seriesEn">Kate Daniels</str>
<str name="titleEn">Magic Slays</str>
</doc>
</result>
</response>

Nous allons nous attacher à obtenir un maximum d’informations dans le résultat de notre requête.

VIII-E-1 - Conseils pour bien suivre le déroulement de cet exemple

Le principe que j’utilise pour profiler un requestHandler Solr est de commencer par une recherche puis d’alterner les paramètres et leurs valeurs jusqu’à trouver une combinaison qui fonctionne. La recherche de départ devrait être générique afin de renvoyer un maximum de résultats, mais vous pouvez aussi le faire avec des cas particuliers pour observer le comportement de votre noyau dans toutes les situations.

Lors du développement de cet exemple, je vais utiliser « magic » comme chaîne de recherche.

Je vous recommande fortement de faire vous-même des variantes de chacune des requêtes que je propose à la suite, de les numéroter et d’en conserver le résultat. Vous n’avez pas besoin de conserver les requêtes elles-mêmes, car Solr en rappelle les paramètres et leurs valeurs lorsqu’il vous envoie la réponse.

VIII-E-2 - Recherche à partir d’une chaîne saisie par l’utilisateur

VIII-E-2-a - Découpage de la chaîne saisie : solr.SearchHandler

Nous avons déjà configuré le découpage du texte dans schema.xml, mais il s’agit ici de choisir la manière dont Solr découpe le paramètre q. En effet, suivant le plugin choisi, Solr met à disposition des paramètres différents.

Solr dispose de deux méthodes principales pour effectuer une recherche : Lucene et DisMax. Vous pouvez changer de l’une à l’autre par l’intermédiaire du paramètre defType, qui indique à Solr quel plugin il doit utiliser pour décomposer le paramètre q.

Exemples :

- http://localhost:8080/solr/livres/select?q=... équivalent à http://localhost:8080/solr/livres/select?defType=lucene&q=...

- http://localhost:8080/solr/livres/select?defType=edismax&q=...

Ces valeurs sont configurables dans solrconfig.xml :

solrconfig.xml : LuceneQParserPlugin

...
<requestHandler name="/searchEn" class="solr.SearchHandler">
<lst name="defaults">
<str name="defType">lucene</str>
<!-- defType "lucene" est la valeur par défaut,
elle est donc facultative --!>
...

solrconfig.xml : ExtendedDismaxQParserPlugin

...
<requestHandler name="/searchEn" class="solr.SearchHandler">
<lst name="defaults">
<str name="defType">edismax</str>
...

Les différents types de plugin pour décomposer le paramètre q sont disponibles dans l’API : Solr QParserPlugin

Avec LuceneQParserPlugin, le principe de base est de chercher dans un unique champ par défaut ou bien de spécifier chacun des champs souhaités directement dans la requête (ce qui peut également être fait par le biais de filtres, comme nous l’avons vu précédemment). Vous devrez configurer le paramètre df (default field) pour qu’il cible un champ fortement analysé, par exemple avec de la racinisation ou de la phonétique (selon le type de données mises à l’index). À moins d’en avoir un besoin spécifique, évitez les n-grams pour la recherche, car ils renvoient trop de faux positifs.

Dans la mesure où df ne référence qu’un seul champ, la chaîne saisie par l’internaute est comparée uniquement à ce champ dans l’index. C’est pour cette raison que ce paramètre est habituellement un champ « catch-all », c’est-à-dire la cible de plusieurs règles copyField dans votre schema.xml. Pour changer la liste des champs recherchés, il faut modifier les règles copyField dans votre schema.xml, puis réindexer la totalité de vos documents.

schema.xml

...
<copyField source="titleEn" dest="stAllEn"/>
<copyField source="seriesEn" dest="stAllEn"/>
<copyField source="synopsisEn" dest="stAllEn"/>
<copyField source="authorNames" dest="stAllEn"/>
...

URL :

http://localhost:8080/solr/livres/select?df=stAllEn&q=...

Équivalent dans solrconfig.xml :

...
<requestHandler name="/searchEn" class="solr.SearchHandler">
<lst name="defaults">
<str name="defType">lucene</str><!-- facultatif -->
<str name="df">stAllEn</str>
...

Puisque LuceneQParserPlugin a besoin de champs spécifiques pour fonctionner convenablement, cela engendre à la fois une augmentation du temps d’indexation et une augmentation de la consommation d’espace disque, sauf si vous avez besoin de ces mêmes champs supplémentaires (avec exactement le même type et les mêmes paramètres) pour une autre fonctionnalité. Toutefois, je n’ai pas encore rencontré de situation qui justifie le partage de champs de cette manière.

Exemple « req-2.0 » :

Requête :

http://localhost:8080/solr/livres/select?
fl=score%20isbn13%20copyrightYear%20nbPages%20nbInSeries%20authorNames%20titleEn%20seriesEn
&sort=score%20desc
&df=stAllEn
&q=magic

Résultat :

<?xml version="1.0" encoding="UTF-8"?>
<response>
<lst name="responseHeader">
<int name="status">0</int>
<int name="QTime">0</int>
<lst name="params">
<str name="df">stAllEn</str>
<str name="fl">score isbn13 copyrightYear nbPages nbInSeries authorNames titleEn seriesEn</str>
<str name="sort">score desc</str>
<str name="q">magic</str>
</lst>
</lst>
<result name="response" numFound="7" start="0" maxScore="0.12109267">
<doc>
<float name="score">0.12109267</float>
<arr name="authorNames"><str>Ilona Andrews</str><str>Gordon, Ilona</str><str>Gordon, Andrew</str></arr>
<int name="copyrightYear">2007</int>
<str name="isbn13">978-0441014897</str>
<int name="nbInSeries">1</int>
<int name="nbPages">260</int>
<str name="seriesEn">Kate Daniels</str>
<str name="titleEn">Magic Bites</str>
</doc>
<doc>
<float name="score">0.11724747</float>
<arr name="authorNames"><str>Ilona Andrews</str><str>Gordon, Ilona</str><str>Gordon, Andrew</str></arr>
<int name="copyrightYear">2008</int>
<str name="isbn13">978-0441015832</str>
<int name="nbInSeries">2</int>
<int name="nbPages">260</int>
<str name="seriesEn">Kate Daniels</str>
<str name="titleEn">Magic Burns</str>
</doc>
<doc>
<float name="score">0.11487859</float>
<arr name="authorNames"><str>Ilona Andrews</str><str>Gordon, Ilona</str><str>Gordon, Andrew</str></arr>
<int name="copyrightYear">2009</int>
<str name="isbn13">978-0441017027</str>
<int name="nbInSeries">3</int>
<int name="nbPages">310</int>
<str name="seriesEn">Kate Daniels</str>
<str name="titleEn">Magic Strikes</str>
</doc>
<doc>
<float name="score">0.108308576</float>
<arr name="authorNames"><str>Ilona Andrews</str><str>Gordon, Ilona</str><str>Gordon, Andrew</str></arr>
<int name="copyrightYear">2009</int>
<str name="isbn13">978-0441017805</str>
<int name="nbInSeries">1</int>
<int name="nbPages">326</int>
<str name="seriesEn">The Edge</str>
<str name="titleEn">On The Edge</str>
</doc>
<doc>
<float name="score">0.076585725</float>
<arr name="authorNames"><str>Ilona Andrews</str><str>Gordon, Ilona</str><str>Gordon, Andrew</str></arr>
<int name="copyrightYear">2010</int>
<str name="isbn13">978-0441019458</str>
<int name="nbInSeries">2</int>
<int name="nbPages">462</int>
<str name="seriesEn">The Edge</str>
<str name="titleEn">Bayou Moon</str>
</doc>
<doc>
<float name="score">0.06769286</float>
<arr name="authorNames"><str>Ilona Andrews</str><str>Gordon, Ilona</str><str>Gordon, Andrew</str></arr>
<int name="copyrightYear">2010</int>
<str name="isbn13">978-0441018529</str>
<int name="nbInSeries">4</int>
<int name="nbPages">366</int>
<str name="seriesEn">Kate Daniels</str>
<str name="titleEn">Magic Bleeds</str>
</doc>
<doc>
<float name="score">0.06769286</float>
<arr name="authorNames"><str>Ilona Andrews</str><str>Gordon, Ilona</str><str>Gordon, Andrew</str></arr>
<int name="copyrightYear">2011</int>
<str name="isbn13">978-0441020423</str>
<int name="nbInSeries">5</int>
<int name="nbPages">308</int>
<str name="seriesEn">Kate Daniels</str>
<str name="titleEn">Magic Slays</str>
</doc>
</result>
</response>

En revanche, avec ExtendedDismaxQParserPlugin, le principe est de laisser votre configuration appliquer une logique pour déterminer l’influence des termes recherchés sur les champs du noyau par rapport à leur fréquence d’apparition, et vice versa. Vous aurez besoin de configurer q.alt (alternate query) et qf (query fields). Ce dernier paramètre remplace avantageusement df car, plutôt qu’un unique champ, il permet de définir une liste de champs auxquels la chaîne recherchée doit être appliquée ainsi que de donner un poids spécifique à chacun de ces champs. Pour changer la liste des champs recherchés, modifiez qf dans votre solrconfig.xml (il n’est pas utile de réindexer les documents).

URL :

http://localhost:8080/solr/livres/select?defType=edismax&qf=stTitleEn^5.0 stSeriesEn^4.0 stSynopsisEn stAuthorNamesEn^2.0&q.alt=*:*&q=...

Équivalent dans solrconfig.xml :

...
<requestHandler name="/searchEn" class="solr.SearchHandler">
<lst name="defaults">
<str name="defType">edismax</str>
<str name="qf">stTitleEn^5.0 stSeriesEn^4.0 stSynopsisEn stAuthorNamesEn^2.0</str>
<str name="q.alt">*:*</str>
...

Exemple « req-2.1 » :

Requête :

http://localhost:8080/solr/livres/select?
fl=score%20isbn13%20copyrightYear%20nbPages%20nbInSeries%20authorNames%20titleEn%20seriesEn
&sort=score%20desc
&defType=edismax
&qf=stTitleEn^5.0%20stSeriesEn^4.0%20stSynopsisEn%20stAuthorNamesEn^2.0
&q.alt=*:*
&q=magic

Résultat :

<?xml version="1.0" encoding="UTF-8"?>
<response>
<lst name="responseHeader">
<int name="status">0</int>
<int name="QTime">0</int>
<lst name="params">
<str name="fl">score isbn13 copyrightYear nbPages nbInSeries authorNames titleEn seriesEn</str>
<str name="sort">score desc</str>
<str name="q">magic</str>
<str name="qf">stTitleEn^5.0 stSeriesEn^4.0 stSynopsisEn stAuthorNamesEn^2.0</str>
<str name="q.alt">*:*</str>
<str name="defType">edismax</str>
</lst>
</lst>
<result name="response" numFound="7" start="0" maxScore="0.35326093">
<doc>
<float name="score">0.35326093</float>
<arr name="authorNames"><str>Ilona Andrews</str><str>Gordon, Ilona</str><str>Gordon, Andrew</str></arr>
<int name="copyrightYear">2007</int>
<str name="isbn13">978-0441014897</str>
<int name="nbInSeries">1</int>
<int name="nbPages">260</int>
<str name="seriesEn">Kate Daniels</str>
<str name="titleEn">Magic Bites</str>
</doc>
<doc>
<float name="score">0.35326093</float>
<arr name="authorNames"><str>Ilona Andrews</str><str>Gordon, Ilona</str><str>Gordon, Andrew</str></arr>
<int name="copyrightYear">2008</int>
<str name="isbn13">978-0441015832</str>
<int name="nbInSeries">2</int>
<int name="nbPages">260</int>
<str name="seriesEn">Kate Daniels</str>
<str name="titleEn">Magic Burns</str>
</doc>
<doc>
<float name="score">0.35326093</float>
<arr name="authorNames"><str>Ilona Andrews</str><str>Gordon, Ilona</str><str>Gordon, Andrew</str></arr>
<int name="copyrightYear">2009</int>
<str name="isbn13">978-0441017027</str>
<int name="nbInSeries">3</int>
<int name="nbPages">310</int>
<str name="seriesEn">Kate Daniels</str>
<str name="titleEn">Magic Strikes</str>
</doc>
<doc>
<float name="score">0.35326093</float>
<arr name="authorNames"><str>Ilona Andrews</str><str>Gordon, Ilona</str><str>Gordon, Andrew</str></arr>
<int name="copyrightYear">2010</int>
<str name="isbn13">978-0441018529</str>
<int name="nbInSeries">4</int>
<int name="nbPages">366</int>
<str name="seriesEn">Kate Daniels</str>
<str name="titleEn">Magic Bleeds</str>
</doc>
<doc>
<float name="score">0.35326093</float>
<arr name="authorNames"><str>Ilona Andrews</str><str>Gordon, Ilona</str><str>Gordon, Andrew</str></arr>
<int name="copyrightYear">2011</int>
<str name="isbn13">978-0441020423</str>
<int name="nbInSeries">5</int>
<int name="nbPages">308</int>
<str name="seriesEn">Kate Daniels</str>
<str name="titleEn">Magic Slays</str>
</doc>
<doc>
<float name="score">0.014130437</float>
<arr name="authorNames"><str>Ilona Andrews</str><str>Gordon, Ilona</str><str>Gordon, Andrew</str></arr>
<int name="copyrightYear">2009</int>
<str name="isbn13">978-0441017805</str>
<int name="nbInSeries">1</int>
<int name="nbPages">326</int>
<str name="seriesEn">The Edge</str>
<str name="titleEn">On The Edge</str>
</doc>
<doc>
<float name="score">0.009991728</float>
<arr name="authorNames"><str>Ilona Andrews</str><str>Gordon, Ilona</str><str>Gordon, Andrew</str></arr>
<int name="copyrightYear">2010</int>
<str name="isbn13">978-0441019458</str>
<int name="nbInSeries">2</int>
<int name="nbPages">462</int>
<str name="seriesEn">The Edge</str>
<str name="titleEn">Bayou Moon</str>
</doc>
</result>
</response>

Vous pouvez observer la différence de score :

Résultats de req-2.0

score

isbn13

seriesEn

nbInSeries

0.12109267

978-0441014897

Kate Daniels

1

0.11724747

978-0441015832

Kate Daniels

2

0.11487859

978-0441017027

Kate Daniels

3

0.108308576

978-0441017805

The Edge

1

0.076585725

978-0441019458

The Edge

2

0.06769286

978-0441018529

Kate Daniels

4

0.06769286

978-0441020423

Kate Daniels

5

Résultats de req-2.1

score

isbn13

seriesEn

nbInSeries

0.35326093

978-0441014897

Kate Daniels

1

0.35326093

978-0441015832

Kate Daniels

2

0.35326093

978-0441017027

Kate Daniels

3

0.35326093

978-0441018529

Kate Daniels

4

0.35326093

978-0441020423

Kate Daniels

5

0.014130437

978-0441017805

The Edge

1

0.009991728

978-0441019458

The Edge

2

Vous l’aurez compris, les différentes versions de DisMax sont prévues pour des requêtes utilisateur contenant des mots dans le désordre et pouvant apparaître dans n’importe quelle partie de vos documents. Si vos documents sont composés uniquement d’un champ texte, comme c’est le cas si vous indexez des fichiers avec Carrot, alors vous pouvez vous contenter de LuceneQParserPlugin. Cependant, si vous développez une application de recherche dans des documents avec plusieurs champs comme je vous en fais la démonstration ici, vous préférerez sans doute ExtendedDismaxQParserPlugin.

Je vous recommande de préparer quelques requêtes req-2.2, req-2.3 (et ainsi de suite, au moins 3 ou 4 de votre cru) afin de les exécuter à nouveau dans le chapitre suivant avec les nouveaux paramètres. Vos variantes pourraient par exemple modifier le poids des champs dans le paramètre qf.

Mes démonstrations dans cet article sont basées sur ExtendedDismaxQParserPlugin, car c’est le plugin utilisé dans la version actuelle de la démo distribuée avec la version actuelle de Solr. Toutefois, la page de documentation de ce plugin précise que son API est expérimentale et qu’elle peut changer de manière incompatible dans le futur. Il est donc possible que les paramètres changent, disparaissent ou deviennent insuffisants dans une prochaine version. En cas de doute, remplacez-le par DisMaxQParserPlugin (defType=dismax).

VIII-E-2-b - Explication de la présence des résultats : solr.HighlightComponent

Ce composant permet d’expliquer à vos internautes pourquoi vous leur affichez les résultats que vous affichez. Il met en valeur les segments de termes que Solr a utilisés pour déterminer quels documents sont utiles.

Chronologiquement, ce composant est exécuté après que les paramètres rows et offset aient pris effet. Le highlighting est calculé uniquement pour le jeu de résultats affiché, pas pour l’ensemble théorique non paginé.

Les paramètres suivants sont utiles :

- highlight (bool) : active ou désactive le composant pour cette requête ;

- hl (bool) : active ou désactive l’affichage du highlighting dans la réponse ;

- hl.fl (str) : liste des champs à renvoyer dans la réponse ; si le paramètre qf est également défini, il est préférable de donner des listes de champs similaires pour l’un et pour l’autre ;

- hl.snippets (int) : nombre maximum de résultats de highlighting à renvoyer pour chaque champ ; attention, ce paramètre a la valeur « 1 » par défaut ;

- hl.simple.pre (str) : chaîne servant de préfixe aux segments de highlighting ; par défaut « <em> » ;

- hl.simple.post (str) : chaîne servant de suffixe aux segments de highlighting ; par défaut « </em> ».

Le paramètre hl.fl prend pour valeur une liste de champs. Souvenez-vous que c’est à travers lui que Solr vous explique pourquoi le document a été choisi dans les résultats de recherche : les champs de ce paramètre doivent impérativement être les mêmes (ou un équivalent par l’intermédiaire de copyField) que ceux analysés par le QParserPlugin pour sortir les documents de l’index. Si vous ne lui donnez pas la bonne liste de champs, ou si leur type est différent du type des champs analysés par le QParserPlugin, alors le Highlighting sera incomplet ou incohérent.

Si hl.fl n’est pas spécifié dans la requête, alors Solr utilise les paramètres df ou qf s’ils sont donnés, ou sinon les champs de LuceneQParser.

Si hl.fl n’est pas précisé dans la requête, alors vous laissez à Solr le soin de choisir l’ordre des champs dans les résultats de highlighting. En revanche, si vous donnez le paramètre, l’ordre des champs dans votre liste sera respecté par Solr au moment d’afficher les résultats. Vous pouvez observer cette différence de comportement avec les résultats de « req-3.1 » et « req-3.1b » (voir plus loin).

Si vous optez pour un champ « catch-all » comme valeur du paramètre hl.fl, alors vos résultats seront peu précis. Il est préférable de prendre le temps d’identifier la liste des champs individuels qui correspondent à ce champ catch-all, car cela donne des indications plus précises dans le résultat de la requête.

Avec un champ « catch-all » pour valeur de hl.fl (ici, puisque hl.fl est vide, Solr prend la valeur de df) :

Requête :

http://localhost:8080/solr/livres/select?
fl=score%20isbn13%20copyrightYear%20nbPages%20nbInSeries%20authorNames%20titleEn%20seriesEn
&sort=score%20desc
&df=stAllEn
&highlight=on
&hl=true
&hl.simple.pre=[hl]
&hl.simple.post=[/hl]
&hl.snippets=100
&q=magic

La réponse nous dit simplement que le terme apparaît dans le document :

...
<lst name="978-0441014897">
<arr name="stAllEn">
<str>[hl]Magic[/hl] Bites</str>
<str>Atlanta would be a nice place to live, if it weren't for
[hl]magic[/hl]... One moment [hl]magic[/hl] dominates, and</str>
<str> your house from monsters. Here skyscrapers topple under onslaught
of [hl]magic[/hl]; werebears and werehyenas</str>
<str> likes her sword a little too much and has a hard time controlling
her mouth. The [hl]magic[/hl] in her blood</str>
</arr>
</lst>
...

Avec des champs individuels pour valeur de hl.fl :

Requête :

http://localhost:8080/solr/livres/select?
fl=score%20isbn13%20copyrightYear%20nbPages%20nbInSeries%20authorNames%20titleEn%20seriesEn
&sort=score%20desc
&df=stAllEn
&highlight=on
&hl=true
&hl.simple.pre=[hl]
&hl.simple.post=[/hl]
&hl.fl=stTitleEn,stSeriesEn,stSynopsisEn,stAuthorNamesEn
&hl.snippets=100
&q=magic

La réponse nous dit dans quels champs le terme apparaît :

...
<lst name="978-0441014897">
<arr name="stTitleEn">
<str>[hl]Magic[/hl] Bites</str>
</arr>
<arr name="stSynopsisEn">
<str>Atlanta would be a nice place to live, if it weren't for
[hl]magic[/hl]... One moment [hl]magic[/hl] dominates, and</str>
<str> your house from monsters. Here skyscrapers topple under onslaught
of [hl]magic[/hl]; werebears and werehyenas</str>
<str> likes her sword a little too much and has a hard time controlling
her mouth. The [hl]magic[/hl] in her blood</str>
</arr>
</lst>
...

Dans la mesure où je préfère avoir les détails, pour la suite de l’article, je ferai en sorte que Solr n’utilise jamais un champ « catch-all » comme paramètre hl.fl. Il s’agit d’une préférence personnelle, vous devrez étudier cela pour chaque application.

Voici maintenant des exemples plus complets.

Exemple « req-3.0 » basé sur « req-2.0 » (donnons à hl.fl soit la même valeur que df, soit la version lemmatisée de son découpage par l’intermédiaire de copyField) :

Requête :

http://localhost:8080/solr/livres/select?
fl=score%20isbn13%20copyrightYear%20nbPages%20nbInSeries%20authorNames%20titleEn%20seriesEn
&sort=score%20desc
&df=stAllEn
&highlight=on
&hl=true
&hl.simple.pre=[hl]
&hl.simple.post=[/hl]
&hl.fl=stTitleEn,stSeriesEn,stSynopsisEn,stAuthorNamesEn
&hl.snippets=100
&q=magic

Résultat :

<?xml version="1.0" encoding="UTF-8"?>
<response>
<lst name="responseHeader">
<int name="status">0</int>
<int name="QTime">20</int>
<lst name="params">
<str name="sort">score desc</str>
<str name="hl.simple.pre">[hl]</str>
<str name="highlight">on</str>
<str name="hl.fl">stTitleEn,stSeriesEn,stSynopsisEn,stAuthorNamesEn</str>
<str name="hl">true</str>
<str name="df">stAllEn</str>
<str name="fl">score isbn13 copyrightYear nbPages nbInSeries authorNames titleEn seriesEn</str>
<str name="hl.snippets">100</str>
<str name="q">magic</str>
<str name="hl.simple.post">[/hl]</str>
</lst>
</lst>
<result name="response" numFound="7" start="0" maxScore="0.12109267">
<!-- [NDLR] Même jeu de résultats qu’avec req-2.0 -->
</result>
<lst name="highlighting">
<lst name="978-0441014897">
<arr name="stTitleEn">
<str>[hl]Magic[/hl] Bites</str>
</arr>
<arr name="stSynopsisEn">
<str>Atlanta would be a nice place to live, if it weren't for [hl]magic[/hl]... One moment [hl]magic[/hl] dominates, and</str>
<str> your house from monsters. Here skyscrapers topple under onslaught of [hl]magic[/hl]; werebears and werehyenas</str>
<str> likes her sword a little too much and has a hard time controlling her mouth. The [hl]magic[/hl] in her blood</str>
</arr>
</lst>
<lst name="978-0441015832">
<arr name="stTitleEn">
<str>[hl]Magic[/hl] Burns</str>
</arr>
<arr name="stSynopsisEn">
<str> after [hl]magic[/hl] gone wrong, Kate Daniels has seen her share of occupational hazards. Normally, waves of</str>
<str> when [hl]magic[/hl] runs rampant. Now Kate's going to have to deal with problems on a much bigger scale: a</str>
</arr>
</lst>
<lst name="978-0441017027">
<arr name="stTitleEn">
<str>[hl]Magic[/hl] Strikes</str>
</arr>
<arr name="stSynopsisEn">
<str> problems than she knows what to do with these days. And in Atlanta, where [hl]magic[/hl] comes and goes like the</str>
</arr>
</lst>
<lst name="978-0441017805">
<arr name="stSynopsisEn">
<str> Wal-Mart, and [hl]magic[/hl] is a fairy tale) and the Weird (where blueblood aristocrats rule, changelings roam</str>
<str>, and the strength of your [hl]magic[/hl] can change your destiny). Only Edgers like Rose can easily travel from</str>
<str> one world to the next, but they never truly belong in either.
Rose thought if she practiced her [hl]magic[/hl]</str>
<str> for [hl]magic[/hl], Declan and Rose must work together to destroy them&#8212;or they'll devour the Edge and everyone</str>
</arr>
</lst>
<lst name="978-0441019458">
<arr name="stSynopsisEn">
<str> [hl]magic[/hl] is a fairytale-and the Weird, where blueblood aristocrats rule, changelings roam, and the strength</str>
<str> of your [hl]magic[/hl] can change your destiny...
Cerise Mar and her unruly clan are cash poor but land rich</str>
</arr>
</lst>
<lst name="978-0441018529">
<arr name="stTitleEn">
<str>[hl]Magic[/hl] Bleeds</str>
</arr>
</lst>
<lst name="978-0441020423">
<arr name="stTitleEn">
<str>[hl]Magic[/hl] Slays</str>
</arr>
</lst>
</lst>
</response>

Exemple « req-3.1 » basé sur « req-2.1 » (donnons à hl.fl soit la même liste de champs que qf, soit un champ équivalent par l’intermédiaire de copyField) :

Requête :

http://localhost:8080/solr/livres/select?
fl=score%20isbn13%20copyrightYear%20nbPages%20nbInSeries%20authorNames%20titleEn%20seriesEn
&sort=score%20desc
&defType=edismax
&qf=stTitleEn^5.0%20stSeriesEn^4.0%20stSynopsisEn%20stAuthorNamesEn^2.0
&q.alt=*:*
&highlight=on
&hl=true
&hl.simple.pre=[hl]
&hl.simple.post=[/hl]
&hl.fl=stTitleEn,stSeriesEn,stSynopsisEn,stAuthorNamesEn
&hl.snippets=100
&q=magic

Résultat :

<?xml version="1.0" encoding="UTF-8"?>
<response>
<lst name="responseHeader">
<int name="status">0</int>
<int name="QTime">40</int>
<lst name="params">
<str name="sort">score desc</str>
<str name="qf">stTitleEn^5.0 stSeriesEn^4.0 stSynopsisEn stAuthorNamesEn^2.0</str>
<str name="hl.simple.pre">[hl]</str>
<str name="q.alt">*:*</str>
<str name="highlight">on</str>
<str name="hl.fl">stTitleEn,stSeriesEn,stSynopsisEn,stAuthorNamesEn</str>
<str name="hl">true</str>
<str name="defType">edismax</str>
<str name="fl">score isbn13 copyrightYear nbPages nbInSeries authorNames titleEn seriesEn</str>
<str name="hl.snippets">100</str>
<str name="q">magic</str>
<str name="hl.simple.post">[/hl]</str>
</lst>
</lst>
<result name="response" numFound="7" start="0" maxScore="0.35326093">
<!-- [NDLR] Même jeu de résultats qu’avec req-2.1 -->
</result>
<lst name="highlighting">
<lst name="978-0441014897">
<arr name="stTitleEn">
<str>[hl]Magic[/hl] Bites</str>
</arr>
<arr name="stSynopsisEn">
<str>Atlanta would be a nice place to live, if it weren't for [hl]magic[/hl]... One moment [hl]magic[/hl] dominates, and</str>
<str> your house from monsters. Here skyscrapers topple under onslaught of [hl]magic[/hl]; werebears and werehyenas</str>
<str> likes her sword a little too much and has a hard time controlling her mouth. The [hl]magic[/hl] in her blood</str>
</arr>
</lst>
<lst name="978-0441015832">
<arr name="stTitleEn">
<str>[hl]Magic[/hl] Burns</str>
</arr>
<arr name="stSynopsisEn">
<str> after [hl]magic[/hl] gone wrong, Kate Daniels has seen her share of occupational hazards. Normally, waves of</str>
<str> when [hl]magic[/hl] runs rampant. Now Kate's going to have to deal with problems on a much bigger scale: a</str>
</arr>
</lst>
<lst name="978-0441017027">
<arr name="stTitleEn">
<str>[hl]Magic[/hl] Strikes</str>
</arr>
<arr name="stSynopsisEn">
<str> problems than she knows what to do with these days. And in Atlanta, where [hl]magic[/hl] comes and goes like the</str>
</arr>
</lst>
<lst name="978-0441018529">
<arr name="stTitleEn">
<str>[hl]Magic[/hl] Bleeds</str>
</arr>
</lst>
<lst name="978-0441020423">
<arr name="stTitleEn">
<str>[hl]Magic[/hl] Slays</str>
</arr>
</lst>
<lst name="978-0441017805">
<arr name="stSynopsisEn">
<str> Wal-Mart, and [hl]magic[/hl] is a fairy tale) and the Weird (where blueblood aristocrats rule, changelings roam</str>
<str>, and the strength of your [hl]magic[/hl] can change your destiny). Only Edgers like Rose can easily travel from</str>
<str> one world to the next, but they never truly belong in either.
Rose thought if she practiced her [hl]magic[/hl]</str>
<str> for [hl]magic[/hl], Declan and Rose must work together to destroy them&#8212;or they'll devour the Edge and everyone</str>
</arr>
</lst>
<lst name="978-0441019458">
<arr name="stSynopsisEn">
<str> [hl]magic[/hl] is a fairytale-and the Weird, where blueblood aristocrats rule, changelings roam, and the strength</str>
<str> of your [hl]magic[/hl] can change your destiny...
Cerise Mar and her unruly clan are cash poor but land rich</str>
</arr>
</lst>
</lst>
</response>

Exemple « req-3.1b » basé sur « req-3.1 » (laissons Solr deviner la valeur de hl.fl d’après la valeur de qf) :

Requête :

http://localhost:8080/solr/livres/select?
fl=score%20isbn13%20copyrightYear%20nbPages%20nbInSeries%20authorNames%20titleEn%20seriesEn
&sort=score%20desc
&defType=edismax
&qf=stTitleEn^5.0%20stSeriesEn^4.0%20stSynopsisEn%20stAuthorNamesEn^2.0
&q.alt=*:*
&highlight=on
&hl=true
&hl.simple.pre=[hl]
&hl.simple.post=[/hl]
&hl.snippets=100
&q=magic

Réponse :

<?xml version="1.0" encoding="UTF-8"?>
<response>
<lst name="responseHeader">
<int name="status">0</int>
<int name="QTime">20</int>
<lst name="params">
<str name="sort">score desc</str>
<str name="qf">stTitleEn^5.0 stSeriesEn^4.0 stSynopsisEn stAuthorNamesEn^2.0</str>
<str name="hl.simple.pre">[hl]</str>
<str name="q.alt">*:*</str>
<str name="highlight">on</str>
<str name="hl">true</str>
<str name="defType">edismax</str>
<str name="fl">score isbn13 copyrightYear nbPages nbInSeries authorNames titleEn seriesEn</str>
<str name="hl.snippets">100</str>
<str name="q">magic</str>
<str name="hl.simple.post">[/hl]</str>
</lst>
</lst>
<result name="response" numFound="7" start="0" maxScore="0.35326093">
<!-- [NDLR] Même jeu de résultats qu’avec req-2.1 -->
</result>
<lst name="highlighting">
<lst name="978-0441014897">
<arr name="stSynopsisEn">
<str>Atlanta would be a nice place to live, if it weren't for [hl]magic[/hl]... One moment [hl]magic[/hl] dominates, and</str>
<str> your house from monsters. Here skyscrapers topple under onslaught of [hl]magic[/hl]; werebears and werehyenas</str>
<str> likes her sword a little too much and has a hard time controlling her mouth. The [hl]magic[/hl] in her blood</str>
</arr>
<arr name="stTitleEn">
<str>[hl]Magic[/hl] Bites</str>
</arr>
</lst>
<lst name="978-0441015832">
<arr name="stSynopsisEn">
<str> after [hl]magic[/hl] gone wrong, Kate Daniels has seen her share of occupational hazards. Normally, waves of</str>
<str> when [hl]magic[/hl] runs rampant. Now Kate's going to have to deal with problems on a much bigger scale: a</str>
</arr>
<arr name="stTitleEn">
<str>[hl]Magic[/hl] Burns</str>
</arr>
</lst>
<lst name="978-0441017027">
<arr name="stSynopsisEn">
<str> problems than she knows what to do with these days. And in Atlanta, where [hl]magic[/hl] comes and goes like the</str>
</arr>
<arr name="stTitleEn">
<str>[hl]Magic[/hl] Strikes</str>
</arr>
</lst>
<lst name="978-0441018529">
<arr name="stTitleEn">
<str>[hl]Magic[/hl] Bleeds</str>
</arr>
</lst>
<lst name="978-0441020423">
<arr name="stTitleEn">
<str>[hl]Magic[/hl] Slays</str>
</arr>
</lst>
<lst name="978-0441017805">
<arr name="stSynopsisEn">
<str> Wal-Mart, and [hl]magic[/hl] is a fairy tale) and the Weird (where blueblood aristocrats rule, changelings roam</str>
<str>, and the strength of your [hl]magic[/hl] can change your destiny). Only Edgers like Rose can easily travel from</str>
<str> one world to the next, but they never truly belong in either.
Rose thought if she practiced her [hl]magic[/hl]</str>
<str> for [hl]magic[/hl], Declan and Rose must work together to destroy them&#8212;or they'll devour the Edge and everyone</str>
</arr>
</lst>
<lst name="978-0441019458">
<arr name="stSynopsisEn">
<str> [hl]magic[/hl] is a fairytale-and the Weird, where blueblood aristocrats rule, changelings roam, and the strength</str>
<str> of your [hl]magic[/hl] can change your destiny...
Cerise Mar and her unruly clan are cash poor but land rich</str>
</arr>
</lst>
</lst>
</response>

Dans le chapitre précédent, nous avons mis en valeur la différence de scoring entre req-2.0 et req-2.1. Elle est expliquée ici par la réponse du composant Highlighting en req-3-0 et req-3.1 : le mot recherché, « magic », n’apparaît pas dans le titre des livres « The Edge ». Il apparaît seulement dans leur synopsis, ce qui est suffisant pour req-2.0 et req-3.0 (les requêtes avec defType=lucene) puisque les champs stTitleEn et stSynopsisEn sont copiés dans le champ stAllEn utilisé pour ces requêtes. Toutefois, dans req-2.1 et req-3.1 (les requêtes avec defType=edismax), le poids du champ stTitleEn est de 5, contre 1 pour le champ stSynopsisEn.

Solr a bel et bien obéi à notre configuration :

- dans req-2.0 et req-3.0 (defType=lucene) : les résultats sont triés uniquement d’après la présence des mots recherchés dans l’ensemble du document, leur importance étant simplement calculée par la position et la fréquence des mots recherchés ;

- dans req-2.1 et req-3.1 (defType=edismax) : le score des documents est modifié selon le champ dans lequel les mots recherchés sont présents.

Reprenons les tableaux du chapitre précédent en ajoutant les informations du composant Highlighting :

Résultats de req-3.0

score

isbn13

seriesEn

nbInSeries

hl

0.12109267

978-0441014897

Kate Daniels

1

stTitleEn * 1, stSynopsisEn * 4

0.11724747

978-0441015832

Kate Daniels

2

stTitleEn * 1, stSynopsisEn * 2

0.11487859

978-0441017027

Kate Daniels

3

stTitleEn * 1, stSynopsisEn * 1

0.108308576

978-0441017805

The Edge

1

stSynopsisEn * 4

0.076585725

978-0441019458

The Edge

2

stSynopsisEn * 2

0.06769286

978-0441018529

Kate Daniels

4

stTitleEn * 1

0.06769286

978-0441020423

Kate Daniels

5

stTitleEn * 1

Résultats de req-3.1

score

isbn13

seriesEn

nbInSeries

hl

0.35326093

978-0441014897

Kate Daniels

1

stTitleEn * 1, stSynopsisEn * 4

0.35326093

978-0441015832

Kate Daniels

2

stTitleEn * 1, stSynopsisEn * 2

0.35326093

978-0441017027

Kate Daniels

3

stTitleEn * 1, stSynopsisEn * 1

0.35326093

978-0441018529

Kate Daniels

4

stTitleEn * 1

0.35326093

978-0441020423

Kate Daniels

5

stTitleEn * 1

0.014130437

978-0441017805

The Edge

1

stSynopsisEn * 4

0.009991728

978-0441019458

The Edge

2

stSynopsisEn * 2

Les explications fournies par le composant Highlighting vous permettent d’afficher à vos clients la raison des résultats ou des extraits de document. Elles vous permettent également d’affiner les autres paramètres de votre application. Par exemple, dans les exemples req-2.1 et req-3.1 (defType=edismax), le résultat de highlighting vous donne des indices pour configurer le poids de chaque champ dans le paramètre qf.

À mesure que la quantité de documents indexés par votre noyau augmente, le scoring des résultats devient de plus en plus stratégique pour votre application. Il est à votre charge de déterminer les requêtes utilisateur les plus fréquentes et les combinaisons de paramètres donnant les résultats les plus pertinents.

Il est ici temps d’appliquer les nouveaux paramètres de req-3.1 aux requêtes req-2.2 et suivantes, à savoir les requêtes que vous avez sans aucun doute préparées au chapitre précédent. Vous pourrez alors mieux observer les différences dans la réponse.

VIII-E-2-c - Explication du score des résultats : solr.DebugComponent

Dans les chapitres précédents, nous avons vu que nous pouvons manipuler l’importance d’un champ par rapport à un autre. Cependant, le score des documents ne correspond pas toujours à ce que l’on pourrait attendre. Solr propose un outil pour étudier les calculs qui l’ont amené à attribuer le score des documents : DebugComponent.

Une question que nous pourrions nous poser est « pourquoi la plupart des documents ont-ils le même score dans les réponses à req-2.1 et req-3.1 ? »

En effet, dans les résultats de ces requêtes, les documents de la série « Kate Daniels » ont tous un score de 0.35326093, tandis que les documents de la série « The Edge » ont des scores individuels.

Exemple « req-4.0 » basé sur « req-2.0 » :

Requête :

http://localhost:8080/solr/livres/select?
fl=score%20isbn13%20copyrightYear%20nbPages%20nbInSeries%20authorNames%20titleEn%20seriesEn
&sort=score%20desc
&df=stAllEn
&debugQuery=on
&debug.explain.structured=true
&q=magic

Réponse :

<?xml version="1.0" encoding="UTF-8"?>
<response>
<lst name="responseHeader">
<int name="status">0</int>
<int name="QTime">0</int>
<lst name="params">
<str name="fl">score isbn13 copyrightYear nbPages nbInSeries authorNames titleEn seriesEn</str>
<str name="debugQuery">on</str>
<str name="df">stAllEn</str>
<str name="sort">score desc</str>
<str name="q">magic</str>
<str name="debug.explain.structured">true</str>
</lst>
</lst>
<result name="response" numFound="7" start="0" maxScore="0.12109267">
<!-- [NDLR] Même jeu de résultats qu'avec req-2.0 -->
</result>
<lst name="debug">
<str name="rawquerystring">magic</str>
<str name="querystring">magic</str>
<str name="parsedquery">stAllEn:magic</str>
<str name="parsedquery_toString">stAllEn:magic</str>
<lst name="explain">
<lst name="978-0441014897">
<bool name="match">true</bool>
<float name="value">0.12109267</float>
<str name="description">fieldWeight(stAllEn:magic in 0), product of:</str>
<arr name="details">
<lst>
<bool name="match">true</bool>
<float name="value">2.236068</float>
<str name="description">tf(termFreq(stAllEn:magic)=5)</str>
</lst>
<lst>
<bool name="match">true</bool>
<float name="value">0.8664686</float>
<str name="description">idf(docFreq=7, maxDocs=7)</str>
</lst>
<lst>
<bool name="match">true</bool>
<float name="value">0.0625</float>
<str name="description">fieldNorm(field=stAllEn, doc=0)</str>
</lst>
</arr>
</lst>
<lst name="978-0441015832">
<bool name="match">true</bool>
<float name="value">0.11724747</float>
<str name="description">fieldWeight(stAllEn:magic in 1), product of:</str>
<arr name="details">
<lst>
<bool name="match">true</bool>
<float name="value">1.7320508</float>
<str name="description">tf(termFreq(stAllEn:magic)=3)</str>
</lst>
<lst>
<bool name="match">true</bool>
<float name="value">0.8664686</float>
<str name="description">idf(docFreq=7, maxDocs=7)</str>
</lst>
<lst>
<bool name="match">true</bool>
<float name="value">0.078125</float>
<str name="description">fieldNorm(field=stAllEn, doc=1)</str>
</lst>
</arr>
</lst>
<lst name="978-0441017027">
<bool name="match">true</bool>
<float name="value">0.11487859</float>
<str name="description">fieldWeight(stAllEn:magic in 2), product of:</str>
<arr name="details">
<lst>
<bool name="match">true</bool>
<float name="value">1.4142135</float>
<str name="description">tf(termFreq(stAllEn:magic)=2)</str>
</lst>
<lst>
<bool name="match">true</bool>
<float name="value">0.8664686</float>
<str name="description">idf(docFreq=7, maxDocs=7)</str>
</lst>
<lst>
<bool name="match">true</bool>
<float name="value">0.09375</float>
<str name="description">fieldNorm(field=stAllEn, doc=2)</str>
</lst>
</arr>
</lst>
<lst name="978-0441017805">
<bool name="match">true</bool>
<float name="value">0.108308576</float>
<str name="description">fieldWeight(stAllEn:magic in 5), product of:</str>
<arr name="details">
<lst>
<bool name="match">true</bool>
<float name="value">2.0</float>
<str name="description">tf(termFreq(stAllEn:magic)=4)</str>
</lst>
<lst>
<bool name="match">true</bool>
<float name="value">0.8664686</float>
<str name="description">idf(docFreq=7, maxDocs=7)</str>
</lst>
<lst>
<bool name="match">true</bool>
<float name="value">0.0625</float>
<str name="description">fieldNorm(field=stAllEn, doc=5)</str>
</lst>
</arr>
</lst>
<lst name="978-0441019458">
<bool name="match">true</bool>
<float name="value">0.076585725</float>
<str name="description">fieldWeight(stAllEn:magic in 6), product of:</str>
<arr name="details">
<lst>
<bool name="match">true</bool>
<float name="value">1.4142135</float>
<str name="description">tf(termFreq(stAllEn:magic)=2)</str>
</lst>
<lst>
<bool name="match">true</bool>
<float name="value">0.8664686</float>
<str name="description">idf(docFreq=7, maxDocs=7)</str>
</lst>
<lst>
<bool name="match">true</bool>
<float name="value">0.0625</float>
<str name="description">fieldNorm(field=stAllEn, doc=6)</str>
</lst>
</arr>
</lst>
<lst name="978-0441018529">
<bool name="match">true</bool>
<float name="value">0.06769286</float>
<str name="description">fieldWeight(stAllEn:magic in 3), product of:</str>
<arr name="details">
<lst>
<bool name="match">true</bool>
<float name="value">1.0</float>
<str name="description">tf(termFreq(stAllEn:magic)=1)</str>
</lst>
<lst>
<bool name="match">true</bool>
<float name="value">0.8664686</float>
<str name="description">idf(docFreq=7, maxDocs=7)</str>
</lst>
<lst>
<bool name="match">true</bool>
<float name="value">0.078125</float>
<str name="description">fieldNorm(field=stAllEn, doc=3)</str>
</lst>
</arr>
</lst>
<lst name="978-0441020423">
<bool name="match">true</bool>
<float name="value">0.06769286</float>
<str name="description">fieldWeight(stAllEn:magic in 4), product of:</str>
<arr name="details">
<lst>
<bool name="match">true</bool>
<float name="value">1.0</float>
<str name="description">tf(termFreq(stAllEn:magic)=1)</str>
</lst>
<lst>
<bool name="match">true</bool>
<float name="value">0.8664686</float>
<str name="description">idf(docFreq=7, maxDocs=7)</str>
</lst>
<lst>
<bool name="match">true</bool>
<float name="value">0.078125</float>
<str name="description">fieldNorm(field=stAllEn, doc=4)</str>
</lst>
</arr>
</lst>
</lst>
<str name="QParser">LuceneQParser</str>
<lst name="timing">
<!-- [NDLR] Données inutiles pour cet exemple -->
</lst>
</lst>
</response>

Exemple « req-4.1 » basé sur « req-2.1 » :

Requête :

http://localhost:8080/solr/livres/select?
fl=score%20isbn13%20copyrightYear%20nbPages%20nbInSeries%20authorNames%20titleEn%20seriesEn
&sort=score%20desc
&defType=edismax
&qf=stTitleEn^5.0%20stSeriesEn^4.0%20stSynopsisEn%20stAuthorNamesEn^2.0
&q.alt=*:*
&debugQuery=on
&debug.explain.structured=true
&q=magic

Réponse :

<?xml version="1.0" encoding="UTF-8"?>
<response>
<lst name="responseHeader">
<int name="status">0</int>
<int name="QTime">10</int>
<lst name="params">
<str name="fl">score isbn13 copyrightYear nbPages nbInSeries authorNames titleEn seriesEn</str>
<str name="debugQuery">on</str>
<str name="sort">score desc</str>
<str name="q">magic</str>
<str name="debug.explain.structured">true</str>
<str name="qf">stTitleEn^5.0 stSeriesEn^4.0 stSynopsisEn stAuthorNamesEn^2.0</str>
<str name="q.alt">*:*</str>
<str name="defType">edismax</str>
</lst>
</lst>
<result name="response" numFound="7" start="0" maxScore="0.35326093">
<!-- [NDLR] Même jeu de résultats qu’avec req-2.1 -->
</result>
<lst name="debug">
<str name="rawquerystring">magic</str>
<str name="querystring">magic</str>
<str name="parsedquery">+DisjunctionMaxQuery((stSynopsisEn:magic | stSeriesEn:magic^4.0 | stAuthorNamesEn:magic^2.0 | stTitleEn:magic^5.0))</str>
<str name="parsedquery_toString">+(stSynopsisEn:magic | stSeriesEn:magic^4.0 | stAuthorNamesEn:magic^2.0 | stTitleEn:magic^5.0)</str>
<lst name="explain">
<lst name="978-0441014897">
<bool name="match">true</bool>
<float name="value">0.35326093</float>
<str name="description">max of:</str>
<arr name="details">
<lst>
<bool name="match">true</bool>
<float name="value">0.014130437</float>
<str name="description">weight(stSynopsisEn:magic in 0), product of:</str>
<arr name="details">
<lst>
<bool name="match">true</bool>
<float name="value">0.097945176</float>
<str name="description">queryWeight(stSynopsisEn:magic), product of:</str>
<arr name="details">
<lst>
<bool name="match">true</bool>
<float name="value">1.1541507</float>
<str name="description">idf(docFreq=5, maxDocs=7)</str>
</lst>
<lst>
<bool name="match">true</bool>
<float name="value">0.08486342</float>
<str name="description">queryNorm</str>
</lst>
</arr>
</lst>
<lst>
<bool name="match">true</bool>
<float name="value">0.14426884</float>
<str name="description">fieldWeight(stSynopsisEn:magic in 0), product of:</str>
<arr name="details">
<lst>
<bool name="match">true</bool>
<float name="value">2.0</float>
<str name="description">tf(termFreq(stSynopsisEn:magic)=4)</str>
</lst>
<lst>
<bool name="match">true</bool>
<float name="value">1.1541507</float>
<str name="description">idf(docFreq=5, maxDocs=7)</str>
</lst>
<lst>
<bool name="match">true</bool>
<float name="value">0.0625</float>
<str name="description">fieldNorm(field=stSynopsisEn, doc=0)</str>
</lst>
</arr>
</lst>
</arr>
</lst>
<lst>
<bool name="match">true</bool>
<float name="value">0.35326093</float>
<str name="description">weight(stTitleEn:magic^5.0 in 0), product of:</str>
<arr name="details">
<lst>
<bool name="match">true</bool>
<float name="value">0.4897259</float>
<str name="description">queryWeight(stTitleEn:magic^5.0), product of:</str>
<arr name="details">
<lst>
<bool name="match">true</bool>
<float name="value">5.0</float>
<str name="description">boost</str>
</lst>
<lst>
<bool name="match">true</bool>
<float name="value">1.1541507</float>
<str name="description">idf(docFreq=5, maxDocs=7)</str>
</lst>
<lst>
<bool name="match">true</bool>
<float name="value">0.08486342</float>
<str name="description">queryNorm</str>
</lst>
</arr>
</lst>
<lst>
<bool name="match">true</bool>
<float name="value">0.72134423</float>
<str name="description">fieldWeight(stTitleEn:magic in 0), product of:</str>
<arr name="details">
<lst>
<bool name="match">true</bool>
<float name="value">1.0</float>
<str name="description">tf(termFreq(stTitleEn:magic)=1)</str>
</lst>
<lst>
<bool name="match">true</bool>
<float name="value">1.1541507</float>
<str name="description">idf(docFreq=5, maxDocs=7)</str>
</lst>
<lst>
<bool name="match">true</bool>
<float name="value">0.625</float>
<str name="description">fieldNorm(field=stTitleEn, doc=0)</str>
</lst>
</arr>
</lst>
</arr>
</lst>
</arr>
</lst>
<lst name="978-0441015832">
<bool name="match">true</bool>
<float name="value">0.35326093</float>
<str name="description">max of:</str>
<arr name="details">
<lst>
<bool name="match">true</bool>
<float name="value">0.012489661</float>
<str name="description">weight(stSynopsisEn:magic in 1), product of:</str>
<arr name="details">
<lst>
<bool name="match">true</bool>
<float name="value">0.097945176</float>
<str name="description">queryWeight(stSynopsisEn:magic), product of:</str>
<arr name="details">
<lst>
<bool name="match">true</bool>
<float name="value">1.1541507</float>
<str name="description">idf(docFreq=5, maxDocs=7)</str>
</lst>
<lst>
<bool name="match">true</bool>
<float name="value">0.08486342</float>
<str name="description">queryNorm</str>
</lst>
</arr>
</lst>
<lst>
<bool name="match">true</bool>
<float name="value">0.12751685</float>
<str name="description">fieldWeight(stSynopsisEn:magic in 1), product of:</str>
<arr name="details">
<lst>
<bool name="match">true</bool>
<float name="value">1.4142135</float>
<str name="description">tf(termFreq(stSynopsisEn:magic)=2)</str>
</lst>
<lst>
<bool name="match">true</bool>
<float name="value">1.1541507</float>
<str name="description">idf(docFreq=5, maxDocs=7)</str>
</lst>
<lst>
<bool name="match">true</bool>
<float name="value">0.078125</float>
<str name="description">fieldNorm(field=stSynopsisEn, doc=1)</str>
</lst>
</arr>
</lst>
</arr>
</lst>
<lst>
<bool name="match">true</bool>
<float name="value">0.35326093</float>
<str name="description">weight(stTitleEn:magic^5.0 in 1), product of:</str>
<arr name="details">
<lst>
<bool name="match">true</bool>
<float name="value">0.4897259</float>
<str name="description">queryWeight(stTitleEn:magic^5.0), product of:</str>
<arr name="details">
<lst>
<bool name="match">true</bool>
<float name="value">5.0</float>
<str name="description">boost</str>
</lst>
<lst>
<bool name="match">true</bool>
<float name="value">1.1541507</float>
<str name="description">idf(docFreq=5, maxDocs=7)</str>
</lst>
<lst>
<bool name="match">true</bool>
<float name="value">0.08486342</float>
<str name="description">queryNorm</str>
</lst>
</arr>
</lst>
<lst>
<bool name="match">true</bool>
<float name="value">0.72134423</float>
<str name="description">fieldWeight(stTitleEn:magic in 1), product of:</str>
<arr name="details">
<lst>
<bool name="match">true</bool>
<float name="value">1.0</float>
<str name="description">tf(termFreq(stTitleEn:magic)=1)</str>
</lst>
<lst>
<bool name="match">true</bool>
<float name="value">1.1541507</float>
<str name="description">idf(docFreq=5, maxDocs=7)</str>
</lst>
<lst>
<bool name="match">true</bool>
<float name="value">0.625</float>
<str name="description">fieldNorm(field=stTitleEn, doc=1)</str>
</lst>
</arr>
</lst>
</arr>
</lst>
</arr>
</lst>
<lst name="978-0441017027">
<bool name="match">true</bool>
<float name="value">0.35326093</float>
<str name="description">max of:</str>
<arr name="details">
<lst>
<bool name="match">true</bool>
<float name="value">0.010597828</float>
<str name="description">weight(stSynopsisEn:magic in 2), product of:</str>
<arr name="details">
<lst>
<bool name="match">true</bool>
<float name="value">0.097945176</float>
<str name="description">queryWeight(stSynopsisEn:magic), product of:</str>
<arr name="details">
<lst>
<bool name="match">true</bool>
<float name="value">1.1541507</float>
<str name="description">idf(docFreq=5, maxDocs=7)</str>
</lst>
<lst>
<bool name="match">true</bool>
<float name="value">0.08486342</float>
<str name="description">queryNorm</str>
</lst>
</arr>
</lst>
<lst>
<bool name="match">true</bool>
<float name="value">0.10820163</float>
<str name="description">fieldWeight(stSynopsisEn:magic in 2), product of:</str>
<arr name="details">
<lst>
<bool name="match">true</bool>
<float name="value">1.0</float>
<str name="description">tf(termFreq(stSynopsisEn:magic)=1)</str>
</lst>
<lst>
<bool name="match">true</bool>
<float name="value">1.1541507</float>
<str name="description">idf(docFreq=5, maxDocs=7)</str>
</lst>
<lst>
<bool name="match">true</bool>
<float name="value">0.09375</float>
<str name="description">fieldNorm(field=stSynopsisEn, doc=2)</str>
</lst>
</arr>
</lst>
</arr>
</lst>
<lst>
<bool name="match">true</bool>
<float name="value">0.35326093</float>
<str name="description">weight(stTitleEn:magic^5.0 in 2), product of:</str>
<arr name="details">
<lst>
<bool name="match">true</bool>
<float name="value">0.4897259</float>
<str name="description">queryWeight(stTitleEn:magic^5.0), product of:</str>
<arr name="details">
<lst>
<bool name="match">true</bool>
<float name="value">5.0</float>
<str name="description">boost</str>
</lst>
<lst>
<bool name="match">true</bool>
<float name="value">1.1541507</float>
<str name="description">idf(docFreq=5, maxDocs=7)</str>
</lst>
<lst>
<bool name="match">true</bool>
<float name="value">0.08486342</float>
<str name="description">queryNorm</str>
</lst>
</arr>
</lst>
<lst>
<bool name="match">true</bool>
<float name="value">0.72134423</float>
<str name="description">fieldWeight(stTitleEn:magic in 2), product of:</str>
<arr name="details">
<lst>
<bool name="match">true</bool>
<float name="value">1.0</float>
<str name="description">tf(termFreq(stTitleEn:magic)=1)</str>
</lst>
<lst>
<bool name="match">true</bool>
<float name="value">1.1541507</float>
<str name="description">idf(docFreq=5, maxDocs=7)</str>
</lst>
<lst>
<bool name="match">true</bool>
<float name="value">0.625</float>
<str name="description">fieldNorm(field=stTitleEn, doc=2)</str>
</lst>
</arr>
</lst>
</arr>
</lst>
</arr>
</lst>
<lst name="978-0441018529">
<bool name="match">true</bool>
<float name="value">0.35326093</float>
<str name="description">max of:</str>
<arr name="details">
<lst>
<bool name="match">true</bool>
<float name="value">0.35326093</float>
<str name="description">weight(stTitleEn:magic^5.0 in 3), product of:</str>
<arr name="details">
<lst>
<bool name="match">true</bool>
<float name="value">0.4897259</float>
<str name="description">queryWeight(stTitleEn:magic^5.0), product of:</str>
<arr name="details">
<lst>
<bool name="match">true</bool>
<float name="value">5.0</float>
<str name="description">boost</str>
</lst>
<lst>
<bool name="match">true</bool>
<float name="value">1.1541507</float>
<str name="description">idf(docFreq=5, maxDocs=7)</str>
</lst>
<lst>
<bool name="match">true</bool>
<float name="value">0.08486342</float>
<str name="description">queryNorm</str>
</lst>
</arr>
</lst>
<lst>
<bool name="match">true</bool>
<float name="value">0.72134423</float>
<str name="description">fieldWeight(stTitleEn:magic in 3), product of:</str>
<arr name="details">
<lst>
<bool name="match">true</bool>
<float name="value">1.0</float>
<str name="description">tf(termFreq(stTitleEn:magic)=1)</str>
</lst>
<lst>
<bool name="match">true</bool>
<float name="value">1.1541507</float>
<str name="description">idf(docFreq=5, maxDocs=7)</str>
</lst>
<lst>
<bool name="match">true</bool>
<float name="value">0.625</float>
<str name="description">fieldNorm(field=stTitleEn, doc=3)</str>
</lst>
</arr>
</lst>
</arr>
</lst>
</arr>
</lst>
<lst name="978-0441020423">
<bool name="match">true</bool>
<float name="value">0.35326093</float>
<str name="description">max of:</str>
<arr name="details">
<lst>
<bool name="match">true</bool>
<float name="value">0.35326093</float>
<str name="description">weight(stTitleEn:magic^5.0 in 4), product of:</str>
<arr name="details">
<lst>
<bool name="match">true</bool>
<float name="value">0.4897259</float>
<str name="description">queryWeight(stTitleEn:magic^5.0), product of:</str>
<arr name="details">
<lst>
<bool name="match">true</bool>
<float name="value">5.0</float>
<str name="description">boost</str>
</lst>
<lst>
<bool name="match">true</bool>
<float name="value">1.1541507</float>
<str name="description">idf(docFreq=5, maxDocs=7)</str>
</lst>
<lst>
<bool name="match">true</bool>
<float name="value">0.08486342</float>
<str name="description">queryNorm</str>
</lst>
</arr>
</lst>
<lst>
<bool name="match">true</bool>
<float name="value">0.72134423</float>
<str name="description">fieldWeight(stTitleEn:magic in 4), product of:</str>
<arr name="details">
<lst>
<bool name="match">true</bool>
<float name="value">1.0</float>
<str name="description">tf(termFreq(stTitleEn:magic)=1)</str>
</lst>
<lst>
<bool name="match">true</bool>
<float name="value">1.1541507</float>
<str name="description">idf(docFreq=5, maxDocs=7)</str>
</lst>
<lst>
<bool name="match">true</bool>
<float name="value">0.625</float>
<str name="description">fieldNorm(field=stTitleEn, doc=4)</str>
</lst>
</arr>
</lst>
</arr>
</lst>
</arr>
</lst>
<lst name="978-0441017805">
<bool name="match">true</bool>
<float name="value">0.014130437</float>
<str name="description">max of:</str>
<arr name="details">
<lst>
<bool name="match">true</bool>
<float name="value">0.014130437</float>
<str name="description">weight(stSynopsisEn:magic in 5), product of:</str>
<arr name="details">
<lst>
<bool name="match">true</bool>
<float name="value">0.097945176</float>
<str name="description">queryWeight(stSynopsisEn:magic), product of:</str>
<arr name="details">
<lst>
<bool name="match">true</bool>
<float name="value">1.1541507</float>
<str name="description">idf(docFreq=5, maxDocs=7)</str>
</lst>
<lst>
<bool name="match">true</bool>
<float name="value">0.08486342</float>
<str name="description">queryNorm</str>
</lst>
</arr>
</lst>
<lst>
<bool name="match">true</bool>
<float name="value">0.14426884</float>
<str name="description">fieldWeight(stSynopsisEn:magic in 5), product of:</str>
<arr name="details">
<lst>
<bool name="match">true</bool>
<float name="value">2.0</float>
<str name="description">tf(termFreq(stSynopsisEn:magic)=4)</str>
</lst>
<lst>
<bool name="match">true</bool>
<float name="value">1.1541507</float>
<str name="description">idf(docFreq=5, maxDocs=7)</str>
</lst>
<lst>
<bool name="match">true</bool>
<float name="value">0.0625</float>
<str name="description">fieldNorm(field=stSynopsisEn, doc=5)</str>
</lst>
</arr>
</lst>
</arr>
</lst>
</arr>
</lst>
<lst name="978-0441019458">
<bool name="match">true</bool>
<float name="value">0.009991728</float>
<str name="description">max of:</str>
<arr name="details">
<lst>
<bool name="match">true</bool>
<float name="value">0.009991728</float>
<str name="description">weight(stSynopsisEn:magic in 6), product of:</str>
<arr name="details">
<lst>
<bool name="match">true</bool>
<float name="value">0.097945176</float>
<str name="description">queryWeight(stSynopsisEn:magic), product of:</str>
<arr name="details">
<lst>
<bool name="match">true</bool>
<float name="value">1.1541507</float>
<str name="description">idf(docFreq=5, maxDocs=7)</str>
</lst>
<lst>
<bool name="match">true</bool>
<float name="value">0.08486342</float>
<str name="description">queryNorm</str>
</lst>
</arr>
</lst>
<lst>
<bool name="match">true</bool>
<float name="value">0.102013476</float>
<str name="description">fieldWeight(stSynopsisEn:magic in 6), product of:</str>
<arr name="details">
<lst>
<bool name="match">true</bool>
<float name="value">1.4142135</float>
<str name="description">tf(termFreq(stSynopsisEn:magic)=2)</str>
</lst>
<lst>
<bool name="match">true</bool>
<float name="value">1.1541507</float>
<str name="description">idf(docFreq=5, maxDocs=7)</str>
</lst>
<lst>
<bool name="match">true</bool>
<float name="value">0.0625</float>
<str name="description">fieldNorm(field=stSynopsisEn, doc=6)</str>
</lst>
</arr>
</lst>
</arr>
</lst>
</arr>
</lst>
</lst>
<str name="QParser">ExtendedDismaxQParser</str>
<lst name="timing">
<!-- [NDLR] Données inutiles pour cet exemple -->
</lst>
</lst>
</response>

Dans les deux résultats ci-dessus, vous noterez la présence d’une ligne <str name="QParser">...</str> indiquant le type de plugin utilisé : lucene, edismax, etc. Cela peut notamment être utile pour vérifier si Solr a pris en compte le paramètre defType.

Afin d’y voir plus clair, voici une version condensée de ces résultats :

Résultats de req-4.0

score

isbn13

seriesEn

nbInSeries

hl

debug

0.12109267

978-0441014897

Kate Daniels

1

stTitleEn*1
stSynopsisEn*4

0.12109267 = (MATCH) fieldWeight(stAllEn:magic in 0), product of:
2.236068 = tf(termFreq(stAllEn:magic)=5)
0.8664686 = idf(docFreq=7, maxDocs=7)
0.0625 = fieldNorm(field=stAllEn, doc=0)

0.11724747

978-0441015832

Kate Daniels

2

stTitleEn*1
stSynopsisEn*2

0.11724747 = (MATCH) fieldWeight(stAllEn:magic in 1), product of:
1.7320508 = tf(termFreq(stAllEn:magic)=3)
0.8664686 = idf(docFreq=7, maxDocs=7)
0.078125 = fieldNorm(field=stAllEn, doc=1)

0.11487859

978-0441017027

Kate Daniels

3

stTitleEn*1
stSynopsisEn*1

0.11487859 = (MATCH) fieldWeight(stAllEn:magic in 2), product of:
1.4142135 = tf(termFreq(stAllEn:magic)=2)
0.8664686 = idf(docFreq=7, maxDocs=7)
0.09375 = fieldNorm(field=stAllEn, doc=2)

0.108308576

978-0441017805

The Edge

1

stSynopsisEn*4

0.108308576 = (MATCH) fieldWeight(stAllEn:magic in 5), product of:
2.0 = tf(termFreq(stAllEn:magic)=4)
0.8664686 = idf(docFreq=7, maxDocs=7)
0.0625 = fieldNorm(field=stAllEn, doc=5)

0.076585725

978-0441019458

The Edge

2

stSynopsisEn*2

0.076585725 = (MATCH) fieldWeight(stAllEn:magic in 6), product of:
1.4142135 = tf(termFreq(stAllEn:magic)=2)
0.8664686 = idf(docFreq=7, maxDocs=7)
0.0625 = fieldNorm(field=stAllEn, doc=6)

0.06769286

978-0441018529

Kate Daniels

4

stTitleEn*1

0.06769286 = (MATCH) fieldWeight(stAllEn:magic in 3), product of:
1.0 = tf(termFreq(stAllEn:magic)=1)
0.8664686 = idf(docFreq=7, maxDocs=7)
0.078125 = fieldNorm(field=stAllEn, doc=3)

0.06769286

978-0441020423

Kate Daniels

5

stSynopsisEn*1

0.06769286 = (MATCH) fieldWeight(stAllEn:magic in 4), product of:
1.0 = tf(termFreq(stAllEn:magic)=1)
0.8664686 = idf(docFreq=7, maxDocs=7)
0.078125 = fieldNorm(field=stAllEn, doc=4)

Résultats de req-4.1

score

isbn13

seriesEn

nbInSeries

hl

debug

0.35326093

978-0441014897

Kate Daniels

1

stTitleEn*1
stSynopsisEn*4

0.35326093 = (MATCH) max of:
0.014130437 = (MATCH) weight(stSynopsisEn:magic in 0), product of:
0.097945176 = queryWeight(stSynopsisEn:magic), product of:
1.1541507 = idf(docFreq=5, maxDocs=7)
0.08486342 = queryNorm
0.14426884 = (MATCH) fieldWeight(stSynopsisEn:magic in 0), product of:
2.0 = tf(termFreq(stSynopsisEn:magic)=4)
1.1541507 = idf(docFreq=5, maxDocs=7)
0.0625 = fieldNorm(field=stSynopsisEn, doc=0)
0.35326093 = (MATCH) weight(stTitleEn:magic^5.0 in 0), product of:
0.4897259 = queryWeight(stTitleEn:magic^5.0), product of:
5.0 = boost
1.1541507 = idf(docFreq=5, maxDocs=7)
0.08486342 = queryNorm
0.72134423 = (MATCH) fieldWeight(stTitleEn:magic in 0), product of:
1.0 = tf(termFreq(stTitleEn:magic)=1)
1.1541507 = idf(docFreq=5, maxDocs=7)
0.625 = fieldNorm(field=stTitleEn, doc=0)

0.35326093

978-0441015832

Kate Daniels

2

stTitleEn*1
stSynopsisEn*2

0.35326093 = (MATCH) max of:
0.012489661 = (MATCH) weight(stSynopsisEn:magic in 1), product of:
0.097945176 = queryWeight(stSynopsisEn:magic), product of:
1.1541507 = idf(docFreq=5, maxDocs=7)
0.08486342 = queryNorm
0.12751685 = (MATCH) fieldWeight(stSynopsisEn:magic in 1), product of:
1.4142135 = tf(termFreq(stSynopsisEn:magic)=2)
1.1541507 = idf(docFreq=5, maxDocs=7)
0.078125 = fieldNorm(field=stSynopsisEn, doc=1)
0.35326093 = (MATCH) weight(stTitleEn:magic^5.0 in 1), product of:
0.4897259 = queryWeight(stTitleEn:magic^5.0), product of:
5.0 = boost
1.1541507 = idf(docFreq=5, maxDocs=7)
0.08486342 = queryNorm
0.72134423 = (MATCH) fieldWeight(stTitleEn:magic in 1), product of:
1.0 = tf(termFreq(stTitleEn:magic)=1)
1.1541507 = idf(docFreq=5, maxDocs=7)
0.625 = fieldNorm(field=stTitleEn, doc=1)

0.35326093

978-0441017027

Kate Daniels

3

stTitleEn*1
stSynopsisEn*1

0.35326093 = (MATCH) max of:
0.010597828 = (MATCH) weight(stSynopsisEn:magic in 2), product of:
0.097945176 = queryWeight(stSynopsisEn:magic), product of:
1.1541507 = idf(docFreq=5, maxDocs=7)
0.08486342 = queryNorm
0.10820163 = (MATCH) fieldWeight(stSynopsisEn:magic in 2), product of:
1.0 = tf(termFreq(stSynopsisEn:magic)=1)
1.1541507 = idf(docFreq=5, maxDocs=7)
0.09375 = fieldNorm(field=stSynopsisEn, doc=2)
0.35326093 = (MATCH) weight(stTitleEn:magic^5.0 in 2), product of:
0.4897259 = queryWeight(stTitleEn:magic^5.0), product of:
5.0 = boost
1.1541507 = idf(docFreq=5, maxDocs=7)
0.08486342 = queryNorm
0.72134423 = (MATCH) fieldWeight(stTitleEn:magic in 2), product of:
1.0 = tf(termFreq(stTitleEn:magic)=1)
1.1541507 = idf(docFreq=5, maxDocs=7)
0.625 = fieldNorm(field=stTitleEn, doc=2)

0.35326093

978-0441018529

Kate Daniels

4

stTitleEn*1

0.35326093 = (MATCH) max of:
0.35326093 = (MATCH) weight(stTitleEn:magic^5.0 in 3), product of:
0.4897259 = queryWeight(stTitleEn:magic^5.0), product of:
5.0 = boost
1.1541507 = idf(docFreq=5, maxDocs=7)
0.08486342 = queryNorm
0.72134423 = (MATCH) fieldWeight(stTitleEn:magic in 3), product of:
1.0 = tf(termFreq(stTitleEn:magic)=1)
1.1541507 = idf(docFreq=5, maxDocs=7)
0.625 = fieldNorm(field=stTitleEn, doc=3)

0.35326093

978-0441020423

Kate Daniels

5

stTitleEn*1

0.35326093 = (MATCH) max of:
0.35326093 = (MATCH) weight(stTitleEn:magic^5.0 in 4), product of:
0.4897259 = queryWeight(stTitleEn:magic^5.0), product of:
5.0 = boost
1.1541507 = idf(docFreq=5, maxDocs=7)
0.08486342 = queryNorm
0.72134423 = (MATCH) fieldWeight(stTitleEn:magic in 4), product of:
1.0 = tf(termFreq(stTitleEn:magic)=1)
1.1541507 = idf(docFreq=5, maxDocs=7)
0.625 = fieldNorm(field=stTitleEn, doc=4)

0.014130437

978-0441017805

The Edge

1

stSynopsisEn*4

0.014130437 = (MATCH) max of:
0.014130437 = (MATCH) weight(stSynopsisEn:magic in 5), product of:
0.097945176 = queryWeight(stSynopsisEn:magic), product of:
1.1541507 = idf(docFreq=5, maxDocs=7)
0.08486342 = queryNorm
0.14426884 = (MATCH) fieldWeight(stSynopsisEn:magic in 5), product of:
2.0 = tf(termFreq(stSynopsisEn:magic)=4)
1.1541507 = idf(docFreq=5, maxDocs=7)
0.0625 = fieldNorm(field=stSynopsisEn, doc=5)

0.009991728

978-0441019458

The Edge

2

stSynopsisEn*2

0.009991728 = (MATCH) max of:
0.009991728 = (MATCH) weight(stSynopsisEn:magic in 6), product of:
0.097945176 = queryWeight(stSynopsisEn:magic), product of:
1.1541507 = idf(docFreq=5, maxDocs=7)
0.08486342 = queryNorm
0.102013476 = (MATCH) fieldWeight(stSynopsisEn:magic in 6), product of:
1.4142135 = tf(termFreq(stSynopsisEn:magic)=2)
1.1541507 = idf(docFreq=5, maxDocs=7)
0.0625 = fieldNorm(field=stSynopsisEn, doc=6)

Pour req-2.0 (defType=lucene), le calcul du score est simplement le produit de quelques fonctions.

Pour req-2.1 (defType=edismax), le calcul du score est un peu plus complexe, car plusieurs champs sont concernés. Le score final est le score maximum de l’ensemble des champs.

Dans notre exemple, les titres de la série « Kate Daniels » contiennent systématiquement le mot recherché en première position. Par ailleurs, alors que le score de synopsisEn varie d’un livre à l’autre, il est toujours plus faible que le score de titleEn à cause de la position du terme recherché et du boost appliqué au titre. Tous ces facteurs expliquent pourquoi le score de ces documents est toujours le même : 0.35326093. En revanche, les titres de la série « The Edge » ne contiennent pas le mot recherché : il figure uniquement dans leur synopsis, selon différentes fréquences et à différentes positions, c’est pourquoi le score global de chaque document est différent.

Cet exemple illustre la différence d’impact entre la position des termes, la fréquence des termes et le boost des champs, sur le score des documents dans les résultats de recherche.

Dans notre situation, req-2.1 donne des résultats mieux adaptés que req-2.0. Dans la suite de l’article, nous allons donc utiliser req-2.1 comme base des requêtes.

VIII-E-2-d - Filtres automatiques sur les résultats : solr.FacetComponent

VIII-E-2-d-i - Fonctionnement général

L’une des grandes fonctionnalités de Solr est de renvoyer des « facettes » sur le jeu de résultats. Ce sont des filtres applicables au jeu de résultats pour permettre au client de lancer des requêtes plus précises.

Chronologiquement, les facettes sont déterminées après le jeu de résultats, mais avant que les paramètres start et rows ne limitent l’affichage. Ainsi, les facettes n’influencent ni le scoring ni la liste de résultats, et elles ne sont pas influencées par la pagination des résultats.

Les conditions techniques pour obtenir les facettes sur un champ sont les suivantes :

- pour un champ texte, il doit avoir un fieldType très peu analysé, par exemple une version brute (« rawText » dans notre schéma d’exemple) ou normalisée (mise en minuscules, etc.) ;

- pour un champ texte, éviter les fieldTypes ayant des analyses lourdes (racinisation, n-grams, etc.) ;

- pour un champ numérique ou date, il est préférable d’utiliser l’un des types « trie » dont le nom commence par « t » : tint, tfloat, tdate, etc. ;

- le fieldType du champ doit être indexed, mais pas obligatoirement stored.

Concernant 2º point ci-dessus, il est possible d’obtenir les facettes sur un champ lourdement analysé, mais l’intérêt en serait limité. En effet, Solr donnerait un très grand nombre de faux positifs (toutes les variantes de chaque terme), or ce sont des termes non destinés à des êtres humains et donc peu adaptés à une UI de recherche.

Solr propose quatre types de facettes :

- Requêtes (facet_queries) : des requêtes arbitraires selon la syntaxe habituelle de Lucene ou Solr ;

- Champs (facet_fields) : les valeurs distinctes du champ et leur nombre d’occurrences (en SQL, cela se rapproche d’un COUNT(1) dans une requête comprenant une clause GROUP BY sur ce champ) ;

- Dates (facet_dates) : cette fonctionnalité est dépréciée, au profit de facet_ranges ci-dessous ;

- Intervalles (facet_ranges) : toutes les valeurs existant entre une borne minimale et une borne maximale, avec un intervalle défini.

Les paramètres généraux de FacetComponent sont :

- facet (bool) : activer ou désactiver le composant pour la requête en cours.

La plupart des paramètres de facet_field et de facet_range ont une valeur globale et une valeur par champ. Vous pouvez par exemple définir un tri par défaut des facettes (facet.sort=count), tout en définissant un tri spécifique pour la facette sur le champ copyrightYear (f.copyrightYear.facet.sort=index). Nous verrons des exemples de cela par la suite.

VIII-E-2-d-ii - Les requêtes-facettes : facet_query

Les paramètres de facet_query sont :

- facet.query (str) : la requête Solr à exécuter : ce paramètre doit être envoyé une fois pour chaque requête-facette.

Voici un bref exemple :

Requête :

http://localhost:8080/solr/livres/select?
fl=score%20isbn13%20copyrightYear%20nbPages%20nbInSeries%20authorNames%20titleEn%20seriesEn
&sort=score%20desc
&defType=edismax
&qf=stTitleEn^5.0%20stSeriesEn^4.0%20stSynopsisEn%20stAuthorNamesEn^2.0
&q.alt=*:*
&rows=0
&facet=on
&facet.query=sword
&facet.query=mercenary
&q=magic

Réponse :

<?xml version="1.0" encoding="UTF-8"?>
<response>
<lst name="responseHeader">
<int name="status">0</int>
<int name="QTime">0</int>
<lst name="params">
<str name="facet">on</str>
<str name="sort">score desc</str>
<str name="qf">stTitleEn^5.0 stSeriesEn^4.0 stSynopsisEn stAuthorNamesEn^2.0</str>
<str name="q.alt">*:*</str>
<str name="defType">edismax</str>
<str name="rows">0</str>
<str name="fl">score isbn13 copyrightYear nbPages nbInSeries authorNames titleEn seriesEn</str>
<arr name="facet.query">
<str>sword</str>
<str>mercenary</str>
</arr>
<str name="q">magic</str>
</lst>
</lst>
<result name="response" numFound="7" start="0" maxScore="0.35326093"/>
<lst name="facet_counts">
<lst name="facet_queries">
<int name="sword">1</int>
<int name="mercenary">4</int>
</lst>
<lst name="facet_fields"/>
<lst name="facet_dates"/>
<lst name="facet_ranges"/>
</lst>
</response>

Si nous exécutons les deux requêtes suggérées ci-dessus, voici le résultat :

Requête "magic+sword" :

http://localhost:8080/solr/livres/select?
fl=score%20isbn13%20copyrightYear%20nbPages%20nbInSeries%20authorNames%20titleEn%20seriesEn
&sort=score%20desc
&defType=edismax
&qf=stTitleEn^5.0%20stSeriesEn^4.0%20stSynopsisEn%20stAuthorNamesEn^2.0
&q.alt=*:*
&rows=0
&omitHeader=true
&facet=on
&facet.query=sword
&facet.query=mercenary
&q=magic%20sword

Réponse "magic+sword" :

<?xml version="1.0" encoding="UTF-8"?>
<response>
<result name="response" numFound="1" start="0" maxScore="0.23749547"/>
<lst name="facet_counts">
<lst name="facet_queries">
<int name="sword">1</int>
<int name="mercenary">1</int>
</lst>
<lst name="facet_fields"/>
<lst name="facet_dates"/>
<lst name="facet_ranges"/>
</lst>
</response>

Requête "magic+mercenary" :

http://localhost:8080/solr/livres/select?
fl=score%20isbn13%20copyrightYear%20nbPages%20nbInSeries%20authorNames%20titleEn%20seriesEn
&sort=score%20desc
&defType=edismax
&qf=stTitleEn^5.0%20stSeriesEn^4.0%20stSynopsisEn%20stAuthorNamesEn^2.0
&q.alt=*:*
&rows=0
&omitHeader=true
&facet=on
&facet.query=sword
&facet.query=mercenary
&q=magic%20mercenary

Réponse "magic+mercenary" :

<?xml version="1.0" encoding="UTF-8"?>
<response>
<result name="response" numFound="4" start="0" maxScore="0.22955763"/>
<lst name="facet_counts">
<lst name="facet_queries">
<int name="sword">1</int>
<int name="mercenary">4</int>
</lst>
<lst name="facet_fields"/>
<lst name="facet_dates"/>
<lst name="facet_ranges"/>
</lst>
</response>

Comme nous pouvons le voir ici, nous pouvons demander à Solr de nous donner les facettes sur n’importe quelle requête. Dans le cas de « magic+sword », l’index ne renvoie qu’un seul résultat, et il contient les termes « sword » et « mercenary ». Dans le cas de « magic+mercenary », l’index renvoie quatre résultats, parmi lesquels le terme « sword » apparaît dans un seul document alors que « mercenary » apparaît dans les quatre documents.

Si vous souhaitez savoir dans quel(s) champ(s) les termes suggérés apparaissent, il vous faut exécuter les requêtes suggérées en activant les paramètres de Highlighting.

VIII-E-2-d-iii - Les champs facettes : facet_field

Les paramètres de facet_field sont :

- facet.field (str) : le nom du champ : ce paramètre doit être envoyé une fois pour chaque nom de champ (il ne contient pas une liste de noms de champs) ;

- facet.prefix (str) : limite les valeurs des facettes à celles correspondant à ce préfixe ;

- facet.sort (str) : le tri des facettes : « index » ou « count ».

- facet.mincount (int) : le nombre minimal d’occurrences du terme pour qu’il apparaisse dans la liste des termes ;

- facet.missing (bool) : valeur virtuelle comptant les documents ne comprenant pas ce champ.

- etc.

Requête :

http://localhost:8080/solr/livres/select?
fl=score%20isbn13%20copyrightYear%20nbPages%20nbInSeries%20authorNames%20titleEn%20seriesEn
&sort=score%20desc
&defType=edismax
&qf=stTitleEn^5.0%20stSeriesEn^4.0%20stSynopsisEn%20stAuthorNamesEn^2.0
&q.alt=*:*
&rows=0
&facet=on
&facet.mincount=1
&facet.sort=count
&facet.field=copyrightYear
&facet.field=authorNames
&facet.field=seriesEn
&facet.field=editors
&facet.field=originalCoverArtBy
&facet.field=originalCoverDesignBy
&facet.field=originalInteriorTextDesignBy
&f.copyrightYear.facet.sort=index
&q=magic

Réponse :

<?xml version="1.0" encoding="UTF-8"?>
<response>
<lst name="responseHeader">
<int name="status">0</int>
<int name="QTime">0</int>
<lst name="params">
<str name="facet">on</str>
<str name="sort">score desc</str>
<str name="facet.mincount">1</str>
<str name="qf">stTitleEn^5.0 stSeriesEn^4.0 stSynopsisEn stAuthorNamesEn^2.0</str>
<str name="q.alt">*:*</str>
<str name="defType">edismax</str>
<str name="rows">0</str>
<str name="f.copyrightYear.facet.sort">index</str>
<str name="fl">score isbn13 copyrightYear nbPages nbInSeries authorNames titleEn seriesEn</str>
<str name="facet.sort">count</str>
<str name="q">magic</str>
<arr name="facet.field">
<str>copyrightYear</str>
<str>authorNames</str>
<str>seriesEn</str>
<str>editors</str>
<str>originalCoverArtBy</str>
<str>originalCoverDesignBy</str>
<str>originalInteriorTextDesignBy</str>
</arr>
</lst>
</lst>
<result name="response" numFound="7" start="0" maxScore="0.35326093"/>
<lst name="facet_counts">
<lst name="facet_queries"/>
<lst name="facet_fields">
<lst name="copyrightYear">
<int name="2007">1</int>
<int name="2008">1</int>
<int name="2009">2</int>
<int name="2010">2</int>
<int name="2011">1</int>
</lst>
<lst name="authorNames">
<int name="Gordon, Andrew">7</int>
<int name="Gordon, Ilona">7</int>
<int name="Ilona Andrews">7</int>
</lst>
<lst name="seriesEn">
<int name="Kate Daniels">5</int>
<int name="The Edge">2</int>
</lst>
<lst name="editors">
<int name="ACE Fantasy">7</int>
</lst>
<lst name="originalCoverArtBy">
<int name="Ward, Chad Michael">5</int>
<int name="Vebell, Victoria">2</int>
</lst>
<lst name="originalCoverDesignBy">
<int name="Fiore DeFex, Annette">5</int>
<int name="Fiore, Annette">2</int>
</lst>
<lst name="originalInteriorTextDesignBy">
<int name="Rosario (del), Kristin">7</int>
</lst>
</lst>
<lst name="facet_dates"/>
<lst name="facet_ranges"/>
</lst>
</response>

facet_field suggère d’ajouter des paramètres « fq » avec la syntaxe de valeur chaîne ou numérique :

- 1 résultat pour : http://localhost:8080/solr/livres/select?q=magic&fq=copyrightYear:2007

- 2 résultats pour : http://localhost:8080/solr/livres/select?q=magic&fq=copyrightYear:2010

- 5 résultats pour : http://localhost:8080/solr/livres/select?q=magic&fq=originalCoverArtBy:« Ward,%20Chad%20Michael »,

- etc.

VIII-E-2-d-iv - Les facettes sur un type date : facet_date

C’est une fonctionnalité DEPRECATED. Il est préférable d’utiliser facet_range à la place.

VIII-E-2-d-v - Les facettes par intervalles : facet_range

Les paramètres de facet_range sont :

- facet.range (str) : le nom du champ à utiliser pour renvoyer une facette d’intervalles : ce paramètre doit être envoyé une fois pour chaque nom de champ (il ne contient pas une liste de noms de champs) ;

- facet.range.start (int/float/date) : la borne minimale des facettes à renvoyer ;

- facet.range.end (int/float/date) : la borne maximale des facettes à renvoyer ;

- facet.range.gap (int/float/date) : l’intervalle des facettes à renvoyer ;

- facet.mincount (int) : le nombre minimal d’occurrences du terme pour qu’il apparaisse dans la liste des termes ;

- facet.missing (bool) : valeur virtuelle comptant les documents ne comprenant pas ce champ.

- etc.

Requête :

http://localhost:8080/solr/livres/select?
fl=score%20isbn13%20copyrightYear%20nbPages%20nbInSeries%20authorNames%20titleEn%20seriesEn
&sort=score%20desc
&defType=edismax
&qf=stTitleEn^5.0%20stSeriesEn^4.0%20stSynopsisEn%20stAuthorNamesEn^2.0
&q.alt=*:*
&rows=0
&facet=on
&facet.mincount=1
&facet.range=createdAt
&f.createdAt.facet.range.start=2011-01-00T00:00:00.000Z
&f.createdAt.facet.range.end=2012-12-00T00:00:00.000Z
&f.createdAt.facet.range.gap=%2B1DAY
&facet.range=nbPages
&f.nbPages.facet.range.start=0
&f.nbPages.facet.range.end=3000
&f.nbPages.facet.range.gap=50
&q=magic

Réponse :

<?xml version="1.0" encoding="UTF-8"?>
<response>
<lst name="responseHeader">
<int name="status">0</int>
<int name="QTime">321</int>
<lst name="params">
<str name="facet">on</str>
<str name="sort">score desc</str>
<str name="facet.mincount">1</str>
<str name="f.nbPages.facet.range.end">3000</str>
<str name="f.createdAt.facet.range.end">2012-12-00T00:00:00.000Z</str>
<str name="qf">stTitleEn^5.0 stSeriesEn^4.0 stSynopsisEn stAuthorNamesEn^2.0</str>
<arr name="facet.range">
<str>createdAt</str>
<str>nbPages</str>
</arr>
<str name="q.alt">*:*</str>
<str name="f.nbPages.facet.range.gap">50</str>
<str name="defType">edismax</str>
<str name="rows">0</str>
<str name="f.createdAt.facet.range.start">2011-01-00T00:00:00.000Z</str>
<str name="fl">score isbn13 copyrightYear nbPages nbInSeries authorNames titleEn seriesEn</str>
<str name="q">magic</str>
<str name="f.createdAt.facet.range.gap">+1DAY</str>
<str name="f.nbPages.facet.range.start">0</str>
</lst>
</lst>
<result name="response" numFound="7" start="0" maxScore="0.35326093"/>
<lst name="facet_counts">
<lst name="facet_queries"/>
<lst name="facet_fields"/>
<lst name="facet_dates"/>
<lst name="facet_ranges">
<lst name="createdAt">
<lst name="counts">
<int name="2011-06-04T00:00:00Z">2</int>
<int name="2011-06-05T00:00:00Z">3</int>
<int name="2011-06-25T00:00:00Z">2</int>
</lst>
<str name="gap">+1DAY</str>
<date name="start">2010-12-31T00:00:00Z</date>
<date name="end">2012-11-30T00:00:00Z</date>
</lst>
<lst name="nbPages">
<lst name="counts">
<int name="250">2</int>
<int name="300">3</int>
<int name="350">1</int>
<int name="450">1</int>
</lst>
<int name="gap">50</int>
<int name="start">0</int>
<int name="end">3000</int>
</lst>
</lst>
</lst>
</response>

facet_range suggère d’ajouter des paramètres « fq » avec la syntaxe d’intervalles :

- 2 résultats pour : http://localhost:8080/solr/livres/select?q=magic&fq=createdAt:[2011-06-04T00:00:00Z%20TO%202011-06-04T23:59:59Z]

- 3 résultats pour : http://localhost:8080/solr/livres/select?q=magic&fq=createdAt:[2011-06-05T00:00:00Z%20TO%202011-06-05T23:59:59Z]

- 3 résultats pour : http://localhost:8080/solr/livres/select?q=magic&fq=nbPages:[300%20TO%20349]

Les paramètres facet.range.start, facet.range.end et facet.range.gap sont tellement liés à chaque champ d’application qu’il est rare de les configurer au niveau global, mais plutôt individuellement pour chaque champ. En effet, quel intérêt de configurer un intervalle global « facet.range.gap=%2B1DAY » si les facet_range ne sont pas toutes de type tdate et si cet intervalle n’est pas cohérent pour toutes ?

VIII-E-2-d-vi - Exemple complet

Exemple « req-5.1 » basé sur « req-2.1 » :

Requête :

http://localhost:8080/solr/livres/select?
fl=score%20isbn13%20copyrightYear%20nbPages%20nbInSeries%20authorNames%20titleEn%20seriesEn
&sort=score%20desc
&defType=edismax
&qf=stTitleEn^5.0%20stSeriesEn^4.0%20stSynopsisEn%20stAuthorNamesEn^2.0
&q.alt=*:*
&rows=0
&facet=on
&facet.query=sword
&facet.query=mercenary
&facet.mincount=1
&facet.sort=count
&facet.field=copyrightYear
&facet.field=authorNames
&facet.field=seriesEn
&facet.field=editors
&facet.field=originalCoverArtBy
&facet.field=originalCoverDesignBy
&facet.field=originalInteriorTextDesignBy
&f.copyrightYear.facet.sort=index
&facet.range=createdAt
&f.createdAt.facet.range.start=2011-01-00T00:00:00.000Z
&f.createdAt.facet.range.end=2012-12-00T00:00:00.000Z
&f.createdAt.facet.range.gap=%2B1DAY
&facet.range=nbPages
&f.nbPages.facet.range.start=0
&f.nbPages.facet.range.end=3000
&f.nbPages.facet.range.gap=50
&q=magic

Dans l’URL ci-dessus, nous voulons obtenir deux facettes « query », sept facettes « field » et deux facettes « range ». Les facettes « field » sont triées par nombre d’occurrences des termes, sauf la facette sur le champ « copyrightYear » qui est triée par ordre d’indexation (donc « ordinal croissant »). Si des termes indexés dans le noyau n’apparaissent pas dans le jeu de résultats, ils ne sont pas renvoyés par Solr (facet.mincount >= 1).

Réponse :

<?xml version="1.0" encoding="UTF-8"?>
<response>
<lst name="responseHeader">
<int name="status">0</int>
<int name="QTime">550</int>
<lst name="params">
<str name="facet">on</str>
<str name="sort">score desc</str>
<str name="facet.mincount">1</str>
<str name="f.nbPages.facet.range.end">3000</str>
<str name="f.createdAt.facet.range.end">2012-12-00T00:00:00.000Z</str>
<str name="qf">stTitleEn^5.0 stSeriesEn^4.0 stSynopsisEn stAuthorNamesEn^2.0</str>
<arr name="facet.range">
<str>createdAt</str>
<str>nbPages</str>
</arr>
<str name="q.alt">*:*</str>
<str name="f.nbPages.facet.range.gap">50</str>
<str name="defType">edismax</str>
<str name="f.copyrightYear.facet.sort">index</str>
<str name="f.createdAt.facet.range.start">2011-01-00T00:00:00.000Z</str>
<str name="fl">score isbn13 copyrightYear nbPages nbInSeries authorNames titleEn seriesEn</str>
<arr name="facet.query">
<str>sword</str>
<str>mercenary</str>
</arr>
<str name="facet.sort">count</str>
<str name="q">magic</str>
<str name="f.createdAt.facet.range.gap">+1DAY</str>
<arr name="facet.field">
<str>copyrightYear</str>
<str>authorNames</str>
<str>seriesEn</str>
<str>editors</str>
<str>originalCoverArtBy</str>
<str>originalCoverDesignBy</str>
<str>originalInteriorTextDesignBy</str>
</arr>
<str name="f.nbPages.facet.range.start">0</str>
</lst>
</lst>
<result name="response" numFound="7" start="0" maxScore="0.35326093"/>
<lst name="facet_counts">
<lst name="facet_queries">
<int name="sword">1</int>
<int name="mercenary">4</int>
</lst>
<lst name="facet_fields">
<lst name="copyrightYear">
<int name="2007">1</int>
<int name="2008">1</int>
<int name="2009">2</int>
<int name="2010">2</int>
<int name="2011">1</int>
</lst>
<lst name="authorNames">
<int name="Gordon, Andrew">7</int>
<int name="Gordon, Ilona">7</int>
<int name="Ilona Andrews">7</int>
</lst>
<lst name="seriesEn">
<int name="Kate Daniels">5</int>
<int name="The Edge">2</int>
</lst>
<lst name="editors">
<int name="ACE Fantasy">7</int>
</lst>
<lst name="originalCoverArtBy">
<int name="Ward, Chad Michael">5</int>
<int name="Vebell, Victoria">2</int>
</lst>
<lst name="originalCoverDesignBy">
<int name="Fiore DeFex, Annette">5</int>
<int name="Fiore, Annette">2</int>
</lst>
<lst name="originalInteriorTextDesignBy">
<int name="Rosario (del), Kristin">7</int>
</lst>
</lst>
<lst name="facet_dates"/>
<lst name="facet_ranges">
<lst name="createdAt">
<lst name="counts">
<int name="2011-06-04T00:00:00Z">2</int>
<int name="2011-06-05T00:00:00Z">3</int>
<int name="2011-06-25T00:00:00Z">2</int>
</lst>
<str name="gap">+1DAY</str>
<date name="start">2010-12-31T00:00:00Z</date>
<date name="end">2012-11-30T00:00:00Z</date>
</lst>
<lst name="nbPages">
<lst name="counts">
<int name="250">2</int>
<int name="300">3</int>
<int name="350">1</int>
<int name="450">1</int>
</lst>
<int name="gap">50</int>
<int name="start">0</int>
<int name="end">3000</int>
</lst>
</lst>
</lst>
</response>

VIII-E-2-e - Statistiques sur le jeu de résultats : solr.StatsComponent

Le composant de statistiques de Solr permet d’obtenir certaines métriques sur les champs numériques du noyau : min, max, sum, count, missing, sumOfSquares, mean et stddev. Toutes ces mesures ne sont pas utiles pour tous les cas d’application, à vous de voir si cela vous est utile et dans quelle proportion.

Ces mesures peuvent être récupérées soit sur un champ (stats.field) soit sur une facette (stats.facet).

Pour le moment, stats.field ne fonctionne que sur des types numériques. Essayer d’obtenir stats.field sur un type différent donne une erreur http-500.

Par ailleurs, stats.facet sur un type date ne donne pas toujours des résultats satisfaisants, car il n’y a pas de notion d’intervalle.

Exemple « req-6.1 » basé sur « req-2.1 » :

Requête :

http://localhost:8080/solr/livres/select?
fl=score%20isbn13%20copyrightYear%20nbPages%20nbInSeries%20authorNames%20titleEn%20seriesEn
&sort=score%20desc
&defType=edismax
&qf=stTitleEn^5.0%20stSeriesEn^4.0%20stSynopsisEn%20stAuthorNamesEn^2.0
&q.alt=*:*
&rows=0
&stats=on
&stats.field=copyrightYear
&stats.field=nbPages
&q=magic

Réponse :

<?xml version="1.0" encoding="UTF-8"?>
<response>
<lst name="responseHeader">
<int name="status">0</int>
<int name="QTime">10</int>
<lst name="params">
<str name="sort">score desc</str>
<str name="stats">on</str>
<str name="qf">stTitleEn^5.0 stSeriesEn^4.0 stSynopsisEn stAuthorNamesEn^2.0</str>
<str name="q.alt">*:*</str>
<str name="defType">edismax</str>
<str name="rows">0</str>
<str name="fl">score isbn13 copyrightYear nbPages nbInSeries authorNames titleEn seriesEn</str>
<str name="q">magic</str>
<arr name="stats.field">
<str>copyrightYear</str>
<str>nbPages</str>
</arr>
</lst>
</lst>
<result name="response" numFound="7" start="0" maxScore="0.35326093"/>
<lst name="stats">
<lst name="stats_fields">
<lst name="copyrightYear">
<double name="min">2007.0</double>
<double name="max">2011.0</double>
<double name="sum">14064.0</double>
<long name="count">7</long>
<long name="missing">0</long>
<double name="sumOfSquares">2.8256596E7</double>
<double name="mean">2009.142857142857</double>
<double name="stddev">1.3451854182690985</double>
</lst>
<lst name="nbPages">
<double name="min">260.0</double>
<double name="max">462.0</double>
<double name="sum">2292.0</double>
<long name="count">7</long>
<long name="missing">0</long>
<double name="sumOfSquares">779840.0</double>
<double name="mean">327.42857142857144</double>
<double name="stddev">69.96870048542453</double>
</lst>
</lst>
</lst>
</response>

Le résultat ci-dessus nous apprend que, pour cette recherche, les années de publication des livres sont comprises entre 2007 et 2011. La moyenne de publication des livres est début 2009. Leur nombre de pages est compris entre 260 et 462, pour un total de 2292 pages, et en moyenne 327 pages par livre.

Exemple « req-6.1b » basé sur « req-6.1 » (ajout d’une facette sur le champ seriesEn) :

Requête :

http://localhost:8080/solr/livres/select?
fl=score%20isbn13%20copyrightYear%20nbPages%20nbInSeries%20authorNames%20titleEn%20seriesEn
&sort=score%20desc
&defType=edismax
&qf=stTitleEn^5.0%20stSeriesEn^4.0%20stSynopsisEn%20stAuthorNamesEn^2.0
&q.alt=*:*
&rows=0
&omitHeader=true
&stats=on
&stats.field=copyrightYear
&stats.field=nbPages
&stats.facet=seriesEn
&q=magic

Réponse :

<?xml version="1.0" encoding="UTF-8"?>
<response>
<result name="response" numFound="7" start="0" maxScore="0.35326093"/>
<lst name="stats">
<lst name="stats_fields">
<lst name="copyrightYear">
<double name="min">2007.0</double>
<double name="max">2011.0</double>
<double name="sum">14064.0</double>
<long name="count">7</long>
<long name="missing">0</long>
<double name="sumOfSquares">2.8256596E7</double>
<double name="mean">2009.142857142857</double>
<double name="stddev">1.3451854182690985</double>
<lst name="facets">
<lst name="seriesEn">
<lst name="Kate Daniels">
<double name="min">2007.0</double>
<double name="max">2011.0</double>
<double name="sum">10045.0</double>
<long name="count">5</long>
<long name="missing">0</long>
<double name="sumOfSquares">2.0180415E7</double>
<double name="mean">2009.0</double>
<double name="stddev">1.5811388300841898</double>
</lst>
<lst name="The Edge">
<double name="min">2009.0</double>
<double name="max">2010.0</double>
<double name="sum">4019.0</double>
<long name="count">2</long>
<long name="missing">0</long>
<double name="sumOfSquares">8076181.0</double>
<double name="mean">2009.5</double>
<double name="stddev">0.7071067811865476</double>
</lst>
</lst>
</lst>
</lst>
<lst name="nbPages">
<double name="min">260.0</double>
<double name="max">462.0</double>
<double name="sum">2292.0</double>
<long name="count">7</long>
<long name="missing">0</long>
<double name="sumOfSquares">779840.0</double>
<double name="mean">327.42857142857144</double>
<double name="stddev">69.96870048542453</double>
<lst name="facets">
<lst name="seriesEn">
<lst name="Kate Daniels">
<double name="min">260.0</double>
<double name="max">366.0</double>
<double name="sum">1504.0</double>
<long name="count">5</long>
<long name="missing">0</long>
<double name="sumOfSquares">460120.0</double>
<double name="mean">300.8</double>
<double name="stddev">43.92265930018355</double>
</lst>
<lst name="The Edge">
<double name="min">326.0</double>
<double name="max">462.0</double>
<double name="sum">788.0</double>
<long name="count">2</long>
<long name="missing">0</long>
<double name="sumOfSquares">319720.0</double>
<double name="mean">394.0</double>
<double name="stddev">96.16652224137046</double>
</lst>
</lst>
</lst>
</lst>
</lst>
</lst>
</response>

Le résultat ci-dessus précise le résultat précédent, en nous donnant le détail : nous avons les mêmes données chiffrées pour chacune des séries de l’index.

VIII-E-3 - Similitude de documents : MoreLikeThis

L’affichage d’un document suppose de lancer une requête Solr avec un filtre sur son ID et d’attendre un seul résultat. Dans ce contexte, les composants de groupes (solr.FacetComponent, solr.HighlightComponent, solr.StatsComponent) ont une utilité très réduite voire nulle. En revanche, il peut être intéressant de proposer aux clients des résultats élargis : c’est ce que permettent solr.MoreLikeThisComponent et solr.MoreLikeThisHandler.

Solr dispose à la fois d’un MoreLikeThisComponent et d’un MoreLikeThisHandler. Le premier est prévu pour être activé sur solr.SearchHandler tandis que le second remplace solr.SearchHandler dans la configuration d’un requestHandler. Puisque l’affichage d’un document n’utilise aucune fonctionnalité de SearchHandler, nous pouvons nous permettre de le remplacer par MoreLikeThisHandler :

conf/solrconfig.xml

...
<requestHandler name="/displayTest" class="solr.MoreLikeThisHandler">
<arr name="components">
<str>query</str>
</arr>
<lst name="defaults">
<bool name="indent">on</bool>
<str name="echoParams">explicit</str>
<str name="wt">xml</str>
</lst>
</requestHandler>
...

MLT obéit au paramètre qf s’il est utilisé avec MoreLikeThisHandler, mais pas avec MoreLikeThisComponent.

Paramètres disponibles uniquement en cas d’utilisation de MoreLikeThisComponent dans un StandardRequestHandler :

- mlt (bool) : activer ou désactiver MLT pour cette requête ;

- mlt.count (int) : le nombre de résultats similaires à renvoyer dans la réponse.

Paramètres disponibles uniquement en cas d’utilisation de MoreLikeThisHandler :

- mlt.match.include (bool) : renvoyer ou non le document source dans la réponse ;

- mlt.match.offset (int) : sur quel résultat du paramètre « q » agir (par défaut le premier) ;

- mlt.interestingTerms (str) : « list », « details » ou « none » ; donne quelques explications sur les termes utilisés pour sortir les résultats similaires ; nécessite « mlt.boost=true ».

Paramètres communs de MoreLikeThisComponent et MoreLikeThisHandler :

- mlt.fl (str) : liste des champs à utiliser pour effectuer la comparaison ; ils devraient avoir les attributs termstermVectors, termPositions et termOffsets ;

- mlt.mintf (float) : Term Frequency - fréquence minimum des termes dans le document source ;

- mlt.mindf (float) : Document Frequency - nombre minimum de documents dans lesquels un terme doit apparaître ;

- mlt.minwl (int) : Word Length - longueur minimale des termes ;

- mlt.boost (bool) : utile pour mlt.interestingTerms ;

- mlt.qf (str) : même contenu que mlt.fl avec la syntaxe de DisMaxQParserPlugin pour booster les champs ;

- etc.

Une fois que vous avez collé le code du nouveau requestHandler dans votre fichier solrconfig.xml et après avoir relancé le noyau, chargez l’URL suivante :

Requête "req-7.1" :

http://localhost:8080/solr/livres/displayTest?
fl=isbn13%20createdAt%20copyrightYear%20nbPages%20nbInSeries%20authorNames%20titleEn%20seriesEn%20synopsisEn%20urlEn%20coverImageEn
&mlt.fl=stSynopsisEn
&mlt.count=5
&mlt.interestingTerms=details
&mlt.mintf=2
&mlt.minwl=3
&mlt.boost=true
&q=isbn13:978-0441014897

Réponse (lire les termes listés dans "interestingTerms") :

<?xml version="1.0" encoding="UTF-8"?>
<response>
<lst name="responseHeader">
<int name="status">0</int>
<int name="QTime">40</int>
<lst name="params">
<str name="mlt.minwl">3</str>
<str name="mlt.boost">true</str>
<str name="mlt.fl">stSynopsisEn</str>
<str name="mlt.mintf">2</str>
<str name="mlt.interestingTerms">details</str>
<str name="mlt.count">5</str>
<str name="fl">isbn13 createdAt copyrightYear nbPages nbInSeries authorNames
titleEn seriesEn synopsisEn urlEn coverImageEn</str>
<str name="q">isbn13:978-0441014897</str>
</lst>
</lst>
<result name="match" numFound="1" start="0">
[NDLR] Code trop long pour être inclus intégralement,
voir fichier joint pour le détail
</result>
<result name="response" numFound="6" start="0">
[NDLR] Code trop long pour être inclus intégralement,
voir fichier joint pour le détail
</result>
<lst name="interestingTerms">
<float name="stSynopsisEn:and">1.0</float>
<float name="stSynopsisEn:her">0.89764106</float>
<float name="stSynopsisEn:the">0.7777778</float>
<float name="stSynopsisEn:magic">0.59200746</float>
<float name="stSynopsisEn:kate">0.59200746</float>
<float name="stSynopsisEn:but">0.3847033</float>
<float name="stSynopsisEn:daniel">0.29600373</float>
<float name="stSynopsisEn:she">0.25646886</float>
</lst>
</response>

Un constat que nous pouvons faire ici est que l’intégralité de notre index est mise dans les résultats de MLT. Ce n’est pas un comportement souhaitable. Nous aimerions mieux avoir des suggestions un peu mieux ciblées. Pour cela, nous allons devoir modifier quelques paramètres de notre noyau.

La première étape est d’identifier les termes qui nous posent problème. Pour cela, manipulez les paramètres mlt.mintf, mlt.mintf et mlt.minwl jusqu’à obtenir un jeu de résultats satisfaisant : ne pas ignorer trop de mots importants, tout en ayant un jeu de résultats convenable. Au bout de quelques essais, vous devriez vous rendre compte que, dans le cas qui nous occupe, deux termes nous empêchent d’avancer : « kate » et « daniel », qui sont le prénom et le nom de l’héroïne de l’une des deux séries de l’index. Ces deux termes figurent dans tous les synopsis de la série, et par conséquent ils causent un conflit avec les recommandations de MoreLikeThis. Nous allons devoir empêcher ces mots de figurer dans l’index, tout du moins pour le champ synopsis.

Ajoutez ceci au fichier stopwords_gen.txt, puis rechargez le noyau et relancez l’indexation de tous les documents :

kate
daniels

Au bout de quelques secondes, lancez de nouveau la requête précédente :

Requête "req-7.1" :

http://localhost:8080/solr/livres/displayTest?
fl=isbn13%20createdAt%20copyrightYear%20nbPages%20nbInSeries%20authorNames%20titleEn%20seriesEn%20synopsisEn%20urlEn%20coverImageEn
&mlt.fl=stSynopsisEn
&mlt.count=5
&mlt.interestingTerms=details
&mlt.mintf=2
&mlt.minwl=3
&mlt.boost=true
&q=isbn13:978-0441014897

Réponse (lire les termes listés dans "interestingTerms") :

<?xml version="1.0" encoding="UTF-8"?>
<response>
<lst name="responseHeader">
<int name="status">0</int>
<int name="QTime">10</int>
<lst name="params">
<str name="mlt.minwl">3</str>
<str name="mlt.boost">true</str>
<str name="mlt.fl">stSynopsisEn</str>
<str name="mlt.mintf">2</str>
<str name="mlt.interestingTerms">details</str>
<str name="mlt.count">5</str>
<str name="fl">isbn13 createdAt copyrightYear nbPages nbInSeries authorNames
titleEn seriesEn synopsisEn urlEn coverImageEn</str>
<str name="q">isbn13:978-0441014897</str>
</lst>
</lst>
<result name="match" numFound="1" start="0">
[NDLR] Code trop long pour être inclus intégralement,
voir fichier joint pour le détail
</result>
<result name="response" numFound="6" start="0">
[NDLR] Code trop long pour être inclus intégralement,
voir fichier joint pour le détail
</result>
<lst name="interestingTerms">
<float name="stSynopsisEn:and">1.0</float>
<float name="stSynopsisEn:her">0.89764106</float>
<float name="stSynopsisEn:the">0.7777778</float>
<float name="stSynopsisEn:magic">0.59200746</float>
<float name="stSynopsisEn:but">0.3847033</float>
<float name="stSynopsisEn:she">0.25646886</float>
</lst>
</response>

Nous pouvons voir que les termes « kate » et « daniel », qui figuraient dans presque tous les documents de notre noyau, sont maintenant ignorés par notre application.

En l’occurrence, les termes restants sont surtout des déterminants. Il nous faut donc inclure ces termes au fichier stopwords (réduction du bruit), puis augmenter ou réduire les paramètres du composant afin d’inclure des mots ayant une fréquence plus faible, mais qui sont plus intéressants pour notre application.

VIII-F - solrconfig.xml (request handlers)

Une fois le schéma finalisé et après avoir déterminé avec assurance les grands types de requêtes que nous allons lancer sur notre schéma, nous pouvons les formaliser dans solrconfig.xml, sous forme de request handlers.

Fichier final :

<?xml version="1.0" encoding="UTF-8" ?>
<config>
<abortOnConfigurationError>${config.abortOnConfigurationError}</abortOnConfigurationError>
<dataDir>${config.dataDir}</dataDir>

<!--
<requestDispatcher handleSelect="true">
<requestParsers enableRemoteStreaming="true" multipartUploadLimitInKB="2048" />
</requestDispatcher>
-->

<requestHandler name="standard" class="solr.SearchHandler" default="true">
<lst name="defaults">
<bool name="indent">on</bool>
<str name="echoParams">explicit</str>
<str name="wt">xml</str>
</lst>
</requestHandler>

<requestHandler name="/searchEn" class="solr.SearchHandler">
<arr name="components">
<str>query</str>
<str>facet</str>
<str>highlight</str>
<str>stats</str>
<str>spellCheck</str>
<str>debug</str>
</arr>
<lst name="defaults">
<bool name="facet">${config.searchRH.facet.isActive}</bool>
<bool name="hl">${config.searchRH.hl.isActive}</bool>
<bool name="stats">${config.searchRH.stats.isActive}</bool>
<bool name="spellcheck">${config.searchRH.spellcheck.isActive}</bool>
<str name="echoHandler">${config.searchRH.echoHandler}</str>
<str name="echoParams">${config.searchRH.echoParams}</str>
<bool name="debugQuery">${config.searchRH.debugQuery}</bool>
<str name="df">stAllEn</str>
<str name="defType">edismax</str>
<str name="qf">stTitleEn^5.0 stSeriesEn^4.0 stSynopsisEn stAuthorNamesEn^2.0</str>
<str name="q.alt">*:*</str>
<str name="fl">score,isbn13,copyrightYear,nbPages,nbInSeries,authorNames,titleEn,seriesEn,coverImageEn</str>
<str name="facet.field">copyrightYear</str>
<str name="facet.field">authorNames</str>
<str name="facet.field">seriesEn</str>
<str name="facet.field">editors</str>
<str name="facet.field">originalCoverArtBy</str>
<str name="facet.field">originalCoverDesignBy</str>
<str name="facet.field">originalInteriorTextDesignBy</str>
<int name="facet.mincount">${config.searchRH.facet.mincount}</int>
<str name="facet.sort">${config.searchRH.facet.sort.default}</str>
<str name="f.copyrightYear.facet.sort">${config.searchRH.facet.sort.alternate}</str>
<str name="facet.range">createdAt</str>
<str name="f.createdAt.facet.range.start">2011-01-00T00:00:00.000Z</str>
<str name="f.createdAt.facet.range.end">2012-12-00T00:00:00.000Z</str>
<str name="f.createdAt.facet.range.gap">+1DAY</str>
<str name="facet.range">nbPages</str>
<int name="f.nbPages.facet.range.start">0</int>
<int name="f.nbPages.facet.range.end">3000</int>
<int name="f.nbPages.facet.range.gap">50</int>
<str name="hl.fl">stTitleEn,stSeriesEn,stSynopsisEn,stAuthorNamesEn</str>
<int name="hl.snippets">${config.searchRH.hl.snippets}</int>
<bool name="hl.useFastVectorHighlighter">${config.searchRH.hl.useFastVectorHighlighter}</bool>
<str name="spellcheck.dictionary">stSpellcheckEn</str>
<bool name="spellcheck.extendedResults">${config.searchRH.spellcheck.extendedResults}</bool>
<int name="spellcheck.count">${config.searchRH.spellcheck.count}</int>
<bool name="spellcheck.collate">${config.searchRH.spellcheck.collate}</bool>
<bool name="spellcheck.collateExtendedResults">${config.searchRH.spellcheck.collateExtendedResults}</bool>
<str name="stats.field">copyrightYear</str>
<str name="stats.field">nbPages</str>
</lst>
</requestHandler>

<requestHandler name="/searchFr" class="solr.SearchHandler">
<arr name="components">
<str>query</str>
<str>facet</str>
<str>highlight</str>
<str>stats</str>
<str>spellCheck</str>
<str>debug</str>
</arr>
<lst name="defaults">
<bool name="facet">${config.searchRH.facet.isActive}</bool>
<bool name="hl">${config.searchRH.hl.isActive}</bool>
<bool name="stats">${config.searchRH.stats.isActive}</bool>
<bool name="spellcheck">${config.searchRH.spellcheck.isActive}</bool>
<str name="echoHandler">${config.searchRH.echoHandler}</str>
<str name="echoParams">${config.searchRH.echoParams}</str>
<bool name="debugQuery">${config.searchRH.debugQuery}</bool>
<str name="df">stAllFr</str>
<str name="defType">edismax</str>
<str name="qf">stTitleFr^5.0 stSeriesFr^4.0 stSynopsisFr stAuthorNamesFr^2.0</str>
<str name="q.alt">*:*</str>
<str name="fl">score,isbn13,copyrightYear,nbPages,nbInSeries,authorNames,titleFr,seriesFr,coverImageFr</str>
<str name="facet.field">copyrightYear</str>
<str name="facet.field">authorNames</str>
<str name="facet.field">seriesFr</str>
<str name="facet.field">editors</str>
<str name="facet.field">originalCoverArtBy</str>
<str name="facet.field">originalCoverDesignBy</str>
<str name="facet.field">originalInteriorTextDesignBy</str>
<int name="facet.mincount">${config.searchRH.facet.mincount}</int>
<str name="facet.sort">${config.searchRH.facet.sort.default}</str>
<str name="f.copyrightYear.facet.sort">${config.searchRH.facet.sort.alternate}</str>
<str name="facet.range">createdAt</str>
<str name="f.createdAt.facet.range.start">2011-01-00T00:00:00.000Z</str>
<str name="f.createdAt.facet.range.end">2012-12-00T00:00:00.000Z</str>
<str name="f.createdAt.facet.range.gap">+1DAY</str>
<str name="facet.range">nbPages</str>
<int name="f.nbPages.facet.range.start">0</int>
<int name="f.nbPages.facet.range.end">3000</int>
<int name="f.nbPages.facet.range.gap">50</int>
<str name="hl.fl">stTitleFr,stSeriesFr,stSynopsisFr,stAuthorNamesFr</str>
<int name="hl.snippets">${config.searchRH.hl.snippets}</int>
<bool name="hl.useFastVectorHighlighter">${config.searchRH.hl.useFastVectorHighlighter}</bool>
<str name="spellcheck.dictionary">stSpellcheckFr</str>
<bool name="spellcheck.extendedResults">${config.searchRH.spellcheck.extendedResults}</bool>
<int name="spellcheck.count">${config.searchRH.spellcheck.count}</int>
<bool name="spellcheck.collate">${config.searchRH.spellcheck.collate}</bool>
<bool name="spellcheck.collateExtendedResults">${config.searchRH.spellcheck.collateExtendedResults}</bool>
<str name="stats.field">copyrightYear</str>
<str name="stats.field">nbPages</str>
</lst>
</requestHandler>

<requestHandler name="/displayEn" class="solr.MoreLikeThisHandler">
<arr name="components">
<str>query</str>
</arr>
<lst name="defaults">
<str name="echoHandler">${config.displayRH.echoHandler}</str>
<str name="echoParams">${config.displayRH.echoParams}</str>
<str name="fl">score,isbn13%20createdAt%20copyrightYear%20nbPages%20nbInSeries%20authorNames%20titleEn%20seriesEn%20synopsisEn%20urlEn%20coverImageEn</str>
<int name="mlt.mintf">${config.displayRH.mlt.mintf}</int>
<int name="mlt.mindf">${config.displayRH.mlt.mindf}</int>
<int name="mlt.minwl">${config.displayRH.mlt.minwl}</int>
<int name="mlt.count">${config.displayRH.mlt.count}</int>
<str name="mlt.fl">stSynopsisEn</str>
<str name="mlt.qf">stSynopsisEn</str>
</lst>
<lst name="invariants">
<str name="df">${config.displayRH.df}</str>
<int name="rows">${config.displayRH.rows}</int>
<bool name="mlt.boost">${config.displayRH.mlt.boost}</bool>
<str name="mlt.interestingTerms">${config.displayRH.mlt.interestingTerms}</str>
<bool name="mlt.match.include">${config.displayRH.mlt.match.include}</bool>
<int name="mlt.match.offset">${config.displayRH.mlt.match.offset}</int>
</lst>
</requestHandler>

<requestHandler name="/displayTest" class="solr.MoreLikeThisHandler">
<arr name="components">
<str>query</str>
</arr>
<lst name="defaults">
<bool name="indent">on</bool>
<str name="echoParams">explicit</str>
<str name="wt">xml</str>
</lst>
</requestHandler>

<requestHandler name="/displayFr" class="solr.MoreLikeThisHandler">
<arr name="components">
<str>query</str>
</arr>
<lst name="defaults">
<str name="echoHandler">${config.displayRH.echoHandler}</str>
<str name="echoParams">${config.displayRH.echoParams}</str>
<str name="fl">score,isbn13,createdAt,copyrightYear,nbPages,nbInSeries,authorNames,titleFr,seriesFr,synopsisFr,urlFr,coverImageFr</str>
<int name="mlt.mintf">${config.displayRH.mlt.mintf}</int>
<int name="mlt.mindf">${config.displayRH.mlt.mindf}</int>
<int name="mlt.minwl">${config.displayRH.mlt.minwl}</int>
<int name="mlt.count">${config.displayRH.mlt.count}</int>
<str name="mlt.fl">stSynopsisFr</str>
<str name="mlt.qf">stSynopsisFr</str>
</lst>
<lst name="invariants">
<str name="df">${config.displayRH.df}</str>
<int name="rows">${config.displayRH.rows}</int>
<bool name="mlt.boost">${config.displayRH.mlt.boost}</bool>
<str name="mlt.interestingTerms">${config.displayRH.mlt.interestingTerms}</str>
<bool name="mlt.match.include">${config.displayRH.mlt.match.include}</bool>
<int name="mlt.match.offset">${config.displayRH.mlt.match.offset}</int>
</lst>
</requestHandler>

<requestHandler name="/update" class="solr.XmlUpdateRequestHandler" />
<requestHandler name="/admin" class="solr.admin.AdminHandlers" />

<requestHandler name="/admin/ping" class="solr.PingRequestHandler">
<lst name="defaults">
<str name="qt">standard</str>
<str name="q">*:*</str>
<str name="echoParams">none</str>
<bool name="echoHandler">false</bool>
</lst>
</requestHandler>

<searchComponent name="termVector" class="solr.TermVectorComponent"/>

<searchComponent name="spellCheck" class="solr.SpellCheckComponent">
<lst name="spellchecker">
<str name="name">grSpellcheckEn</str>
<str name="field">grAllEn</str>
<str name="queryAnalyzerFieldType">grammedText</str>
<str name="spellcheckIndexDir">./grSpellcheckEn</str>
<str name="buildOnOptimize">${config.spellCheck.buildOnOptimize}</str>
</lst>
<lst name="spellchecker">
<str name="name">grSpellcheckFr</str>
<str name="field">grAllFr</str>
<str name="queryAnalyzerFieldType">grammedText</str>
<str name="spellcheckIndexDir">./grSpellcheckFr</str>
<str name="buildOnOptimize">${config.spellCheck.buildOnOptimize}</str>
</lst>
<lst name="spellchecker">
<str name="name">stSpellcheckEn</str>
<str name="field">stAllEn</str>
<str name="queryAnalyzerFieldType">stemmedEnglishText</str>
<str name="spellcheckIndexDir">./stSpellcheckEn</str>
<str name="buildOnOptimize">${config.spellCheck.buildOnOptimize}</str>
</lst>
<lst name="spellchecker">
<str name="name">stSpellcheckFr</str>
<str name="field">stAllFr</str>
<str name="queryAnalyzerFieldType">stemmedFrenchText</str>
<str name="spellcheckIndexDir">./stSpellcheckFr</str>
<str name="buildOnOptimize">${config.spellCheck.buildOnOptimize}</str>
</lst>
</searchComponent>
</config>

IX - Conclusion générale

Solr évolue très rapidement. J’encourage le lecteur à s’inscrire aux listes de diffusion pour se tenir au courant des évolutions, pour se renseigner sur les pratiques communes ou pour poser une question.

Le projet Solr utilise Subversion et Jira pour répertorier ses bugs et les demandes de fonctionnalités. Nous pouvons également y lire quels bugs/fonctionnalités sont prévus pour chaque release, même future.

(1)Ce tableau est tiré du Wiki de Solr : Field Options By Use Case

(2)Le fichier mapping-ISOLatin1Accent.txt est fourni avec Solr

(3)Le fichier stopwords_en.txt est fourni avec Solr sous le nom stopwords.txt

(4)Le fichier stopwords_fr.txt est tiré du site de Snowball

(5)Le fichier wdftypes_en.txt est tiré du repository SVN de Solr

(6)À titre d’exemple, le repository SVN de Solr propose des documents dans différents formats