Archive for Mayo, 2007

OpenSearch

Lunes, Mayo 28th, 2007

OpenSearch es un estándar para la publicación de resultados de una búsqueda en un formato adecuado para la sindicación y agregación. Es una forma de que las páginas web y los motores de búsqueda publiquen sus resultados de forma accesible.

Es un protocolo creado por a9.com (propiedad de Amazon) y actualmente es usado por muchos navegadores (principalmente Internet Explorer 7 y Firefox 2) en sus combos de buscadores.

Para incluir opensearch en un buscador propio, basta con escribir un pequeño xml que mapee el campo de búsqueda con una petición web. Luego adicionalmente podemos incluir una etiqueta en el sitio web para que el navegador sepa que esa página dispone de opensearch.



El fichero xml

Es un fichero muy simple en el que se indica que es un documento opensearch, como se mapea la peticion de consulta del buscador y que icono (tipo favicon) queremos usar. Luego pueden usarse más opciones, pero para un caso simple bastaría con esto (además no todos lo navegadores soportan todas las opciones).

XML:

  1. <?xml version="1.0" encoding="UTF-8"?>
  2.  <OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/">
  3.    <ShortName>Nombre</ShortName>
  4.    <Description>Descripcion</Description>
  5.    <Tags>Tags</Tags>
  6.    <Url type="text/html" method="get" template="URL?parametro={searchTerms}"/>
  7.    <Image width="16" height="16">Ruta al favicon</Image>
  8.  </OpenSearchDescription>



Por ejemplo, para el buscador de la wikipedia el fichero xml sería:

XML:

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/">
  3.     <ShortName>SearchPedia</ShortName>
  4.     <Description>Encuentra en Wikipedia</Description>
  5.     <Tags>searchpedia search wikipedia buscador </Tags>
  6.     <Url type="text/html" method="get" template="http://searchpedia.compuglobalhipermega.net/sear
  7. ch.php?index=titulo&amp;stem=on&amp;q={searchTerms}" />
  8.     <Image width="16" height="16">http://searchpedia.compuglobalhipermega.net/wiki.ico</Image>
  9. </OpenSearchDescription>


Y ya sólo queda poner la etiqueta de notificación de sistema de búsqueda:

HTML:

  1. <link rel="search" type="application/opensearchdescription+xml" title="SearchPedia" href="http://searchpedia.compuglobalhipermega.net/opensearch.xml">



Y para acabar de rematar la faena, podemos incluir un enlace para que al pulsar en el se instale nuestro sistema de búsqueda. Por ejemplo: SearchPedia.

Para realizar esto último, debes usar el siguiente código javascript:

JAVASCRIPT:

  1. window.external.AddSearchProvider('Ruta absoluta al fichero XML')


Referencias

Mejoras en SearchPedia: el buscador de la wikipedia

Domingo, Mayo 27th, 2007

Se han realizado varias mejoras sobre el buscador de wikipedia.

La primera es que se ha mejorado considerablemente el tiempo del reconstrucción del índice. En concreto esta era la única carácterística que había defraudado cuando se desarrolló y mostró como hacer un buscador para la wikipedia. En aquella prueba me quejaba de que tardara casi 15 minutos en construir el índice.

Tras postear en el foro de sphinx y confirmarme allí que no era el tiempo esperado para esa operación, me dí cuenta de que los índices que tienen las tablas del mediawiki están optimizados para sus consultas pero no para las que queremos realizar al indizar los artículos.

Una vez optimizado eso, se reduce el tiempo de construcción a unos 5 minutos, algo mucho más lógico.

También se han corregido algunos errores en la paginación cuando las consultas contenían caracteres que necesitaban codificación (acentos y ñ).

Soporte para stemming

Como mejora principal, se ha realizado un cambio relevante en el motor de búsqueda. Se ha incluido el soporte de stemming para castellano en sphinx. A partir de ahora en el formulario del buscador se incluye un checkbox donde se indica si se quiere realizar una búsqueda con stemming o no.

Ahora el interfaz queda asi:

Formulario searchpedia

Resultados searchpedia

Ahora las búsquedas pueden ser ampliadas con resultados relacionados (y por supuesto relevantes) ya que el stemming (o lematización) hace que las palabras búscadas sean reducidas a su lexema.

Por ejemplo si realizamos la búsqueda electricidad sin stemming nos devolverá 11 artículos, pero si realizamos esa misma búsqueda electricidad con soporte de lematización obtendremos 117 resultados. Estos resultados adicionales incluyen términos como eléctrica, eléctrico y otras palabras que puedan ser reducidas al mismo lexema "electr".

Aparte de no estar desechando resultados interesantes para la búsqueda se tiene la gran ventaja de que todas las búsquedas que se realicen sobre palabras que deriven el el mismo lexema devolverán exactamente los mismos resultados. Por ejemplo obtendremos lo mismo si consultamos electricidad, electrica o electrico

A disfrutar del buscador.

Stemmer en castellano para SPHINX

Sábado, Mayo 26th, 2007

He hablado más de una vez del indexer/searcher Sphinx en el blog y de que es una herramienta genial para realizar buscadores de texto completo de alto rendimiento. Si todavía no conoces sphinx echa un ojo a:



Lo que ahora quiero presentar es un stemmer en castellano que desarrollé hace unos meses para sphinx.

¿Qué es un stemmer?

Un stemmer es un algoritmo que realiza un proceso de stemming o lematización por el cual cada palabra es reducida a su lexema o raíz.

Este proceso es realmente útil para aumentar el recall o exhaustividad de un sistema de recuperación de información. Es muy importante tener en cuenta que el stemming es completamente dependiente del idioma en el que está la información a procesar.

Genial, pero ¿Qué es un stemmer?

Imaginemos por un momento que tenemos un directorio de empresas y un buscador asociado. Supongamos que disponemos en base de datos los siguientes registros:

  • ID Nombre
  • 1 "Fontanería Los rápidos"
  • 2 "Perez Fontaneros"
  • 3 "El fontanero feliz"

Si no disponemos de una herramienta de lematización, tendríamos los siguientes resultados:

  • Consulta ID-resultante
  • "fontanería" 1
  • "fontaneros" 2
  • "fontanero" 3

Esos resultados no sólo serían diferentes entre sí, sino que además estarían incompletos. En cambio si para esos mismo registros de base de datos usamos un stemmer para castellano:

  • Consulta ID-resultante
  • "fontanería" 1 2 3
  • "fontaneros" 1 2 3
  • "fontanero" 1 2 3

Creo que ya ha quedado claro la importancia de usar técnicas de stemming.



Sphinx y el stemmer

En este caso en particular, sphinx provee de dos stemmers, uno para inglés y otro para ruso (ya que el desarrollador es ruso). Si queremos realizar un buscador web con sphinx que aplique técnicas de stemming sólo hay una opción: desarrollar el stemmer nosotros mismos.

Después de estudiar un poco como es la parte de lematización en sphinx por dentro se ve que sería suficiente con desarrollar una función en C/C++ que recibida una palabra por parámetro devuelva su lexema y luego integrarlo con un par de directivas y actualización de los makes.

¿Por dónde empezar?. Bueno, pues es importante destacar que hay una persona que se encargó en su día de definir los algoritmos de stemming y es el verdadero referente en el tema. Se llama Martin Porter , el ya nos ha hecho todo el trabajo sucio y nos pone en bandeja la codificación.



Fases del stemmer

  • Fase 0: Eliminar pronombres
  • Fase 1: Eliminar sufijos estándar
  • Fase 2a: Eliminar sufijos verbales que empiecen por y
  • Fase 2b: Eliminar otros sufijos
  • Fase 3: Eliminar sufijos residuales
  • Final: Convertir acentos



Instalación e integración del stemmer con Sphinx

La versión de sphinx para la que se va a explicar todo es la 0.9.7 (la última).

El stemmer en si, es un único fichero sphinxstemes.cpp con la función que realiza la lematización. Pero para integrarlo en sphinx es necesario incluir un par de líneas en 4 archivos:

En el fichero sphinx.h, en la línea 251 esta definido el enum de morfologías. Se debe cambiar por este:

CODE:

  1. /// morphology flags
  2. enum ESphMorphology
  3. {
  4.     SPH_MORPH_NONE        = 0,
  5.     SPH_MORPH_STEM_EN         = (1UL<<1),
  6.     SPH_MORPH_STEM_RU_CP1251    = (1UL<<2),
  7.     SPH_MORPH_STEM_RU_UTF8    = (1UL<<3),
  8.     SPH_MORPH_SOUNDEX         = (1UL<<4),
  9.     SPH_MORPH_STEM_ES         = (1UL<<5),
  10.     SPH_MORPH_UNKNOWN         = (1UL<<30)
  11. };


En el fichero sphinx.cpp, línea 8300 aproximadamente, cambiar el método GetWordID por:

CODE:

  1. DWORD CSphDict_CRC32::GetWordID ( BYTE * pWord )
  2. {
  3.     if ( m_iMorph & SPH_MORPH_STEM_EN )
  4.         stem_en ( pWord );
  5.     if ( m_iMorph & SPH_MORPH_STEM_ES )
  6.         stem_es ( pWord );
  7.     if ( m_iMorph & SPH_MORPH_STEM_RU_CP1251 )
  8.         stem_ru_cp1251 ( pWord );
  9.     if ( m_iMorph & SPH_MORPH_STEM_RU_UTF8 )
  10.         stem_ru_utf8 ( (WORD*)pWord );
  11.     if ( m_iMorph & SPH_MORPH_SOUNDEX )
  12.         stem_soundex ( pWord );
  13.  
  14.     return FilterStopword ( sphCRC32 ( pWord ) );
  15. }


En el fichero sphinxutils.cpp, hay que modificar el parser del fichero de configuración. en la línea 419 la función sphConfMorphology debe tener esta pinta:

CODE:

  1. DWORD sphConfMorphology ( const CSphConfigSection & hIndex, bool bUseUTF8 )
  2. {
  3.     if ( !hIndex("morphology") )
  4.         return SPH_MORPH_NONE;
  5.  
  6.     const CSphString & sOption = hIndex["morphology"];
  7.  
  8.     DWORD iMorph = SPH_MORPH_UNKNOWN;
  9.     DWORD iStemRu = ( bUseUTF8 ? SPH_MORPH_STEM_RU_UTF8 : SPH_MORPH_STEM_RU_CP1251 );
  10.  
  11.     if ( sOption=="stem_en" )
  12.         iMorph = SPH_MORPH_STEM_EN;
  13.  
  14.     else if ( sOption=="stem_es" )
  15.         iMorph = SPH_MORPH_STEM_ES;
  16.  
  17.     else if ( sOption=="stem_ru" )
  18.         iMorph = iStemRu;
  19.  
  20.     else if ( sOption=="stem_enru" )
  21.         iMorph = iStemRu | SPH_MORPH_STEM_EN;
  22.  
  23.     else if ( sOption=="soundex" )
  24.         iMorph = SPH_MORPH_SOUNDEX;
  25.  
  26.     else if ( sOption.IsEmpty() || sOption=="none" )
  27.         iMorph = SPH_MORPH_NONE;
  28.  
  29.     return iMorph;
  30. }

Por último en el fichero sphinxstem.h, se debe poner la definición de la función de stemming. Añadir

CODE:

  1. /// stem lowercase Spanish word
  2. void    stem_es ( BYTE * pWord );

Una vez modificado esto, sólo queda actualizar los make para reconstruir el proyecto. Incluir sphinxstemes en am__objects

CODE:

  1. am__objects_1 = sphinx.$(OBJEXT) sphinxexcerpt.$(OBJEXT) \
  2.     sphinxquery.$(OBJEXT) sphinxsoundex.$(OBJEXT) \
  3.     sphinxstemen.$(OBJEXT) sphinxstemes.$(OBJEXT) sphinxstemru.$(OBJEXT) \
  4.     sphinxutils.$(OBJEXT) md5.$(OBJEXT) sphinxstd.$(OBJEXT)

y también el cpp en los fuentes

