Fulltekstsøk i skinner ved hjelp av Elasticsearch

I denne artikkelen skal jeg vise deg hvordan du implementerer fulltekstsøk ved hjelp av Ruby on Rails og Elasticsearch. Alle brukes i dag til å skrive inn et søkeord og få forslag samt resultater med søkeordet uthevet. Hvis du mispellerer hva du prøver å søke, har du automatisk korrigering også en fin funksjon, som vi kan se på nettsteder som Google eller Facebook. 

Å implementere alle disse funksjonene ved å bruke bare en relasjonsdatabase som MySQL eller Postgres, er ikke grei. Av denne grunn bruker vi Elasticsearch, som du kan tenke på som en database som er spesifikt bygget og optimalisert for søk. Det er åpen kildekode og det er bygget på toppen av Apache Lucene. 

En av de fineste funksjonene i Elasticsearch er som avslører sin funksjonalitet ved hjelp av REST API, så det finnes biblioteker som pakker inn den funksjonaliteten for de fleste programmeringsspråk.

Vi presenterer Elasticsearch

Tidligere nevnte jeg at Elasticsearch er som en database for søk. Det ville være nyttig hvis du er kjent med noen av terminologiene rundt den.

  • Felt: Et felt er som et nøkkelverdierpar. Verdien kan være en enkel verdi (streng, heltall, dato) eller en nestet struktur som en matrise eller en gjenstand. Et felt er lik en kolonne i et bord i en relasjonsdatabase.
  • Dokument: Et dokument er en liste over felt. Det er et JSON-dokument som er lagret i Elasticsearch. Det er som en rad i et bord i en relasjonsdatabase. Hvert dokument er lagret i en indeks og har en type og et unikt id.  
  • Type: En type er som et bord i en relasjonsdatabase. Hver type har en liste over felt som kan angis for dokumenter av den typen.
  • Hovedsiden: En indeks er ekvivalent til en relasjonsdatabase. Den inneholder definisjonen for flere typer og lagrer flere dokumenter.

En ting å merke seg her er at i Elasticsearch, når du skriver et dokument til en indeks, analyseres dokumentfeltene, ord for ord, for å gjøre søk enkelt og raskt. Elasticsearch støtter også geolocation, slik at du kan søke dokumenter som ligger innenfor en bestemt avstand på et bestemt sted. Det er akkurat slik Foursquare implementerer søk.

Jeg vil gjerne nevne at Elasticsearch ble bygget med høy skalerbarhet i tankene, så det er veldig enkelt å bygge en klynge med flere servere og ha høy tilgjengelighet selv om noen servere går ned. Jeg skal ikke dekke detaljer om hvordan å planlegge og distribuere ulike typer klynger i denne artikkelen.

Installere Elasticsearch

Hvis du bruker Linux, kan du muligens installere Elasticsearch fra en av depotene. Den er tilgjengelig i APT og YUM.

Hvis du bruker Mac, kan du installere den ved hjelp av Homebrew: brygge installere elastisk søk. Etter at elastisk søk ​​er installert, vil du se listen over relevante mapper i terminalen din:

For å verifisere at installasjonen virker, skriv inn elasticsearch i terminalen din for å starte den. Kjør deretter curl localhost: 9200 i terminalen din, og du bør se noe som:

Installer Elastic HQ

Elastic HQ er et overvåkingsplugin som vi kan bruke til å administrere Elasticsearch fra nettleseren, ligner phpMyAdmin for MySQL. For å installere det, bare kjør i terminalen din:

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

Når den er installert, naviger til http: // localhost: 9200 / _plugin / hq i nettleseren din:

Klikk på Koble og du vil se en skjerm som viser statusen til klyngen:

På denne tiden, som du kanskje forventer, opprettes ingen indekser eller dokumenter ennå, men vi har vår lokale forekomst av Elasticsearch installert og kjører.

Opprette et skinnerapplikasjon

Jeg skal lage en veldig enkel Rails-applikasjon, der du kan legge til artikler i databasen, slik at vi kan utføre et fulltekstsøk på dem ved hjelp av Elasticsearch. Begynn med å opprette et nytt Rails-program:

skinner nye elastikkskinner

Deretter genererer vi en ny artikkel ressurs med stillas:

skinner genererer stillas Artikkel tittel: streng tekst: tekst

Nå må vi legge til en ny rotrute, slik at vi som standard kan se listen over artikler. Redigere config / routes.rb:

Rails.application.routes.draw rot til: 'artikler # indeks' ressurser: artikler slutt 

