#278 Search with Sunspot
- Download:
- source codeProject Files in Zip (231 KB)
- mp4Full Size H.264 Video (21.6 MB)
- m4vSmaller H.264 Video (11.8 MB)
- webmFull Size VP8 Video (16.1 MB)
- ogvFull Size Theora Video (28.3 MB)
Con Sunspot podemos añadir búsqueda de texto completo en nuestras aplicaciones Ruby. Utiliza Solr en segundo plano y tiene muchas características interesantes. En este episodio veremos cómo usar Sunspot para añadir este tipo de búsquedas en una aplicación Rails, utilizando la sencilla aplicación de blog que hemos visto en los episodios anteriores.
En esta aplicación hay una página que muestra varios artículos y en la que queremos implementar la posibilidad de buscar texto en ellos. Esto se puede complicar considerablemente si utilizamos SQL para hacerlo, así que el mejor enfoque suele ser utilizar un motor de búsqueda de texto completo como Sunspot.
Instalación de Sunspot
Sunspot se instala como una gema de la forma habitual, añadiéndolo al Gemfile
y ejecutando bundle
.
source 'http://rubygems.org' gem 'rails', '3.0.9' gem 'sqlite3' gem 'nifty-generators' gem 'sunspot_rails'
Una vez que hayamos instalado la gema y sus dependencias tenemos que generar el fichero de configuración de Sunspot, lo que podemos hacer ejecutando
$ rails g sunspot_rails:install
Esta orden crea un fichero YAML en /config/sunspot.yml
. No tenemos que hacer por el momento ningún cambio en los valores configurados por defecto.
No tenemos que instalar Solr por separado porque viene incluido en la propia gema por lo que funciona tal cual, lo que hace que su uso en tiempo de desarrollo sea bastante más cómodo . Para arrancar Solr tenemos que ejecutar
$ rake sunspot:solr:start
Los que estén ejecutando OS X Lion y no hayan instalado el entorno de ejecución de Java podrán hacerlo al lanzar la orden. La orden también creará más archivos de configuración avanzada, que no veremos por el momento y cuyo funcionamiento podemos consultar en la documentación.
Uso de Sunspot
Una vez que hemos instalado Sunspot podemos utilizarlo sobre nuestro modelo Article
. Para añadir la búsqueda de texto completo utilizaremos el método searchable
.
class Article < ActiveRecord::Base attr_accessible :name, :content, :published_at has_many :comments searchable do text :name, :content end end
Este método recibe un bloque en cuyo interior definiremos los atributos sobre los que queremos realizar las búsquedas, para que Sunspot pueda saber qué datos tiene que indexar. Podemos usar el método text
para definir los atributos sobre los que se lanzarán búsquedas de texto completo. En el caso de nuestros artículos lo haremos contra los campos name
y content
.
Sunspot indexará automáticamente los nuevos registros, pero no los ya existentes. Podemos decirle que vuelva a generar todo el índice con
$ rake sunspot:reindex
Una vez que todos los artículos se encuentren en nuestra base de datos Solr ya podemos añadir un campo de búsqueda en la parte superior de la página.
<% title "Articles" %> <%= form_tag articles_path, :method => :get do %> <p> <%= text_field_tag :search, params[:search] %> <%= submit_tag "Search", :name => nil %> <% end %> <!-- rest of view omitted -->
Este formulario se envía a la acción index
utilizando GET por lo que los parámetros aparecerán en la URL. Para realizar una búsqueda con Sunspot invocaremos a search
sobre el modeloy le pasaremos un bloque, en el cual podemos invocar a varios métodos para gestionar búsquedas más complejas. Empezaremos con el método fulltext
y le pasaremos los parámetros recibidos por el formulario. Por último le asignaremos los resultados a la variable @search
a la cual podemos invocar el método results
para recuperar los artículos.
def index @search = Article.search do fulltext params[:search] end @articles = @search.results end
Ya podemos probar esto recargando la página de artículos y buscando una palabra cualquiera. Cuando lo hagamos veremos el listado de los artículos que la contienen.
La búsqueda devuelve un listado de los artículos que contienen el término de búsqueda, ya se encuentre en las columnas nombre
o en su content
.
Por supuesto, en el bloque searchable
podemos hacer muchas más cosas. Podemos utilizar boost
para ponderar los resultados, de forma que si hay una correspondencia en el título del artículo tenga más peso que si se encuentra en el contenido.
class Article < ActiveRecord::Base attr_accessible :name, :content, :published_at has_many :comments searchable do text :name, :boost => 5 text :content end end
Esto es muy importante si queremos ordenar los resultados por relevancia. En este caso los artículos cuyos título contengan el término de búsqueda aparecerán antes en los resultados.
Los atributos que aparezcan en el bloque searchable
no tienen por qué ser columnas reales de la base de datos: podemos utilizar cualquier método definido en el modelo. Por ejemplo podemos crear una columna publish_month
que devuelva una cadena conteniendo el nombre del mes y el año de publicación del artículo, y luego buscaremos sobre este atributo como si estuviese almacenado en la base de datos.
class Article < ActiveRecord::Base attr_accessible :name, :content, :published_at has_many :comments searchable do text :name, :boost => 5 text :content, :publish_month end def publish_month published_at.strftime("%B %Y") end end
Antes de poder hacer búsquedas sobre esta nueva columna tenemos que volver a indexar los registros ejecutando rake sunspot:reindex
, y una vez que lo hagamos podemos buscar los artículos por el mes de su publicación.
Como alternativa a la creación de un método podemos pasar un bloque y realizar la búsqueda contra los resultados de dicho bloque. Por ejemplo, un artículo tiene muchos comentarios por lo que queremos tener la posibilidad de buscar en dichos comentarios.
class Article < ActiveRecord::Base attr_accessible :name, :content, :published_at has_many :comments searchable do text :name, :boost => 5 text :content, :publish_month text :comments do comments.map(&:content) end end def publish_month published_at.strftime("%B %Y") end end
El contenido dentro del bloque es una instancia de Article
por lo que en su interior podemos recuperar los comentarios y mapear el contenido de cada comentario. Aunque esto devuelve un array Sunspot lo gestionará e indexará todos los comentarios de forma que luego se pueda buscar sobre ellos.
Búsqueda contra atributos
¿Y si queremos añadir algún tipo de búsqueda que vaya más allá de la búsqueda de texto completo, tal vez buscar algún atributo específico? Para esto le podemos decir el tipo del atributo que queremos buscar: cadena, entero, flotante o incluso una fecha. Para que el atributo published_at
sea indexable podemos utilizar el método time
.
class Article < ActiveRecord::Base attr_accessible :name, :content, :published_at has_many :comments searchable do text :name, :boost => 5 text :content, :publish_month text :comments do comments.map(&:content) end time :published_at end def publish_month published_at.strftime("%B %Y") end end
Podemos utilizar esto en ArticlesController
para restringir las búsquedas sólo a aquellos artículos cuya fecha published_at
sea anterior a la hora actual, usando el método with
.
def index @search = Article.search do fulltext params[:search] with(:published_at).less_than(Time.zone.now) end @articles = @search.results end
Con esto la búsqueda ya no devolverá artículos que no hayan sido publicados todavía. Los atributos que se pueden pasar están bien documentados en el wiki de Sunspot.
Búsqueda facetada
La búsqueda facetada permite filtrar los resultados de búsqueda basándonos en ciertos atributos tales como el mes en que se publicó el artículo. Supongamos que queremos un listado de los meses en los que hay artículos publicados. Cuando hagamos clic en uno de estos enlaces se filtrará el listado de artículos de forma que sólo aparecerán los que hayan sido publicados en dicho mes.
Para esto primero tenemos que añadir un atributo string
al bloque searchable
de nuestro método publish_month
.
class Article < ActiveRecord::Base attr_accessible :name, :content, :published_at has_many :comments searchable do text :name, :boost => 5 text :content, :publish_month text :comments do comments.map(&:content) end time :published_at string :publish_month end def publish_month published_at.strftime("%B %Y") end end
Podemos convertir esto en una faceta llamando a facet
en el bloque search
de ArticlesController
.
def index @search = Article.search do fulltext params[:search] with(:published_at).less_than(Time.zone.now) facet(:publish_month) end @articles = @search.results end
Ya podemos listar dichas facetas en la página index
con el siguiente código:
<div id="facets"> <h3>Published</h3> <ul> <% for row in @search.facet(:publish_month).rows %> <li> <% if params[:month].blank? %> <%= link_to row.value, :month => row.value %> (<%= row.count %>) <% else %> <strong><%= row.value %></strong> (<%= link_to "remove", :month => nil %>) <% end %> </li> <% end %> </ul> </div>
En este código iteramos sobre los elementos de la faceta publish_month
y los mostramos. Si invocamos a .facet
en nuestro objeto @search
y le pasamos el atributo por el que queremos filtrar la faceta (en este caso :publish_month
) y luego invocamos a .rows
devolverá todas las opciones de facetas para dicho atributo.
Si invocamos row.value
devuelve el valor de dicho atributo, esto es “January 2011”. También podemos invocar row.count
para devulver el número de artículos que coincidan con dicho valor. Si hay un parámetro month
en la cadena de búsqueda mostraremos el valor así como un enlace “remove” que quitará dicho parámetros, con lo que tenemos la funcionalidad de que podemos especificar una faceta determinada y pasarla mediante el parámetro month
.
Si ahora recargamos la página (tras volver a generar los índices) veremos un listado de facetas en un panel, cada una de las cuales muestra un mes y el número de artículos publicados en dicho mes. Si escogemos un mes veremos que aparece como parámetro month
en la URL pero los artículos no se filtran. Para corregirlo, tenemos que añadir otra llamada a with
en el controlador para que filtre por el mes si aparece como parámetro month
.
def index @search = Article.search do fulltext params[:search] with(:published_at).less_than(Time.zone.now) facet(:publish_month) with(:publish_month, params[:month]) ↵ if params[:month].present? end @articles = @search.results end
Cuando ahora seleccionemos un mes el listado se filtrará correctamente y sólo aparecerán los artículos publicados en dicho mes.
Si se pulsa en el enlace “remove” volveremos al listado completo. Esto funciona también en conjunción con los resultados de búsquedaasí que si buscamos una cadena de texto el listado del panel mostrará los meses con artículos en los que aparezca dicha cadena.
Las facetas son un gran complemento a las búsquedas de texto completo.
Con esto finalizamos este episodio. Sunspot permite añadir búsqueda de texto completo en nuestras aplicaciones Rails y tiene muchas funcionalidades extra que no hemos visto hoy. Para más información, se puede consultar el wiki de Sunspot.