Zoeken in volledige tekst in rails met Elasticsearch

In dit artikel laat ik je zien hoe je full-text search kunt implementeren met Ruby on Rails en Elasticsearch. Iedereen wordt tegenwoordig gebruikt om een ​​zoekterm in te voeren en suggesties en resultaten te krijgen met de zoekterm gemarkeerd. Als je verkeerd speldt wat je probeert te zoeken, is automatisch corrigeren ook een leuke functie, zoals we kunnen zien op websites zoals Google of Facebook. 

Het implementeren van al deze functies met alleen een relationele database zoals MySQL of Postgres is niet eenvoudig. Om deze reden gebruiken we Elasticsearch, dat u kunt beschouwen als een database die specifiek is gebouwd en geoptimaliseerd voor zoeken. Het is open source en het is gebouwd op de top van Apache Lucene. 

Een van de leukste functies van Elasticsearch is dat de functionaliteit ervan wordt blootgelegd met behulp van de REST API, dus er zijn bibliotheken die die functionaliteit verpakken voor de meeste programmeertalen..

Introductie van Elasticsearch

Eerder vermeldde ik dat Elasticsearch als een database voor zoeken is. Het zou handig zijn als u bekend bent met een deel van de terminologie eromheen.

  • Veld: Een veld is als een sleutel / waarde-paar. De waarde kan een eenvoudige waarde (tekenreeks, geheel getal, datum) of een geneste structuur zoals een array of een object zijn. Een veld is vergelijkbaar met een kolom in een tabel in een relationele database.
  • Document: Een document is een lijst met velden. Het is een JSON-document dat is opgeslagen in Elasticsearch. Het is als een rij in een tabel in een relationele database. Elk document wordt opgeslagen in een index en heeft een type en een uniek ID.  
  • Type: Een type is als een tabel in een relationele database. Elk type heeft een lijst met velden die kunnen worden gespecificeerd voor documenten van dat type.
  • Inhoudsopgave: Een index is het equivalent van een relationele database. Het bevat de definitie voor meerdere typen en slaat meerdere documenten op.

Een ding om op te merken is dat in Elasticsearch, wanneer u een document naar een index schrijft, de documentvelden worden geanalyseerd, woord voor woord, om zoeken gemakkelijk en snel te maken. Elasticsearch ondersteunt ook geolocatie, zodat u documenten kunt zoeken die zich op een bepaalde afstand van een bepaalde locatie bevinden. Dat is precies hoe Foursquare het zoeken implementeert.

Ik wil graag vermelden dat Elasticsearch is gebouwd met hoge schaalbaarheid in het achterhoofd, dus het is heel eenvoudig om een ​​cluster met meerdere servers te bouwen en een hoge beschikbaarheid te hebben, zelfs als sommige servers uitvallen. Ik ga niet in op de details over het plannen en implementeren van verschillende soorten clusters in dit artikel.

Elasticsearch installeren

Als u Linux gebruikt, kunt u mogelijk Elasticsearch installeren vanuit een van de archieven. Het is beschikbaar in APT en YUM.

Als je Mac gebruikt, kun je het installeren met Homebrew: zet elastiekzoeker op brouwsel. Nadat elasticsearch is geïnstalleerd, ziet u de lijst met relevante mappen in uw terminal:

Om te controleren of de installatie werkt, typt u elasticsearch in uw terminal om het te starten. Ren dan krul localhost: 9200 in uw terminal, en u zou iets als moeten zien:

Installeer elastisch hoofdkantoor

Elastic HQ is een monitoring-plug-in die we kunnen gebruiken om Elasticsearch vanuit de browser te beheren, vergelijkbaar met phpMyAdmin voor MySQL. Om het te installeren, voer je gewoon in je terminal:

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

Zodra het is geïnstalleerd, navigeer je naar http: // localhost: 9200 / _plugin / hq in je browser:

Klik op Aansluiten en je ziet een scherm met de status van het cluster:

Op dit moment zijn er, zoals je zou verwachten, nog geen indexen of documenten gemaakt, maar we hebben ons lokale exemplaar van Elasticsearch geïnstalleerd en actief.

Een Rails-applicatie maken

Ik ga een heel eenvoudige Rails-applicatie maken, waar je artikelen aan de database kunt toevoegen, zodat we er met behulp van Elasticsearch een full-text-zoekopdracht op kunnen uitvoeren. Begin met het maken van een nieuwe Rails-applicatie:

rails nieuwe elastische zoekrails

Vervolgens genereren we een nieuwe artikelbron met steigers:

rails genereren steiger Artikel titel: string tekst: tekst

Nu moeten we een nieuwe rootroute toevoegen, zodat we standaard de lijst met artikelen kunnen zien. Bewerk config / routes.rb:

Rails.application.routes.draw do root to: 'articles # index' resources: articles end 

