ElasticSearch para español, acentos y raíz de palabras

Sobre ElasticSearch hemos comentado largo y tendido en el blog, pero es hora de escribir algo, que muchos ignoramos al implementar motores de búsqueda centrados en textos en español. Hablaremos de como mejorar las búsquedas para textos indexados en idioma español, sobre todo textos que contengan acentos y el caracter “ñ”, además como buscar palabras por su raíz.

El problema

Cuando iniciamos un proyecto con Elasticsearch y no somos muy expertos en el tema, lo normal es que al crear un índice lo hagamos “como viene”, es decir, simplemente hacemos POST a ElasticSearch y este creará el índice en la primera pegada y comenzará a insertar documentos JSON según vayamos POSTeando.

Imaginemos que acabamos de iniciar una instancia de ElasticSearch para un proyecto que implementa un buscador en idioma español. Lo normal sería desde nuestro backend o componente de recuperación de información (ej: un bot) comenzar a poblar nuestro ElasticSearch. Ejemplo:

POST /articulos/_doc
{
  "title": "Los brasileños están de celebración"
}

Cuando ejecutamos la operación anterior sin más configuraciones previas, todo funciona bien, se crea el índice de artículos y se indexa en ElasticSearch el documento. Pero el objetivo es implementar un motor de búsqueda, por lo que la capacidad fundamental tiene que estar en que se pueda encontrar el contenido guardado en nuestro ElasticSearch, hagamos ahora consultas sobre ElasticSearch para recuperar el texto anterior.

GET /articulos/_search
{
  "query": {
    "query_string": {
      "query": "celebración",
      "default_field": "title"
    }
  }
}

Al ejecutar la consulta de búsqueda libre anterior el resultado es:

{
  "took" : 1,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 1,
      "relation" : "eq"
    },
    "max_score" : 0.2876821,
    "hits" : [
      {
        "_index" : "articulos",
        "_type" : "_doc",
        "_id" : "sNLe7nQBzOchJkxEg0-_",
        "_score" : 0.2876821,
        "_source" : {
          "title" : "Los brasileños están de celebración"
        }
      }
    ]
  }
}

Como vemos es excelente, pero el problema está cuando el usuario que escribe la consulta, por ejemplo tiene problemas de ortografía y no sabe que celebración lleva acento o olvida la ñ de brasileños.

GET /articulos/_search
{
  "query": {
    "query_string": {
      "query": "celebracion",
      "default_field": "title"
    }
  }
}

Respuesta:

{
  "took" : 1,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 0,
      "relation" : "eq"
    },
    "max_score" : null,
    "hits" : [ ]
  }
}

Y aquí es donde se complicó, ya nuestro motor de búsquedas no es tan bueno, por lo que tenemos un problema. Veamos como resolverlo.

Solución: Mapping, analizador y template

En nuestro curso de ES hablamos de los conceptos rimbombantes que aparecen en el encabezado de este apartado, si quieres saber más en los detalles puedes verlo allí.

La solución para resolver este problema, y el de llevar las palabras a la raíz (stemmer) está en crear una plantilla o template de índices (como debe ser siempre) y crear un analizador personalizado que aplique los filtros específicos para el idioma español. ElasticSearch es genial pero como la mayoría de las tecnologías viene preparado por defecto para procesar textos en inglés.

Debajo se muestra el mapping con analizador y filtros que usaremos para indexar adecuadamente contenidos en idioma español.

PUT _template/articulostemplate
{
  "index_patterns": "articulos",
  "settings": {
    "number_of_shards": 2,
    "number_of_replicas": 1,
    "analysis": {
      "analyzer":{
      "mianalizador": {
          "tokenizer": "standard",
          "filter":  [ "lowercase", "asciifolding", "default_spanish_stopwords", "default_spanish_stemmer" ]
      }
    },
    "filter" : {
        "default_spanish_stemmer" : {
            "type" : "stemmer",
            "name" : "spanish"
        },
        "default_spanish_stopwords": {
            "type":        "stop",
            "stopwords": [ "_spanish_" ]
        }
    }
   }
  },
  "mappings": {
      "properties": {
        "title": {
          "type": "text",
          "analyzer": "mianalizador",
          "fields": {
            "keyword": {
              "type": "keyword",
              "ignore_above": 256
            }
          }
        }
      }
    }
}

Fijarse, que al campo title, del índice articulos le hemos indicado que usará el analizador mianalizador, esto significa, que cada vez que se insertemos un documento JSON será analizado por el motor de indexación siguiendo esa reglas.

Cuales son los pasos entonces para poner ese mapping en producción en nuestro Elasticsearch:

1 – Borremos el índice de articulos previamente creado que no funcionaba muy bien para el idioma español.

DELETE articulos

2- Creemos el mapping tal cual la instrucción mostrada arriba.

3- Probemos crear el mismo documento ahora.

POST /articulos/_doc
{
  "title": "Los brasileños están de celebración"
}

4- Probemos buscar ahora.

GET /articulos/_search
{
  "query": {
    "query_string": {
      "query": "celebracion",
      "default_field": "title"
    }
  }
}
GET /articulos/_search
{
  "query": {
    "query_string": {
      "query": "celebración",
      "default_field": "title"
    }
  }
}

En las dos búsquedas de arriba el resultado es el mismo, no importa si se omite el acento por la persona que hace la búsqueda, el resultado es el texto tal cual como se mandó a guardar.

{
  "took" : 0,
  "timed_out" : false,
  "_shards" : {
    "total" : 2,
    "successful" : 2,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 1,
      "relation" : "eq"
    },
    "max_score" : 0.2876821,
    "hits" : [
      {
        "_index" : "articulos",
        "_type" : "_doc",
        "_id" : "z9L77nQBzOchJkxE8U-3",
        "_score" : 0.2876821,
        "_source" : {
          "title" : "Los brasileños están de celebración"
        }
      }
    ]
  }
}

Verificación de la indexación en el motor

Por último podemos verificar como ElasticSearch a indexado el texto de campos específicos para un índice, de acuerdo a la configuración del mapping.

GET articulos/_analyze
{
  "field": "title",
  "text":  "Los brasileños están de celebración"
}

Con la línea anterior vemos como se analizó este texto antes de guardarlo, el resultado es:

{
  "tokens" : [
    {
      "token" : "brasilen",
      "start_offset" : 4,
      "end_offset" : 14,
      "type" : "<ALPHANUM>",
      "position" : 1
    },
    {
      "token" : "estan",
      "start_offset" : 15,
      "end_offset" : 20,
      "type" : "<ALPHANUM>",
      "position" : 2
    },
    {
      "token" : "celebracion",
      "start_offset" : 24,
      "end_offset" : 35,
      "type" : "<ALPHANUM>",
      "position" : 4
    }
  ]
}

Las mismas reglas de indexación se siguen para búsqueda, por eso ahora nuestro elásticsearch encuentra más resultados, si aplicaramos la misma consulta para el criterio de búsqueda (ahora para probar la ñ) sería:

GET articulos/_analyze
{
  "field": "title",
  "text":  "brasilenos"
}

Y el resultado:

{
  "tokens" : [
    {
      "token" : "brasilen",
      "start_offset" : 0,
      "end_offset" : 10,
      "type" : "<ALPHANUM>",
      "position" : 0
    }
  ]
}

Si nos fijamos entre lo que guardamos y lo que buscamos coincide el “token” : “brasilen”, esto es sencillamente genial, y es lo que hace que ElasticSearch sea tan grande, junto a los índices invertidos y el sharding claro 🙂

Espero les sea útil esta entrada.