Antes de ponerte a integrar ElasticSearch con Rails como un loco, ¿estás seguro de que necesitas utilizarlo? Debes asegurarte de que es así. A veces, el problema que intentamos resolver con Elasticsearch se resuelve más rápidamente con algunas consultas avanzadas e indices. Nos equivocamos llegando a la solución por el camino más largo.
Por lo general, hay dos casos en los que el uso de Elasticsearch tiene sentido:
- Búsqueda de texto completo: Obvio, es para lo que Elasticsearch fue construido.
- Denormalización de datos complejos: Debido a que por lo general tratamos de normalizar nuestros datos para que coincidan con nuestros modelos, podemos encontrar algunos problemas de rendimiento cuando se consultan a través de estas tablas normalizadas. Elasticsearch nos permite consultar estos datos de manera más rápida.
Elasticsearch on Rails
A continuación vamos a describir un ejemplo de integración entre Elasticsearch y Rails:
Instalación
En el entorno de desarrollo, usando OS X, puedes instalar Elasticsearch mediante el comando:
➜ ~ brew install elasticsearch
Si utilizas otra plataforma o quieres instalar una version especifica, puedes seguir las indicaciones descritas por Elasticsearch.
Inicialización
Para inicializar Elasticsearch, junto con otros recursos, se puede generar una tarea rake (github) dentro del proyecto:
namespace :check do desc 'Check and start Mysql, Redis and Elasticsearch' task :environment do if RUBY_PLATFORM == 'x86_64-darwin14' puts 'This rake task will check if all dependencies are running, if not it will try to run. ' if `pgrep mysql` == '' print 'Starting Mysql ...' `mysql.server start &` puts `pgrep mysql`.blank? ? ' [ KO ]' : ' [ OK ]' end if `ps aux | grep '[e]lasticsearch'` == '' print 'Starting Elasticsearch ...' `elasticsearch -d --path.conf=#{Rails.root}/config` puts `ps aux | grep '[e]lasticsearch'`.blank? ? ' [ KO ]' : ' [ OK ]' end if `pgrep redis` == '' print 'Starting Redis ...' `/usr/local/Cellar/redis/3.0.5/bin/redis-server /usr/local/etc/redis.conf &` puts `pgrep redis`.blank? ? ' [ KO ]' : ' [ OK ]' end if `pgrep sidekiq` == '' print 'Starting Sidekiq ...' `bundle exec sidekiq -d` puts `ps aux | grep '[s]idekiq'`.blank? ? ' [ KO ]' : ' [ OK ]' end end end end
Esta tarea se puede lanzar mediante:
➜ ~ rake check:environment
Por último, para comprobar que Elasticsearch está funcionando correctamente, puedes lanzar:
➜ ~ curl -X GET http://localhost:9200/
Por defecto, Elasticsearch se inicializa en el puerto 9200 de la máquina local. Si se quisiera cambiar la configuración, ejecutando la tarea rake, cogería el fichero de configuración, elastisearch.yml (github), del directorio config del proyecto.
Gemfile
La instalación en el proyecto es muy simple, únicamente hay que añadir al gemfile (github):
# Gemfile # ... gem 'elasticsearch-model' gem 'elasticsearch-rails' gem 'sidekiq'
Puedes ampliar información de la utlización de las gemas aquí y sidekiq. Sidekiq lo utilizaremos para la indexación asíncrona de los datos.
Configuración
Una vez añadidas las gemas al gemfile, generamos el inicializer. Una posible configuración podría ser:
# config/initializers/elasticsearch.rb Elasticsearch::Model.client = Elasticsearch::Client.new url: ENV['ELASTICSEARCH_URL'] || 'http://localhost:9200/' Elasticsearch::Model::Response::Response.__send__ :include, Elasticsearch::Model::Response::Pagination::Kaminari ActiveRecord::Base.establish_connection ActiveRecord::Base.connection.tables.each do |table| next if table == 'schema_migrations' if table == 'users' if User.__elasticsearch__.index_exists? User.__elasticsearch__.refresh_index! else User.__elasticsearch__.create_index! User.import end end end
Dentro de este ‘inicializer’, si tuviésemos definida la variable de entorno ELASTICSEARCH_URL, se conectaría a dicha url; en caso contrario, se conectará a la url definida por defecto. De igual manera, se inicializa manualmente la paginación con kaminari e indexamos las tabla que queramos. En concreto, en este ejemplo, al recorrer las tablas de nuestro modelo, si el índice existe, lo refrescamos; y, si no, lo creamos e importamos los datos que se tenga.
En el caso de que se desee mostrar el log de las peticiones que se realizan, basta con modificar la siguiente línea:
Elasticsearch::Model.client = Elasticsearch::Client.new log:true, url: ENV['ELASTICSEARCH_URL'] || 'http://localhost:9200/'
Indexación del Modelo
Para simplificar la indexación de un modelo e incluirle métodos genéricos de búsqueda, generamos un concern donde se incluyen los módulos necesarios de Elasticsearch y la definición de los métodos de búsqueda que se quieran tener por defecto.
Teniendo el concern definido, suponiendo que necesitemos indexar un modelo como puede ser User, que tiene atributos como first_name , last_name , useraname y email , podemos definirlo de la siguiente forma:
class User < ActiveRecord::Base ... include Searchable after_save { Indexer::User.perform_async(self.id, :index) } after_destroy { Indexer::User.perform_async(self.id, :delete) } index_name "users-#{Rails.env}" ## Se define el numero de shards y replicas del indice, junto con un analizador que utiliza filtros para ingles, español, acentos y opciones de autocompletado. settings index: { number_of_shards: 2, number_of_replicas: 2 }, analysis: { analyzer: { default: { type: 'custom', tokenizer: 'standard', filter: %w(standard uppercase lowercase word_delimiter my_ascii_folding english_stemmer spanish_stemmer autocomplete_filter) } }, filter:{ my_ascii_folding: { type: 'asciifolding', preserve_original: true }, english_stemmer: { type: 'stemmer', name: 'english' }, spanish_stemmer: { type: 'stemmer', name: 'light_spanish' }, autocomplete_filter: { type: 'edge_ngram', min_gram: 3, max_gram: 20 } } } do mappings dynamic: false do indexes :first_name, type: :string indexes :last_name, type: :string indexes :username, type: :completion, payloads: true indexes :email, type: :string end end def as_indexed_json(options={}) as_json(only: [:id, :first_name, :last_name, :username, :email]) end ... end
Además de la configuración del índice, la inclusión de analizadores y filtros, y el mapeo de atributos, se han añadido los callbacks after_save y after_destroy, que lanzan llamadas asíncronas para actualizar registros de Elasticsearch en el caso de que un registro de tu modelo en Rails se modifique y/o elimine.
Por otro lado, index_name “users-#{Rails.env}” permite asignar el nombre del índice según el entorno y así no mezclar datos cuando tengamos varias aplicaciones ejecutándose en distintos entornos.
Por ultimo, definiendo el método as_indexed_json , generamos el json del modelo que Elasticsearch recibirá.
Concluyendo, después del primer post donde introducimos los conceptos básicos de Elasticsearch, en este segundo hemos visto una aproximación entre Elasticsearch y Rails. Mostramos cómo integrar con Ruby on Rails un motor de búsqueda como es Elasticsearch; el cuál, a parte de ofrecernos la posibilidad de realizar búsquedas en los modelos de Rails, permite analizar dichos datos.