Maak de database door de opdracht uit te voeren rake db: migreren. Als je begint rails server, open je browser, navigeer naar localhost: 3000 en voeg een paar artikelen toe aan de database, of download gewoon het bestand db / seeds.rb met dummy data die ik heb aangemaakt, zodat je niet veel tijd hoeft te besteden aan het invullen van formulieren.

Zoekopdracht toevoegen

Nu we onze kleine Rails-app hebben met artikelen in de database, zijn we klaar om onze zoekfunctionaliteit toe te voegen. We beginnen met het toevoegen van de referentie aan beide officiële Elasticsearch Gems:

edel 'elastisch zoekwoord-model' edelsteen 'elastisch zoeker-rails'

Op veel websites is het heel gebruikelijk om op alle pagina's een tekstvak te hebben voor zoeken in het bovenste menu. Om die reden ga ik een formulier gedeeltelijk aanmaken app / views / search / _form.html.erb.Zoals u ziet, verzend ik het formulier met GET, dus het is gemakkelijk om de URL voor een specifieke zoekopdracht te kopiëren en plakken.

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

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

<% end %>

Voeg een verwijzing naar het formulier toe aan de lay-out van de hoofdwebsite. Bewerk app / views / layouts / application.html.erb.

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

Nu hebben we ook een controller nodig om de daadwerkelijke zoekopdracht uit te voeren en de resultaten weer te geven, dus we genereren deze door de opdracht uit te voeren rails g nieuwe controller Zoeken.

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

Zoals je kunt zien, bel ik de methode zoeken op het artikelmodel. We hebben dit nog niet gedefinieerd, dus als we op dit punt een zoekopdracht proberen uit te voeren, krijgen we een foutmelding. We hebben ook geen route toegevoegd voor de SearchController op de config / routes.rb bestand, dus laten we dit doen:

Rails.application.routes.draw do root to: 'articles # index' middelen: artikelen krijgen "search", naar: "search # search" end

Als we kijken naar de documentatie voor de edelsteen 'Elasticsearch-rails',  we moeten twee modules opnemen over de modellen die we willen laten indexeren in Elasticsearch, in ons geval Article.rb.

vereisen 'elasticsearch / model' class Article < ActiveRecord::Base include Elasticsearch::Model include Elasticsearch::Model::Callbacks end

Het eerste model injecteert de zoekmethode die we onder meer in onze vorige controller gebruikten. De tweede module integreert met ActiveRecord-callbacks om elk exemplaar van een artikel dat we opslaan in de database te indexeren en het werkt ook de index bij als we het artikel uit de database wijzigen of verwijderen. Dus het is allemaal transparant voor ons.

Als u de gegevens eerder in de database hebt geïmporteerd, staan ​​die artikelen nog steeds niet in de Elasticsearch-index; alleen de nieuwe worden automatisch geïndexeerd. Om deze reden moeten we ze handmatig indexeren, en het is gemakkelijk als we beginnen rails console. Dan hoeven we alleen maar te rennen irb (hoofd)> Article.import.

Nu zijn we klaar om de zoekfunctionaliteit te proberen. Als ik 'ruby' typ en klik op zoeken, zijn hier de resultaten:

Zoeken Markeren

Op veel websites kunt u op de pagina met zoekresultaten zien hoe de term waarnaar u zocht, is gemarkeerd. Dit is heel eenvoudig te doen met Elasticsearch.

Bewerk app / modellen / article.rb en wijzig de standaard zoekmethode:

def self.search (query) __elasticsearch __. search (query: multi_match: query: query, velden: ['title', 'text'], highlight: pre_tags: [''], post_tags: [''], velden: title: , text: ) eindigen

Standaard is de zoeken methode wordt gedefinieerd door de edel 'elasticsearch-models', en het proxy-object __elasticsearch__ wordt verstrekt om toegang te krijgen tot de wrapper-klasse voor de Elasticsearch-API. Dus we kunnen de standaardquery aanpassen met behulp van de standaard JSON-opties zoals geleverd door de documentatie. 

De zoekmethode verpakt nu de resultaten die overeenkomen met de query met de opgegeven HTML-tags. Daarom moeten we ook de pagina met zoekresultaten bijwerken zodat we HTML-tags veilig kunnen renderen. Om dit te doen, bewerk app / views / search / search.html.erb.

Zoekresultaten

<% 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 %>

Uw zoekopdracht kwam niet overeen met documenten.

<% end %>

Voeg een CSS-stijl toe aan app / assets / stylesheets / search.scss, voor de gemarkeerde tag:

.search_results em background-color: yellow; lettertype: normaal; lettertype: vet; 

Probeer opnieuw naar 'ruby' te zoeken:

Zoals u kunt zien, is het gemakkelijk om de zoekterm te markeren, maar niet ideaal, omdat we een JSON-query moeten verzenden zoals gespecificeerd door de Elasticsearch-documentatie, en we hebben geen enkele vorm van abstractie.

Searchkick Gem

