Autocomplete avec Solr

Voilà la reprise, les premiers froids, les feuilles qui tombent, la nostalgie des vacances, etc. Mais la rentrée est également pour moi l'opportunité de dépoussiérer un peu mon cerveau et ce blog, avec du temps consacré à l'indispensable… Veille techno. Parce que la veille, c'est comme le sport. Quand on ne pratique pas, on s'encrasse vite.

Bref, dans cet article, nous allons utiliser Solr, excellent moteur de recherche dont je vous ai déjà parlé, et implémenter l'autocomplétion de la recherche.

Solr pour l'autocomplétion, ou sortir la Grosse Bertha pour tuer une mite

Les plus attentifs d'entre vous se demanderont sans doute pourquoi utiliser Solr, puissant moteur de recherche full-text en java, pour une bête autocomplétion. Et effectivement, l'outil serait disproportionné si nous n'étions pas dans un des cas suivants :

  1. Vous utilisez déjà Solr pour votre moteur de recherche. Dans ce cas, l'utiliser pour l'autocomplétion ne rajoutera pas une grosse charge ;
  2. Vous souhaitez mettre en place une autocomplétion un peu avancée, avec séparation par facettes, etc ;

Solr, depuis la version 1.4, propose un nouveau module de recherche : Terms Component. Alors que Solr est généralement utilisé pour effectuer des recherches au niveau d'un document, ce module permet d'accéder au infos au niveau des termes présents dans les champs, permettant de répondre à la requête : «Quels sont les termes présent dans tel(s) champ(s), et à combien de document correspondent-ils ?».

Par ailleurs, notez que nous allons créer une autocomplétion, et pas une autosuggestion. Pour bien cerner la différence, je vous recommande la lecture de cet excellent article sur les différents types d'assistance à la recherche.

autocomplete

Pour être précis, nous allons construire ça. Ceux qui lisent via un lecteur rss et qui n'auraient pas l'image, vous n'avez qu'à aller sur le site.

Pour les autres, vous remarquerez un bête champ de recherche tout ce qu'il y a de plus classique. L'autocomplétion, en revanche, est un tantinet évoluée, puisqu'elle s'effectue sur deux champs différents. Vous êtes prêts ? C'est parti !

Choisir les bons outils

À l'heure ou j'écris, la version stable de Solr est la 1.4.1. Elle présente toutefois un bug dans le formatage json des résultats. Nous utiliserons donc Solr dans sa version nightly build.

L'autocomplete sera assuré par jQuery, librairie javascript que l'on ne présente plus, et par son trés bon complément jQuery UI.

Notez l'existance d'une trés complète (mais assez complexe) librairie ajax pour Solr. Elle constitue pratiquement un framework à elle toute seule, et pour le coup, nous la laisserons de côté.

Le code ! Le code !

Commençons par le HTML. Ici, rien que du classique.

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="fr">
  <head>
    <script type="text/javascript" src="./js/jquery-1.4.2.min.js"></script>
    <script type="text/javascript" src="./js/jquery-ui-1.8.5.custom.min.js"></script>
    <script type="text/javascript" src="./js/solr.js"></script>

    <link rel="stylesheet" type="text/css" media="screen" href="./css/ui-lightness/jquery-ui-1.8.5.custom.css" />

  </head>
  <body>
    <form action="" method="get">
      <input type="text" id="search" name="q" />
      <input type="submit" value="Search" />
    </form>

    <div id="result">
      <p>Results go here</p>
    </div>
  </body>
</html>

Un banal champ texte, l'inclusion des scripts nécessaires, et c'est tout. Circulez, ya rien à voir !

La configuration Solr n'est pas trés intéressante non plus. J'ai pris la première install de Solr qui m'est passée sous la main. Vous pouvez vous réferer au schema.xml d'un précédent article.

Le seul fichier vraiment intéressant est le javascript :

