Elastic Search para Product Managers

6 March 2022| Tags: datos, product management, elastic search

Introducción

Este artículo es parte de la serie de píldoras para Product Managers que quieren tomarle el pulso a la tecnología, que puedan probarla, instalarla y demostrar sus capacidades. En taniwa creemos que es osado presentarse como PM o Leader de un proyecto en cuyo ámbito de conocimiento seas un ignorante.

Saber es poder

Piensa en ElasticSearch (ES) siempre que tu producto necesite de:

  1. Búsquedas indexadas, osea, buscar un texto en diferentes partes de un documento de forma inteligente.
  2. Agrupaciones por tipologías de documentos. Por ejemplo un catálogo de Ikea y sus facetas (“Deporte”, “Color”, “Talla”).
  3. Velocidad tanto en las búsquedas como en las sugerencias de la barra de búsqueda.
  4. Centralizar mucha información de forma que puedas buscar en ella en tiempo real. Los logs de aplicaciones por grandes que sean.

ES en un producto viejuno, basado en Apache Lucene, pero que ha sabido mantenerse en la lucha haciendo muy bien unas cuantas cosas que muchas empresas quieren en sus productos:

  • Búsquedas textuales, en el idioma que sea y aplicando algoritmos de text-mining muy flexibles.
  • Robusto. Montar un clúster de ES que funcione de verdad es fácil (no como otros).
  • Rápido. Inserciones velocísimas y paralelas y fulgurantes búsquedas en millones de documentos.
  • Interfaz REST y resultados en JSON. Desde el primer día, lo que le adelantó en su tiempo a los demás (ahora ya no).

Además de su motor de búsquedas e indexación tiene otros elementos cargados de valor como:

  • Logstash: Inserciones de los documentos desde ficheros de log, por ejemplo.
  • Kibana: Consola Web para explotar y visualizar los datos.

Suelen instalarse juntas y por eso se habla de una solución ELK.

No vamos a incluir en este artículo cómo instalar ES pero tienes muchas opciones que en orden de preferencia para nosotros son:

  1. Cloud de ES, que te permite soltarlo en máquinas de AWS, Azure o Google Cloud entre otras.
  2. Bajarte un Docker en local y ponerte a jugar.
  3. Crearte tu propio clúster. No tan preferida si no es necesario.

Amazon tiene un servicio propio similar que se llama OpenSearch con su historia de robos que puedes leer aquí Amazon: NO ESTÁ BIEN - por qué tuvimos que cambiar el licenciamiento de Elastic

Documentos y Mappings

Un documento es una estructura de campos cada uno con su tipo de datos. Por ejemplo una entrada de este blog tendría:

titulo: texto
contenido: texto más largo
fecha: Fecha con hora estilo 2022-12-31 00:23:40
etiquetas: "product manager", "taniwa"
puntuación: Entero de 1 a 10.

ElasticSearch te permite crear un índice en el que albergar tus documentos. Con que guardes un documento en un índice al que le das un nombre, ElasticSearch ya se hace cargo de todo:

  1. Crear el índice con un mapping automático.
  2. Distribuir el índice entre los diferentes nodos.
  3. Recibir queries y responder muy rápido.

Un mapping es la asociación de los campos de tus documentos con un tipo de dato y un analizador de texto.

Un analizador lo que hace es construir un índice inverso de tus documentos para luego, cuando buscas una cadena de 3 palabras, traerte los documentos con más “aciertos”.

Blog1: Perros
Este es el blog1 que habla de perros rabiosos

Blog2: Monos
Aquí se hablaría de monos rabiando.

Índice inverso:
habla    => Blog1
perros   => Blog1
rabiosos => Blog1
...
hablaría => Blog2
monos    => Blog2
rabiando => Blog2

Si mi búsqueda es “artículo que habla de monos”

[artículo] aparece en: [ningún blog]
[que] aparece en: [blog1]
[habla] aparece en: [blog1]
[de] aparece en [blog1 blog2]
[monos] aparece en: [blog2]

Como blog1 tiene 3 aciertos y blog2 tiene 2 aciertos, ES devolvería primero blog1, que no parece lo más adecuado en este caso.

Seleccionar bien el analizador es lo que hace que seamos muy “listos” en las búsquedas o al menos en las que pensamos que necesitamos responder.

Nota para PM, define el tipo de búsquedas y ejemplos que tendrá que defender tu motor de ES.

