A continuació s’explica com afegir un potent motor de cerca al vostre motor de Rails

Foto de Simon Abrams a Unsplash

Per la meva experiència com a desenvolupador de Ruby on Rails, vaig haver de tractar molt d’afegir funcions de cerca a aplicacions web. De fet, gairebé totes les aplicacions en què he treballat necessitaven la funcionalitat del motor de cerca, mentre que moltes d’elles tenien un motor de cerca com a funció principal.

Moltes de les aplicacions que fem servir cada dia serien inútils sense un bon motor de cerca. A Amazon, per exemple, podeu trobar un producte específic en pocs segons dels més de 550 milions de productes disponibles al lloc web, tot gràcies a una cerca de text complet en combinació amb filtres de categoria, facetes i un sistema de recomanació.

Airbnb us permet cercar un apartament combinant una cerca espacial amb filtres en funció de les característiques de la casa, com ara la mida, el preu, les dates disponibles, etc.

I Spotify, Netflix, Ebay, Youtube ... depenen en gran mesura d’un motor de cerca.

En aquest article, vaig a descriure com desenvolupar un dorsal API Ruby on Rails 5 mitjançant Elasticsearch. Elasticsearch és actualment la plataforma de cerca de codi obert més popular segons DB Engines Ranking.

Aquest article no entrarà en els detalls d’Elasticsearch ni en què es compara amb els seus competidors com Sphinx i Solr. En el seu lloc, proporciona una guia pas a pas sobre com implementar un backend API JSON amb Ruby on Rails i Elasticsearch mitjançant un enfocament de desenvolupament basat en proves.

Aquest article tracta sobre:

  1. Configuració d'Elasticsearch per a entorns de prova, desenvolupament i producció
  2. Configureu l'entorn de prova Ruby on Rails
  3. Indexació de models amb Elasticsearch
  4. Punt final de l'API de cerca

Com en el meu article anterior, Com millorar el vostre rendiment amb una arquitectura sense servidor, tractaré tot en un tutorial pas a pas. A continuació, podeu provar-ho per vosaltres mateixos i tenir un exemple de treball senzill sobre el qual construir quelcom més complex.

L’aplicació de mostra és un motor de cerca de pel·lícules. Tindrà un únic punt final de l'API JSON que podeu utilitzar per fer una cerca de text complet de títols i resums de pel·lícules.

1. Configuració d'Elasticsearch

Elasticsearch és un motor de cerca i anàlisi distribuït i RESTful capaç de resoldre un nombre creixent de casos d’ús. Com a cor de la pila elàstica, emmagatzema les vostres dades de manera centralitzada perquè pugueu reconèixer allò esperat i descobrir allò inesperat. - www.elastic.co/products/elasticsearch