Searchkick-juweel wordt geleverd door Instacart en het is een abstractie bovenop de officiële Elasticsearch-edelstenen. Ik ga de highlight-functionaliteit refactoren, dus we beginnen met toevoegen gem 'searchkick' naar het gemfile. De eerste klasse die we moeten wijzigen, is het Article.rb-model:

klasse artikel < ActiveRecord::Base searchkick end

Zoals je kunt zien, is het veel eenvoudiger. We moeten de artikelen opnieuw indexeren en het commando uitvoeren hark searchkick: indexeren CLASS = artikel. Om de zoekterm te markeren, moeten we een extra parameter doorgeven aan de zoekmethode van onze search_controller.rb.

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

Het laatste bestand dat we moeten wijzigen is views / search / search.html.erb omdat de resultaten nu door searchkick in een ander formaat worden geretourneerd:

Zoekresultaten voor: <%= 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 %>

Uw zoekopdracht kwam niet overeen met documenten.

<% end %>

Nu is het tijd om de applicatie opnieuw uit te voeren en de zoekfunctionaliteit te testen:

Merk op dat ik als zoekterm 'dato' heb ingevoerd. Ik heb dit expres gedaan om je dat standaard te laten zienis opgezet om de geïndexeerde tekst te analyseren en meer tolerant te zijn met spelfouten.

autosuggest

Autosuggest of typeahead voorspelt wat een gebruiker zal typen, waardoor de zoekervaring sneller en gemakkelijker wordt. Houd er rekening mee dat, tenzij u duizenden records hebt, het het beste is om aan de clientzijde te filteren.

Laten we beginnen met het toevoegen van de typeahead-plug-in, die beschikbaar is via de gem 'bootstrap-typeahead-rails', en voeg het toe aan je Gemfile. Vervolgens moeten we wat JavaScript toevoegen app / assets / javascripts / application.js zodat wanneer u begint te typen in het zoekvak, er enkele suggesties verschijnen.

// = vereisen jQuery // = vereisen jquery_ujs // = vereisen turbolinks // = vereisen bootstrap-typeahead-rails // = require_tree. var ready = function () var engine = new Bloodhound (datumTokenizer: function (d) console.log (d); return Bloodhound.tokenizers.whitespace (d.title);, queryTokenizer: Bloodhound.tokenizers.whitespace, remote: url: '... / search / typeahead /% QUERY'); var promise = engine.initialize (); belofte .one (function () console.log ('success');) .fail (function () console.log ('error')); $ ("# term"). typeahead (null, name: "article", displayKey: "title", source: engine.ttAdapter ()); $ (Document) .ready (ready); $ (document) .on ('page: load', klaar);

Een paar opmerkingen over het vorige fragment. In de laatste twee regels, omdat ik turbolinks niet heb uitgeschakeld, is dit de manier om de code aan te sluiten die ik bij het laden van de pagina wil uitvoeren. In het eerste deel van het script kun je zien dat ik Bloodhound gebruik. Het is de suggestie-engine typeahead.js en ik stel ook het JSON-eindpunt in om de AJAX-verzoeken te doen om de suggesties te krijgen. Daarna bel ik initialiseren () op de engine en ik zet typeahead in het veld voor zoektekst in met behulp van de id "term".

Nu moeten we de back-endimplementatie voor de suggesties doen, laten we beginnen met het toevoegen van de route, bewerken app / config / routes.rb.

Rails.application.routes.draw do root to: 'articles # index' resources: artikelen krijgen "search", naar: "search # search" get 'search / typeahead /: term' => 'search # typeahead' end

Vervolgens ga ik de implementatie toevoegen app / controllers / search_controller.rb.

def typeahead render json: Article.search (params [: term], fields: ["title"], limit: 10, load: false, spellingfouten: below: 5,). map do | article | title: article.title, value: article.id end end

Deze methode retourneert de zoekresultaten voor de term die is ingevoerd met JSON. Ik zoek alleen op titel, maar ik kan ook de body van het artikel specificeren. Ik beperk ook het aantal zoekresultaten tot maximaal 10.

Nu zijn we klaar om de typeahead-implementatie uit te proberen:

Conclusie

Zoals u kunt zien, maakt het gebruik van Elasticsearch met Rails het doorzoeken van onze gegevens heel gemakkelijk en zeer snel. Hier heb ik je laten zien hoe je de laagstaande edelstenen gebruikt door Elasticsearch kunt gebruiken, evenals de edelsteen Searchkick, een abstractie die een aantal details verbergt over hoe Elasticsearch werkt. 

Afhankelijk van uw specifieke behoeften, kunt u Searchkick misschien gebruiken en uw zoekopdracht in volledige tekst snel en eenvoudig implementeren. Aan de andere kant, als u een aantal andere complexe query's hebt, waaronder filters of groepen, moet u mogelijk meer informatie krijgen over de details van de querytaal op Elasticsearch en uiteindelijk gebruikmaken van de elastische zoekmachinemodellen van het lagere niveau en elastische zoekopdracht. rails'.