Recherche à facette sous Solr
Aujourd'hui, je vous propose de poursuivre nos articles sur les technologies de moteur de recherche avec un nouveau tutoriel pour Solr. Au menu : comment mettre en place une recherche à facette. Avec pour démarrer un rappel du principe de la recherche à facette, et ensuite, la config de Solr proprement dite.
Facette ? Késako ?
Commençons par un petit rappel historique (ça donne un côté intellectuel à l'article) : dans la brève mais fulgurante histoire du web, deux paradigmes de recherche se sont principalement imposés :
D'abord, la recherche navigationnelle dans une taxonomie ((Admirez au passage le savant emploi de mots techniques qui n'a d'autres utilité que de me donner l'air d'être un expert)) (ex : Dmoz). Le principe est simple : on présente à l'internaute l'intégralité du contenu disponible, trié par catégories, sous-catégories, sous-sous-catégories, etc. et dans lequel il devra naviguer jusqu'à trouver ce qui l'intéresse.
Ce modèle n'est plus qu'occasionnellement utilisé, car une simple recherche de contenu peut parfois se transformer en véritable expédition spéléologique, apte à décourager même les plus téméraires.
Ensuite naquit la recherche directe par requêtes sous formes de mots-clés, qui prit son essort lorsque le Dieu Google démontra qu'il était possible de la mettre en place à l'échelle du web entier. Simple, direct, idéale pour l'internaute pressé qui sait ce qu'il veut.
Chaque méthode présente ses avantages et des inconvénients. La recherche par facette vise à combiner le meilleur des deux mondes. Elle offre à l'utilisateur la possibilité d'affiner une recherche directe en navigant dans une liste de critères contextuels et indépendants.
Un petit dessin vaut mieux qu'un long discours, voici un mini-mockup (réalisé grâce à l'excellent Pencil) qui vous permettra de vous faire une idée d'un coup d'œil.
- «Écran» représente une facette de recherche.
- «Bureautique» est un critère qui permet d'appliquer une contrainte pour une facette particulière.
- Le «15» à côté du critère «500Go» indique combien de résultats correspondent à cette contrainte particulière.
- Le fil d'ariane permet d'afficher les contraintes déjà appliquées, et de les supprimer.
Le principe est simple : une zone de texte pour les requêtes directes, et en fonction des résultats renvoyés, on présentera à l'internaute différents critères, organisés par facettes, qui lui permettront de raffiner sa recherche en appliquant des contraintes sur les résultats.
Bon, je résume un peu, mais il existe de trés bonnes ressources pour se renseigner sur le principe des recherches à facettes.
Maintenant que la théorie est derrière nous, laissons place à la pratique.
Configurer Solr
Pour l'exemple, comme je n'ai pas envie de travailler sur le sempiternel site de e-commerce, nous allons refondre le moteur du site du célèbre (mais pas trés beau) marmiton.org. Notre moteur renverra donc des recettes en guise de résultats.
La première étape est de décider des critères de recherche que nous allons mettre en place. Ne faites pas comme chez Dell, choisissez-les avec parcimonie, sous peine de voir l'expérience utilisateur se dégrader rapidement.
Voici les critères de recherche que nous allons mettre en place :
Critère | Valeurs |
Repas | Petit-déjeuner, déjeuner, diner, goûter, diner |
Plat | Entrée, apéritif, plat principal, dessert, sauce |
Difficulté | Débutant, confirmé, expert |
Budget | 1€-5€, 5€-10€, 10€-15€, 15€+ |
Saison | Printemps, été, automne, hiver |
Ces critères vont définir la manière dont nous allons indexer nos données. Voici la portion intéressante du schema.xml correspondant.
<field> <field name="id" type="string" indexed="true" stored="true" required="true" /> <field name="nom" type="text" indexed="true" stored="true" required="true" /> <field name="repas" type="string" indexed="true" stored="false"/> <field name="plat" type="string" indexed="true" stored="false"/> <field name="difficulte" type="string" indexed="true" stored="false"/> <field name="prix" type="sfloat" indexed="true" stored="false"/> <field name="saison" type="string" indexed="true" stored="false"/> </field>
Vous remarquerez la déplorable optimisation des noms et des types des champs, ainsi que l'excessive simplification de la définition du schéma. Cela dit, on s'en fout, c'est juste pour la démo.
Pour les fainéants, voici également quelques données de tests à inclure directement.
<add> <doc> <field name="id">1</field> <field name="nom">Soufflé aux crevettes</field> <field name="repas">Dîner</field> <field name="plat">Plat principal</field> <field name="difficulte">Expérimenté</field> <field name="prix">5</field> <field name="saison">Automne</field> <field name="created_at_dt">2009-01-02T00:00:00.000Z</field> </doc> <doc> <field name="id">2</field> <field name="nom">Charlottes aux groseilles</field> <field name="repas">Déjeuner</field> <field name="plat">Dessert</field> <field name="difficulte">Débutant</field> <field name="prix">7</field> <field name="saison">Été</field> <field name="created_at_dt">2009-01-05T00:00:00.000Z</field> </doc> <doc> <field name="id">3</field> <field name="nom">Gloubigoulba</field> <field name="repas">Petit-déjeuner</field> <field name="plat">Plat principal</field> <field name="difficulte">Astronomique</field> <field name="prix">23492</field> <field name="saison">Hiver</field> <field name="created_at_dt">2009-02-02T00:00:00.000Z</field> </doc> <doc> <field name="id">4</field> <field name="nom">Soupe à la grimace</field> <field name="repas">Dîner</field> <field name="plat">Entrée</field> <field name="difficulte">Expérimenté</field> <field name="prix">3</field> <field name="saison">Hiver</field> <field name="created_at_dt">2009-06-02T00:00:00.000Z</field> </doc> <doc> <field name="id">5</field> <field name="nom">Salades de bobards</field> <field name="repas">Déjeuner</field> <field name="plat">Entrée</field> <field name="difficulte">Débutant</field> <field name="prix">9</field> <field name="saison">Toutes</field> <field name="created_at_dt">2009-06-18T00:00:00.000Z</field> </doc> <doc> <field name="id">6</field> <field name="nom">Gratin de couleuvres</field> <field name="repas">Dîner</field> <field name="plat">Plat principal</field> <field name="difficulte">Expérimenté</field> <field name="prix">2</field> <field name="saison">Toutes</field> <field name="created_at_dt">2009-09-09T00:00:00.000Z</field> </doc> </add>
Avant toute chose, je vous encourage à tester la bonne indexation des données. C'est bon ? Alors passons à la recherche à facette.
Place aux facettes
Il existe trois types de facettes possibles. Pour vous en convaincre, essayez de rajouter le paramètre facet=true, qui active la recherche par facette.
http://localhost:8983/solr/select?q=*:*&facet=true
Notez l'apparition de nouvelles clés dans l'affichage des résultats.
… <lst name="facet_counts"> <lst name="facet_queries"/> <lst name="facet_fields"/> <lst name="facet_dates"/> </lst>
Nos trois types sont donc :
- Les facettes par champ (field faceting) : Comptent les résultats en partitionnant selon un champ donné ;
- Les facettes par requêtes (query faceting) : Comptent les résultats qui correspondent à certaines requêtes ;
- Les facettes par date (date faceting) : Comptent les résultats en partitionnant selon des intervalles de dates ;
Commençons par le plus simple, et certainement le plus utilisé : les facettes par champ. Rien de plus simple, il vous suffit d'ajouter le nom du champ dans l'url, grâce au paramètre facet.field, et… et c'est tout. Il est bien entendu possible d'ajouter plusieurs paramètres à la fois, pour autant de facettes.
<lst name="facet_counts"> <lst name="facet_queries"/> <lst name="facet_fields"> <lst name="repas"> <int name="Dîner">3</int> <int name="Déjeuner">2</int> <int name="Petit-déjeuner">1</int> </lst> <lst name="saison"> <int name="Hiver">2</int> <int name="Toutes">2</int> <int name="Automne">1</int> <int name="Été">1</int> </lst> <lst name="difficulte"> <int name="Expérimenté">3</int> <int name="Débutant">2</int> <int name="Astronomique">1</int> </lst> </lst> <lst name="facet_dates"/> </lst>
Le résultat affiche maintenant la liste des facettes, avec le nombre de réponses pour chaque paramètre. Vous pensiez que ça allait être compliqué ? Au passage, vous pouvez consulter la liste des paramètres disponibles.
Note : le calcul des facettes s'effectue toujours dans le contexte de la recherche actuelle. Si ma recherche initiale (q=) ne retourne que 3 résultats, seuls ces trois là seront pris en compte.
Les facettes par requêtes
Les facettes par requêtes permettent de spécifier des requêtes (au format classique) plutôt que d'utiliser directement les valeurs des champs. C'est particulièrement utile pour les intervalles (ex : les prix).
Les requêtes sont spécifiées via le champ facet.query. Ajoutez plusieurs requêtes pour plusieurs facettes.
<lst name="facet_counts"> <lst name="facet_queries"> <int name="prix:[* TO 5]">3</int> <int name="prix:[5 TO 10]">3</int> <int name="prix:[10 TO *]">1</int> </lst> <lst name="facet_fields"/> <lst name="facet_dates"/> </lst>
Les facettes par dates
Il serait bien entendu possible d'obtenir des facettes sur des champs dates en utilisant des requêtes, mais pourquoi se compliquer la vie ? Grâce aux facettes par date, il suffit d'indiquer une date de début, une date de fin, et un intervalle.
Utile si je veux connaître le nombre de commentaires postés pour chaque heure de la journée, ou le nombre de recettes postées par mois de l'année.
Les paramètres utiles sont :
- facet.date : indique quel sera le champ pris en compte ;
- facet.date.start : quelle est la date de début de la période prise en compte ;
- facet.date.end : je vous laisse deviner ;
- facet.date.gap : l'invervalle de partitionnememt ;
NOW/YEAR signifie que je démarre mon calcul du début de l'année. La syntaxe est relativement simple, je vous laisse consulter la doc.
Sélectionner un critère
Avec tout ça, vous êtes désormais capables d'effectuer des recherches par facettes sur des requêtes de bases. Il nous reste à voir comment restreindre notre recherche en sélectionnant un critère particulier. Pour ce faire, nous allons utiliser le paramètre **fq** (pour filter query), qui nous permettra d'appliquer un filtre sur les résultats renvoyés par la requête initiale.
Pour l'exemple, reprenons notre requête initiale, avec facettes sur divers champs de la recette.
Admettons que je cherche une idée de recette pour le repas de noël, je vais restreindre ma recherche en n'affichant que les plats réalisables en hiver.
J'applique donc mon filtre (fq:Hiver), et j'en profite pour supprimer la facette sur le champ «saison» qui ne sert plus à rien. Rien ne m'empêche de raffiner ma requête selon plusieurs critères en utilisant plusieurs fois le paramètre fq.
Vous savez tout. Il ne reste plus qu'à gérer tous ces paramètres côté frontend, en affichant le fil d'ariane, les critères et les effets kikoolol qui vont bien.