Opprett databasen ved å kjøre kommandoen rake db: migrere. Hvis du starter skinner server, åpne nettleseren din, naviger til localhost: 3000 og legg til noen artikler i databasen, eller bare last ned filen db / seeds.rb med dummy data som jeg har opprettet, slik at du ikke trenger å bruke mye tid på å fylle ut skjemaer.

Legge til søk

Nå som vi har vår lille Rails-app med artikler i databasen, er vi klare til å legge til søkefunksjonen vår. Vi skal begynne med å legge til referansen til begge offisielle Elasticsearch Gems:

perle 'elasticsearch-model' perle 'elasticsearch-rails'

På mange nettsteder er det veldig vanlig å ha en tekstboks for søk i toppmenyen på alle sider. Derfor skal jeg lage et skjema delvis på app / visninger / søk / _form.html.erb.Som du ser, sender jeg skjemaet ved hjelp av GET, så det er enkelt å kopiere og lime inn nettadressen for et bestemt søk.

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

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

<% end %>

Legg til en referanse til skjemaet til hovedwebsidens layout. Redigere app / visninger / oppsett / application.html.erb.

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

Nå trenger vi også en kontroller til å utføre det faktiske søket og vise resultatene, så vi genererer det som kjører kommandoen skinner g ny kontroller Søk.

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

Som du kan se, ringer jeg metoden Søke på artikkelen modellen. Vi har ikke definert det ennå, så hvis vi prøver å utføre et søk på dette tidspunktet, får vi en feil. Vi har heller ikke lagt til en rute for SearchController på config / routes.rb fil, så la oss gjøre det:

Rails.application.routes.draw root: "articles # index" ressurser: artikler får "søk", til: "søk # søk" slutten

Hvis vi ser på dokumentasjonen for perlen 'elasticsearch-skinnene',  Vi må inkludere to moduler på modellene som vi vil bli indeksert i Elasticsearch, i vårt tilfelle Article.rb.

krever 'elastikkforskning / modell' klasse Artikkel < ActiveRecord::Base include Elasticsearch::Model include Elasticsearch::Model::Callbacks end

Den første modellen injiserer søkemetoden som vi brukte blant annet i vår tidligere kontroller. Den andre modulen integreres med ActiveRecord tilbakeringinger for å indeksere hver forekomst av en artikkel som vi lagrer i databasen, og den oppdaterer også indeksen hvis vi endrer eller sletter artikkelen fra databasen. Så det er helt gjennomsiktig for oss.

Hvis du importerte dataene til databasen tidligere, er disse artiklene fortsatt ikke i Elasticsearch-indeksen; bare de nye er indeksert automatisk. Av denne grunn må vi indeksere dem manuelt, og det er lett hvis vi starter skinner konsoll. Da må vi bare løpe irb (hoved)> Artikkel.import.

Nå er vi klare til å prøve søkefunksjonen. Hvis jeg skriver 'ruby' og klikker søk, her er resultatene:

Søk Høydepunkt

På mange nettsteder kan du se på søkeresultatsiden hvordan uttrykket du søkte etter er uthevet. Dette er veldig enkelt å gjøre ved hjelp av Elasticsearch.

Redigere app / modeller / article.rb og endre standard søkemetode:

def self.search (spørring) __elasticsearch __. search (spørring: multi_match: spørring: spørring, felt: ['tittel', 'tekst'], høydepunkt: pre_tags: [''], post_tags: [''], felt: title: , text: ) slutten

Som standard er Søke Metoden er definert av perlen 'elasticsearch-models', og proxy objektet __elasticsearch__ er gitt for å få tilgang til wrapper klassen for Elasticsearch API. Så vi kan endre standardspørsmålet ved hjelp av standard JSON-alternativer som angitt av dokumentasjonen. 

Nå vil søkemetoden pakke inn resultatene som samsvarer med spørringen med de angitte HTML-kodene. Av denne grunn må vi også oppdatere søkeresultatsiden slik at vi kan gjengi HTML-koder trygt. For å gjøre det, rediger app / visninger / søk / search.html.erb.

Søkeresultater

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

Søket ditt stemte ikke overens med noen dokumenter.

<% end %>

Legg til en CSS-stil til app / assets / stilark / search.scss, for den merkede taggen:

.search_results em bakgrunnsfarge: gul; font-style: normal; font-weight: bold; 

Prøv å søke etter 'ruby' igjen:

Som du kan se, er det enkelt å markere søkeordet, men ikke ideelt, da vi må sende et JSON-spørring som angitt av Elasticsearch-dokumentasjonen, og vi har ingen form for abstraksjon.

Searchkick Gem

Searchkick perle leveres av Instacart, og det er en abstraksjon på toppen av de offisielle Elasticsearch-perlene. Jeg skal refactor høydepunktet funksjonalitet, så vi begynner med å legge til perle 'searchkick' til gemfile. Den første klassen som vi trenger å endre, er Article.rb-modellen:

klasse artikkel < ActiveRecord::Base searchkick end

Som du kan se, er det mye enklere. Vi må reindexere artiklene igjen, og utføre kommandoen rake searchkick: reindex CLASS = Artikkel. For å markere søkeordet må vi sende en ekstra parameter til søkemetoden fra vår search_controller.rb.

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

Den siste filen vi må endre er visninger / søk / search.html.erb som resultatene returneres i et annet format ved søkekick nå:

Søkeresultater for: <%= 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 %>

Søket ditt stemte ikke overens med noen dokumenter.

<% end %>

Nå er det på tide å kjøre programmet igjen og teste søkefunksjonen:

Legg merke til at jeg skrev inn som søkeord 'dato'. Jeg gjorde dette med vilje til å vise deg det som standard søkekicker satt opp for å analysere teksten indeksert og være mer permissiv med feilstavinger.

Autosuggest

Autosuggest eller typeahead forutsier hva en bruker vil skrive, noe som gjør søkeopplevelsen raskere og enklere. Husk at med mindre du har tusenvis av poster, kan det være best å filtrere på klientsiden.

La oss begynne med å legge til typen plugin, som er tilgjengelig gjennom perle 'bootstrap-typeahead-skinner', og legg det til Gemfile. Deretter må vi legge til noen JavaScript til app / eiendeler / Javascript / application.js slik at når du begynner å skrive i søkeboksen, vises noen forslag.

// = krever jquery // = krever jquery_ujs // = krever turbolinks // = krever bootstrap-typeahead-skinner // = require_tree. var klar = funksjon () var motor = ny Bloodhound (datumTokenizer: funksjon (d) console.log (d); return Bloodhound.tokenizers.whitespace (d.title);, queryTokenizer: Bloodhound.tokenizers.whitespace, fjernkontroll: url: '... / søk / typeahead /% QUERY'); var løfte = engine.initialize (); løfte .done (funksjon () console.log ('suksess');) .fail (funksjon () console.log ('error')); $ ("# term"). typeahead (null, name: "article", displayKey: "title", kilde: engine.ttAdapter ()); $ (Document) .ready (klar); $ (dokument) .on ('side: last', klar);

Noen kommentarer om forrige utdrag. I de to siste linjene, fordi jeg ikke har deaktivert turbolinks, er det veien å koble opp koden jeg vil kjøre på sidelast. På den første delen av manuset kan du se at jeg bruker Bloodhound. Det er typen engineer.js forslagsmotor, og jeg oppretter også JSON-sluttpunktet for å gjøre AJAX-forespørslene for å få forslagene. Etter det ringer jeg initialize () på motoren, og jeg satte opp typeahead på søke tekstfeltet ved hjelp av id "term".

Nå må vi gjøre implementeringen av backend for forslagene, la oss begynne med å legge til ruten, rediger app / config / routes.rb.

Rails.application.routes.draw root: "articles # index" -ressurser: artikler får "søk" til: "søk # søk" få "søk / typeahead /: term '=>' search # typeahead 'end

Deretter skal jeg legge til implementeringen på app / kontrollere / search_controller.rb.

def typeahead render json: Article.search (params [: term], felt: ["title"], limit: 10, load: false, feilstavinger: nedenfor: 5,). title: article.title, value: article.id slutten

Denne metoden returnerer søkeresultatene for begrepet som er innført ved hjelp av JSON. Jeg søker bare etter tittel, men jeg kunne også spesifisere artikkelenes kropp. Jeg begrenser også antall søkeresultater til 10 maksimum.

Nå er vi klare til å prøve implementering av typehodet:

Konklusjon

Som du kan se, bruker Elasticsearch med Rails gjør det enkelt og veldig raskt å søke på dataene våre. Her viste jeg deg hvordan du bruker de edelstener med lavt nivå som Elasticsearch tilbyr, samt Searchkick-perlen, som er en abstraksjon som skjuler noen detaljer om hvordan Elasticsearch fungerer. 

Avhengig av dine spesifikke behov, kan du gjerne bruke Searchkick og få fulltekstssøkingen din gjennomført raskt og enkelt. På den annen side, hvis du har noen andre komplekse spørsmål, inkludert filtre eller grupper, kan du kanskje lære mer om detaljene i spørrespråket på Elasticsearch og ende opp med å bruke de nederste nivåene 'elasticsearch-models' og 'elasticsearch- skinner.