Inicio > Plugins, Refactorización > Buscando con Searchlogic

Buscando con Searchlogic

Searchlogic es un plugin muy útil que nos permite buscar en modelos de forma mucho más ágil, por medio de named scopes (introducidos en Rails 2.x). Los named scopes (la mejor traducción no-literal que se me ocurre es: “filtros de alcance“) son básicamente filtros SQL con nombre, pero esto es más fácil de mostrar que de explicar. La principal ventaja de estos filtros es que nos permiten hacer nuestro código más DRY (Don’t Repeat Yourself, es decir: menos repetitivo), lo que a su vez acelera muchísimo el tiempo de desarrollo.

1- Instalación de Searchlogic

Por supuesto, la instalación de Searchlogic es muy simple:

sudo gem install searchlogic

Una vez instalado el gem agregamos la siguiente línea a nuestro archivo de configuración de entorno:

#config/environment.rb
config.gem "searchlogic"

Aunque es más recomendable el método anterior, también se puede instalar como plugin:

script/plugin install git://github.com/binarylogic/searchlogic.git

2- Conceptos Básicos

Searchlogic es un plugin muy completo y flexible: nos brinda filtros y una clase para encapsularlos, métodos de ordenamiento, helpers para nuestras vistas, integración con otros plugins (como WillPaginate), etc. No vamos a cubrir todos sus aspectos, por lo menos no en esta ocasión; pero sí conoceremos algunos conceptos básicos que nos darán el puntapié necesario para animarnos a usarlo en nuestras aplicaciones. Comenzaremos viendo su concepto central: filtros.

Filtros (Named Scopes)

Una vez instalado, Searchlogic nos permite utilizar cierto número de filtros predeterminados, que en realidad son los famosos named_scopes de Rails. En la documentación hay una lista completa de todas las opciones que podemos usar; aquí conoceremos sólo algunas.

Searchlogic no necesita nada de configuración: podemos utilizar sus filtros directamente. Supongamos que tenemos un modelo llamado User con algunos de los campos más comunes: username, email, password y age (nombre de usuario, email, contraseña y edad, respectivamente). También ya tenemos varios usuarios registrados, y lo que queremos es ver cuáles de los usuarios registrados menores a 20 años tienen una cuenta de email dentro del Reino Unido (termina en “.co.uk“). Para ello podemos levantar nuestra consola rails y simplemente ingresar lo siguiente:

User.age_lt(20).email_ends_with(".co.uk")

Searchlogic nos devuelve una lista con los usuarios que cumplen con dichos criterios. Podemos concatenar toda la cantidad de filtros que sea necesario, uno tras otro. El método age_lt significa age_less_than (edad menor a); y como ésta, existen también otras formas de comparación similares, junto con sus abreviaturas. En la documentación podemos encontrar todas las posibilidades.

Es importante notar que podemos usar estos filtros con cualquier campo. Si por ejemplo tuviéramos un campo que sea login_count para almacenar la cantidad de veces que el usuario inició sesión en nuestro sitio, podemos buscar todos los usuarios del Reino Unido que hayan entrado más de 100 veces o más de la siguiente forma:

User.email_ends_with(".co.uk").login_count_gte(100)
# gte es la abreviatura de "greater than or equal"
# y significa "mayor o igual a".

Negando Filtros

De la misma forma que podemos buscar los usuarios cuyos emails terminen con “.co.uk”, también podemos buscar aquellos cuyos emails no terminen con dicha cadena:

User.email_does_not_end_with(".co.uk")

#Otro ejemplo:
User.username_not_like("juan")
#devuelve todos aquellos cuyo nombre de usuario no contiene la cadena "juan"

Podemos hacer lo mismo con cualquier otro filtro.

Combinando Filtros

Puede ser el caso que querramos buscar a todos los usuarios cuyo nombre (name) o nombre de usuario (username) contengan la cadena “juan”. Para ello podemos combinar los filtros en un mismo llamado por medio de la palabra “or”, de la siguiente forma:

User.name_or_username_like("juan")

Todos o Cualquiera

Los filtros aceptan también más de un elemento, pero para ello debemos usar alguno de los modificadores especiales para múltiples parámetros: all (todos) o any (cualquiera). Veamos algunos ejemplos:

#Busca los usuarios con cuenta de email del Reino Unido o de Argentina:
User.email_ends_with_any(".co.uk", ".ar")

#Busca los usuarios que tengan "Juan" y "Perez" dentro de su nombre:
User.name_like_all("Juan", "Perez")
#Algunos resultados pueden ser:
#  Juan Perez Aguilar
#  Juan Marcos Perez

#También podemos pasar un Array:
User.name_like_all(["Juan", "Perez"])

Ordenamiento