Los analizadores hacen cosas como:

  • Pasar todo a minúscula. Al insertar los documentos el campo título pasaría de “Perros” a “perro” y lo mismo al buscar “Perro …”
  • Cortar los textos en palabras (tokenizer). Puede separar por espacios, por “,” ó “.” u otras.
  • Pueden quitar todas las etiquetas HTML que en pincipio no aportan a las búsquedas.
  • Pueden eliminar palabras cortas que no aportan valor semántico (stop words) como preposiciones y artículos. Osea “Aquí se hablaría de monos rabiando” quedaría “hablaría monos”
  • Pueden llevar palabras a su raíz (stemming). Por ejemplo “Jugar, Jugamos, Jugaríamos, " podrían sustituirse por el verbo en infinitivo “jugar”.
  • Tratar con sinónimos. “Perro” y “Can” son la misma cosa.

Esto pemite que la anterior búsqueda resulte así:

La búsqueda es "artículo que habla de monos" y queda: "articulo hablar mono"
Osea el analizador ha quitado las vocales con tilde, la stopword "que" y ha reducido "habla" a "hablar" y "monos" a "mono".
El mismo analizador ha tratado los textos de antes con lo que ahora:

[artículo] aparece en: [ningún blog]
[hablar] aparece en: [blog1 blog2]
[monos] aparece en: [blog2]

Ahora blog1 tiene 1 acierto y blog2 tiene 2 aciertos, con lo que ES devolvería primero blog2, que si parece que es lo que queremos.

ES nos da de partida los analizadores preparados, pero también podemos montarnos el nuestro tratando los textos de salida de una parte como entrada de otro.

Si imaginamos millones de documentos, con cientos de campos, unos de texto libre, otros de textos limitados (colores por ejemplo), fechas, números y precios, ES lo gestiona bien y rápido permitiéndonos dividir la carga sin enterarnos, traer resultados con sentido, filtrar por campos de cualquier tipo y agrupar. ES agrupa a toda pastilla devolviendo los documentos que pertenecen a cada valor de cada campo: Tipo de Producto, por colores, por talla, por rango de precio y por deporte.

Pasamos a la parte más técnica en la que vamos a hablar con un ES usando llamadas http y viendo los resultados JSON.

Leer la documentación de ElasticSearch siempre ayuda y siempre está ahí como ayuda.

Queries comunes

  1. Ejemplos en un terminal Linux o Mac
  2. Instalar “curl” y jq

Ver el mapping de un índice:

curl -u elastic:$ES_PWD https://XXXXXXXXXXXXXXXXXX.eu-west-2.aws.cloud.es.io/catalogo_productos/_mapping | jq

Ver el mapping de un campo de un índice :

curl -u elastic:$ES_PWD https://XXXXXXXXXXXXXXXXXX.eu-west-2.aws.cloud.es.io/catalogo_productos/_mapping/field/tipo_producto 
{
  "catalogo_productos": {
    "mappings": {
      "tipo_producto": {
        "full_name": "tipo_producto",
        "mapping": {
          "tipo_producto": {
            "type": "text",
            "fields": {
              "keyword": {
                "type": "keyword",
                "ignore_above": 256
              }
            }
          }
        }
      }
    }
  }
}

Si añadimos un campo, sus datos se almacenan pero no se indexan y por tanto no estará disponible para búsquedas.

Si necesitamos cambiar el mapping de un campo, crear un nuevo índice con el mapping correcto y reindexar los datos del índice original al nuevo.

curl -X PUT "-u elastic:$ES_PWD https://XXXXXXXXXXXXXXXXXX.eu-west-2.aws.cloud.es.io/catalogo_productos/_mapping?pretty" -H 'Content-Type: application/json' -d'
{
  "properties": {
    "newid": {
      "type": "keyword",
      "index": false
    }
  }
}
'

Tipos de dato para los mappings

Tenemos varios tipos de dato: boolean, long, and string types. “String” es el tipo de dato en el que somos fuertes con ES. Podemos indexar (analizar) campos de tipo cadena a “text” o como “keyword” sabiendo que:

  • text Se analizan para poder ser buscados con toda la potencia de ES.
  • keyword Las cadenas de dejan como vienen para acciones como ordenar por ellas o filtrar (Tráeme sólo los productos de color azul).

Los tipos de los campos están agrupados por familia. Los tipos dato de la misma familia se comportan igual respecto a las búsquedas pero pueden almacenarse de diferente manera y con diferente peso por razones de rendimiento.

Hay dos grandes grupos de familias, “keyword” y “text”. Otros tipos de familias tienen un tipo de campo simple, por ejemplo la familia de “booleanos” consiste un campo de tipo “boolean”.

Tipos comunes

  • binary: Binario como una cadena en Base64.

  • boolean: Campo Boolean que acepta los valores JSON true y false, pero también cadenas qe pueden ser “true” y “false”.

  • keyword: Es una familia.

    • keyword, usado como contenido estructurado como IDs, email direcciones, hostnames, códigos de estado, códigos postales, o etiquetas.
    • constant_keyword para campos que siempre almacenan el mismo valor.
    • wildcard para contenido no estructurado generado por algoritomos.
  • date: JSON no tiene un tipo de dato fecha, así que en Elasticsearch pueden ser:

    • Cadenas con formato de fecha, p.e. “2015-01-01” ó “2015/01/01 12:10:30”.
    • Número representando milisegundos-desde-epoch.
    • Número representando segundos-desde-epoch.

    Para usar fechas con múltiples formatos:

    PUT my-index-000001
    {
    "mappings": {
        "properties": {
        "date": {
            "type":   "date",
            "format": "yyyy-MM-dd HH:mm:ss||yyyy-MM-dd||epoch_millis"
        }
        }
    }
    }
    
  • object: En JSON los documentos son gerárquicos: un documento puede contener objetos que a su vez contengan otros objetos.

    Ejemplo:

    PUT my-index-000001/_doc/1
    { 
        "region": "US",
        "manager": { 
            "age":     30,
            "name": { 
            "first": "John",
            "last":  "Smith"
            }
        }
    }
    

    Internamente:

    {
        "region":             "US",
        "manager.age":        30,
        "manager.name.first": "John",
        "manager.name.last":  "Smith"
    }
    

    Mapping explícito:

    PUT my-index-000001
    {
    "mappings": {
        "properties": { 
        "region": {
            "type": "keyword"
        },
        "manager": { 
            "properties": {
            "age":  { "type": "integer" },
            "name": { 
                "properties": {
                "first": { "type": "text" },
                "last":  { "type": "text" }
                }
            }
            }
        }
        }
    }
    }
    
  • nested:Versión especial de un objecto que permite que un array de objetos se indexe de manera que puedan ser buscados independientemente Útil con array. Ver ejemplo en el enlace

  • text family: La familia “text” , incluyendo “text” y “match_only_text”.

    • text, para el uso tradicional de contenido de tipo texto como el contenido de un email o la descripción de un producto.
    • match_only_text, variante que optimiza el almacenamiento que elimina el scoring y que se usa sobretodo en mensajes de log.

    El parámetro “analyzer” en el “mapping” se usa para los campos tipo “text” tanto en tiempo de indexación como al lanzar las búsquedas.

    Los campos tipo “text” son indexados por defecto y no están disponibles para agregaciones u ordenaciones.

    • annotated-text: Text con marcas especiales. Usaod para identificar entidades (named entities).
    • completion: Para sugerencias de autocompletado..
    • search_as_you_type: Para completar las cadenas de búsquedas mientras tecleas.

Analizadores

ES trae analizadores incluidos:

  • Standard Analyzer:

    El analizador estándar divide el texto en términos en los límites de las palabras, según lo definido por el algoritmo de segmentación de texto Unicode. Elimina la mayoría de los signos de puntuación, pone los términos en minúsculas y admite la eliminación de palabras vacías.

  • Simple Analyzer:

    El analizador simple divide el texto en términos cuando encuentra un carácter que no es una letra. Todos los términos se escriben en minúsculas.

  • Whitespace Analyzer:

    El analizador de espacios en blanco divide el texto en términos siempre que encuentra algún carácter de espacio en blanco. No pone en minúsculas los términos.

  • Stop Analyzer:

    El analizador stop es como el analizador simple, pero también admite la eliminación de “stop words”.

  • Keyword Analyzer

    El analizador de palabras clave es un analizador “noop” que acepta cualquier texto que se le dé y emite exactamente el mismo texto como un solo término.

  • Pattern Analyzer:

    El analizador de patrones utiliza una expresión regular para dividir el texto en términos. Admite las minúsculas y las “stop words”.

  • Language Analyzers:

    Elasticsearch ofrece muchos analizadores específicos para cada idioma, como el inglés o el francés.

  • Fingerprint Analyzer:

    El analizador de huellas dactilares es un analizador especializado que crea una huella dactilar que puede utilizarse para la detección de duplicados.

Analizadores personalizados

Cuando los analizadores incorporados no satisfacen tus necesidades, puedes crear un analizador personalizado que utilice la combinación adecuada de:

  • cero o más filtros de caracteres
  • un tokenizador
  • cero o más filtros de tokens.

Un ejemplo:

curl -X DELETE "localhost:9200/my-index-000001?pretty"
curl -X PUT -u elastic:$ES_PWD https://XXXXXXXXXXXXXXXXXX.eu-west-2.aws.cloud.es.io/tnw_sample01 -H 'Content-Type: application/json' -d'
{
  "settings": {
    "analysis": {
      "analyzer": {
        "my_custom_analyzer_stop": {
          "type": "custom", 
          "tokenizer": "standard",
          "char_filter": [
            "html_strip"
          ],
          "filter": [
            "lowercase",
            "asciifolding",
            "stop"
          ]
        }
      }
    }
  }
}
'

Para comprobar cómo funciona con un texto:

curl -X POST -u elastic:$ES_PWD https://XXXXXXXXXXXXXXXXXX.eu-west-2.aws.cloud.es.io/tnw_sample01/_analyze -H 'Content-Type: application/json' -d'
{
  "analyzer": "my_custom_analyzer_stop",
  "text": "Hey <spam>you<spam> where are you going wit that gun in your hand <b>déjà vu</b>?"
}
'
curl -X POST -u elastic:$ES_PWD https://XXXXXXXXXXXXXXXXXX.eu-west-2.aws.cloud.es.io/catalogo_productos_tnw/_analyze -H 'Content-Type: application/json' -d'
{
  "analyzer": "standard",
  "text": "Hey <spam>you<spam> where are you going wit that gun in your hand <b>déjà vu</b>?"
}
'

Para traer sólo unos campos:

curl -X GET -u elastic:$ES_PWD https://XXXXXXXXXXXXXXXXXX.eu-west-2.aws.cloud.es.io/catalogo_productos/_search?size=22 -H 'Content-Type: application/json' -d'{
   "_source": ["product_description","material_description","tipo_producto"],
  "query" : {"match_all" : {}}
}
' | jq

Recomendaciones generales:

  1. Sinónimos: Está bien utilizar una base de datos de sinónimos para materiales, formas y términos de diseño.
  2. Campos a buscar: Sólo el conjunto principal. palabras clave y textos.
  3. Campos de agregación: Palabras clave y coincidencia de términos.
  4. Textos: Inglés, sin palabras de parada, stemming, corrección de errores ortográficos.
  5. Colores: Ver: este artículo y este).
  6. IMPORTANTE: Crear los mapeos y analizadores antes de ingerir los datos y después de realizar el análisis.

Mappings:

Creando un índice usando un mapping en un fichero:

curl -X PUT -u elastic:$ES_PWD https://XXXXXXXXXXXXXXXXXX.eu-west-2.aws.cloud.es.io/catalogo_productos_tnw -H 'Content-Type: application/json' -d @map.json | jq

Borrar el índice:

curl -X DELETE -u elastic:$ES_PWD https://XXXXXXXXXXXXXXXXXX.eu-west-2.aws.cloud.es.io/catalogo_productos_tnw 

Reindexar, me llevo los datos de un índice a otro con el nuevo mapping.

curl -X POST -u elastic:$ES_PWD https://XXXXXXXXXXXXXXXXXX.eu-west-2.aws.cloud.es.io/_reindex -H 'Content-Type: application/json' -d '
{
  "source": {
    "index": "catalogo_productos_v3"
  },
  "dest": {
    "index": "catalogo_productos_tnw"
  }
}
'

Queries

Simple Query interface

Documentación aquí Example:

curl -X POST -u elastic:$ES_PWD https://XXXXXXXXXXXXXXXXXX.eu-west-2.aws.cloud.es.io/catalogo_productos_tnw/_search -H 'Content-Type: application/json' -d '
{
  "query": {
    "simple_query_string" : {
        "query": "aged dark timber floor",
        "fields": ["material_description", "product_description"],
        "default_operator": "or"
    }
  }
}' | jq

Ejemplo:

Recupera documentos con “aged dark” Y “timber” O “floor” Y NO “ceiling”” y busca sólo en los campos “material_description” y “product_description” multiplicando por 5 la relevancia cuando haya coincidencia en material_description sobre product_description:

"query": "\"aged dark\" +(timber | floor) -ceiling",
"default_operator": "and"
 "fields": ["material_description^5", "product_description"]

Antentos al parámetro “size” para traer más documentos.

curl -X POST -u elastic:$ES_PWD https://XXXXXXXXXXXXXXXXXX.eu-west-2.aws.cloud.es.io/catalogo_productos_tnw/_search?size=2 -H 'Content-Type: application/json' -d '
{
  "query": {
    "simple_query_string" : {
        "query": "aged dark timber floor",
        "fields": ["colors_named_closest^2", "search_category","material_description.english", "product_description.english^2","product_name^3","supplier_name^4","supplier_tags","material_family","material_name"],
        "default_operator": "or"
    }
  }
}' | jq
curl -X POST -u elastic:$ES_PWD https://XXXXXXXXXXXXXXXXXX.eu-west-2.aws.cloud.es.io/catalogo_productos_tnw/_search?size=2 -H 'Content-Type: application/json' -d '
{
  "query": {
    "simple_query_string" : {
        "query": "aged dark timber floor",
        "fields": ["colors_named_closest", "search_category","material_description.english^2", "product_description.english^3","product_name","supplier_name","supplier_tags^2","material_family","material_name^2"],
        "default_operator": "or"
    }
  }
}' | jq
curl -X POST -u elastic:$ES_PWD https://XXXXXXXXXXXXXXXXXX.eu-west-2.aws.cloud.es.io/catalogo_productos_tnw/_search?size=2 -H 'Content-Type: application/json' -d '
{
  "query": {
    "simple_query_string" : {
        "query": "marble with mild veining",
        "fields": ["colors_named_closest", "search_category","material_description.english^2", "product_description.english^3","product_name","supplier_name","supplier_tags^2","material_family","material_name^2"],
        "default_operator": "or"
    }
  }
}' | jq

Para probar un analizador u otro con una cadena específica:

curl -X POST -u elastic:$ES_PWD https://XXXXXXXXXXXXXXXXXX.eu-west-2.aws.cloud.es.io/catalogo_productos_tnw/_analyze -H 'Content-Type: application/json' -d '
{
  "analyzer": "standard",
  "text":  "the darken wooden floors"
}
' | jq

curl -X POST -u elastic:$ES_PWD https://XXXXXXXXXXXXXXXXXX.eu-west-2.aws.cloud.es.io/catalogo_productos_tnw/_analyze -H 'Content-Type: application/json' -d '
{
  "analyzer": "sl_english",
  "text":  "the darken wooden floors"
}
' | jq

Synomyms

Parece razonable incorporar sinónimos al analizador de búsquedas e índices.

WordNet es una base de datos léxica de relaciones semánticas entre palabras en más de 200 idiomas. WordNet vincula las palabras en relaciones semánticas que incluyen sinónimos, hipónimos y merónimos.

Un ejemplo para recuperar una lista de sinónimos de “timber”

# To use WordNet python library:
# pip install nltk 
import nltk
nltk.download('wordnet')
from nltk.corpus import wordnet as wn

print ("----------------------------------------------------------------------------")
synonyms = []

for syn in wn.synsets("timber"):
    for i in syn.lemmas():
        synonyms.append(i.name())

print(set(synonyms))

print ("----------------------------------------------------------------------------")

Ejemplo de cómo añadir sinónimos al analizador english_analyzer (sl_english):

    "analysis": {
        "filter": {
            "sl_synonyms": {
                "type": "synonym",
                "synonyms": [
                "papapa, pepepe, popopo",
                "wood, timber"
                ]
            },
            "english_stop": {
                "type":       "stop",
                "stopwords":  "_english_" 
              },
              "english_keywords": {
                "type":       "keyword_marker",
                "keywords":   ["example"] 
              },
              "english_stemmer": {
                "type":       "stemmer",
                "language":   "english"
              },
              "english_possessive_stemmer": {
                "type":       "stemmer",
                "language":   "possessive_english"
              }
        },
        "analyzer": {
            "sl_english": {
                "tokenizer": "standard",
                "char_filter": [
                "html_strip"
                ],
                "filter": [
                "lowercase",
                "asciifolding",
                "sl_synonyms",
                "english_possessive_stemmer",
                "english_stop",
                "english_keywords",
                "english_stemmer"     
                ]
            }
        }
    }

Foto de mentatdgt en Pexels

SO WHAT DO YOU THINK ?

Contact us and tell us your needs
+34 644 237 135

Contact hola@taniwa.es