Segons el rànquing dels motors de cerca per DB-Engines, Elasticsearch és, amb diferència, la plataforma de motors de cerca més populars avui (a l'abril de 2018). I això des de finals del 2015, quan Amazon va anunciar el llançament d’AWS Elasticsearch Service, una manera d’iniciar un clúster Elasticsearch a través de la consola de gestió d’AWS.

Tendència de classificació del motor de cerca DB Engines

Elasticsearch és de codi obert. Podeu descarregar la vostra versió preferida del lloc web i executar-la on vulgueu. Tot i que recomano utilitzar el servei AWS Elasticsearch per a entorns de producció, prefereixo executar Elasticsearch a la meva màquina local per provar-la i desenvolupar-la.

Primer, descarregueu la versió més recent (actualment) d’Elasticsearch (6.2.3) i descomprimiu-la. Obriu un terminal i executeu-lo

$ wget https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-6.2.3.zip
$ unzip elasticsearch-6.2.3.zip

També podeu descarregar Elasticsearch des del navegador aquí i descomprimir-lo amb el programa que preferiu.

2. Configuració de l'entorn de prova

Construirem una aplicació de backend amb l'API Ruby on Rails 5. Hi haurà un model que representi pel·lícules. Elasticsearch l’indexa i es pot cercar mitjançant un punt final de l’API.

En primer lloc, creem una nova aplicació ferroviària. A la mateixa carpeta que heu descarregat anteriorment Elasticsearch, executeu l'ordre per generar una nova aplicació de rails. Si no coneixeu Ruby on Rails, llegiu aquesta guia d'introducció per configurar el vostre entorn.

Cerca de noves pel·lícules de $ Rails --api; Cerca de pel·lícules en CD

L'ús de l'opció API no inclou tot el middleware que s'utilitza principalment per a aplicacions del navegador. Exactament el que volem. Llegiu-ne més directament al manual de Ruby on Rails.

Ara afegim totes les joies que necessitem. Obriu el fitxer Gemma i afegiu el codi següent:

# Gemfile
... # Integració de les pedres precioses Elasticsearch 'elasticsearch-model' Juwel 'barres de cerca de goma'
Grup: Desenvolupament: feu una prova ... # Test Framework Gemstone 'rspec' Gemstone "Rspec-Rails" end
Grup: Feu una prova ... # Neteja la base de dades entre proves Edelstein 'database_cleaner' # Inicieu i atureu ES per programació per a les proves Edelstein 'elasticsearch-extensions' End ...

Afegim dues gemmes d’Elasticsearch que proporcionen tots els mètodes necessaris per indexar el nostre model i executar consultes de cerca. Les proves rspec, rspec-rails, database_cleaner i elasticsearch s’utilitzen per provar.

Després de desar el fitxer gem, executeu la instal·lació del paquet per instal·lar qualsevol gema que hàgiu afegit.

Ara configurem Rspec executant l'ordre següent:

Crea rails rspec: instal·la

Aquesta ordre crea una carpeta d'especificacions i hi afegeix spec_helper.rb i rails_helper.rb. Es poden utilitzar per personalitzar rspec segons les vostres necessitats d'aplicació.

En aquest cas, afegim un bloc DatabaseCleaner a rail_helper.rb perquè cada prova s'executi en una base de dades buida. També canviem spec_helper.rb per iniciar un servidor de proves d’Elasticsearch cada cop que s’inicia el conjunt de proves i el tornem a tancar quan s’acabi el conjunt de proves.

Aquesta solució es basa en l'article Testing Elasticsearch in Rails de Rowan Oulton. Molts aplaudeixen per ell!

Comencem amb DatabaseCleaner. A spec / rails_helper.rb, afegiu el codi següent:

# spec / rails_helper.rb ... RSpec.configure do | config | ...
config.before (: suite) do DatabaseCleaner.strategy =: Transacció DatabaseCleaner.clean_with (: shortening) end
config.around (: each) do | exemple | DatabaseCleaner.cleaning feu example.run end end end

A continuació, vegem la configuració del servidor de proves d’Elasticsearch. Hem d’afegir alguns fitxers de configuració perquè Rails sàpiga on és el nostre executable d’Elasticsearch. També especifica en quin port s’ha d’executar en funció de l’entorn actual. Per fer-ho, afegiu un fitxer de configuració nou a la carpeta de configuració:

# config / elasticsearch.yml
Desenvolupament: & Estàndard es_bin: '../elasticsearch-6.2.3/bin/elasticsearch' Amfitrió: 'http: // localhost: 9200' Port: '9200' Comprova: es_bin: '../elasticsearch-6.2.3/ bin / elasticsearch 'host:' http: // localhost: 9250 'port:' 9250 'periodificació: <<: * producció estàndard: es_bin:' ../elasticsearch-6.2.3/bin/elasticsearch 'host:' http: // localhost: 9400 'Port:' 9400 '

Si no heu creat l'aplicació Rails a la carpeta on heu descarregat Elasticsearch o si utilitzeu una versió diferent d'Elasticsearch, haureu d'ajustar la ruta es_bin aquí.

Ara afegiu un fitxer nou a la carpeta d'inicialització que es llegeix des de la configuració que acabeu d'afegir:

# config / initializers / elasticsearch.rb
Quan existeix File.ex? ("config / elasticsearch.yml") config = YAML.load_file ("config / elasticsearch.yml") [Rails.env] .symbolize_keys Elasticsearch :: Model.client = Elasticsearch :: Client.new (config) end

Per últim, canviem spec_helper.rb per incloure la configuració de la prova d’Elasticsearch. Això significa que inicieu i atureu un servidor de proves d'Elasticsearch i creeu / suprimiu índexs d'Elasticsearch per al nostre model Rails.

# spec / spec_helper.rb
requereixen "elasticsearch / extensions / test / cluster" requereixen "yaml"
RSpec.configure do | config | ... # Si cal, inicieu un clúster en memòria per a Elasticsearch es_config = YAML.load_file ("config / elasticsearch.yml") ["test"] ES_BIN = es_config ["es_bin"] ES_PORT = es_config ["port"]
config.before: all, elasticsearch: true do Elasticsearch :: Extensions :: Test :: Cluster.start (ordre: ES_BIN, port: ES_PORT.to_i, node: 1, timeout: 120), si Elasticsearch :: Extensions :: Test :: Cluster.running? (Ordre: ES_BIN, a: ES_PORT.to_i) Final
# Atureu el clúster Elasticsearch després de la prova config.after: suite do Elasticsearch :: Extensions :: Test :: Cluster.stop (Ordre: ES_BIN, Port: ES_PORT.to_i, Node: 1) si Elasticsearch :: Extensions :: Test :: Clúster.execució? (Ordre: ES_BIN, a: ES_PORT. To_i) Final
# Creeu índexs en tots els models de cerca elàstics config.before: each, elasticsearch: true do ActiveRecord :: Base.descendants.each do | model | si model.respond_to? (: __ elasticsearch__) Inicieu el model .__ elasticsearch __. create_index! model .__ elasticsearch __. refresh_index! Rescue => Elasticsearch :: Transport :: Transport :: Fehler :: NotFound # Això finalitza els errors "No existeix l'índex" que s'escriuen a la consola Rescue => e STDERR.puts "Quan es crea l'índex d'Elasticsearch per a {{ model.name} s'ha produït un error: # {e.inspect} "Final Final Final Final
# Esborreu els índexs de tots els models de cerca elàstics per garantir un estat net entre les proves config.after: each, elasticsearch: true do ActiveRecord :: Base.descendants.each do | model | si model.respond_to? (: __ elasticsearch__) Iniciar el model .__ elasticsearch __. delete_index! Rescue => Elasticsearch :: Transport :: Transport :: Error :: NotFound # Això finalitza els errors "L'índex no existeix" que s'escriuen a la consola Rescue => e STDERR.puts "Quan s'elimina l'índex d'Elasticsearch de # { model.name} s'ha produït un error: # {e.inspect} "Final Final Final Final
El final

Hem definit quatre blocs:

  1. Un bloc abans (: tot) que inicia un servidor de proves d'Elasticsearch si encara no s'executa
  2. un bloc after (: suite) que aturarà el servidor de proves d'Elasticsearch si s'està executant
  3. Un bloc anterior (: cada) que crea un nou índex Elasticsearch per a cada model configurat amb Elasticsearch
  4. Un bloc after (: each) que esborra tots els índexs d'Elasticsearch

Afegir elasticsearch: cert garanteix que només les proves marcades amb elasticsearch executen aquests blocs.

Em sembla que aquesta configuració funciona molt bé si feu totes les proves una vegada, per exemple abans d’un desplegament. Per contra, si utilitzeu un enfocament de desenvolupament basat en proves i feu les proves amb molta freqüència, és possible que hàgiu de fer canvis menors a aquesta configuració. No voleu iniciar i aturar el servidor de proves d'Elasticsearch cada vegada que el proveu.

En aquest cas, podeu comentar el bloc after (: suite) en què es deté el servidor de prova. Podeu apagar-lo manualment o mitjançant un script quan ja no el necessiteu.

requereixen 'elasticsearch / extensions / test / cluster' es_config = YAML.load_file ("config / elasticsearch.yml") ["test"] ES_BIN = es_config ["es_bin"] ES_PORT = es_config ["port"] Elasticsearch :: Extensions: : Prova :: Cluster.stop (Ordre: ES_BIN, Port: ES_PORT.to_i, Node: 1)

3. Indexació de models amb Elasticsearch

Ara comencem a implementar el nostre model de pel·lícula amb funcions de cerca. Utilitzem un enfocament de desenvolupament basat en proves. Això vol dir que primer escrivim proves, determinem que fallen i després escrivim codi per fer-les passar.

Primer cal afegir el model de pel·lícula, que té quatre atributs: un títol (cadena), una visió general (text), una url d’imatge (cadena) i un valor mitjà de vot (flotant).

$ rails g model Títol de la pel·lícula: Resum de cadena: URL de la imatge de text: Cadena vote_average: float
$ Rails db: migrar

Ara és el moment d’afegir Elasticsearch al nostre model. Escrivim una prova per veure si el nostre model està indexat.

# spec / models / movie_spec.rb necessita "Rails_Helper"
RSpec.describe Movie, elasticsearch: true,: type =>: model, hauria d'esperar-se indexat (Movie .__ elasticsearch __. Index_exists?). veritat és fi final

Aquesta prova verifica que s'ha creat un índex d'Elasticsearch per a Movie. Recordeu, abans de començar a provar, crearem automàticament un índex Elasticsearch en tots els models que responguin al mètode __elasticsearch__. Això significa per a tots els models que contenen els mòduls Elasticsearch.

Executeu la prova per veure si falla.

paquet exec rspec spec / models / movie_spec.rb

La primera vegada que executeu aquesta prova, haureu de notar que s'inicia el servidor de proves d'Elasticsearch. La prova falla perquè no hem afegit cap mòdul Elasticsearch al nostre model de pel·lícula. Arreglem-ho ara. Obriu el model i afegiu el següent Elasticsearch:

# app / models / movie.rb
Gran pel·lícula

Això afegeix alguns mètodes d'Elasticsearch al nostre model de pel·lícula, com ara el mètode __elasticsearch__ que falta (que va produir l'error en la prova anterior) i el mètode de cerca que utilitzarem més endavant.