En SQL es posible ordenar los resultados que obtenemos, y Searchlogic no se ha olvidado de ello. Para ordenar los resultados anteriores por nombre de usuario podemos hacer lo siguiente:

#Ordena por nombre de usuario desde la A a la Z
User.email_ends_with(".co.uk").login_count_gte(100).ascend_by_username
#Ordena por nombre de usuario desde la Z a la A
User.email_ends_with(".co.uk").login_count_gte(100).descend_by_username

De forma similar a los demás filtros, “ascend_by” y “descend_by” son filtros dinámicos, y en su caso particular deben ser precedidos por el nombre del campo a ordenar (username).

Rendimiento

De aquí nos puede surgir la duda de si Searchlogic implementa todos los named scopes posibles para cada modelo de forma predeterminada. Por suerte esto no es así, sino que los crea a medida que los vamos utilizando. Además, una vez creado el named scope, Searchlogic no lo vuelve a crear si lo necesitamos de nuevo. Por lo tanto, no hay que temer por nuestros recursos: el rendimiento con Searchlogic es muy similar al rendimiento que obtendríamos si usáramos nuestros propios named scopes.

Algo más que vale destacar en cuanto a rendimiento: Searchlogic no ejecuta ningún query en la base de datos sino hasta que llamemos al último filtro en la cadena. En el caso anterior, por ejemplo, la siguiente consulta SQL se ejecuta sólo luego de aplicarse el filtro “login_count_gte“:

SELECT * FROM `users` WHERE ((users.login_count >= 100) AND (users.email LIKE '%.co.uk'))

3- La clase Search

Searchlogic también nos permite buscar utilizando una clase llamada Search. Esta clase nos sirve para añadir o quitar filtros a una búsqueda, cuyas condiciones quedan almacenadas en la instancia. Igual que en el caso anterior, Searchlogic ejecuta un sólo query a la hora de buscar. En este caso, sin embargo, lo ejecuta luego de que llamemos a algunos de los métodos de búsqueda. Veamos un ejemplo:

@search = User.search #Crea el objeto Search para el modelo User
@search.age_lt(20)
@search.email_ends_with(".co.uk")
# Hasta aquí todavía no se ha hecho ningún query a la base de datos

@search.all #ejecuta el query y devuelve todos los registros
@search.first #ejecuta el query y devuelve sólo el primer registro
@search.count #devuelve el número de registros encontrados
#etc.

Otra posibilidad menos atractiva pero potencialmente más flexible es acceder directamente al Hash de filtros, de la siguiente manera:

@search = User.search #Crea el objeto Search
@search.conditions[:age_lt] = 20 # notar la diferencia de sintaxis
@search.conditions[:name_begins_with] = "a"
# Otra forma que puede ser útil en algunos casos:
@search.conditions.merge!({:email_ends_with => ".co.uk"})
#Ahora @search contiene el siguiente hash:
# {:age_lt=>20, :email_ends_with=>".co.uk", :name_begins_with=>"a"}
@search.all #devuelve todos los resultados filtrados

Para quitar un filtro del objeto Search, basta con llamar su método delete y pasarle la condición que queremos borrar:

@search.delete(:name_begins_with)
#=> {:age_lt=>20, :email_ends_with=>".co.uk"}

4- Búsquedas Avanzadas

Hay varias formas de búsqueda que requieren utilizar conceptos un poco más avanzados, como los JOINS o INCLUDES de SQL. Sin embargo, Searchlogic nuevamente nos facilita la tarea con sus potentes herramientas. Para esta sección vamos a suponer que los usuarios de la tabla Users “tienen muchos” Permissions (permisos) a través de una relación habtm (explicar los conceptos básicos de este tipo de relación escapa a nuestro propósito en este momento).

Buscando en clases Asociadas

De acuerdo con la relación que tenemos entre los usuarios y sus permisos, supongamos que queremos buscar a todos los usuarios que tengan permiso para comentar:

@search = User.search
@search.permissions_name_is("comment")
@search.all # ejecuta la consulta

La consula SQL generada es:

SELECT `users`.* FROM `users` INNER JOIN `permissions_users` ON `permissions_users`.user_id = `users`.id INNER JOIN `permissions` ON `permissions`.id = `permissions_users`.permission_id WHERE (permissions.name = 'comment')

En otras palabras, podemos acceder a la descendencia de clases simplemente concatenándola al llamar al filtro. Otro ejemplo, con más niveles de descendencia, en donde los usuarios tienen un rol que a su vez tiene muchos permisos, y en donde queremos encontrar todos los usuarios que puedan comentar:

@search = User.search
@search.role_permissions_name_is("comment")
@search.all # ejecuta la consulta

Si queremos resolver un problema de consultas N+1, también podemos especificar un INCLUDE en alguno de los métodos de búsqueda:

@results = @search.all(:include => :permissions) # ejecuta dos consultas SQL
@results.first.permissions # no ejecuta ninguna consulta SQL

Las consultas SQL ejecutadas por la primera línea:

SELECT `users`.* FROM `users` INNER JOIN `permissions_users` ON `permissions_users`.user_id = `users`.id INNER JOIN `permissions` ON `permissions`.id = `permissions_users`.permission_id WHERE (permissions.name = 'comment')
SELECT `permissions`.*, t0.user_id as the_parent_record_id FROM `permissions` INNER JOIN `permissions_users` t0 ON `permissions`.id = t0.permission_id WHERE (t0.user_id IN (1,200))

Procedimientos de Filtro (Scoped Procedures)

En nuestra aplicación queremos filtrar a los usuarios que no sean del Reino Unido (UK) y cuya edad se encuentre entre los 12 y 20 años inclusive (es decir, que sean adolescentes). Supongamos que queremos hacer esto en muchos lugares de nuestra aplicación, pero volver a llamar a ambos filtros uno por uno, definitivamente, no es muy DRY.

En casos como este, podemos recurrir a los Scope Procedures, que básicamente significa que podemos darle un nombre a esa combinación de filtros y añadirla como un procedimiento de nuestro modelo User. Para el ejemplo, vamos a nombrar esa combinación de filtros uk_teens (“adolescentes del Reino Unido” en inglés):

#Para crear el Scope Procedure
@search = User.search
@search.email_does_not_end_with(".co.uk")
@search.age_lt(20).age_gt(12)
User.scope_procedure :uk_teens, lambda{ @search }

#Otra forma de hacerlo, pero no tan limpia:
User.scope_procedure :uk_teens, lambda { User.email_does_not_end_with(".co.uk").age_lt(20).age_gt(12) }

# Finalmente, para utilizar el Scope Procedure
User.uk_teens.all # devuelve todos los resultados
User.search(:uk_teens => true) # lo mismo que lo anterior

Es importante notar el uso del método lambda, que lo que hace es recibir un bloque que va a ser ejecutado cada vez que llamamos al Scoped Procedure. Explicar esto a fondo escapa del ámbito de este tutorial. Otro elemento importante es el parámetro que le pasamos al método search, que le dice a Searchlogic que quiere utilizar el método uk_teens como si fuera un filtro más. Podemos utilizar la misma convención para llamar a cualquier named_scope que hayamos declarado manualmente.

También es importante advertir que el ejemplo en la documentación oficial no funciona (tal como está a la fecha de publicación de este artículo). El ejemplo en este artículo demuestra el uso correcto, a mi entender, de los Scoped Procedures.

Conclusión

En este tutorial, basado en la documentación oficial de Searchlogic, cubrimos los aspectos básicos de Searchlogic y algunos de los avanzados. Como fue mencionado antes, el plugin es muy flexible, y los usos que le podemos dar son tan variados como nuestra imaginación. Decidí no cubrir la parte de búsqueda con formularios, explicada en la documentación y en el Railscast de Searchlogic, porque es útil sólo para casos muy específicos (vistas de administrador) y se va del eje central de este artículo.

Por favor, no dudes en dejar un rápido comentario ante cualquier duda, corrección o sugerencia! También podés subscribirte al feed o por email (en la barra de navegación) para recibir notificaciones de los últimos artículos.

About these ads
  1. 13/01/2010 en 17:46 | #1

    Excelente tutorial!
    Gracias por el aporte

  2. Soledad
    31/10/2010 en 20:52 | #3

    Hola gabi como estas? te comento estoy haciendo un trabajo para obtener mi titulo, y basicamente tengo un modelo de productos y otro de categorias, y deseo hacer que en el front end, al hacer click en una categoria me busque los productos de esa categoria. mi consulta es saber si con esta gem puedo hacerlo. y si tenes idea como como, te agradeceria que me orientes un poco, soy bastante nueva en rails, saludos y gracias por tu aporte!

    • 08/11/2010 en 08:16 | #4

      Hola Soledad, un gusto poder ayudarte!
      Siendo que tenés un modelo de productos y uno de categorías, lo primero por supuesto sería asegurarse que estén correctamente asociados en ambas direcciones por medio de las llamadas has_many. Una vez que eso funciona correctamente, deberías poder conseguir todos los productos asociadas a una categoría mediante category.products. Este comportamiento no es de Searchlogic sino propio de ActiveRecord.
      Espero que te sirva! Si te puedo ayudar en algo más no dudes en contactarme.

  1. No trackbacks yet.

Deja un comentario

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s

Seguir

Recibe cada nueva publicación en tu buzón de correo electrónico.

%d personas les gusta esto: