viernes, 11 de febrero de 2011

Búsquedas por campos de texto en MySQL

Versión: MySQL 5.1.x

Hace un tiempo publiqué un post sobre el problema de búsquedas por campos de texto usando PostgreSQL:
http://romanmussi.blogspot.com/2009/12/busquedas-por-campos-de-texto-en.html

Hoy vamos a considerar la misma cuestión en MySQL.

El problema

Supongamos que nos piden hacer una búsqueda flexible por nombre de cliente. La idea es que el usuario pueda ingresar como criterio de búsqueda un nombre o parte del nombre sin necesidad de considerar mayúsculas, minúsculas y letras acentuadas.

La solución en MySQL

Desde el punto de vista del código la solución en MySQL puede ser muy sencilla y prácticamente transparente:

SELECT * FROM clientes WHERE nombre LIKE ‘%martin%’

Se usa LIKE, estándar SQL, para comparar una cadena de texto con un determinado patrón. Y se usa % que representa cero o más caracteres para hacer más flexible la comparación (permite hacer la búsqueda parcial).

Esto debería recuperar clientes de nombre:
MARTIN, SAN MARTIN, San Martin, Martín, San Martín, Etc.

¿Listo? ¿Ya podemos volver a jugar al Berusky? Mmmm… no tan rápido. Para que esta búsqueda funcione tenemos que comprender mejor como hace MySQL para realizar las comparaciones entre textos.

CHARACTER SET y COLLATE

Lo principal es comprender lo que son los conjuntos de caracteres y los “cotejos” (1). Un conjunto de caracteres es un conjunto de símbolos y su correspondiente codificación. Un cotejo es un conjunto de reglas para comparar los símbolos de un conjunto de caracteres (2).

El servidor MySQL soporta muchos conjuntos de caracteres. Los más conocidos en el mundo hispanoparlante son LATIN1 y UTF8. A su vez, un conjunto de caracteres tiene siempre como mínimo un cotejo, aunque puede tener varios. Por ejemplo, el conjunto de caracteres LATIN1 tiene los siguientes cotejos:

latin1_german1_ci (Alemán DIN-1)
latin1_swedish_ci (Sueco/Finlandés)
latin1_general_ci (Multilingüe)
latin1_general_cs (Multilingüe, sensible a mayúsculas)
latin1_spanish_ci (Español moderno)

Como se observa, existe una convención para nombres de cotejos: comienzan con el nombre del conjunto de caracteres al que están asociados, luego incluyen el nombre del idioma, y terminan con _ci (no distingue entre mayúsculas y minúsculas), _cs (distingue entre mayúsculas y minúsculas), o _bin (binario).

El cotejo le indica al motor el modo en que debe compara los símbolos de un conjunto de caracteres. Veamos como ejemplo que dicen algunos cotejos de uso habitual sobre los caracteres que deben ser considerados equivalentes a la letra “i” (i minúscula).

Para el cotejo latin1_general_ci la “i” es equivalente a: “I” (i mayúscula). Esto significa que el motor solo va a considerar como equivalente de la i minúscula a la i mayúscula.

Para el cotejo latin1_spanish_ci la “i” es equivalente a: “I Ì Í Î Ï ì í î ï”. En este caso el motor va a considerar a la i minúscula como equivalente de un conjunto mucho más amplio de opciones.

Aplicando las reglas anteriores si el motor evalúa: ‘Martín’ LIKE ‘%martin%’
• devuelve falso en latin1_general_ci
• devuelve verdadero en latin1_spanish_ci

Para consultar el detalle de reglas de equivalencia de cada cotejo es muy útil el sitio:
http://www.collation-charts.org/

Una vez que entendemos como MySQL realiza la comparación entre caracteres simplemente debemos asegurarnos que utilice el cotejo que se ajusta a nuestras necesidades, y eso depende de las configuraciones. Hay configuraciones por defecto para conjuntos de caracteres y cotejos en cuatro niveles: servidor, base de datos, tabla, y conexión. Y existen configuraciones específicas para campos, por ejemplo. Puede consultar en detalle como se establecen las configuraciones y como se evalúan las precedencias en el Manual de MySQL (2). Aquí simplemente mencionaremos dos formas de forzar el uso de un cotejo específico para resolver el problema de la búsqueda de manera sencilla.

1) Puede definir el conjunto de caracteres y cotejo por defecto de la columna

Cada columna de tipo “carácter” (CHAR, VARCHAR, o TEXT) tiene un conjunto de caracteres y cotejo de columna. En general la configuración por defecto de la columna tiene la mayor precedencia al momento de efectuar la comparación lo que resuelve nuestros problemas. La sintaxis de definición de columnas tiene cláusulas opcionales para especificar el conjunto de caracteres y la colación.

Ejemplo:

CREATE TABLE clientes
(
nombre VARCHAR(20) CHARACTER SET latin1 COLLATE latin1_spanish_ci
);

Puede modificar esos valores en una columna ya existente mediante phpMyAdmin ó el comando ALTER TABLE, según le resulte más cómodo.

2) Puede especificar el cotejo a utilizar al definir la consulta

Si no desea modificar las definiciones por defecto en la base de datos puede especificar el cotejo a utilizar al definir la consulta SQL, del siguiente modo:

SELECT * FROM clientes
WHERE nombre LIKE ‘%martin%’ COLLATE latin1_spanish_ci

En este caso solo debe tener la precaución de que el conjunto de caracteres coincida con el definido en la columna (latin1, utf8, etc.).

Con cualquier de estos dos procedimientos logrará hacer funcionar las consultas como necesita.

Notas

(1) En el Manual de MySQL 5.0 en Español se traduce COLLATE como “colación”. Aquí hemos preferido “cotejo” por considerarlo más adecuado ó amigable. De todos modos es una cuestión de gustos.

(2) Para un análisis en profundidad de conjuntos de caracteres y cotejamiento véase:

Manual de MySQL 5.1 (Inglés)
Chapter 9. Internationalization and Localization
http://dev.mysql.com/doc/refman/5.1/en/internationalization-localization.html

Manual de MySQL 5.0 (Español, corresponde a una versión anterior pero sigue siendo útil para el tema)
Capítulo 10. Soporte de conjuntos de caracteres
http://dev.mysql.com/doc/refman/5.0/es/charset.html

martes, 30 de marzo de 2010

Desarrollo ágil

Hace poco terminé de leer Scrum y XP desde las trincheras, de Henrik Kniberg. El libro es recomendable y contiene, entre otras cosas, buenas descripciones de casos prácticos de uso de Scrum (tiene algo de XP pero en menor profundidad).
En este momento me gustaría compartir un párrafo del prólogo de Mike Cohn que me parece muy bueno:

"Tanto Scrum como Programación Extrema (XP) requieren que los equipos completen algún tipo de producto potencialmente liberable al final de cada iteración. Estas iteraciones están diseñadas para ser cortas y de duración fija. Este enfoque en entregar código funcional cada poco tiempo significa que los equipos Scrum y XP no tienen tiempo para teorías. No persiguen dibujar el modelo UML perfecto en una herramienta CASE, escribir el documento de requisitos perfecto o escribir código que se adapte a todos los cambios futuros imaginables. En vez de eso, los equipos Scrum y XP se enfocan en que las cosas se hagan. Estos equipos aceptan que puede que se equivoquen por el camino, pero también son conscientes de que la mejor manera de encontrar dichos errores es dejar de pensar en el software a un nivel teórico de análisis y diseño y sumergirse en él, ensuciarse las manos y comenzar a construir el producto."

Creo que es un excelente resumen de lo que significa el "espíritu ágil" en el desarrollo de software.

jueves, 18 de febrero de 2010

Caso de éxito: Uso de CakePHP en el Instituto Nacional de Educación Tecnológica

Nota: Esta es la versión en español de un artículo que publiqué en "The Bakery", sitio de la comunidad de usuarios de CakePHP. Ver artículo en inglés en: http://bakery.cakephp.org/articles/view/success-story-using-cakephp-at-the-instituto-nacional-de-educacion-tecnologica

Antecedentes y objetivos del proyecto