Torneu a executar la prova i comproveu que ha passat.

paquet exec rspec spec / models / movie_spec.rb

Genial. Tenim un model de pel·lícula indexada.

Per defecte, Elasticsearch :: Model crea un índex amb tots els atributs del model i en deriva automàticament els seus tipus. Normalment no ho volem. Ara ajustarem l’índex del model perquè tingui el comportament següent:

  1. Només s’han d’indexar el títol i la visió general
  2. S'hauria d'utilitzar la derivació (és a dir, quan es cerquin "actors", les pel·lícules amb el text "actor" també s'han de retornar i viceversa)

També volem que el nostre índex s’actualitzi cada vegada que s’afegeix, s’actualitza o se suprimeix una pel·lícula.

Posem això a prova afegint el següent codi a movie_spec.rb

# spec / models / movie_spec.rb RSpec.describe Movie, elasticsearch: true,: type =>: model do ...
descriviu "#search" per fer abans (: tothom) feu Movie.create (títol: "Roman Holiday", visió general: "Una pel·lícula de comèdia romàntica americana del 1953 ...", image_url: "wikimedia.com/Roman_holiday.jpg", vote_average: 4.0) Film .__ elasticsearch __. Refresh_index Finalitzar hauria d 'esperar "títol de l' índex" (Movie.search ("Holiday"). Records.length) .to eq (1) finalitzar hauria de fer "descripció general de l 'índex" esperar (Movie.search ("comèdia"). Records. length) .to eq (1) end it should "not index image_path" do expect (Movie.search ("Roman_holiday.jpg"). records.length) .to eq (0) end it "should not index vote_average" espero (Movie.search ("4.0"). Records.length) .to eq (0) end end
El final

Fem una pel·lícula abans de cada prova perquè hem configurat DatabaseCleaner per aïllar cada prova. Pel·lícula .__ elasticsearch __. Refresh_index es requereix per garantir que el nou conjunt de dades de pel·lícules estigui disponible immediatament per a la cerca.

Executeu la prova com abans i comproveu que falla.

Sembla que la nostra pel·lícula no està indexada. Això es deu al fet que encara no hem dit al nostre model què hem de fer si canvien les dates de la pel·lícula. Afortunadament, això es pot solucionar afegint un altre mòdul al nostre model de pel·lícula:

Gran pel·lícula

Elasticsearch :: Model :: Callbacks també actualitza el document d'Elasticsearch cada vegada que s'afegeix, canvia o suprimeix una pel·lícula.

Vegem com canvia la sortida de la prova.

D'ACORD. El problema ara és que el nostre mètode de cerca també retorna consultes que coincideixen amb els atributs vote_average i image_url. Per solucionar-ho, hem de configurar el mapatge d'índexs d'Elasticsearch. Per tant, hem d’indicar específicament a Elasticsearch quins atributs de model indexar.

# app / models / movie.rb
Gran pel·lícula
# Índex de configuració de l'índex d'ElasticSearch: {number_of_shards: 1} feu assignacions dinàmiques: feu índexs "equivocats": índexs del títol: visió general final final final

Torneu a executar la prova i comproveu que ha passat.

Guai. Afegim ara un stemmer perquè no hi hagi diferència entre "actor" i "actor". Com sempre, primer escriurem la prova i veurem que falla.

descriviu "#search" per fer abans (: tothom) feu Movie.create (títol: "Roman Holiday", visió general: "Una pel·lícula de comèdia romàntica americana del 1953 ...", image_url: "wikimedia.com/Roman_holiday.jpg", vote_average: 4.0) Film .__ elasticsearch __. Refresh_index El final
...
hauria de "derivar-se del títol" (Movie.search ("Holidays"). records.length) .to eq (1) end
hauria de "derivar-se de la visió general" (Movie.search ("film"). records.length) .to eq (1) end
El final

Tingueu en compte que estem provant els dos mètodes: les vacances també haurien de ser festius i les pel·lícules també haurien de retornar-les.

Per tornar a passar aquestes proves, hem de canviar el mapatge d'índexs. Aquesta vegada, afegim un analitzador d’anglès als dos camps:

Gran pel·lícula
# Índex de configuració de l'índex d'ElasticSearch: {number_of_shards: 1} feu assignacions dinàmiques: feu índexs "equivocats": títol, analitzador: índexs "anglès": visió general, analitzador: "anglès" end end end

Torneu a executar les proves per veure si han passat.

Elasticsearch és una plataforma de cerca molt potent i podríem afegir moltes funcions al nostre mètode de cerca. Tot i això, això queda fora de l’abast d’aquest article. Així que ens aturarem aquí i crearem la part del controlador de l’API JSON que s’utilitzarà per accedir al mètode de cerca.

4. Cerqueu el punt final de l'API

L'API de cerca que hem creat ha de permetre als usuaris fer una cerca de text complet a la taula Pel·lícules. La nostra API té un únic punt final que es defineix de la manera següent:

URL: GET / api / v1 / movies
Params: * q = [string] obligatori
URL d'exemple: GET / api / v1 / movies? Q = Roma
Exemple de resposta: [{"_index": "pel·lícules", "_ tipus": "pel·lícula", "_ id": "95088", "_ puntuació": 11.549209, "_ font": {"id": 95088, " title ":" Roma "," overview ":" Un retrat de Roma pràcticament sense acció, colorit i impressionista a través dels ulls d'un dels ciutadans més famosos. "," image_url ":" https://image.tmdb.org/t/p/ w300 / rqK75R3tTz2iWU0AQ6tLz3KMOU1.jpg "," vote_average ": 6.6," created_at ":" 2018-04-14T10: 30: 49.110Z "," updated_at ":" 2018-04-14T10: 30: 49.110Z "}, .. .]

Aquí definim el nostre punt final mitjançant algunes pràctiques recomanades de disseny d'API RESTful:

  1. L'URL ha de codificar l'objecte o el recurs, mentre que l'acció que s'ha de dur a terme s'ha de codificar mitjançant el mètode HTTP. En aquest cas, el recurs són les pel·lícules (col·lecció) i fem servir el mètode HTTP GET (ja que sol·licitem dades del recurs sense causar efectes secundaris). Utilitzarem paràmetres d’URL per definir millor com s’haurien de recuperar aquestes dades. En aquest exemple q = [cadena], que especifica una consulta de cerca. Per obtenir més informació sobre el disseny d’APIs RESTful, consulteu l’article de Mahesh Haldar, Directrius per al disseny d’APIs RESTful: pràctiques recomanades.
  2. També afegim versions de versions a la nostra API afegint v1 al nostre URL de punt final. La versió de l'API és molt important, ja que us permet introduir noves funcions que són incompatibles amb versions anteriors sense trencar cap client dissenyat per a versions anteriors de l'API.

D'ACORD. Comencem per la implementació.

Com sempre, comencem amb proves fallides. Dins de la carpeta d'especificacions, creem l'estructura de carpetes que reflecteix la nostra estructura d'URL de punt final de l'API. Això significa Controlador → API → v1 → pel·lícules_espec.rb

Podeu fer-ho manualment o des del vostre terminal:

mkdir -p spec / controller / api / v1 && Touch spec / controller / api / v1 / movies_spec.rb

Les proves que escriurem aquí són proves del controlador. No cal que comproveu la lògica de cerca definida al model. En lloc d'això, provarem tres coses:

  1. Una sol·licitud GET a / api / v1 / movies? Q = [cadena] crida a Movie.search amb [cadena] com a paràmetre
  2. La sortida de Movie.search es torna en format JSON
  3. Es torna un estat d’èxit
Una prova del controlador ha de provar el comportament del controlador. Una prova del controlador no hauria de fallar a causa de problemes al model.
(Recepta 20 - Rails 4 receptes de prova. Noel Rappin)

Convertim això en codi. A spec / controller / api / v1 / movies_spec.rb, afegiu el codi següent:

# spec / controller / api / v1 / movies_spec.rb necessita 'Rails_Helper' RSpec.describe Api :: V1 :: MoviesController, introdueix :: sol·licita fer # Cerca una pel·lícula amb text que descrigui el títol de la pel·lícula "GET / api / v1 / movies? q = "let (: title) {" title title "} let (: url) {" / api / v1 / movies? q = # {title} "}
"crida a Movie.search amb els paràmetres correctes" espera (movie) .to receive (: search). finalitza amb (title) get url
"retorna la sortida de Movie.search" permet (Movie) .to receive (: search) .and_return ({}) get url expect (response.body) .to eq ({}. to_json) end
Hi ha un estat d’èxit que permet tornar (pel·lícula). Per rebre (: cerca). Amb (títol) obtingueu una URL que espera (resposta). per tenir èxit end end end

La prova falla immediatament perquè Api :: V1 :: MoviesController no està definit. Creeu l'estructura de carpetes com abans i afegiu el controlador de la pel·lícula.

mkdir -p app / controller / api / v1 && Touch app / controller / api / v1 / movies_controller.rb

Ara afegiu el següent codi a app / controller / api / v1 / movies_controller.rb:

# app / controller / api / v1 / movies_controller.rb module Api module V1 class MoviesController

És hora de fer la nostra prova i veure que falla.

Totes les proves fallen, ja que encara hem d'afegir una ruta per al punt final. A config / routes.rb afegiu el codi següent:

# config / routes.rb Rails.application.routes.draw do namespace: api do namespace: v1 do resources: movies, only: [: index] end end end

Torneu a fer les proves i veureu què passa.

El primer error és que hem de trucar a Movie.search al nostre controlador. El segon es queixa de la resposta. Afegim el codi que falta a movies_controller:

# app / controller / api / v1 / movies_controller.rb Module Api Module V1 Class MoviesController

Feu la prova i comproveu si hem acabat.

Sí. Això és tot. Hem acabat una aplicació de backend molt senzilla que permet als usuaris navegar per un model a través de l'API.

Podeu trobar el codi complet a la meva reposició de GitHub aquí. Podeu omplir la taula de pel·lícules amb algunes dades executant Rails db: seed perquè pugueu veure l’aplicació en acció. Això importarà aproximadament 45.000 pel·lícules d’un conjunt de dades descarregat de Kaggle. Consulteu el fitxer Llegeix-me per obtenir més informació.

Si us ha agradat aquest article, recomaneu-lo fent clic a la icona de tafaneria que hi ha a la part inferior d’aquesta pàgina perquè hi hagi més gent que el pugui veure a Mitjà