Recherche de texte intégral dans Rails avec Elasticsearch

Dans cet article, je vais vous montrer comment implémenter la recherche de texte intégral à l'aide de Ruby on Rails et Elasticsearch. De nos jours, tout le monde est habitué à saisir un terme de recherche et à obtenir des suggestions ainsi que des résultats avec le terme de recherche mis en évidence. Si vous essayez de mal orthographier ce que vous essayez de rechercher, la correction automatique est également une fonctionnalité intéressante, comme nous pouvons le constater sur des sites tels que Google ou Facebook.. 

Implémenter toutes ces fonctionnalités en utilisant uniquement une base de données relationnelle telle que MySQL ou Postgres n'est pas simple. Pour cette raison, nous utilisons Elasticsearch, que vous pouvez considérer comme une base de données spécifiquement conçue et optimisée pour la recherche. C'est open source et il est construit sur Apache Lucene. 

Une des fonctionnalités les plus intéressantes d’Elasticsearch est qu’elle expose ses fonctionnalités à l’aide de l’API REST. Il existe donc des bibliothèques qui encapsulent ces fonctionnalités pour la plupart des langages de programmation..

Présentation de Elasticsearch

Un peu plus tôt, j'ai mentionné qu'Elasticsearch est comme une base de données pour la recherche. Il serait utile si vous connaissez la terminologie qui l’entoure..

  • Champ: Un champ est comme une paire clé-valeur. La valeur peut être une valeur simple (chaîne, entier, date) ou une structure imbriquée telle qu'un tableau ou un objet. Un champ est similaire à une colonne dans une table dans une base de données relationnelle.
  • Document: Un document est une liste de champs. C'est un document JSON qui est stocké dans Elasticsearch. C'est comme une ligne dans une table dans une base de données relationnelle. Chaque document est stocké dans un index et possède un type et un identifiant unique.  
  • Type: Un type est comme une table dans une base de données relationnelle. Chaque type contient une liste de champs pouvant être spécifiés pour des documents de ce type..
  • Indice: Un index est l'équivalent d'une base de données relationnelle. Il contient la définition de plusieurs types et stocke plusieurs documents..

Une chose à noter ici est que dans Elasticsearch, lorsque vous écrivez un document dans un index, les champs de document sont analysés mot par mot afin de rendre la recherche facile et rapide. Elasticsearch prend également en charge la géolocalisation, vous permettant ainsi de rechercher des documents situés à une certaine distance d'un lieu donné. C’est exactement comme cela que Foursquare implémente la recherche.

Je voudrais mentionner qu'Elasticsearch a été conçu avec une grande évolutivité. Il est donc très facile de créer un cluster avec plusieurs serveurs et d'avoir une haute disponibilité, même si certains serveurs tombent en panne. Je ne vais pas aborder les détails de la planification et du déploiement de différents types de clusters dans cet article..

Installer Elasticsearch

Si vous utilisez Linux, vous pouvez éventuellement installer Elasticsearch à partir de l'un des référentiels. Il est disponible dans APT et YUM.

Si vous utilisez Mac, vous pouvez l'installer avec Homebrew: brasser installer elasticsearch. Une fois que elasticsearch est installé, vous verrez la liste des dossiers pertinents de votre terminal:

Pour vérifier que l'installation fonctionne, tapez elasticsearch dans votre terminal pour le démarrer. Puis courir curl localhost: 9200 dans votre terminal, et vous devriez voir quelque chose comme:

Installer Elastic HQ

Elastic HQ est un plugin de surveillance que nous pouvons utiliser pour gérer Elasticsearch à partir du navigateur, similaire à phpMyAdmin pour MySQL. Pour l'installer, lancez simplement dans votre terminal:

/usr/local/Cellar/elasticsearch/2.2.0_1/libexec/bin/plugin -install royrusso / elasticsearch-HQ

Une fois installé, accédez à http: // localhost: 9200 / _plugin / hq dans votre navigateur:

Cliquer sur Relier et vous verrez un écran montrant l'état du cluster:

Comme vous vous en doutez bien, à l'heure actuelle, aucun index ni document n'est créé, mais notre instance locale d'Elasticsearch est installée et en cours d'exécution..

Création d'une application Rails

Je vais créer une application Rails très simple, dans laquelle vous pouvez ajouter des articles à la base de données afin que nous puissions effectuer une recherche en texte intégral sur eux à l'aide d'Elasticsearch. Commencez par créer une nouvelle application Rails:

rails nouveaux elasticsearch-rails

Ensuite, nous générons une nouvelle ressource Article avec un échafaudage:

rails générer échafaudage Titre de l'article: chaîne de texte: texte

Nous devons maintenant ajouter une nouvelle route racine pour pouvoir voir par défaut la liste des articles. modifier config / routes.rb:

Rails.application.routes.draw fait pour root: ressources 'articles # index': articles end 

Créer la base de données en exécutant la commande rake db: migrer. Si vous commencez serveur de rails, ouvrez votre navigateur, accédez à localhost: 3000 et ajoutez quelques articles à la base de données, ou téléchargez simplement le fichier db / seeds.rb avec les données factices que j'ai créées pour vous éviter de passer beaucoup de temps à remplir des formulaires..

Ajout de recherche

Maintenant que nous avons notre petite application Rails avec des articles dans la base de données, nous sommes prêts à ajouter notre fonctionnalité de recherche. Nous allons commencer par ajouter la référence aux deux gemmes Elasticsearch officielles:

gem 'elasticsearch-model' gem 'elasticsearch-rails'

Sur de nombreux sites Web, il est très courant d’avoir une zone de texte à rechercher dans le menu supérieur de toutes les pages. Pour cette raison, je vais créer un formulaire partiel sur app / views / search / _form.html.erb.Comme vous pouvez le constater, j'envoie le formulaire à l'aide de GET. Il est donc facile de copier et coller l'URL d'une recherche spécifique..

<%= form_for :term, url: search_path, method: :get do |form| %> 

<%= text_field_tag :term, params[:term] %> <%= submit_tag "Search", name: nil %>

<% end %>

Ajoutez une référence au formulaire dans la mise en page principale du site Web. modifier app / views / layouts / application.html.erb.

 <%= render 'search/form' %> <%= yield %> 

Maintenant, nous avons également besoin d’un contrôleur pour effectuer la recherche proprement dite et afficher les résultats. Nous le générons donc en exécutant la commande. rails g nouveau contrôleur.

classe SearchController < ApplicationController def search if params[:term].nil? @articles = [] else @articles = Article.search params[:term] end end end 

Comme vous pouvez le voir, j'appelle la méthode chercher sur le modèle Article. Nous n’avons pas encore défini cela, donc si nous essayons d’effectuer une recherche à ce stade, nous obtiendrons une erreur. De plus, nous n’avons pas ajouté de route pour le SearchController sur le config / routes.rb fichier, alors faisons-le:

Rails.application.routes.draw root vers: ressources "articles # index": les articles sont "recherche", vers: "recherche # recherche" end

Si nous regardons la documentation de la gemme 'elasticsearch-rails',  nous devons inclure deux modules sur les modèles que nous voulons indexer dans Elasticsearch, dans notre cas Article.rb.

nécessite la classe 'elasticsearch / model' Article < ActiveRecord::Base include Elasticsearch::Model include Elasticsearch::Model::Callbacks end

Le premier modèle introduit entre autres la méthode de recherche que nous utilisions dans notre contrôleur précédent. Le second module s'intègre aux rappels ActiveRecord pour indexer chaque instance d'un article que nous enregistrons dans la base de données et met également à jour l'index si nous modifions ou supprimons l'article de la base de données. Donc tout est transparent pour nous.

