Ricerca full-text in Rails con Elasticsearch

In questo articolo mostrerò come implementare la ricerca full-text utilizzando Ruby on Rails e Elasticsearch. Tutti vengono utilizzati al giorno d'oggi per inserire un termine di ricerca e ottenere suggerimenti e risultati con il termine di ricerca evidenziato. Se sbagliate l'ortografia di quello che state cercando, l'auto-correzione è anche una bella funzionalità, come possiamo vedere su siti Web come Google o Facebook. 

Implementare tutte queste funzionalità utilizzando solo un database relazionale come MySQL o Postgres non è semplice. Per questo motivo, utilizziamo Elasticsearch, che puoi considerare come un database appositamente creato e ottimizzato per la ricerca. È open source ed è costruito su Apache Lucene. 

Una delle più belle funzionalità di Elasticsearch è che espone le sue funzionalità utilizzando l'API REST, quindi ci sono librerie che includono questa funzionalità per la maggior parte dei linguaggi di programmazione.

Presentazione di Elasticsearch

In precedenza, ho menzionato che Elasticsearch è come un database per la ricerca. Sarebbe utile se hai familiarità con alcuni dei termini che lo circondano.

  • Campo: Un campo è come una coppia chiave-valore. Il valore può essere un valore semplice (stringa, numero intero, data) o una struttura nidificata come una matrice o un oggetto. Un campo è simile a una colonna in una tabella in un database relazionale.
  • Documento: Un documento è un elenco di campi. È un documento JSON memorizzato in Elasticsearch. È come una riga in una tabella in un database relazionale. Ogni documento è memorizzato in un indice e ha un tipo e un ID univoco.  
  • genere: Un tipo è come una tabella in un database relazionale. Ogni tipo ha un elenco di campi che possono essere specificati per i documenti di quel tipo.
  • Indice: Un indice è l'equivalente di un database relazionale. Contiene la definizione per più tipi e memorizza più documenti.

Una cosa da notare qui è che in Elasticsearch, quando si scrive un documento in un indice, i campi del documento vengono analizzati, parola per parola, per rendere la ricerca facile e veloce. Elasticsearch supporta anche la geolocalizzazione, quindi puoi cercare documenti che si trovano entro una certa distanza da una determinata posizione. Questo è esattamente il modo in cui Foursquare implementa la ricerca.

Vorrei ricordare che Elasticsearch è stato progettato tenendo presente l'alta scalabilità, quindi è molto semplice creare un cluster con più server e disporre di un'elevata disponibilità anche se alcuni server non funzionano. Non tratterò le specifiche di come pianificare e distribuire diversi tipi di cluster in questo articolo.

Installare Elasticsearch

Se stai usando Linux, è possibile installare Elasticsearch da uno dei repository. È disponibile in APT e YUM.

Se usi Mac, puoi installarlo usando Homebrew: preparare installare elasticsearch. Dopo aver installato elasticsearch, vedrai l'elenco delle cartelle rilevanti nel tuo terminale:

Per verificare che l'installazione funzioni, digita elasticsearch nel tuo terminale per avviarlo. Quindi corri arricciamento localhost: 9200 nel tuo terminale e dovresti vedere qualcosa del tipo:

Installa il quartier generale elastico

Elastic HQ è un plug-in di monitoraggio che possiamo utilizzare per gestire Elasticsearch dal browser, in modo simile a phpMyAdmin per MySQL. Per installarlo, esegui semplicemente il tuo terminale:

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

Una volta installato, accedere a http: // localhost: 9200 / _plugin / hq nel browser:

Clicca su Collegare e vedrai una schermata che mostra lo stato del cluster:

Al momento, come puoi immaginare, non sono ancora stati creati indici o documenti, ma abbiamo installato ed eseguito la nostra istanza locale di Elasticsearch.

Creazione di un'applicazione Rails

Creerò un'applicazione Rails molto semplice, in cui è possibile aggiungere articoli al database in modo da poter eseguire una ricerca full-text su di essi utilizzando Elasticsearch. Inizia creando una nuova applicazione Rails:

rotaie nuove rotaie elasticsearch

Successivamente generiamo una nuova risorsa articolo con scaffolding:

rails genera scaffold Titolo dell'articolo: testo stringa: testo

Ora dobbiamo aggiungere una nuova route root, in modo che possiamo vedere per default l'elenco degli articoli. modificare config / routes.rb:

Rails.application.routes.draw fa la root a: 'articoli # index' risorse: articoli fine 

