#306 ElasticSearch Part 1
- Download:
- source codeProject Files in Zip (83.4 KB)
- mp4Full Size H.264 Video (31.6 MB)
- m4vSmaller H.264 Video (16.1 MB)
- webmFull Size VP8 Video (14.5 MB)
- ogvFull Size Theora Video (44.2 MB)
A continuación se muestra una página de una aplicación Rails que muestra un listado de artículos. Queremos implementar la funcionalidad de búsqueda y, como los artículos muestran sobre todo contenido de texto, utilizaremos un motor de búsqueda textual en lugar de buscar cadenas mediante consultas SQL.
Ya hemos visto este tema en otros episodios. En el episodio 120 usamos Thinking sphinx y en el episodio 278 usamos Sunspot con Solr. En este episodio vamos a utilizar Elasticsearch para implementar las búsquedas de texto en nuestra aplicación.
Elasticsearch es un motor de búsqueda textual basado en Apache Lucene, igual que Solr. Dispone de una API REST con la que podemos comunicarnos mediante JSON. Elasticsearch no es un proyecto específico de Ruby, por lo que usaremos una gema llamada Tire para comunicarnos con Elasticsearch. Tire se puede utilizar en cualquier proyecto Ruby, pero implementa una interesante funcionalidad de modelos que hace que sea muy sencillo integrarlo con Rails. Hay incluso una plantilla de aplicación Rails que podemos utilizar para crear una nueva aplicación que utilice Elasticsearch.
$ rails new searchapp -m https://raw.github.com/karmi/tire/master/examples/rails-application-template.rb
Con esto configuraremos una nueva aplicación Rails con Elasticsearch y Tire. Si la plantilla no detecta una instalación activa de Elasticsearch lo descargará e instalará automáticamente, realizando una configuración específica para esta aplicación. Después de que finalice la orden podemos visitar http://localhost:3000/
donde veremos una aplicación básica que nos permite utilizar Elasticsearch para buscar en algunos registros. Merece la pena examinar el código fuente de esta plantilla porque es un buen ejemplo de todo lo que se puede hacer con una plantilla de aplicación en Rails.
Cómo añadir Elastic Search a nuestra aplicación
La aplicación de ejemplo es muy interesante pero, ¿cómo podemos utilizar Elasticsearch en nuestra propia aplicación? El primer paso es instalarlo. Los que estén usando Homebrew en OS X lo tienen muy fácil; los que no disponen de instrucciones de descarga en la web de Elasticsearch .
$ brew install elasticsearch
Tras la instalación de Elasticsearch con Homebrew veremos instrucciones sobre cómo dejarlo funcionando. Lo arrancaremos con esta orden (nótese que puede ser diferente dependiendo de la versión de Elasticsearch):
$ elasticsearch -f -D es.config=/usr/local/Cellar/elasticsearch/0.18.1/config/elasticsearch.yml
Con esta orden se arranca el servidor en el puerto 9200, por donde podremos comunicarnos manualmente si queremos mediante la API REST basada en JSON. Como vamos a usar Tire, lo siguiente que haremos será instalarlo. Como es habitual, esto se hace añadiendo la gema al Gemfile
de la aplicación y ejecutando bundle
.
source 'http://rubygems.org' gem 'rails', '3.1.3' # Bundle edge Rails instead: # gem 'rails', :git => 'git://github.com/rails/rails.git' gem 'sqlite3' # Gems used only for assets and not required # in production environments by default. group :assets do gem 'sass-rails', '~> 3.1.4' gem 'coffee-rails', '~> 3.1.1' gem 'uglifier', '>= 1.0.3' end gem 'jquery-rails' gem 'tire'
Podemos añadir Tire en cualquier modelo sobre el que queramos hacer búsquedas añadiendo dos módulos. Queremos buscar en los artículos por lo que añadiremos lo siguiente al modelo Article
de nuestra aplicación:
class Article < ActiveRecord::Base belongs_to :author has_many :comments include Tire::Model::Search include Tire::Model::Callbacks end
El primero de estos dos módulos añade varios métodos de búsqueda e indexado, mientras que el segundo añade funciones asíncronas que se ejecutan para actualizar el índice según se crean, actualizan o borran los artículos.
Ya tenemos varios artículos en la base de datos de nuestra aplicación, y estos no estarán incluidos en el índice. Pero como todos los artículos ya están definidios en el fichero seed.rb
de nuestra aplicación, si volvemos a cargar estos registros serán indexados automaticamente.
$ rake db:setup
Cómo añadir el formulario de búsqueda
Una vez que se hayan indexado nuestros artículos podemos añadir un formulario para buscar en la página de los artículos. La plantilla de esta página presenta el siguiente aspecto:
<h1>Articles</h1> <div id="articles"> <% @articles.each do |article| %> <h2> <%= link_to article.name, article %> <span class="comments">(<%= pluralize(article.comments.size, 'comment') %>)</span> </h2> <div class="info"> by <%= article.author.name %> on <%= article.published_at.strftime('%b %d, %Y') %> </div> <div class="content"><%= article.content %></div> <% end %> </div>
Añadamos el siguiente sencillo formulario bajo el encabezado de la página. Este formulario nos llevará a la misma página de artículos que estamos viendo utilizando GET.
<%= form_tag articles_path, method: :get do %> <p> <%= text_field_tag :query, params[:query] %> <%= submit_tag "Search", name: nil %> </p> <% end %>
Cuando se envíe este formulario de búsqueda invocará la acción index
de ArticlesController
, que ahora mismo devuelve todos los artículos. Pero modificaremos el código para comprobar si está presente el parámetro query
en cuyo caso utilizaremos el método search
de Tire.
def index if params[:query].present? @articles = Article.search(params[:query]) else @articles = Article.all end end
Si ahora recargamos la página veremos el formulario de búsqueda, pero si intentamos enviar el formulario con un término de búsqueda veremos un error.
El error lo provoca la llamada a article.comments.size
para mostrar el número de comentarios que tiene cada artículo, por lo que parece que las asociaciones no funcionan en los artículos devueltos por Tire.
Tire intenta minimizar en lo posible el acceso a la base de datos, por lo que cuando llamamos a Article.search
lo que se devuelven no son los modelos de ActiveRecord sino un conjunto de resultados obtenidos a partir de lo que hay almacenado en el índice. El índice no sabe acerca de la asociación de comentarios, por lo que no hay forma de cargarlos. Para corregir esto podemos utilizar la opción load
para invocar a search
y decirle a Tire cómo cargar los registros de la base de datos.
def index if params[:query].present? @articles = Article.search(params[:query], load: true) else @articles = Article.all end end
La búsqueda devolverá ahora los resultados correctos:
Por supuesto sería mucho mejor si todos los datos que necesitásemos estuviesen contenidos en el índice de forma que no tuviésemos que utilizar la opción load
. Esto es posible, pero lo veremos en el próximo episodio. Lo que veremos a continuación es cómo personalizar más la consulta mediante otras opciones, lo que se hace redefiniendo el método search
del modelo Article
para que acepte un hash de parámetros que le pasa el usuario.
def self.search(params) tire.search(load: true) do query { string params[:query]} if params[:query].present? end end
Como estamos redefiniendo el método search
de Tire tenemos que utilizar tire.search
para llamar al método redefinido y, como queremos recuperar los modelos reales, tenemos que utilizar la opción load: true
. En lugar de pasar los parámetros de búsqueda directamente a este método hemos utilizado un bloque para poder personalizar aún más la consulta con otras opciones. En este bloque invocaremos a query
y le pasamos otro bloque, en el cual pasamos los parámetros al método string
pero sólo si dichos parámetros aparecen.
Se puede ahora simplificar ArticlesController
para que invoque nuestro nuevo método search
en el modelo pasándole el hash de parámetros:
def index @articles = Article.search(params) end
Si ahora recargamos la página de artículos todo seguirá funcionando igual.
Pero hay un artículo que no queremos que aparezca en los resultados porque tienen una fecha de publicación en el futuro. Cambiaremos la búsqueda para que no incluya aquellos artículos que tengan fecha de publicación en el futuro. Para hacer esto tan sólo tenemos que añadir un filtro al bloque search
del modelo.
def self.search(params) tire.search(load: true) do query { string params[:query]} if params[:query].present? filter :range, published_at: {lte: Time.zone.now } end end
El primer argumento es el tipo de filtro que buscamos, en este caso un filtro de rango. Lo siguiente que queremos es pasar un hash que contiene los atributos sobre los que queremos filtrar. En este caso filtramos por published_at
e incluimos sólo aquellos artículos cuyo atributo published_at
sea menor o igual que la hora actual.
Cabe preguntarse qué otras opciones hay disponibles a la hora de personalizar las búsquedas. Hay algo de documentación, pero es bastante dispersa. Un buen sitio por donde empezar es el README de Tire, aunque el comienzo es bastante confuso porque en él se discute el indexado y el mapeado, que en principio no nos interesan demasiado porque estamos haciendo un mapeado dinámico. También hay documentación adicional proporcionada por Tire que merece la pena leer.
Es buena idea también leer la documentación de Elasticsearch porque la mayoría de las opciones de Tire coinciden directamente con opciones de Elasticsearch. La página sobre el DSL de consultas contiene una sección completa sobre las posibilidades de filtrado incluyendo el filtro de rangos que hemos utilizando antes, con todas sus opciones. Los fragmentos de código aparecen en JSON pero es muy fácil adaptarlos para su uso con Tire.