CODE:

  1. SRC_SPHINX = sphinx.cpp sphinxexcerpt.cpp sphinxquery.cpp sphinxsoundex.cpp sphinxstemen.cpp sphinxstemes.cpp sphinxstemru.cpp sphinxutils.cpp md5.cpp sphinxstd.cpp

Configuración

Una vez recompilado sphinx con el soporte para el stemmer en castellano, podemos configurar un índice que lo use índicandoselo en el fichero de configuración de esta manera (omitidos los campos que no cambian de una configuración normal):

CODE:

  1. index x
  2. {
  3.         morphology              = stem_es
  4.         charset_type            = utf-8
  5.         charset_table            = 0..9, A..Z->a..z, _, a..z, U+C9->U+E9, U+C1->U+E1, \
  6.                                    U+DA->U+FA, U+D1->U+F1, U+D3->U+F3, U+CD->U+ED, U+E1, \
  7.                                    U+E9, U+FA, U+F1, U+F3, U+ED
  8. }



Hay que tener mucho cuidado con la charset_table, porque es ahí, dónde se definen que caracteres aceptamos para el índice y cuales reconvertimos. El formato U+XX es unicode, y podeis ver unas tablas de códigos en unicode.org.

En la charset_table definida se aceptan los números, los guiones bajos, las letras acentuadas (sólo acentos del castellano) y las letras sin acentuar. Además son convertidas de mayusculas a minúsculas (eso quiere decir la -> ). Una mala configuración de este parámetro puede darnos muchos dolores de cabeza, asi que recomiendo estudiarlo con detenimiento. Por ejemplo, en el caso del búscador de la wikipedia, no se acepta el guión bajo, ya que es el usado en los títulos a modo de espacio.

Si por alguna razon, no quisieramos usar stemming pero si indizar texto en castellano, habría que retocar esta tabla para convertir los caracteres acentuados a su equivalente sin acentuar (más la ñ).



Pruebas del stemmer
En la web de Porter, se da una lista de 28000 palabras y su lematización correcta. El stemmer ha sido probado con esa lista y cotejando el resultado esperado con el obtenido. A excepción de 5 palabras (que debo revisar) todo va igual.



Descarga del stemmer

El stemmer al igual que el proyecto sphinx, es GPL, asi que puedes usarlo con libertad.
Puedes descargarlo de aqui: stemmer-castellano



Donde ver un buscador con el stemmer aplicado

El stemmer se puede ver en buscador de la wikipedia y también en el agregador de blogs agregax, un proyecto muy interesante que lleva a cabo Pau Iglesias (recientemente le hicieron un artículo en El País).

strtolower y los acentos en php

Miércoles, Mayo 23rd, 2007

En php existe una función muy simple que te pasa una cadena dada a minúsculas. Esta función es strtolower y, a priori no tiene mucha complicación. Pero cuando un día empiezas a ver tu base de datos o sitio web con palabras malformadas te mosquea, empiezas a estudiar el problema y concluyes que no pasa bien a minúsculas los caracteres que no pertenecen al ascii-7. En teoría depende del locale de la maquina, pero ¿porque hacer código dependiente de mil variables de sistema?.

Por lo tanto la función, tan necesaria como simple, no nos vale por si misma para el propio idioma español ya que convierte cadenas como "LOGROÑO" a "logroÑo" y "DÍA" a "dÍa".

La solución pasa por hacer un paso previo al strtower que convierta estos caracteres:

PHP:

  1. function strtolowerExtended($str)
  2. {     
  3.         $low = array(chr(193) => chr(225), //á
  4.                      chr(201) => chr(233), //é
  5.                      chr(205) => chr(237), //í­
  6.                      chr(211) => chr(243), //ó
  7.                      chr(218) => chr(250), //ú
  8.                      chr(220) => chr(252), //ü
  9.                      chr(209) => chr(241)  //ñ
  10.                      );
  11.  
  12.  
  13.       return strtolower(strtr($str,$low));
  14.  
  15. }

Es mejor poner los caracteres acentuados con chr o hexadecimal para no tener problemas cuando cambias a un sistema de ficheros sin soporte local para esos caracteres.

Consejo: cuando usas una función php, lee los comentarios de la gente bajo la explicación de la sintaxis en la referencia de la web, te ahorrará trabajo.