Creare il database eseguendo il comando rake db: migrate. Se inizi rails server, apri il tuo browser, vai a localhost: 3000 e aggiungi alcuni articoli al database, oppure scarica semplicemente il file db / seeds.rb con i dati fittizi che ho creato in modo da non dover perdere molto tempo a compilare i moduli.

Aggiungere ricerca

Ora che abbiamo la nostra piccola app Rails con articoli nel database, siamo pronti ad aggiungere la nostra funzionalità di ricerca. Inizieremo aggiungendo il riferimento ad entrambe le gemme Elasticsearch ufficiali:

gemma 'elasticsearch-model' gemma 'elasticsearch-rails'

Su molti siti web, è molto comune avere una casella di testo per la ricerca nel menu in alto su tutte le pagine. Per questo motivo, creerò un modulo parziale su app / views / ricerca / _form.html.erb.Come puoi vedere, sto inviando il modulo utilizzando GET, quindi è facile copiare e incollare l'URL per una ricerca specifica.

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

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

<% end %>

Aggiungi un riferimento al modulo al layout del sito web principale. modificare app / views / layout / application.html.erb.

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

Ora abbiamo anche bisogno di un controller per eseguire la ricerca effettiva e visualizzare i risultati, quindi lo generiamo eseguendo il comando rails g new controller Search.

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

Come puoi vedere, sto chiamando il metodo ricerca sul modello dell'articolo. Non l'abbiamo ancora definito, quindi se proviamo a eseguire una ricerca a questo punto, riceviamo un errore. Inoltre, non abbiamo aggiunto un percorso per SearchController su config / routes.rb file, quindi facciamolo così:

Rails.application.routes.draw effettua il root su: risorse 'articles # index': gli articoli ottengono "search", a: "search # search" end

Se guardiamo la documentazione per la gemma 'elasticsearch-rails',  abbiamo bisogno di includere due moduli sui modelli che vogliamo essere indicizzati in Elasticsearch, nel nostro caso Article.rb.

richiedere l'articolo di classe "elasticsearch / model" < ActiveRecord::Base include Elasticsearch::Model include Elasticsearch::Model::Callbacks end

Il primo modello inietta il metodo di ricerca che stavamo usando nel nostro precedente controller, tra gli altri. Il secondo modulo si integra con i callback di ActiveRecord per indicizzare ogni istanza di un articolo che salviamo nel database e aggiorna anche l'indice se modifichiamo o cancelliamo l'articolo dal database. Quindi è tutto trasparente per noi.

Se hai precedentemente importato i dati nel database, questi articoli non sono ancora nell'indice Elasticsearch; solo quelli nuovi sono indicizzati automaticamente. Per questo motivo, dobbiamo indicizzarli manualmente, ed è facile iniziare console di rotaie. Quindi dobbiamo solo correre irb (main)> Article.import.

Ora siamo pronti per provare la funzionalità di ricerca. Se digito "ruby" e clicco su search, ecco i risultati:

Evidenziazione della ricerca

Su molti siti Web, è possibile visualizzare nella pagina dei risultati di ricerca come viene evidenziato il termine cercato. Questo è molto facile da fare usando Elasticsearch.

modificare app / modelli / article.rb e modifica il metodo di ricerca predefinito:

def self.search (query) __elasticsearch __. search (query: multi_match: query: query, fields: ['title', 'text'], evidenziare: pre_tags: [''], post_tags: [''], campi: titolo: , testo: ) fine

Di default, il ricerca il metodo è definito dalla gem 'elasticsearch-models' e l'oggetto proxy __elasticsearch__ viene fornito per accedere alla classe wrapper per l'API Elasticsearch. Quindi possiamo modificare la query predefinita usando le opzioni JSON standard fornite dalla documentazione. 

Ora il metodo di ricerca comprenderà i risultati che corrispondono alla query con i tag HTML specificati. Per questo motivo, abbiamo anche bisogno di aggiornare la pagina dei risultati di ricerca in modo che possiamo rendere sicuri i tag HTML. Per fare ciò, modifica app / views / ricerca / search.html.erb.

risultati di ricerca

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

La tua ricerca non corrisponde a nessun documento.

<% end %>

Aggiungi uno stile CSS a app / beni / fogli di stile / search.scss, per il tag evidenziato:

.search_results em background-color: yellow; stile font: normale; font-weight: bold; 

Prova a cercare di nuovo "rubino":

Come puoi vedere, è facile evidenziare il termine di ricerca, ma non è l'ideale, poiché abbiamo bisogno di inviare una query JSON come specificato dalla documentazione di Elasticsearch, e non abbiamo alcun tipo di astrazione.

