Archive for the ‘Motores de búsqueda’ Category

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

Como crear tu propio buscador.
Instalación y configuración de SPHINX ( II )

Viernes, Marzo 23rd, 2007

Continuando el post de instalación de sphinx vamos a ver un ejemplo práctico, que es lo que estamos desenado todos.

He descargado los RFC desde la web del IETF con un script en PHP y algo de expresiones regulares, e insertados en una tabla. En total son unos 4400 documentos con mucho texto para juguetear.

La base de datos tiene una sola tabla bastante simple, aquí está el código SQL para crearla:

SQL:
  1. CREATE TABLE `rfc_en` (
  2.   `id` int(11) NOT NULL AUTO_INCREMENT,
  3.   `titulo` varchar(255) collate latin1_spanish_ci NOT NULL,
  4.   `autor` varchar(255) collate latin1_spanish_ci NOT NULL,
  5.   `enlace` varchar(255) collate latin1_spanish_ci NOT NULL,
  6.   `contenido` mediumblob NOT NULL,
  7.   `fecha` date NOT NULL,
  8.   PRIMARY KEY  (`id`),
  9.   KEY `fecha` (`fecha`)
  10. ) ENGINE=MyISAM  DEFAULT CHARSET=latin1 COLLATE=latin1_spanish_ci AUTO_INCREMENT=1;



El archivo de configuracion se va a mostrar por partes.

Apartado source

CODE:
  1. source rfcsrc
  2. {
  3.         type            = mysql
  4.         sql_host      = host
  5.         sql_user      = miuser
  6.         sql_pass     = mipass
  7.         sql_db         = rfc
  8.         sql_port       = 3306 
  9.  
  10.         sql_query_pre       =
  11.         sql_query              = \
  12.                SELECT id, titulo FROM rfc_en
  13.         sql_query_post     =
  14. }
  15.  
  16. source rfc2src : rfcsrc
  17. {
  18.         sql_query       =\
  19.                 SELECT id, titulo,contenido FROM rfc_en
  20. }



Se han definido dos índices, en uno se va a tener en cuenta el título de los RFC y en el otro vamos a hacer un poco el bruto indizando todo el contenido del RFC (llegan a ser 300KB o más) además del título. En el primer source se ve la información de conexión a base de datos y la sentencia sql de construcción del índice.

La sentencia se construye igual que cualquier SELECT de SQL pero con la peculiaridad de que el primer campo a recoger debe ser la clave primaria (recordar que esta DEBE ser un entero único de 32 bits). Todos los campos que aparezcan a continuación serán utilizados para el índice. De momento solo usaremos datos de texto, más adelante se detallará como introducir otro tipo de campos para búsquedas más avanzadas.

El segundo source esta definido mediante una herencia del primero, lo cual nos evita escribir 10 líneas ( los desarrolladores somos vagos y podemos estar un día escribiendo un script para evitar repetir la misma acción 2 veces :D ). Solo sobreescribe la sentencia SQL y se mantiene el resto.

Apartado index

CODE:
  1. index rfc1
  2. {
  3.         source                = rfcsrc
  4.         stopwords           =
  5.         min_word_len     = 1
  6.         charset_type       = sbcs
  7.         path                    = ruta-de-instalacion/var/data/rfc1
  8.         morphology        = none
  9. }
  10.  
  11.  
  12. index rfc2 : rfc1
  13. {
  14.         source                = rfc2src
  15.         path                    = ruta-de-instalacion/var/data/rfc2
  16. }



Cada índice hace referencia a su fuente de datos definida más arriba.


Apartado indexer: simplemente forzamos a 32 megas el máximo usado por el indizador.

CODE:
  1. indexer
  2. {
  3.         mem_limit      = 32M
  4. }


Apartado search: se ponen las rutas de los log y el descriptor de proceso. Se define el puerto asociado al demonio y otros parámetros de control. Si no se está seguro de que poner en algun sitio, dejarlo tal cual viene en el fichero esqueleto.

La directiva address es un mecanismo de seguridad, podemos forzar al demonio a que solo conteste las peticiones dirigidas a cierta ip. Si tenemos el script php en la misma máquina que sphinx se puede restringir a la direccion de loopback 127.0.0.1 y nos cubriremos las espaldas por si legan peticiones externas. Si sphinx esta instalado en una máquina diferente al origen de la consulta, pero se dispone de red local se puede asociar el demonio a la ip privada en vez de la pública.

CODE:
  1. searchd
  2. {
  3.        
  4.         # address        = 127.0.0.1
  5.         # address        = 192.168.0.1
  6.         port                  = 3312
  7.         log                   = ruta-de-instalacion/var/log/searchd.log
  8.         query_log         = ruta-de-la-instalacion/var/log/query.log
  9.         read_timeout    = 5
  10.         max_children    = 30
  11.         pid_file              =ruta-de-instalacionvar/log/searchd.pid
  12.         max_matches   = 1000
  13. }


Con esto ya tenemos fichero de configuración listo.

Así que al lio, lanzamos el indexer:

$ bin/indexer --config etc/sphinx.conf --all
indexing index 'rfc'...
collected 4398 docs, 0.2 MB
sorted 0.0 Mhits, 100.0% done
total 4398 docs, 222430 bytes
total 0.297 sec, 749621.68 bytes/sec, 14821.90 docs/sec
indexing index 'rfc2'...
collected 4398 docs, 214.1 MB
sorted 27.6 Mhits, 100.0% done
total 4398 docs, 214094137 bytes
total 22.821 sec, 9381369.37 bytes/sec, 192.72 docs/sec

En var/data estarán ahora los dos índices generados y tendrán dos tamaños bastante diferentes: 200 KB frente a 68 MB. Hay que decir que los índices tan grandes pueden ser habituales pero, al contrario que este caso, lo serán por el hecho de que las tablas indizadas tengan una cantidad de registros bastante alta en vez de tener una media de 150 KB de texto por registro.

$ ls -lh var/data/
total 70M
-rw-r--r-- 1 root root 0 2007-03-23 20:49 rfc2.spa
-rw-r--r-- 1 root root 68M 2007-03-23 20:49 rfc2.spd
-rw-r--r-- 1 root root 79 2007-03-23 20:49 rfc2.sph
-rw-r--r-- 1 root root 1,1M 2007-03-23 20:49 rfc2.spi
-rw-r--r-- 1 root root 0 2007-03-23 20:49 rfc.spa
-rw-r--r-- 1 root root 203K 2007-03-23 20:49 rfc.spd
-rw-r--r-- 1 root root 62 2007-03-23 20:49 rfc.sph
-rw-r--r-- 1 root root 23K 2007-03-23 20:49 rfc.spi

Y ahora lanzamos el demonio searchd:

$ bin/searchd --config etc/sphinx.conf
Sphinx 0.9.7-RC2
Copyright (c) 2001-2006, Andrew Aksyonoff

using config file 'etc/sphinx.conf'...

Llegados a este punto ya estamos en posicion para empezar a lanzar consultas al demonio y para ello usaremos el API PHP.
Un poquito de código:

PHP:
  1. function querySPHINX($tag,$off,$contenido)
  2. {
  3.     $port = 3312;
  4.  
  5.         if($contenido)
  6.         {
  7.             $indice = "rfc2";
  8.         }
  9.         else
  10.         {
  11.          $indice = "rfc";
  12.         }
  13.     if ($off == "")
  14.     {
  15.         $off = 0;
  16.     }
  17.    
  18.     $cl = new SphinxClient ()
  19.     $cl->SetServer ( "localhost", $port );   
  20.     $cl->SetLimits ($off, 10 )
  21.     $cl->SetMatchMode ( SPH_MATCH_ALL );   
  22.     $resultado = $cl->Query ( $tag, $indice );
  23.     $num_encontrados = $resultado['total_found'];
  24.    
  25.     if($num_encontrados == 0)
  26.     {
  27.             $cl->SetMatchMode ( SPH_MATCH_ANY );
  28.             $resultado = $cl->Query ( $tag, $indice );
  29.             $num_encontrados = $resultado['total_found'];
  30.     }
  31.     return  array_keys($resultado['matches']);   
  32. }



Con esta función se hace una consulta muy simple basada en la keyword a buscar, el offset de resultados a devolver (para paginar) y un booleano para decidir si consultar el contenido o no. Primero creamos el objeto y se le asocia el servidor y puerto del demonio. A continuación se le indica que queremos obtener 10 resultados a partir del offset $off (0 por omisión), se selecciona el modo de consulta y se lanza la misma.

Los modos de consulta son:

  • SPH_MATCH_ALL: el documento devuelto debe contener todas las palabras de la búsqueda
  • SPH_MATCH_ANY: el documento devuelto debe contener alguna de las palabras de la búsqueda
  • SPH_MATCH_PHRASE: el documento devuelto debe contener todas las palabras de la búsqueda y en el mismo orden

La variable $resultado devuelve bastante información pero de momento nos quedamos con dos elementos:

  • $resultado['total_found'] : numero de resultados encontrados para la búsqueda
  • $resultado['matches'] : array con los identificadores de documento como índice. Por eso se recuperan las claves con array_keys



Espero que esta guía haya sido de utilidad y que la gente se anime a instalarlo y cacharrear un poco.

Podeis probar la demo. Me gustaría no estar en un hosting compartido ni tener un ping tal alto para que vierais que esto es realmente rápido. Si alguien quiere el ejemplo le puedo facilitar los scripts de construcción de la base de datos, el resto está practicamente en las entradas.

Actualizacion
He cambiado la demo de sitio, porque la gente de dreamhost me tiraba el sphinx (ya decia yo que era demasiado chollo para un plan cutre de hosting compartido instalar un servicio como si nada ). Ahora está en una máquina con buenos recursos. Así que ya no hay excusa, los tiempos deberían ser bastante buenos.

Enlaces de interés

Comparativa de sistemas de búsquedas FULLTEXT

Martes, Marzo 20th, 2007

Leyendo una entrada un poco antigua de MySQL Performance Blog me encuentro con un documento bastante interesante sobre comparativas de búsquedas de texto completo en MySQL hechas con diferentes sistemas: Lucene, Sphinx, TgSearch y el propio MySQL.

El documento en cuestión no es otro que High Performance Full Text Search for Database Content presentado en la EuroOSCON 2006.

De él extraigo estos gráficos que hablan por si solos (hacer click para ver en un tamaño decente).

Tiempo de ejecución de una consulta booleana

Comparativa Boolean Search



Tiempo de construcción del índice

Index Building Time



Tamaño del índice

Tamaño del índice



Tiempo de ejecución de consulta de tipo ‘phrase’'

Tiempo de ejecución de consulta de tipo ‘phrase’



Tiempo de ejecución de consulta normal

Tiempo de ejecución de consulta normal



Como siempre sacar los gráficos de contexto es algo muy feo, esto tiene muchos matices y por eso recomiendo la lectura completa del documento. Aún así, hay una cosa en común en todos los resultados y es que SPHINX barre con mucha diferencia a cualquiera de los otros sistemas.

Ya no teneis excusa para instalar SPHINX