A mediados de 2008 en el Instituto Nacional de Educación Tecnológica (INET, http://www.inet.edu.ar/) comenzamos a evaluar el funcionamiento de los sistemas de gestión usados por la Unidad de Información del organismo. La Unidad de Información tiene a su cargo, entre otras responsabilidades, el mantenimiento de información del Registro Federal de Instituciones de Educación Técnica Profesional previsto por la Ley Nº 26.058. Se trata de un listado de aproximadamente 3.500 instituciones que imparten educación técnica profesional en todo el País. El Registro contiene, además de la información básica de cada institución (características, ubicación, dirección, teléfono, datos del director, etc.), información sobre planes de estudio, títulos y certificados emitidos, matricula de alumnos en cada plan de estudios, etc.

En ese momento la información era gestionada mediante dos aplicaciones complementarias: una aplicación Web desarrollada en Java que se utilizaba para la actualización de información, y una aplicación de escritorio desarrollada en Visual Basic 5 para la consulta de datos (usando un esquema cliente / servidor). Para la gestión de datos se utilizaba Oracle 10g.

Los desarrollos existentes eran limitados por lo que resultaba necesario realizar una serie de modificaciones y actualizaciones para facilitar las tareas del área y mejorar la calidad y cantidad de información disponible. El inconveniente era que los equipos que habían desarrollado las aplicaciones y diseñado la base de datos original ya no se desempeñaban en la institución. Por otro lado, además de heterogéneas desde el punto de vista de los lenguajes utilizados (Java, VB5), las aplicaciones existentes fueron desarrolladas sin la aplicación de patrones de diseño, estándares, o Frameworks; todos elementos que hubieran facilitado su mantenimiento y actualización.

Para resolver los inconvenientes detectados y mejorar la calidad de los sistemas en el corto y mediano plazo se elaboró un plan de trabajo que incluía:

1) Migración de los sistemas a una nueva plataforma tecnológica (Servidor Linux Ubuntu Server, Servidor Web Apache 2, Motor de base de datos PostgreSQL, Lenguaje de programación PHP, Framework CakePHP)

2) Rediseño de la base de datos (el modelo de datos existente presentaba muchas limitaciones)

3) Desarrollo de una nueva aplicación que integrara los desarrollos anteriores, mejorara sus prestaciones, y posibilitara el mantenimiento y la mejora continúa.

Desarrollo

Se comenzó a trabajar en el proyecto a fines de 2008. Entre octubre y diciembre definimos la nueva arquitectura de la aplicación, realizamos el rediseño de la base de datos y elaboramos un breve diseñó de lo que debería ser la nueva aplicación, todas tareas que prepararon el terreno para el desarrollo propiamente dicho que se llevó adelante durante 2009.

En enero de 2009 se incorporaron dos personas al equipo de trabajo: un DBA y encargado de la nueva plataforma tecnológic, y un desarrollador. Es interesante destacar que el programador tenía buenos conocimientos de PHP pero no había utilizado CakePHP por lo que estuvo durante enero y febrero capacitandose en la nueva tecnología. A partir de marzo se comenzó con el desarrollo de la nueva aplicación. El ciclo total de desarrollo insumió 3 meses, de marzo a mayo. En junio la nueva aplicación fue puesta en producción en la intranet de INET reemplazando los desarrollos anteriores.

El desarrollo resultó muy exitoso. En consecuencia las autoridades de la institución decidieron reforzar el equipo de trabajo para continuar con la mejora continua de la nueva aplicación y con el proyecto de ampliar la informatización de sistemas a otras áreas del INET. Es así que en agosto se incorporaron dos nuevos programadores. Como en el caso anterior los nuevos programadores tenían buenos conocimientos de PHP y programación en general pero no de CakePHP por lo que comenzaron estudiando el Framework. Nuevamente en un lapso muy breve y menor al previsto (en poco más de un mes) los nuevos programadores se pudieron incorporar al desarrollo de la aplicación en producción sin inconvenientes. Colaboró con esta rápida integración el papel del desarrollador con más experiencia que funcionó como mentor y líder del resto del equipo.

Para 2010 se prevé ampliar el equipo de desarrollo; continuar con el mantenimiento y mejora de la aplicación desarrollada; y abordar nuevos desarrollos para áreas aún no informatizadas pero que tienen relación natural con el Registro de Instituciones (el algunos casos los nuevos programas se integrarán como módulos al desarrollo actual, en otros casos se tratará de aplicaciones separadas pero que trabajaran con la misma base de datos central).