$(document).ready(function() {
  $('#search').autocomplete({
    source: function(request, response) {
      $.ajax({
        url: 'http://localhost:8983/solr/terms?terms.fl=name',
        dataType: 'jsonp',
        data: {
          wt: 'json',
          'json.nl': 'arrarr',
          'terms.prefix': request.term,
          'terms.sort': 'index',
          'terms.limit': 5,
          omitHeader: 'true'
        },
        jsonp: 'json.wrf',
        success: function(data) {
          response($.map(data.terms.name, function(item) {
            return {
              label: item[0] + ' (' + item[1] + ')',
              value: item[1]
            };
          }));
        }
      })
    }
  });
});

Quelques explications. Dans l'url utilisée par le widget d'autocomplétion, terms.fl désigne le champ qui sera utilisé pour la requête (ici : «name»).

Notez les options «datatype: jsonp», «wt: json», «json.nl: arrarr», «jsonp: wrf». Elles permettent de faire fonctionner l'appel ajax en cross-domain (via jsonp au lieur de json), et d'obtenir un formatage correct du résultat de la part de Solr.

Enfin, dans le callback «result», nous formattons le résultat en utilisant la fonction $.map de jquery.

Nous avons maintenant un widget de recherche qui réalise une autocomplétion sur le champ «name». C'est pas mal, mais nous souhaitions une autocomplétion à facette sur différents champs.

L'autocomplétion à facette

Pour ce faire, nous allons modifier deux choses. D'abord, il va falloir définir un template de widget spécifique pour afficher le champ recherché. Ensuite, la fonction qui parse le résultat renvoyé par Solr va devoir prendre en compte les différents champs.

$(document).ready(function() {

  // Widget autocomplete spécifique
  // cf. http://jqueryui.com/demos/autocomplete/#categories
  $.widget( "custom.catcomplete", $.ui.autocomplete, {
    _renderMenu: function( ul, items ) {
      var self = this,
        currentCategory = "";
      $.each( items, function( index, item ) {
        if ( item.category != currentCategory ) {
          ul.append( "" + item.category + "" );
          currentCategory = item.category;
        }
        self._renderItem( ul, item );
      });
    }
  });

  $('#search').catcomplete({
    source: function(request, response) {
      $.ajax({
        url: 'http://localhost:8983/solr/terms?terms.fl=name&terms.fl=tag_fr',
        dataType: 'jsonp',
        data: {
          wt: 'json',
          'json.nl': 'arrarr',
          'terms.prefix': request.term,
          'terms.sort': 'index',
          'terms.limit': 5,
          omitHeader: 'true'
        },
        jsonp: 'json.wrf',
        success: function(data) {

          answer = new Array();

          $.each(data.terms, function(facet, terms) {
            answer = answer.concat($.map(terms, function(item) {
              return {
                label: item[0] + ' (' + item[1] + ')',
                value: item[1],
                category: facet
              };
            }));
          });
          response(answer);
        }
      })
    }
  });
});

Notez l'url utilisée dans le widget autocomplete : il y a maintenant deux options «terms.fl», mais il pourrait y en avoir beaucoup plus.

La fonction d'analyse des résultats est un peu plus complexe. Pour chaque champ renvoyé par solr, elle construit un tableau de termes, en y adjoignant la catégorie, c'est à dire le champ courant. Tous les tableaux sont ensuite concaténés, en renvoyés en réponse.

La catégorie est utilisée par le template de rendu du widget, pour être affichée dans le menu d'autocomplétion.

Dans nos styles, il faudra ajouter ceci :

.ui-autocomplete-category {
  font-weight: bold;
  padding: .2em .4em;
  margin: .8em 0 .2em;
  line-height: 1.5;
}

Et voilà ! Un widget d'autocomplétion un poil moins que basique. Partant de cette base, je vous laisse imaginer les améliorations les plus raffinées. Si vous avez des idées de démo, je suis preneur.

-- Edit --

J'ai oublié de mentionner le problème de la sécurité. Bien entendu, c'est une trés mauvaise idée de laisser Solr en accès libre depuis l'extérieur. Donc ou bien vous créez un proxy, ou bien vous créez un requesthandler spécifique en lecture seule. C'est tout.