homeASCIIcasts

278: Búsquedas con Sunspot 

(view original Railscast)

Other translations: En Ja Fr

Other formats:

Written by Juan Lupión

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.

Nuestro blog.

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.

/Gemfile

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.

/app/models/article.rb

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.

/app/views/articles/index.html.erb

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

/app/controllers/articles_controller.rb

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.

El listado de artículos filtrados.

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.

/app/models/article.rb

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.

/app/models/article.rb

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.

Los artículos filtrados por mes de 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.

/app/models/article.rb

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.

/app/models/article.rb

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.

/app/controllers/articles_controller.rb

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.

/app/models/article.rb

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.

/app/controllers/articles_controller.rb

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:

/app/views/articles/index.html.erb

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

/app/controllers/articles_controller.rb

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.

Artículos filtrados usando el mes como faceta.

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.

En la barra lateral aparecen los meses de publicación de los artículos buscados.

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.