El papel de CakePHP

CakePHP fue fundamental para el éxito del proyecto. Destacamos lo que consideramos algunas de las ventajas principales de utilizar el Framework.

Provee mucha funcionalidad útil lista para usar. Cake evita que tengamos que “inventar la rueda” cada vez. Para los problemas comunes que debemos resolver cuando encaramos un desarrollo de mediana complejidad Cake ya tiene una solución disponible. En nuestro caso las cosas más útiles fueron: autenticación (auth), manejo de sesiones, sanitización automática de datos, validación de datos a nivel del modelo. También nos resultaron muy útiles los distintos Helpers: HTML, formularios, paginación, AJAX, Javascript, etc. Y en realidad este es un listado parcial e incompleto de las utilidades disponibles. Además, si en el core no encontramos una solución siempre se pueden revisar los plugins.

Promueve el uso de estándares y patrones de diseño. El uso de Cake facilita mucho la comunicación entre programadores al establecer convenciones de programación y utilizar patrones como MVC que organizan los componentes de una aplicación. Esto resulta invalorable en el mediano plazo. Cuando no se utilizan estándares ó patrones cada programador codifica como y donde le parece correcto, lo que a mediano plazo produce muchos problemas para el mantenimiento. Ya sabemos, cada programador codifica diferente. Con Cake sabemos de inmediato y con mucha aproximación donde debemos tocar el código para modificar algo de una determinada vista o funcionalidad.

Tiene una activa comunidad. Cake tiene una gran comunidad y eso resulta valioso cuando el equipo se enfrenta a un problema difícil de resolver. Tanto las listas de distribución como la documentación en línea y “the bakery” nos han sido de gran utilidad. En este punto Cake tiene una ventaja comparativa respecto de otros frameworks de desarrollo con comunidades más cerradas o menos activas.

Como efectos de uso de Cake podemos mencionar:

Mejora la productividad. Resultó en cierta medida sorprendente la productividad del equipo de desarrollo. Si bien el desarrollo es de mediana complejidad la rapidez con que se lo pudo poner en producción es llamativa en comparación con otros proyectos de similar envergadura que hemos podido observar en nuestra experiencia previa. La facilidad con que Cake permite implementar funcionalidad de base (común a cualquier aplicación) colabora mucho en esta alta productividad.

Simplifica la incorporación de nuevos programadores. La experiencia nos ha mostrado que la incorporación de nuevos programadores se ve facilitada por el uso de Cake. El período de aprendizaje del Framework ha sido de entre uno y dos meses. Luego de este período los nuevos miembros se han logrado incorporar al equipo de desarrollo sin inconvenientes. Las convenciones, los estándares y el uso de patrones de diseño facilitan mucho la comunicación en el equipo y provee de un lenguaje técnico común y significativo. Esta característica facilita la incorporación de nuevos programadores por crecimiento del proyecto, y también permite mitigar en parte los efectos de la rotación de personal, algo que resulta muy habitual en la actualidad en este segmento del mercado de trabajo. Es casi imposible hoy por hoy evitar la perdida de algún recurso humano capacitado porque la competencia es muy fuerte. En este contexto el uso de Cake permitiría reemplazar el recurso perdido sin un impacto tan significativo como en otros escenarios.

Resulta atractivo para los programadores. En nuestro caso los programadores que se incorporaron al equipo no tenían experiencia anterior con Cake. Luego de un breve período de capacitación todos se sienten cómodos y adoptaron el Framework con interés, y varios lo comenzaron a utilizar para sus proyectos personales.

Hasta aquí entonces las ventajas que encontramos en el uso de Cake. Ciertamente muchas de esas ventajas se pueden obtener utilizando otros frameworks y hay muchos actualmente disponibles que son seguramente tan buenos como Cake. Pero en estas notas no estamos interesados en hacer una comparativa de frameworks, aquí lo que queremos es contar nuestra experiencia como un equipo de desarrollo que está “en las trincheras”, haciendo aplicaciones que son utilizadas y evaluadas cotidianamente por personal del INET. En este contexto podemos decir que CakePHP nos ha sido muy útil y ha satisfecho perfectamente las expectativas. Esperamos que nuestra experiencia sirva a otros programadores que están evaluando el uso de Cake.

