Publicidad:
La Coctelera

ElasticSearch y Couchdb river

En este post vamos a ver como incorporar un motor de búsqueda en una aplicación (Rails) para indexar documentos almacenados en Couchdb.

ElasticSearch es un motor de búsqueda construido sobre Lucene, es Open Source (Apache 2), es distruido y presenta un interfaz Restful. ElasticSearch cuenta con una funcionalidad denominada River. River es un sistema de plugins que una vez instalado en ElasticSearch permite extraer datos (o enviarle datos) que será indexados e incoporados al cluster de ElasticSearch, para su posterior consulta. ElasticSearch cuenta con 4 diferentes rivers listos para ser instalados: Couchdb, RabbitMQ, Twitter y Wikipedia. Aquí nos centraremos en el uso del river para Couchdb.

El river para Couchdb se basa en una funcionalidad de Couchdb denominada "Continuous Changes API". Una vez te conectas a este API y manteniendo la conexión abierta Couchdb te enviará las modificaciones y seguirá manteniendo la conexión abierta para enviarte las siguientes modificaciones hasta que decidas cerrar la conexión. Siempre tendremos una conexión abierta (por river definido) pero Couchdb es capaz de manejar un gran número de conexiones sin problemas.

Como instalamos ElasticSearch y el river de Couchdb:

El propio river de Couchdb con la configuración que le hemos dado se enganchará a la siguiente url, para ir recibiendo las modificaciones que hagamos en la base de datos de couchdb e ir indexando documentos.

http://localhost:5984/couchdb_myapp_development/_changes?feed=continuous&include_docs=true&heartbeat=10000

Sin embargo con esta configuración incluimos en el mismo índice todos los documentos de Couchdb, si queremos establecer un indice para dos tipos de documentos como por ejemplo Product y Person, debemos establecer un filtro a la hora de crear el river. Este filtro debe hacer referencia al nombre de un filtro especificado a la hora de definir un design document

Debemos tener en cuenta que nuestros documentos de couchdb tienen especificado un atributo 'type' que permite realizar el filtrado.

Por último crearemos un documento de tipo 'Product' y realizaremos una búsqueda

bit.ly y prototype acortando urls

Esto va rápido, que luego soy un pesado.

Queremos utilizar un servicio para acortar las urls de nuestra aplicación a la hora de compartir dichas urls en redes sociales.

Vamos a hacerlo directamente con JavaScript (lo podríamos hacer en servidor a la hora de definir la url del recurso, pero no es el caso). Tenemos soluciones con jQuery y MootTools. Pero y con Prototype ("ese gran abandonado").

Prototype no tiene soporte para hacer peticiones ajax a otros dominios así que utilizaremos Ajax.JSONRequest para poder hacer este tipo de peticiones (con razón se abandona :P).

Así que simplemente nos montamos una petición con los parámetros oportunos al api de bit.ly y a aquí va el ejemplo

Ahh se me olvidaba es viernes :P y jsfiddle.net lo peta!

Sencillo renderer de CSV en Rails 3

En el artículo anterior vimos como generar texto en formato CSV en ruby 1.9.2 y como utilizar una sencilla clase dentro de una aplicación rails 3 que generaba texto en dicho formato. En este artículo vamos a ver como incorporar lo aprendido para que nuestra aplicación responda a peticiones de formato CSV de una manera muy sencilla.

Partimos del siguiente controlador que simplemente crea un objecto ActiveRecord::Relation (nuestra aplicación no tiene muchos usuarios no nos preocupamos por recuperar todos los registros)

 class Admin::UsersController < Admin::AdminController
 
   def index
     @users = User.scoped
   end
 end
 

Ahora simplemente debemos declarar que nuestro controlador y/o acción responde a peticiones csv

 class Admin::UsersController < Admin:AdminController
   respond_to :html, :csv
 
   def index
     @users = User.scoped
     respond_with(@users)
   end
 end
 

Sin embago si abrimos nuestro navegador y vamos a "/admin/users.csv" nos encontramos con:

Template is missing

Missing template admin/users/index with {:handlers=>[:erb, :rjs, :builder, :rhtml, :rxml], :formats=>[:csv], :locale=>[:es, :es]}

Al utilizar el respond_with y gracias al respond_to :csv hemos indicado a la aplicación que responde a peticiones de formato :csv, pero rails primero comprobará si hemos añadido un bloque para dicho formato o si tenemos una plantilla para hacer el render (index.csv.erb), sino ejecutará un responder por defecto. Y este responder por defecto producirá el error mostrado anteriormente ya que intenta ejecutar igualmente render(:csv => @users) pero no tenemos ninguna plantilla definida.

Pero en rails 3 podemos definir que nuestra aplicación responda a xml y json y funcionariá sin definir ninguna plantilla. Y esto es porque rails 3 define "renderers" tanto para xml y json

 class Admin::UsersController < Admin:AdminController
   respond_to :html, :csv, :json, :xml
 
   def index
     @users = User.scoped
     respond_with(@users)
   end
 end
 
 module ActionController
   module Renderers
     ...
 
     add :json do |json, options|
       json = json.to_json(options) unless json.respond_to?(:to_str)
       json = "#{options[:callback]}(#{json})" unless options[:callback].blank?
       self.content_type ||= Mime::JSON
       self.response_body  = json
     end
 
     add :xml do |xml, options|
       self.content_type ||= Mime::XML
       self.response_body  = xml.respond_to?(:to_xml) ? xml.to_xml(options) : xml
     end
 
     ...
   end
 end
 

Entonces simplemente lo que necesitamos es añadir un nuevo "renderer" para el formato csv. Nos creamos el initializer csv_renderer.rb

 ActiveSupport.on_load :action_controller do
   ActionController.add_renderer :csv do |relation, options|
     csv_instance = CsvDelegator.new(relation)
 
     options = {:type => "text/csv; charset=utf8", :filename => csv_instance.filename}.merge(options)
     send_data csv_instance.to_csv, options.slice(:type, :filename)
   end
 end
 

Y no necesitamos añadir nada más, bueno si definir nuestra clase CsvDelegator, pero eso no tiene mucha historia y os lo dejo a vosotros

Y bueno unos links que pueden resultar de interes:

NOTA: ¿Por qué en los diferentes ejemplos utilizan ActionController::Renderers.add en lugar de ActionController.add_renderer? ¡Qué alguien me cuente!

CSV y ruby 1.9 y el 'encoding' (yo lo escribo 'enconding')

En algunos de los proyectos rails en los que he trabajado hemos tenido la necesidad de exportar datos en formato CSV y siempre la primera opción era utilizar FasterCSV, pero en mi actual proyecto en rails estamos desarrollando en ruby 1.9.2 y me he encontrado con algunas sorpresas para incorpar la exportación en formato CSV en la aplicación.

En primer lugar después de incluir fastercsv en mi Gemfile y tratar de arrancar la aplicación me encuentro con el siguiente mensaje:

Please switch to Ruby 1.9's standard CSV library. It's FasterCSV plus
support for Ruby 1.9's m17n encoding engine.

Bueno parece que no necesito ningúna libreria extra solo utilizar la librería standard de CSV de Ruby 1.9. Pues nada me creo un initializer dependencies.rb e incluyo un require 'csv'. Ahora manos a la obra con el CSV en ruby 1.9, empezamos probando cosas, en un irb:

CSV.generate{|csv| csv << ["nombre", "apellidos"]} => "nombre,apellidos\n"

Bien pinta bien, ahora unos carácteres especiales (unas tildes por ejemplo)

CSV.generate{|csv| csv << ["dirección", "población"]} => "direcci\xC3\xB3n,poblaci\xC3\xB3n\n"

Vale ya aparecen los primeros problemas con la codificación. Yo siempre he trabajo en "UTF-8" tanto a nivel de aplicación como en base de datos, así que supongo que deberé especificar la codificación en algún sitio. Esto en ruby 1.9 es fácil, e igual de fácil pasarselo a la libreria de CSV.

CSV.generate(:encoding => "".encoding){|csv| csv << ["dirección", "población"]} => "dirección,población\n"

La cosa mejora y me creo un fichero dentro del directorio lib de mi aplicación rails 3

 class CsvBlog
   def self.to_csv
     CSV.generate(:encoding => "".encoding) { |csv|
       csv << ["dirección", "población"]
     }
   end
 end
 

Ahora en lugar de arrancar el irb arranco la consola de rails:

CsvBlog.to_csv
/lib/csv_blog.rb:5: invalid multibyte char (US-ASCII)
/lib/csv_blog.rb:5: syntax error, unexpected $end, expecting ']'
csv << ["dirección", "población"]

Bueno parece que rails 3 no establece la codificación para los ficheros dentro del directorio lib de la aplicación, así que toca especificar la codificación en este nuevo fichero y ya no tenemos mayor problema. Pero queda feo tener que especificar la codificación al generar el csv, pero miremos lo que ocurre:

>> CSV.generate{|csv| puts "".encoding;csv << ["dirección"] }
UTF-8
=> "direcci\xC3\xB3n\n"
>> CSV.generate{|csv| puts csv.encoding;csv << ["dirección"] }
US-ASCII
=> "direcci\xC3\xB3n\n"
>> CSV.generate(""){|csv| puts csv.encoding;csv << ["dirección"] }
UTF-8
=> "dirección\n"

La codificación de nuestros datos es UTF-8 pero si no se especifica a la librería mediante la opción :encoding o mediate un string (extrae la codificación de el), el objeto csv nos hace un estropicio.

Hasta aquí todo en la próxima entrega comentaré como integrar esto con los Responders o Renderers incluidos en rails 3, el tema nos va a quedar muy sencillo.

Mis últimos meses con RoR

Después de unos cuantos meses de abandono de mi blog, me he propuesto con este post resumir lo que he hecho durante este tiempo, esperando no tardar tanto tiempo en volver a escribir alguna tontuna.

En ASPgems hemos estado organizando charlas de formación interna una vez al mes, abiertas a cualquiera que se quisiera apuntar, para contar/exponer materias de interes para nosotros. Y con estas charlas llego mi estreno, con una charla sobre testing de aplicaciones ruby/rails junto a mi compañero @jorgegorka, incluso hay un vídeo, en el canal de aspgems en vimeo. No fue nada espectular, no se podía esperar otra cosa si yo andaba ahí metido, después sobre todo de querer mostrar código en directo y abandonarlo a los 10 minutos :)  También hemos realizado alguna charla más informal al estilo "lightning talks" muy insperadoras de lo que esta por llegar...en la web

También durante este tiempo he estado revisando código de aquí y de allá, he liberado pequeños pedacitos de código en forma de plugins para aplicaciones tog: tog_activity y tog_wall para compensar todo el curro que se ha dado la gente de tog

He seguido apoyando mundo-pepino sobre todo después de una charla del madrid-rb sobre capybara de @porras que me empujo a actualizar mundo-pepino para que se pudieran utilizar las últimas versiones de cucumber y para que se pudiera utilizar capybara o webrat, y de aquí surgió la Paco's release gracias Nando! ;)

También participe en el Desafio Abredatos junto con: @eLafo, @jorgegorka y @leptom y bueno la verdad que lo pasamos genial creando DesenchufaTuCasa, viendo la formula 1 el domingo, zampandonos unas pizzas del dominos, vamos zampando toda la comida rápida que pudimos.

Y bueno también he tenido tiempo de participar en el pasado BugMash, he  entrado en la Rails Contributors, he estado en Amsterdam una semanita de vacaciones.

Y lo que viene seguro que es mejor, empezando la semana que viene con el viaje a la Euruko 2010

human_name and error_message_for controversy

Parace que nuestros amigos ActiveRecord::Base.human_name y ActionView::Base::Helpers.error_messages_for parece que no están en la misma onda o ola o como queráis. El método human_name intenta proporcionar un nombre más "humano" a nuestros modelos de active record y error_messages_for intenta proporcionarnos unos bonitos mensajes de error al intentar crear/editar nuestro modelo de active record.

Además error_messages_for tiene multitud de opciones que nos permiten definir enteramente el contenido de los mensajes, su estructura html y otras cosillas. Pero el problema viene cuando tratamos con los valores por defecto. Este método captura la variable de instancia a partir de su primer parámetro y debe obtener la variable options[object_name] de dicha variable si la opción object_name no es pasada como parámetro. Y aquí llegamos a la controversia, ¿Que clave de nuestros locale recuperamos para generar el object_name en caso de que no lo proporcione el programador?

Pues como suponéis error_messages_for no recupera la misma clave que human_name con lo que nos surge un problema.

 def error_messages_for(*params)
   options[:object_name] ||= params.first
   ...
     I18n.with_options :locale => options[:locale], :scope => [:activerecord, :errors, :template] do |locale|
       ...
       object_name = options[:object_name].to_s.gsub('_', ' ')
       object_name = I18n.t(object_name, :default => object_name, :scope => [:activerecord, :models], :count => 1)
       ...
     end
   ...
 end
 
 
 def human_name(options = {})
   defaults = self_and_descendants_from_active_record.map do |klass|
     :"#{klass.name.underscore}"
   end 
   defaults << self.name.humanize
   I18n.translate(defaults.shift, {:scope => [:activerecord, :models], :count => 1, :default => defaults}.merge(options))
 end
 

Pero ante la llegada inminente de Rails 3 o eso nos comentaba Yehuda en el grupo del core team se han puesto de acuerdo estos muchachos con la ayuda de ActiveModel y tenemos esto.

 module ActiveModel::Naming
   # Transform the model name into a more humane format, using I18n. By default,
   # it will underscore then humanize the class name (BlogPost.model_name.human #=> "Blog post").
   # Specify +options+ with additional translating options.
   def human(options={})
     # No nos interesa que clave recupera pero vemos que será la misma
     ...
   end
 end
 
 module ActionView
   module Helpers
     module ActiveModel
       
       def error_messages_for(*params)
         ...
         if object.class.respond_to?(:model_name)
           options[:object_name] ||= object.class.model_name.human.downcase
         end
         ...
       end
     end
   end
 end
 

Así por ahora solo nos queda esperar o pasarle a error_messages_for el parámetro object_name con le valor que necesitemos. Ciau

Precarga de Asociaciones - Usandolo a mi antojo

Si nos vamos a echar un ojo al código fuente de Rails que se encarga de la precarga de asociaciones preload_associations, el código que se ejecuta al utilizar un :include => :patatas nos encontramos con lo siguiente:

Application developers should not use this module directly.

Pero yo no estoy totalmente de acuerdo con esto y suelo utilizar la funcionalidad que ofrece este módulo a mi antojo. Por un lado no me gusta meter includes al definir las asociaciones en los modelos, sino hacerlo al recuperar los registros en los controladores y añadir los includes que necesite para las vistas que vaya a mostrar.

También suele ocurrir que el número de includes a utilizar es grande por lo que una vez recuperados los registros en el controlador (paginados o no) realizo la carga de las asociaciones a posteriori. (¿Por qué haberlo hecho en el modelo?)

Vale hasta aquí tal vez no haya sido nada convincente y seguro que mis razones sean puramente estéticas sin entrar en detalles de rendimiento. Aún así me he encontrado con un caso, tal vez un "patrón", en el que la precarga de asociaciones puede reducir el número de query's a realizar en la base de datos. Siempre hablando del eager loading que realiza Rails desde la versión 2.1 y cuando no se introducen condiciones en los registros asociados.

El caso consta de los modelos comment, user y profile descritos aquí

Básicamente un perfil de usuario puede ser comentado a través de un comentario root "root_graffity". Este comentario root puede recibir comentarios "sons" y cada uno de esos comentarios acepta respuestas "replys". En el caso de querer recuperar lo que he denominado "graffities" junto con todos sus datos debería hacer lo que muestro aquí Como vemos se realizan 7 consultas  a la base de datos, y el detalle está en que a la tabla de users y profiles se accede en dos ocasiones. Esto último lo podemos evitar si realizar un precarga de asociaciones tal y como muestro aquí

Si estamos cargando asociaciones auto-referenciadas (o algo así), es decir, aquellas que cargan modelos de la misma clase, si esta clase necesita asociaciones de otras clases. Veo que puede ser de utilidad agrupar los modelos de la primera clase y para ese conjunto cargar sus asociaciones. Ya que lo que hace Rails en el módulo en cuestión es recuperar los registros asociados a partir de las claves que contiene la colección de registros cuyas asociaciones se quieren precargar.

Alé que es sábado y habrá que beberse unas cervezas! A vuestra salud!

Una de asociaciones has_many :through en ultrasphinx

Durante las últimas semanas o mejor dicho, el último par de meses he estado trabajando con proyectos que presentan funcionalidades de búsqueda de texto en aplicaciones Rails, utilizando el plugin ultrasphinx.

En esta entrada no voy a contar que es ultrasphinx ni como utilizarlo en detalle, pero si intentaré explicar como solucione un problema que tuve al introducir una nueva asociación sobre la que la aplicación debía realizar búsquedas.

Un punto que conviene no olvidar cuando se utiliza la instrucción:

 class model < ActiveRecord::Base
   is_indexed
 end
 

Es que con esta instrucción y tras ejecutar las tareas que incorpora el plugin de ultrasphinx se generan los índices sobre los que se realizarán las consultas. Estos índices quedan definidos por una o varias queries que podemos consultar en los ficheros *.conf y que también generan las tareas de ultrasphinx. Llevarnos estas queries a nuestro query browser favorito y jugar con ellas nos permite comprobar porque los resultados que devuelve nuestra aplicación no son los que esperábamos, al menos en cierta medida.

Y ya vamos con el problema que me encontre. El problema radica en como generar el índice que permite realizar búsquedas a través de asociaciones has_many. Teniendo los siguientes modelos:

 class User < ActiveRecord::Base
   has_many :recipes
   has_many :ingredients, :through => :recipes
 end
 class Recipe < ActiveRecord::Base
   has_many :ingredients
 end
 

La búsqueda a implementar son búsquedas de usuarios por ingredientes, vale pues a configurar el índice con un poco de DSL:

 class User < ActiveRecord::Base
   ...
   is_indexed :fields => [{:field => 'email', :as => 'user_email'}],
              :include => [{
                 :association_name => "ingredient", 
                 :field => 'name',  
                 :association_sql => "LEFT OUTER JOIN recipies on recipes.user_id = users.id LEFT OUTER JOIN ingredients on ingredients.recipe_id = recipes.id"}]
   ...
 end
 

Pero esto no funciona, ya que el índice solo lo forman el primer ingrediente de cada receta. Hecho que puede llevar perfectamente a que la jodas. La pista me la dio la query que se genera en los ficheros .conf. Cogí esa query la coloque en el query browser y solo aparecieron los nombres de los primeros ingredientes, el resto de ingredientes no existían.

En la query se realiza una agrupación por users.id lo que lleva a que se pierdan el resto de ingredientes. La solución era sencilla crear un nuevo campo en la query en la que se guardarán en forma de lista los ingredientes asociados. ¿Pero eso como lo llevamos al DSL de ultrasphinx?

La solución la encontré en este hilo "How to set up a has_many :through association with Ultrasphinx"

Tenemos que indicarle a ultrasphinx que queremos concatenar los ingredientes, para que aparezcan todos y cada uno de los ingredientes asociados a la receta. Así que nos quedamos con esto:

 class User < ActiveRecord::Base
   ...
   is_indexed :fields => [{:field => 'email', :as => 'user_email'}],
              :concatenate => [{ 
                   :class_name => 'Ingredient', 
                   :field => 'name',
                   :association_sql => "LEFT OUTER JOIN recipies on recipes.user_id = users.id LEFT OUTER JOIN ingredients on ingredients.recipe_id = recipes.id", 
                   :as => "ingredient_list"
               }]
   ...
 end
 

Respondiendo al autor del hilo, gracias! me ha ahorrado bastante tiempo.