Searchkick Gem

Searchkick gem è fornito da Instacart ed è un'astrazione in cima alle gemme ufficiali di Elasticsearch. Ho intenzione di ridefinire la funzionalità di evidenziazione, quindi iniziamo aggiungendo gemma 'searchkick' al file gemma. La prima classe che dobbiamo cambiare è il modello Article.rb:

articolo di classe < ActiveRecord::Base searchkick end

Come puoi vedere, è molto più semplice. Abbiamo bisogno di reindicizzare nuovamente gli articoli ed eseguire il comando rastrello searchkick: reindex CLASS = Articolo. Per evidenziare il termine di ricerca, è necessario passare un parametro aggiuntivo al metodo di ricerca dal nostro 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

L'ultimo file che dobbiamo modificare è views / search / search.html.erb come i risultati vengono restituiti in un formato diverso da searchkick ora:

Cerca risultati per: <%= 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 %>

La tua ricerca non corrisponde a nessun documento.

<% end %>

Ora è il momento di eseguire nuovamente l'applicazione e testare la funzionalità di ricerca:

Si noti che ho inserito come termine di ricerca "dato". L'ho fatto apposta per mostrarti che per impostazione predefinita searchkickè impostato per analizzare il testo indicizzato ed essere più permissivo con errori di ortografia.

autosuggest

Autosuggest o typeahead predice ciò che un utente digiterà, rendendo l'esperienza di ricerca più veloce e più semplice. Tieni presente che, a meno che tu non abbia migliaia di record, potrebbe essere meglio filtrare dal lato client.

Iniziamo aggiungendo il plugin typeahead, che è disponibile attraverso il gem 'bootstrap-typeahead-rails', e aggiungilo al tuo Gemfile. Successivamente, dobbiamo aggiungere qualche JavaScript a app / Attività / javascripts / application.js in modo che quando inizi a digitare nella casella di ricerca, vengono visualizzati alcuni suggerimenti.

// = richiede jquery // = richiede jquery_ujs // = richiede turbolinks // = richiede 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 (); promise .done (function () console.log ('success');) .fail (function () console.log ('error')); $ ("# term"). typeahead (null, name: "article", displayKey: "title", source: engine.ttAdapter ()); $ (Document) .ready (pronto); $ (document) .on ('page: load', ready);

Alcuni commenti sullo snippet precedente. Nelle ultime due righe, perché non ho disabilitato i turbolinks, questo è il modo per collegare il codice che voglio eseguire al caricamento della pagina. Nella prima parte della sceneggiatura, puoi vedere che sto usando Bloodhound. È il motore di suggerimento typeahead.js e sto anche impostando l'endpoint JSON per rendere le richieste AJAX per ottenere i suggerimenti. Dopo, chiamo inizializzare() sul motore, e ho impostato typeahead sul campo di testo di ricerca usando il suo id "term".

Ora, abbiamo bisogno di fare l'implementazione del backend per i suggerimenti, iniziamo aggiungendo il percorso, modifica app / config / routes.rb.

Rails.application.routes.draw fa root a: 'articoli # index' risorse: gli articoli ottengono "cerca", a: "cerca # cerca" ottieni 'cerca / typeahead /: term' => 'cerca # typeahead' fine

Successivamente, aggiungerò l'implementazione app / controllers / search_controller.rb.

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

Questo metodo restituisce i risultati della ricerca per il termine inserito utilizzando JSON. Sto solo cercando per titolo, ma potrei specificare anche il corpo dell'articolo. Sto anche limitando il numero di risultati di ricerca a 10 al massimo.

Ora siamo pronti per provare l'implementazione typeahead:

Conclusione

Come puoi vedere, l'uso di Elasticsearch con Rails rende la ricerca dei nostri dati davvero facile e molto veloce. Qui vi ho mostrato come usare le gemme di basso livello fornite da Elasticsearch, così come la gemma Searchkick, che è un'astrazione che nasconde alcuni dettagli di come Elasticsearch funziona. 

A seconda delle tue esigenze specifiche, potresti essere felice di utilizzare Searchkick e ottenere la tua ricerca full-text implementata rapidamente e facilmente. D'altra parte, se hai altre query complesse che includono filtri o gruppi, potresti dover imparare di più sui dettagli del linguaggio di query su Elasticsearch e finire con l'uso dei modelli di elasticsearch gemelli di livello inferiore e di elasticsearch- Rails'.