Gestión del proyecto

Unas últimas palabras para poner en perspectiva la experiencia referida. CakePHP es parte importante de la solución pero no deja de ser una herramienta que cobra sentido en un contexto más amplio. También ha sido fundamental para el éxito del proyecto el uso de metodologías ágiles para la gestión. Hemos usado para ello una combinación de técnicas tomando ideas de XP y Scrum, adaptándolas a las peculiares características institucionales y los recursos del INET. El detalle de estas metodologías y sus adaptaciones exceden el contenido de estas notas pero no queríamos dejar de mencionarlo para que no quede una imagen muy simplista de un proceso tan complejo como es el desarrollo de una aplicación corporativa.

Equipo: Alejandro Vilar, Hernán Valle, Leandro Walker, Luciano Aguerre.

martes, 8 de diciembre de 2009

Búsquedas por campos de texto en PostgreSQL

(Nota: Para las pruebas se utilizó PostgreSQL 8.3.x)

El problema

Supongamos que nos piden hacer una búsqueda flexible por nombre de cliente. La idea es que el usuario pueda ingresar como criterio de búsqueda un nombre o parte del nombre sin necesidad de considerar mayúsculas, minúsculas y letras acentuadas.

Alternativas de solución
Lo más sencillo, aunque incorrecto, sería hacer una simple evaluación de igualdad con el operador “=”
SELECT * FROM clientes WHERE nombre = ‘MARTIN’


Esta estrategia es básica y tiene muchas limitaciones. En principio, no busca por parte del nombre y no encontraría un cliente que se llame “SAN MARTIN”.
PostgreSQL provee algunas funciones de comparación de cadenas de texto más poderosas que son útiles para estos casos. Veamos las más comunes y utilizadas.

LIKE
Es un estándar para comparación de cadenas de texto en SQL. Permite comparar una cadena de texto con un determinado patrón. El formato más utilizado para una comparación flexible utiliza % para representar cero o más caracteres:

Algunos ejemplos:
‘martin’ LIKE ‘martin’ > Verdadero (igual a operador =)
‘martin’ LIKE ‘%rt%’ > Verdadero (%...% permite detectar coincidencia parcial de textos)
‘san martin’ LIKE ‘%martin%’ > Verdadero
‘San Martin’ LIKE ‘%martin%’ > Falso
‘San Martín’ LIKE ‘%martin%’ > Falso

La limitación de LIKE para lo que queremos hacer es que resulta sensible a mayúsculas y minúsculas, lo que hace muy estricta la búsqueda.

ILIKE
ILIKE no es un estándar SQL sino una extensión de PostgreSQL. Se puede utilizar para hacer una búsqueda que no tome en cuenta mayúsculas y minúsculas (case insensitive) y por lo tanto es más flexible. Siguiendo con el ejemplo anterior:

‘San Martin’ ILIKE ‘%martin%’ > Verdadero
‘San Martín’ ILIKE ‘%martin%’ > Falso
‘San Martín’ ILIKE ‘%martín%’ > Verdadero

El problema es que ILIKE es insensible a mayúsculas y minúsculas pero no ignora caracteres acentuados y por eso la anteúltima comparación devuelve falso. O sea, ILIKE considera "e" igual a "E", pero no considera "é" igual a "e". No realiza una equiparación entre letras acentuadas y no acentuadas.

TO_ASCII
Para resolver este problema podemos utilizar, en ciertas circunstancias, la función TO_ASCII, que nos permite convertir las letras acentuadas antes de realizar la comparación. TO_ASCII convierte todas las vocales acentuadas en su equivalente no acentuado. Ejemplo: TO_ASCII(‘San Martín’) devuelve "San Martin". Con ello las letras acentuadas dejan de generar inconvenientes en las comparaciones.

Ejemplo de uso:
TO_ASCII(‘San Martín’) ILIKE TO_ASCII(‘%martin%’) > Verdadero

Si volvemos al ejemplo del principio, con el siguiente SELECT:
SELECT * FROM clientes WHERE TO_ASCII(nombre) ILIKE TO_ASCII(‘%martin%’)

Vamos a recuperar clientes de nombre:
MARTIN, SAN MARTIN, San Martin, Martín, San Martín, Etc.