Si vous avez précédemment importé les données dans la base de données, ces articles ne figurent toujours pas dans l'index Elasticsearch. seuls les nouveaux sont indexés automatiquement. Pour cette raison, nous devons les indexer manuellement, et c'est facile si nous commençons console de rails. Ensuite, nous devons seulement courir irb (principal)> Article.import.

Nous sommes maintenant prêts à essayer la fonctionnalité de recherche. Si je tape 'ruby' et que je clique sur recherche, voici les résultats:

Recherche en surbrillance

Sur de nombreux sites Web, vous pouvez voir sur la page des résultats de la recherche comment le terme que vous avez recherché est mis en évidence. C'est très facile à faire avec Elasticsearch..

modifier app / models / article.rb et modifiez la méthode de recherche par défaut:

def self.search (requête) __elasticsearch __. search (requête: multi_match: requête: requête, champs: ['titre', 'texte'], mettez en surbrillance: pre_tags: [''], post_tags: [''], champs: titre: , texte: ) fin

Par défaut, le chercher La méthode est définie par la gem 'elasticsearch-models', et l'objet proxy __elasticsearch__ est fourni pour accéder à la classe d'enveloppe de l'API Elasticsearch. Nous pouvons donc modifier la requête par défaut en utilisant les options JSON standard fournies par la documentation.. 

Désormais, la méthode de recherche encapsulera les résultats correspondant à la requête avec les balises HTML spécifiées. Pour cette raison, nous devons également mettre à jour la page de résultats de la recherche afin de pouvoir restituer les balises HTML en toute sécurité. Pour ce faire, éditez app / views / search / search.html.erb.

Résultats de la recherche

<% if @articles %>
    <% @articles.each do |article| %>
  • <%= link_to article.try(:highlight).try(:title) ? article.highlight.title[0].html_safe : article.title, controller: "articles", action: "show", id: article._id %>

    <% if article.try(:highlight).try(:text) %> <% article.highlight.text.each do |snippet| %>

    <%= snippet.html_safe %>…

    <% end %> <% end %>
  • <% end %>
<% else %>

Votre recherche ne correspond à aucun document.

<% end %>

Ajouter un style CSS à app / assets / stylesheets / search.scss, pour la balise en surbrillance:

.search_results em background-color: yellow; style de police: normal; poids de police: gras; 

Essayez de rechercher à nouveau 'ruby':

Comme vous pouvez le constater, il est facile de mettre le terme de recherche en surbrillance, mais ce n’est pas idéal, car nous devons envoyer une requête JSON comme spécifié dans la documentation Elasticsearch. Nous n’avons aucune sorte d’abstraction..

Searchkick Gem

Searchkick est fourni par Instacart et constitue une abstraction au-dessus des pierres précieuses officielles d'Elasticsearch. Je vais refactoriser la fonctionnalité de surbrillance, nous allons donc commencer par ajouter bijou 'chercheur' au gemfile. La première classe que nous devons changer est le modèle Article.rb:

article de classe < ActiveRecord::Base searchkick end

Comme vous pouvez le constater, c'est beaucoup plus simple. Nous devons réindexer les articles à nouveau et exécuter la commande rake searchkick: reindex CLASS = Article. Pour mettre en évidence le terme recherché, nous devons transmettre un paramètre supplémentaire à la méthode de recherche de notre search_controller.rb.

classe SearchController < ApplicationController def search if params[:term].nil? @articles = [] else term = params[:term] @articles = Article.search term, fields: [:text], highlight: true end end end

Le dernier fichier que nous devons modifier est views / search / search.html.erb comme les résultats sont retournés dans un format différent par searchkick maintenant:

Résultats de recherche pour: <%= params[:term] %>

<% if @articles %>
    <% @articles.with_details.each do |article, details| %>
  • <%= link_to article.title, controller: "articles", action: "show", id: article.id %>

    <%= details[:highlight][:text].html_safe %>…

  • <% end %>
<% else %>

Votre recherche ne correspond à aucun document.

<% end %>

Il est maintenant temps de relancer l'application et de tester la fonctionnalité de recherche:

Notez que j'ai entré comme terme de recherche 'dato'. Je l'ai fait exprès pour vous montrer que par défaut searchkickest mis en place pour analyser le texte indexé et être plus permissif avec les fautes d'orthographe.

Autosuggest

Autosuggest ou typeahead prédit ce que l'utilisateur va taper, rendant l'expérience de recherche plus rapide et plus facile. N'oubliez pas que, sauf si vous avez des milliers d'enregistrements, il peut être préférable de filtrer du côté client..

Commençons par ajouter le plugin typeahead, disponible via le gem 'bootstrap-typeahead-rails', et l'ajouter à votre Gemfile. Ensuite, nous devons ajouter du JavaScript à app / assets / javascripts / application.js de sorte que lorsque vous commencez à taper dans le champ de recherche, des suggestions apparaissent.

// = nécessite jquery // = nécessite jquery_ujs // = nécessite turbolinks // = requiert bootstrap-typeahead-rails // = require_tree. var ready = function () var engine = new Bloodhound (datumTokenizer: function (d) console.log (d); renvoie Bloodhound.tokenizers.whitespace (d.title);, queryTokenizer: Bloodhound.tokenizers.whitespace, remote: url: '… / search / typeahead /% QUERY'); var promise = engine.initialize (); promis .done (function () console.log ('succès');) .fail (function () console.log ('erreur')); $ ("# terme"). typeahead (null, nom: "article", displayKey: "titre", source: engine.ttAdapter ()); $ (document) .ready (prêt); $ (document) .on ('page: load', prêt);

Quelques commentaires sur l'extrait précédent. Dans les deux dernières lignes, comme je n’ai pas désactivé les liens turboliens, c’est le moyen de brancher le code que je veux exécuter lors du chargement de la page. Sur la première partie du script, vous pouvez voir que j'utilise Bloodhound. C'est le moteur de suggestion de typeahead.js, et je configure également le point de terminaison JSON pour que les demandes AJAX puissent recevoir les suggestions. Après cela, j'appelle initialiser() sur le moteur, et j'ai configuré typeahead sur le champ de recherche en utilisant son identifiant "terme".

Maintenant, nous devons faire l’implémentation d’arrière-plan pour les suggestions, commençons par ajouter la route, éditer app / config / routes.rb.

Rails.application.routes.draw root sur: "articles # index" ressources: les articles sont "recherche", vers: "recherche # recherche" get 'recherche / typeahead /: term' => 'recherche # typeahead' end

Ensuite, je vais ajouter la mise en œuvre sur app / controllers / search_controller.rb.

def typeahead render json: Article.search (params [: terme], champs: ["titre"], limite: 10, chargement: faux, fautes d'orthographe: ci-dessous: 5,). map do | article | title: article.title, valeur: article.id end end

Cette méthode renvoie les résultats de la recherche pour le terme entré à l'aide de JSON. Je cherche uniquement par titre, mais je pourrais aussi spécifier le corps de l'article. Je limite également le nombre de résultats de recherche à 10 maximum.

Nous sommes maintenant prêts à essayer l'implémentation typeahead:

Conclusion

Comme vous pouvez le constater, l'utilisation d'Elasticsearch avec Rails rend la recherche de nos données extrêmement simple et rapide. Ici, je vous ai montré comment utiliser les gemmes de bas niveau fournies par Elasticsearch, ainsi que le gemme Searchkick, une abstraction qui cache certains détails du fonctionnement d’Elasticsearch.. 

En fonction de vos besoins spécifiques, vous pouvez utiliser volontiers Searchkick et mettre en œuvre votre recherche en texte intégral rapidement et facilement. D'autre part, si vous avez d'autres requêtes complexes comprenant des filtres ou des groupes, vous devrez peut-être en savoir plus sur les détails du langage de requête sur Elasticsearch et finir par utiliser les gems de niveau inférieur 'elasticsearch-models' et 'elasticsearch- des rails'.