Archive for Mayo, 2007

MySQL, índices y cintas de video

Martes, Mayo 22nd, 2007

La mayoría de la gente que trabaja con MySQL (ójala fueran todos), sabe que los índices son una parte importantísima del rendimiento en las consultas a base de datos. Para los no conocedores del tema, se pueden crear índices (y se deben…) para que cuando queremos obtener un conjunto de registros, segmentados por algún tipo de condición, estos sean devueltos con un mínimo de rapidez. La regla suele ser que por cada campo de un WHERE se debe tener un índice. Luego se puede estudiar el comportamiento y optimizar estos índices con la maravillosa instrucción EXPLAIN.

A mi juicio, esto tiene una excepción, y son las consultas de texto completo en las que es mejor usar una herramienta externa de consulta de índices de texto completo (fulltext).

No quiero entrar en todo el tema de teoría de índices y demás, pero básicamente se puede resumir en que se reduce un poco el rendimiento de una inserción o actualización en base de datos en pro de la rapidez en consulta.

Generalmente se trabaja con tablas en las que se escribe esporádicamente y se lee muy a menudo de manera que es muy importante mantener esos índices. Hoy me he encontrado el único caso en el que es muchísimo mejor no tener índice alguno en las tablas a utilizar y, aunque es un caso muy particular, no es imposible que ocurra.

Se basa en las siguientes premisas:

  • Se debe llebar una o varias tablas con una cantidad de registros muy alta (varios millones)
  • Se llenan de seguido, no esporádicamente. No sufren lecturas (SELECT) durante el tiempo de inserción
  • Son tablas de caracter transitorio: un script lee una fuente de datos (ficheros) las rellena y cuando ha acabado, serán consultadas y borradas.

En este caso, es imprescindible para el rendimiento que las tablas donde se va a realizar la inserción no tengan índices ( aparte de la clave primaria claro) para que se hagan lo más rápido posible. Una vez finalizado el proceso de inserción, se construyen los índices necesarios, se ejecutan las consultas pertinentes y posteriormente se borran las tablas.

Si medimos el tiempo de ejecución del modelo clásico en fragmentos de 10.000 inserciones se verá que el tiempo para ejecutar esos mismo fragmentos aumenta mucho segun va pasando el tiempo y llenandose la tabla. Sin embargo el modelo de “patada a la teoría” mantiene una buena tasa de inserción constante. Posteriormente el tiempo de creación de índices es despreciable comparado con lo que tardaba la manera original con índices.

He intentado reproducir lo que me ha ocurrido hoy en una máquina de desarrollo y ha pasado en varios órdenes de magnitud menor pero aún así se consigue el mismo objetivo en la mitad de tiempo. 1 millon de inserciones (de las cuales la mitad derivan en una insercion adicional) en 250 segundos frente a 600 segundos por el mismo trabajo. Posteriormente la creación de los índices no supera los 60 segundos.

Conclusión: picardía y ganas de enredar acaban en beneficio para el rendimiento.

Convertir cadenas a su equivalente semántico

Martes, Mayo 15th, 2007

A menudo, necesitamos pasar una cadena de su forma real a una forma que se mucho más amigable con las URLs. Esto se realiza con objetivos de posicionamiento y SEO en general.

Imaginemos que tenemos una página dinámica que va mostrar una noticia.

El primer paso sería que reciba un ID como parámetro GET y muestre el contenido.

nuestroperiodico.com/noticia.php?id=23


El segundo paso sería poner el título urlencodeado ( nuevo palabro, usar funcion urlencode de tu lenguaje favorito) para que los robots de los motores de búsqueda vean reflejado en las url términos importantes del contenido.

nuestroperiodico.com/noticia.php?id=23&titulo=El+alcalde+ha+muerto


Pero, ¿que pasa si el título de la noticia contiene caracteres que no son muy agradables de urlencodear como acentos o símbolos? Para el título: "El Tráfico: Madrid colapsado por las obras" tendríamos esta url:

nuestroperiodico.com/noticia.php?id=23
&titulo=El+Tr%E1fico%3A+Madrid+colapsado+por+obras


Debemos convertir el título a algo más agradable para una url y para ello haremos los siguientes pasos:

  • Convertir la cadena a minúsculas
  • Quitar símbolos de puntuación y otros símbolos incomodos (como las barras y comillas)
  • Convertir los acentos a sus equivalentes sin acentuar (mas la ñ)


Mucho mejor, quedaría así:

nuestroperiodico.com/noticia.php?id=23
&titulo=el-trafico-madrid-colapsado-por-obras


Pero, ¿porqué pararnos aquí?, las url (en terminos SEO) tienen una longitud a partir de la cual no les gustan a los algoritmos de posicionamiento. Asi que vamos a dejar sólamente la información que realmente interesa. A nuestroperiodico no le interesará nunca posicionarse por los terminos "el" o "por", son términos tan comunes que no tienen ninguna relevancia. En términos de recuperación de información se denominan palabras vacías. Estaría bien tener un último filtro que nos quitara las palabras vacías dejando la url de nuestra noticia así:

nuestroperiodico.com/noticia.php?id=23
&titulo=trafico-madrid-colapsado-obras


¿Y cómo narices hago todo esto? Dejo un poquito de código para los interesados (para poder descargarlo solo pinchar encima de "plain text" y se pone en modo plano).



La vida después de esto

Esto sólo es un primer paso , el gran paso se da cuando se pone uno a juguetear con el mod_rewrite de Apache (otros servidores web tienen modulos o plugins con objetivos similares). No es el objetivo de este artículo pero con mod_rewrite llegaríamos a esta url (para lo que también usaríamos el proceso de conversión a cadenas semánticas):

nuestroperiodico.com/nacional/trafico-madrid-colapsado-obras/23


Código fuente

Determina si una palabra es vacía o no. Generalmente se tienen como mínimo las preposiciones y artículos. Se pueden ir añadiendo las que a cada uno le parezca más oportuno. Si la lista crece, no estaría nada mal sacarla a un fichero externo.

PHP:
  1. function esVacia($palabra)
  2. {
  3.     $esVacia = false;
  4.     $vacias = array("a", "ante", "bajo", "con", "contra", "de", "desde", "en",
  5.                     "entre", "hacia", "hasta", "para", "por", "segun", "sin", "so",
  6.                     "sobre", "tras", "durante", "mediante","la","las","los","el","y","del");
  7.     foreach($vacias as $v)
  8.     {
  9.         if($v==$palabra)
  10.         {
  11.             $esVacia = true;
  12.             break;
  13.         }
  14.     }
  15.     return $esVacia;
  16. }


Cambia los acentos y tildes por sus equivalentes.

PHP:
  1. function cambiaAcentos($str)
  2. {
  3.  if(is_utf($str))
  4.  {
  5.      $str = utf8_decode($str);
  6.  }
  7.  
  8.   $str = htmlentities($str);
  9.   $str = preg_replace('/&([a-zA-Z])(uml|acute|grave|circ|tilde);/','$1',$str);
  10.   return html_entity_decode($str);
  11. }



Pequeño apoyo para cadenas utf. No he probado mucho con ella (usar con cuidado).

PHP:
  1. function is_utf ($t)
  2. {
  3.  if ( @preg_match ('/.+/u', $t) )
  4.       return 1;
  5. }



Función maestra que convierte las cadenas a su equivalente semántico. Se le pasa como parámetro opcional la eliminación de palabras vacías. Cuidado con el orden de cambiar los acentos y pasar la cadena a minúsculas. Debe ser exactamente ese, ya que la función strtolower de php no pasa a minúsculas las mayúsculas acentuadas (o ñ).

PHP:
  1. function strtosem($str,$eliminaVacias=true)
  2. {
  3.  
  4.     $str = cambiaAcentos($str);
  5.     $str = strtolower($str);
  6.  
  7.     $str_partes = split("[ ()/']",trim($str));
  8.  
  9.  
  10.     if($eliminaVacias)
  11.     {
  12.  
  13.         $partes_destino = array();
  14.  
  15.         foreach($str_partes as $parte)
  16.         {
  17.             $parte = trim($parte);
  18.             if(!esVacia($parte) && !empty($parte))
  19.             {
  20.                 $partes_destino[] = $parte;
  21.             }
  22.         }
  23.         $str = implode("-",$partes_destino);
  24.     }
  25.     else
  26.     {
  27.         $str = implode("-",$str_partes);
  28.     }
  29.  
  30.  
  31.  
  32.     return $str;
  33.  
  34. }

Reflexión sobre searchpedia

Martes, Mayo 15th, 2007

Parece ser, que lo que sólo era un experimento con motores de búsqueda ha gustado y eso me alegra. No pretendo hacer la competencia a la wikipedia ( aunque quisiera no dispongo de los dump actualizados ), sólo demostrarme a mi mismo que se podía hacer y compartir con todos el proceso de creación.

La noticia salió a portada en barrapunto y me dejó bastante tráfico durante unos días. Algún otro blog se hizo eco y luego hubo otros que simplemente son unos cutres, como es el caso de esta gente, que fusilaron el post al completo, sin ni siquiera citar, ni preocuparse de poner bien el contenido (el código fuente y los enlaces). ¿Pretendían hacer el texto suyo? Parece que no, porque pone "Aqui esta el texto original". Pero entonces, ¿Porqué no citar?. ¿Porque no hacer una pequeña reseña de 6 líneas enlazando a la fuente original?

A veces no entiendo nada.

Buscador para la wikipedia (jugando con sphinx)

Sábado, Mayo 12th, 2007

Desde hace algún tiempo, estaba pensando en hacer un buscador que indizara una gran cantidad de texto para comprobar que problemas reales se tienen cuando se trata con cantidades ingentes de información. Para ello habia dos opciones, escoger una base de datos con muchos registros o escoger una con menos registros pero si una cantidad grande de texto por cada uno de ellos.

Pero, ¿Cuantos son muchos registros?. Respecto a temas de indización y búsquedas de texto completo considero imposible (en cuanto a rendimiento) seguir usando el motor FULLTEXT de MySQL a partir de unos 300.000 registros.

Se sopesaron las diferentes bases de datos (por supuesto, libres) que podrían usarse para este estudio y se llego a dos en concreto:

  • Wikipedia: unos 200.000 registros con mucho texto
  • FreeDB: alrededor de 1.000.000 con poco texto


Teniendo en cuenta, que el FreeDB tiene muchos resultados duplicados y poco texto (que afearía un poco los resultados), decidí aventurarme por la wikipedia en castellano. El idioma también fue un detonante, ya que tenia intención de aplicarle despues un stemmer en castellano.

Al llegar a este punto solo queda definir las fases:

  • Descarga e importación de la base de datos
  • Configuración y construcción de los índices
  • Creación de un pequeño sitio web para el buscador



Descarga e importación de la base de datos

La Wikipedia ofrece la descarga de unos dump de base de datos en formato xml (y algunos en sql), que luego pueden ser importados a la base de datos que tengamos de mediawiki. La página que explica todo esto es http://en.wikipedia.org/wiki/Wikipedia:Database_download.

Para ver que parte de la wikipedia necesitaba y no descargar absolutamente todo (pesa bastante), eché un ojo a este esquema de la base de datos .

Las tablas necesarias son únicamente:

  • text: contiene el texto de las páginas
  • page: contiene los titulos y ciertos parámetros como si es redirección o el espacio de nombres
  • revision: contiene la referencia de revisiones



Los dump en castellano en cuestión se pueden bajar de http://download.wikimedia.org/eswiki/latest/.

Ponemos a bajar el dump que nos vale eswiki-latest-pages-articles.xml.bz2

Para poder crear las tablas de MySQL a partir del dump se tuvo que usar las herramientas de las que provee el propio software de MediaWiki. Así que tras descargarlo y desempaquetarlo, se configuran los parametros de acceso en el archivo LocalSettings.php y mediante el script importDump.php se rellenan las tablas deseadas tal y como se explica aqui.



Configuración y construcción de los índices

Aquí llega la parte seria, se va a indizar una tabla de 1GB de texto. Puntos interesantes a estudiar:

  • Tamaño del índice
  • Tiempo de construcción
  • Tiempo de consulta del índice para consultas simples
  • Tiempo de consulta del índice para consultas complejas (booleanas, frase...)

La herramienta que se va a usar es Sphinx, porque es la que conozco (obvio pero importante) y porque mas que el medio en este estudio ella es el fín. Es decir, lo que quiero demostrar es que esta herramienta es muy rápida aun cuando la cantidad de información es muy grande.

Se definen dos índices: uno para el título del artículo y otro para el contenido.

Definicion del titulo

CODE:
  1. source titulosrc
  2. {
  3.         type                            = mysql
  4.         sql_host                        = host
  5.         sql_user                        = user
  6.         sql_pass                        = pass
  7.         sql_db                          = wikidb
  8.         sql_port                        = 3306
  9.         sql_query_pre           =
  10.         sql_query                       = \
  11.                 SELECT page_id, page_title \
  12.                 FROM page WHERE page_namespace = 0 \
  13.                 AND page_is_redirect = 0
  14.  
  15. }
  16.  
  17. index titulo
  18. {
  19.         source                  = titulosrc
  20.         path                    = /ruta-sphinx/var/data/titulo
  21.         docinfo                 = extern
  22.         morphology              = none
  23.         min_word_len            = 1
  24.         charset_type            = utf-8
  25.         charset_table            = 0..9, A..Z->a..z, a..z, U+C9->U+E9, U+C1->U+E1, \
  26.                                    U+DA->U+FA, U+D1->U+F1, U+D3->U+F3, U+CD->U+ED, U+E1, \
  27.                                    U+E9, U+FA, U+F1, U+F3, U+ED
  28. }

Definicion del contenido

CODE:
  1. source contenidosrc
  2. {
  3.         type                            = mysql
  4.         sql_host                        = host
  5.         sql_user                        = user
  6.         sql_pass                        = pass
  7.         sql_db                          = wikidb
  8.         sql_port                        = 3306
  9.         sql_query_pre           =
  10.         sql_query                       = \
  11.                 SELECT page_id, page_title,old_text \
  12.                 FROM page,revision,text \
  13.                 WHERE page_id = rev_page AND \
  14.                 rev_text_id = old_id AND \
  15.                 page_namespace = 0 \
  16.                 AND page_is_redirect = 0
  17. }
  18. index contenido
  19. {
  20.         source                  = contenidosrc
  21.         path                    = /ruta-sphinx/var/data/contenido
  22.         docinfo                 = extern
  23.         morphology              = none
  24.         min_word_len            = 1
  25.         charset_type            = utf-8
  26.         charset_table            = 0..9, A..Z->a..z, _,a..z, U+C9->U+E9, U+C1->U+E1, \
  27.                                    U+DA->U+FA, U+D1->U+F1, U+D3->U+F3, U+CD->U+ED, U+E1, \
  28.                                    U+E9, U+FA, U+F1, U+F3, U+ED
  29. }



Ahora toca crear los índices con el indexer de sphinx. Y es justo aqui cuando me llevo una sorpresa desagradable: tarda muchísimo en general en índice de contenido, unos 15 minutos. Los índices generados ocupan 4.1 megas el del titulo, y 530 megas el del contenido.


Creación de un pequeño sitio web para el buscador

El site se ha hecho plagiando al amigo google un poco (y a todos en general) e intentando que tuviera el interfaz más simple posible. El buscador de la wikipedia permite hacer consultas sin saber absolutamente nada sobre tecnología (aparte claro, de rellenar un campo y darle a buscar) pero posibilita también a la vez el uso de consultas avanzadas. Para ello se usa el modo de consulta extendido.

La función de query a sphinx tiene esta pinta:

PHP:
  1. function querySPHINX($tag,$indice,$off=0)
  2. {
  3.         $port = 3312;
  4.  
  5.         $cl = new SphinxClient ();
  6.         $cl->SetServer ( "localhost", $port );
  7.         $cl->SetLimits ($off, 10 );
  8.         $cl->SetMatchMode ( SPH_MATCH_EXTENDED );
  9.         $resultado = $cl->Query ( $tag, $indice );
  10.         $num_encontrados = $resultado['total_found'];
  11.  
  12.  
  13.         if($num_encontrados!=0 && is_array($resultado) && is_array($resultado["matches"]))
  14.         {
  15.                 //como los ids resultantes son los indices, se devuelve con array_keys
  16.                 return  array($num_encontrados,array_keys($resultado['matches']));
  17.         }
  18.         else
  19.         {
  20.                 return false;
  21.         }
  22. }



Las consultas normales llevan un AND implícito: si consultamos seat ibiza sobre el titulo veremos que solo nos saca el resultado que tiene las dos palabras a diferencia de seat | ibiza

Se pueden negar términos. Poniendo un - se pueden excluir palabras de la búsqueda. Por ejemplo si buscamos ibiza obtendremos unos resultados normales pero si ponemos ibiza -seat -dama -ciudad -isla tendremos unos resultados mucho más filtrados.

Es posible hacer busquedas de frase poniendo comillas. Entonces no sólo se buscarán todas las palabras sino que además deberán estar en el mismo orden. Esto permite hacer busquedas de fragmentos mayores (sobre el contenido) "Proverbios que dicen las viejas tras el fuego".

Por último, se dispone de un tipo de consulta, a mi juicio muy útil y potente que es el modo de frase por proximidad. Si el modo de frase buscaba una serie de palabras juntas y en un orden concreto, el modo por proximidad añade un cuantificador que permite que las palabras no estén exactamente igual a la frase pero si cerca. Pero ¿Cuánto de cerca?, pues es configurable. Si hacemos la consulta "kernel linux" se obtendrán los resultados que en el contenido tengan "kernel linux" y en cambio para "kernel linux"~5 se recogerán los resultados de los artículos que tengan las palabras kernel y linux en una distancia máxima de 5 palabras.


Se ha añadido también un pequeño informe de tiempos de ejecución que establece tres fases a medir:

  • Consulta a sphinx: tiempo de query a sphinx. Es realmente el parametro a estudiar
  • Recuperación de datos: tiempo de recogida de la información de la base de datos.
  • Salida a pantalla: construcción de resúmenes (sólo en caso de contenido) e impresión por pantalla

En este informe podemos ver lo realmente rápido que es sphinx, teniendo en cuenta que una búsqueda en el contenido (que es el índice grande) tarda del orden de 2 milisegundos. No es nada comparado con los 20 milisegundos que le cuesta construir los resúmenes y escupir por pantalla la información.



Datos obtenidos

Los tiempos que se ponen a continuación son los de búsqueda en sphinx siempre sobre el índice de contenido y no incluyen en el tiempo ni consulta a base de datos ni impresión por pantalla.



Conclusiones

Sphinx es realmente rápido para búsquedas de texto completo aun cuándo se tiene un índice muy grande. Además nos provee de unos modos de búsqueda avanzada muy útiles al introducir unos operadores muy fáciles e intuitivos. Pero no todo es bueno, el tiempo que se tardó en construir el índice es, a mi juicio, excesivo ( revisaré los parámetros por si es cosa mía, que también es posible).



Por hacer
En breve se le harán un par de retoques al buscador pero adelanto que se le incorporará un stemmer de castellano que yo mismo hice para sphinx. Es posible que escriba las consultas para motor fullext de mysql y postee la comparativa de resultados. Tampoco descarto, ahora que ya tengo la base de datos montada, probar otras alternativas como CLucene. No estaría de más convertir el código de wiki a html en los resúmenes de los resultados (que pereza).