Que es lo que estábamos buscando. Ahora bien, esta práctica función solo soporta la conversión a ASCII desde encodings LATIN1, LATIN2, LATIN9, y WIN1250. Hasta hace un tiempo esto no era un problema porque el encoding más habitual de las bases de datos era LATIN1. El problema se presenta cuando la base de datos está codificada con UTF8, algo cada vez más habitual, porque TO_ASCII falla y no nos permite realizar búsquedas que ignoren los acentos. La cuestión es: ¿Existe algo semejante a TO_ASCII con bases de datos UTF8?

Comparación que ignore acentos con encoding UTF8
El tema no parece sencillo desde el punto de vista técnico. En principio y por lo que se puede observar en los foros técnicos no está previsto que PostgreSQL incorpore en el corto plazo alguna función nativa que resuelva el problema (algunos mencionan un nuevo módulo contrib que va a permitir remover los acentos a partir de la versión 8.5, pero es todo extraoficial).

Existen muchas propuestas en los foros, en algunos casos pequeñas variaciones sobre la misma idea. La mejor solución que encontré para resolver el problema es crear una función que reemplace caracteres acentuados por sus equivalentes no acentuados y usarla en la comparación en lugar de TO_ASCII.

La función de conversión es:
CREATE OR REPLACE FUNCTION sp_ascii(character varying)
RETURNS text AS
$BODY$
SELECT TRANSLATE
($1,
'áàâãäéèêëíìïóòôõöúùûüÁÀÂÃÄÉÈÊËÍÌÏÓÒÔÕÖÚÙÛÜçÇ',

'aaaaaeeeeiiiooooouuuuAAAAAEEEEIIIOOOOOUUUUcC');
$BODY$
LANGUAGE 'sql';

Y se usa del siguiente modo:
SELECT * FROM clientes WHERE SP_ASCII(nombre) ILIKE SP_ASCII(‘%martin%’)

Esto ha funcionado correctamente con bases de datos encoding UTF8 recuperando:
MARTIN, SAN MARTIN, San Martin, Martín, San Martín, Etc.

Una nota sobre rendimiento. Con la función SP_ASCII la recuperación de información se vuelve un poco más lenta. No es algo muy notable pero puede incidir si la base de datos es muy grande. Para hacer más rápida la búsqueda se puede limitar la conversión de caracteres a los que resultan relevantes en cada caso. Por ejemplo, en la base de datos usada para pruebas solo se encuentran algunas letras acentuadas por lo que fueron eliminados el resto de los casos de la función de conversión que quedó reducida a:

SELECT TRANSLATE($1, 'áéíóúÁÉÍÓÚçÇ','aeiouAEIOUcC');

Con este sencillo expediente la búsqueda se realiza en la mitad del tiempo. Se debería probar en cada caso y hacer las evaluaciones correspondientes. Como alternativa, en los foros algunos usuarios recomiendan crear un índice funcional para mejorar la performance.

Referencias

Más información sobre LIKE, ILIKE y otras formas de comparación de cadenas (SIMILAR TO, POSIX) en:
http://www.postgresql.org/docs/8.3/interactive/functions-matching.html

Información sobre TO_ASCII y sus limitaciones:
http://www.postgresql.org/docs/current/interactive/functions-string.html

Discusión en la comunidad de PostgreSQL sobre el tema, en potugués (“problema com acentos no UTF8”):
http://www.mail-archive.com/pgbr-geral@listas.postgresql.org.br/msg15630.html

Versiones de la “función de conversión” para UTF8:
http://www.mail-archive.com/pgbr-geral@listas.postgresql.org.br/msg15632.html
http://wiki.postgresql.org/wiki/Strip_accents_from_strings,_and_output_in_lowercase

Función de conversión en perl:
http://www.mail-archive.com/pgsql-general@postgresql.org/msg128804.html

Blog en francés que trata la misma problemática:
http://code18.blogspot.com/2009/03/comparaison-de-texte-postgresql.html

Artículo muy completo sobre UNICODE:
http://www.joelonsoftware.com/articles/Unicode.html

Bienvenidos!

Hola,
Aquí voy a publicar notas sobre:
  • tecnologías de la información
  • desarrollo de software
  • soluciones a problemas técnicos
  • etc.
Hasta pronto!