Ingeniería

Igual, pero diferente: Aumento del poder de Elasticsearch con sinónimos

El uso de sinónimos es sin duda una de las técnicas más importantes del cinturón de herramientas de un ingeniero de búsquedas. Si bien los principiantes a veces subestiman su importancia, casi ningún sistema de búsqueda de la vida real puede funcionar sin ellos. Al mismo tiempo, incluso los usuarios avanzados en ocasiones subestiman algunas complejidades y sutilezas que surgen de su uso. Los filtros de sinónimos son parte del proceso de análisis que convierte el texto de entrada en términos que pueden buscarse, y si bien es relativamente fácil comenzar con ellos, su uso puede ser variado y requerir un mayor entendimiento de los conceptos para aplicarlos correctamente en un escenario del mundo real.

Ha habido algunas mejoras recientes en torno al análisis en Elasticsearch últimamente. Lo más destacable probablemente sea la funcionalidad que permite volver a cargar los analizadores al momento de la búsqueda, que a su vez permite que se cambien y vuelvan a cargar los sinónimos al momento de la búsqueda. Además de presentar esta API nueva, en este blog responderemos algunas preguntas comunes sobre el uso de sinónimos y señalaremos algunas advertencias frecuentes en torno a su uso.

¿Por qué usar sinónimos?

Para comprender la utilidad y flexibilidad de los sinónimos, echemos un vistazo a cómo funcionan internamente la mayoría de los motores de búsqueda actuales. Los documentos y las búsquedas se analizan y reducen a sus unidades más pequeñas, usualmente denominadas tokens, que son en esencia símbolos abstractos. El proceso de buscar coincidencias durante la búsqueda usa la similitud de texto simple, que es el motivo por el cual incluso pequeños errores ortográficos (“cas”) o el uso del plural de una palabra (“casas”) en una búsqueda no coincidirán con un documento que solo contenga el singular (“casa”). Las herramientas como derivadores o búsquedas imprecisas abordan algunos de estos problemas más comunes, pero no cierran la brecha entre conceptos e ideas relacionados o entre usos ligeramente diferentes del vocabulario en los documentos y las búsquedas.

Aquí es donde se lucen los sinónimos. Los orígenes griegos de la palabra son el prefijo σύν (syn, “juntos”) y ὄνομα (ónoma, “nombre”). El origen del término ya muestra que los sinónimos describen palabras diferentes con exactamente o casi el mismo significado en el mismo idioma o dominio. En la práctica, esto puede abarcar sinónimos generales (“cansado” frente a “adormilado”), abreviaturas (“lb” frente a “libra”), diferentes variaciones en la escritura de productos en búsquedas de comercio electrónico (“iPod” frente a “i-Pod”), pequeñas diferencias idiomáticas (como “ascensor” en español de España frente a “elevador” en español de América), jerga especializada frente a lenguaje común (“canino” frente a “perro”) o simplemente denotar el mismo concepto de dos maneras (“universo” o “cosmos”). Gracias a que proporciona reglas de sinónimos adecuadas, el ingeniero de búsquedas puede brindar información sobre cuáles palabras en su dominio tienen un significado similar y, por lo tanto, deberían tratarse de forma similar.

Es importante que un motor de búsqueda sepa cuáles términos de los documentos y las búsquedas deberían coincidir, a pesar de que parezcan diferentes. Como esto es muy específico del dominio, los usuarios deben proporcionar las reglas adecuadas. Los filtros de sinónimos, que se pueden usar en analizadores personalizados, reemplazan o agregan tokens adicionales según las reglas definidas por el usuario, ya sea al momento de la indexación para almacenar, por ejemplo, ambas variaciones de una palabra en un documento indexado o al momento de la búsqueda para ampliar los términos de búsqueda y buscar coincidencias con documentos más relevantes. Hablaremos sobre algunas ventajas y desventajas de estos dos enfoques más adelante.

Cuándo estar atento al uso de sinónimos

Los filtros de sinónimos son una herramienta muy flexible, lo que lleva a las personas a usarlos en exceso en ciertas situaciones. Por ejemplo, en ocasiones se usan como reemplazo directo de los derivadores, con grandes archivos de sinónimos que contienen variaciones gramaticales de verbos y sustantivos. Si bien este enfoque es posible, el rendimiento suele ser peor y el mantenimiento más difícil que cuando se usan derivadores o lematizadores. Lo mismo sucede con la corrección de errores ortográficos. Si hay solo un puñado de errores de ortografía muy comunes, como en un entorno de comercio electrónico, a veces es recomendable intentar corregirlos usando sinónimos. Pero si el problema es más general, usar búsquedas imprecisas o técnicas de n-gramas de caracteres es un enfoque más sostenible. Considera también las alternativas a la ampliación de sinónimos en la cadena de análisis. En ocasiones, mejorar documentos en un pipeline de ingesta o algún otro proceso del lado del cliente resulta más flexible y controlable que usar sinónimos en el proceso de análisis más restringido. Por ejemplo, podrías detectar entidades con nombre en tus documentos usando marcos de trabajo de reconocimiento de entidades con nombre (NER) comunes y codificarlas en identificadores únicos en tu pipeline de preprocesamiento o al momento de la ingesta. Si luego aplicas el mismo proceso a las búsquedas de tu usuario antes de enviarlas a Elasticsearch, obtienes el mismo efecto, pero generalmente obtienes un mayor control.

Además, es tentador usar sinónimos para otras nociones de “igualdad”, como agrupar ciertas especies de animales bajo un término en común o incluso crear soporte para la taxonomía en tu dominio. Aquí es donde se torna muy interesante y hay mucho para explorar, pero ten en cuenta que los sinónimos no siempre son la mejor opción y que pueden hacer que tu sistema se comporte de maneras inesperadas si no se usan con cuidado.

Sinónimos al momento de la indexación frente a sinónimos al momento de la búsqueda

Se usan sinónimos en analizadores que se pueden usar al momento de indexación o de búsqueda. Una de las preguntas más frecuentes en torno al uso de filtros de sinónimos en Elasticsearch es: “¿debería usarlos al momento de la indexación, de la búsqueda o en ambos?”. Comencemos por la aplicación del filtrado de sinónimos en el momento de la indexación. Esto significa que los términos en los documentos indexados se reemplazan o amplían de una vez por todas, y el resultado persiste en el índice de búsqueda.

Los sinónimos al momento de la indexación tienen varias desventajas:

  • El índice puede volverse más grande debido a que deben indexarse todos los sinónimos.
  • La puntuación de búsqueda, que depende de las estadísticas de términos, puede verse afectada dado que también se cuentan los sinónimos, y las estadísticas de palabras menos comunes pueden quedar distorsionadas.
  • No se pueden cambiar las reglas de sinónimos de los documentos existentes sin realizar una reindexación.

Las últimas dos, especialmente, son una gran desventaja. La única ventaja potencial de los sinónimos al momento de la indexación es el rendimiento, dado que se paga el costo del proceso de expansión por adelantado y no es necesario hacerlo nuevamente al momento de la búsqueda, lo que potencialmente resultaría en más términos para los que se necesitan buscar coincidencias. Sin embargo, esto no suele ser un verdadero problema en la práctica.

En cambio, usar sinónimos en analizadores al momento de la búsqueda no presenta muchos de los problemas anteriores:

  • El tamaño del índice no se modifica.
  • Las estadísticas de términos en el corpus permanecen iguales.
  • Los cambios en las reglas de sinónimos no requieren reindexar los documentos.

Estas ventajas suelen superar la única desventaja de tener que realizar la expansión de sinónimos cada vez al momento de la búsqueda y tener potencialmente más términos para los cuales se necesitan buscar coincidencias. Además, la expansión de sinónimos al momento de la búsqueda permite usar el filtro de token synonym_graph más sofisticado, que puede ocuparse correctamente de sinónimos con varias palabras y está diseñado para usarse solamente como parte de un analizador de búsquedas.

En general, las ventajas de usar sinónimos al momento de la búsqueda suelen superar cualquier pequeña mejora de rendimiento que puedes obtener si los usas al momento de la indexación.

Sin embargo, solía haber otra advertencia al usar sinónimos al momento de la búsqueda. Si bien cambiar las reglas de sinónimos no requiere la reindexación de los documentos, para cambiarlas debías cerrar temporalmente el índice y volver a abrirlo. Esto era necesario porque los analizadores se instancian al momento de la creación del índice, cuando se reinicia un nodo o cuando se vuelve a abrir un índice cerrado. Para hacer cambios en un archivo de reglas de sinónimos visible para el índice, primero se debía actualizar el archivo en todos los nodos, luego cerrar el índice y volver a abrirlo. Pero ya no es así.

Volver a cargar sinónimos

A partir de Elasticsearch 7.3, ya no es necesario volver a abrir los índices para ver los cambios en los archivos de sinónimos. Agregamos un endpoint nuevo que posibilita activar que se vuelvan a cargar los recursos del analizador a demanda. Llamar a este endpoint nuevo volverá a cargar todos los analizadores de un índice que contienen componentes marcados como actualizables. Esto, a su vez, hace que dichos componentes solo puedan usarse al momento de la búsqueda.

En los filtros de sinónimos, marcarlos como actualizables y llamar a la API para volver a cargar hace que los cambios en el archivo de configuración de sinónimos de cada nodo sean visibles en el proceso de análisis. No es posible actualizar reglas de sinónimos que son parte de la definición de filtros (mediante el parámetro synonyms), pero deberían usarse principalmente para pruebas ad-hoc. En todo caso, configurar sinónimos con un archivo de configuración tiene varias ventajas:

  • Son fáciles de gestionar. En un sistema de producción, puede haber muchas reglas de sinónimos, y como afectan en gran medida la relevancia de búsqueda, deberían tratarse como una pieza integral de la configuración cuya versión debe controlarse y probarse con cualquier actualización.
  • Por lo general, los sinónimos derivan de otras fuentes o se crean mediante un algoritmo que se ejecuta en tus datos. La lectura desde los archivos evita la necesidad de colocarlos en la configuración de filtros.
  • El mismo archivo de sinónimos puede usarse en diferentes filtros.
  • Los conjuntos de reglas de sinónimos más grandes ocupan mucha memoria en el estado de cluster de Elasticsearch que almacena metainformación sobre los ajustes del índice. Para no aumentar innecesariamente el tamaño del cluster, se recomienda almacenar los conjuntos de reglas de sinónimos más grandes en archivos de configuración.

A modo de demostración, supongamos que colocas un archivo my_synonyms.txt inicial con la siguiente regla única en el directorio config de tus nodos de Elasticsearch. Supongamos que inicialmente el archivo solo contiene la regla siguiente:

universe, cosmos

A continuación, debemos definir un analizador que consulte este archivo en un filtro de sinónimos:

PUT /synonym_test
{
  "settings": {
    "index": {
      "analysis": {
        "analyzer": {
          "synonym_analyzer": {
            "tokenizer": "whitespace",
            "filter": ["my_synonyms"]
          }
        },
        "filter": {
          "my_synonyms": {
            "type": "synonym",
            "synonyms_path": "my_synonyms.txt",
            "updateable": true
          }
        }
      }
    }
  }
}

Observa que marcamos el filtro de sinónimos como updateable. Esto es importante porque solo los filtros actualizables se vuelven a cargar cuando llamamos al nuevo endpoint para volver a cargar, pero esto también tiene el efecto secundario de que los analizadores que contienen filtros actualizables ya no se pueden usar al momento de la indexación. Pero comprobemos primero que los sinónimos se apliquen correctamente ejecutando una prueba rápida con el endpoint _analyze:

GET /synonym_test/_analyze
{
  "analyzer": "synonym_analyzer",
  "text": "cosmos"
}

Esto debería devolver dos tokens, y uno también debería ser “universe”, como se espera. Agreguemos otra regla al archivo synonyms.txt con una segunda línea:

lift, elevator

Anteriormente, en este momento debías cerrar el índice y volver a abrirlo para que estos cambios se aplicaran. Ahora, simplemente puedes llamar al endpoint nuevo:

POST /synonym_test/_reload_search_analyzers

La solicitud no requiere un cuerpo, pero se puede restringir a uno o más índices mediante los patrones típicos de comodines de índice. La respuesta incluye información sobre los analizadores que se volvieron a cargar y los nodos afectados:

{
  [...],
  "reload_details": [{
    "index": "synonym_test",
    "reloaded_analyzers": ["synonym_analyzer"],
    "reloaded_node_ids": ["FXbmbgG_SsOrNRssrYcPow"]
  }]
}

Ejecutar la solicitud _analyze anterior para el término “lift” ahora también devuelve “elevator” como token de segundo sinónimo.

Sin embargo, hay algunos puntos que se deben tener en cuenta. Como ya mencionamos, un filtro marcado como updateable debe usarse al momento de la búsqueda, por lo que la forma correcta de usar el analizador de sinónimos que definimos anteriormente en un campo sería la siguiente:

POST /synonym_test/_mapping
{
  "properties": {
    "text_field": {
      "type": "text",
      "analyzer": "standard",
      "search_analyzer": "synonym_analyzer"
    }
  }
}

Además, la acción de volver a cargar funciona solo para sinónimos que se cargan desde archivos; no se admite el cambio de sinónimos definidos a través de los ajustes de un filtro. Por último, en la práctica debes asegurarte de aplicar las actualizaciones a los archivos de sinónimos en todos los nodos de tu cluster. Si el analizador de algunos nodos detecta versiones diferentes del archivo, puedes obtener resultados de búsqueda diferentes según el nodo que se use en una búsqueda. Si sucede esto en relación con un sinónimo, lo primero que debes comprobar es que los archivos de sinónimos sean iguales en cada nodo y luego debes activar nuevamente la acción de volver a cargar.

En resumen, el endpoint _reload_search_analyzer nuevo te permite revisar y cambiar rápidamente los sinónimos al momento de la búsqueda sin necesidad de volver a abrir los índices. Por ejemplo, si examinas los logs de búsqueda, puedes determinar si los usuarios buscan términos diferentes a los que existen en los documentos indexados y realizar esas adiciones sobre la marcha. Sin embargo, agregar sinónimos puede tener efectos secundarios no esperados sobre la relevancia, por lo que se recomienda realizar primero algún tipo de prueba (ya sea pruebas A/B o algo como la API de evaluación de clasificación) antes de aplicar los cambios directamente en producción.

Ser parte de la cadena (de análisis)

Otra pregunta frecuente sobre los filtros de sinónimos es su comportamiento en cadenas de análisis más complejas. En la mayoría de las situaciones, usarás algún carácter común o filtros de token antes del filtro de sinónimos, como un filtro lowercase. Esto significa que todos los tokens que pasen por la cadena de análisis se cambiarán a minúsculas antes de aplicar el filtro de sinónimos. ¿Esto significa que los sinónimos de entrada en tus reglas de sinónimos deben estar en minúsculas también para que haya coincidencia? Probemos con este simple ejemplo:

PUT /test_index
{
  "settings": {
    "index": {
      "analysis": {
        "analyzer": {
          "synonym_analyzer": {
            "tokenizer": "whitespace",
            "filter": ["lowercase", "my_synonyms"]
          }
        },
        "filter": {
          "my_synonyms": {
            "type": "synonym",
            "synonyms": ["Eins, Uno, One", "Cosmos => Universe"]
          }
        }
      }
    }
  }
}
GET /test_index/_analyze
{
  "analyzer": "synonym_analyzer",
  "text": "one"
}

Puedes verificar que el texto de entrada en minúsculas se amplía a tres tokens en el ejemplo anterior, lo que muestra que las minúsculas también se aplican a las reglas de filtro de sinónimos. Además, se vuelve a escribir el lado derecho de las reglas de reemplazo como la regla anterior “Cosmos => Universe”, como se puede observar en el resultado en minúsculas en el siguiente caso:

GET /test_index/_analyze
{
  "analyzer": "synonym_analyzer",
  "text": "cosmos"
}

En general, los filtros de sinónimos vuelven a escribir las entradas en el tokenizador y los filtros usados en la cadena de análisis anterior. Sin embargo, existen algunas excepciones destacadas: Varios filtros que dan como resultado tokens apilados (como common_grams o el filtro phonetic) no tienen permitido preceder filtros de sinónimos y generarán errores si intentas hacerlo. Otros, como los filtros de palabras compuestas o los filtros de sinónimos en sí, se omiten cuando preceden a otro filtro de sinónimos en la cadena. La última regla es importante para posibilitar el encadenamiento de filtros de sinónimos. Veremos esto en acción en el ejemplo siguiente.

Entonces, ¿qué sucede si colocas dos o más filtros de sinónimos seguidos? ¿El resultado del primero será la entrada del último y hará que el encadenamiento de filtros de sinónimos sea una especie de operación transitiva? Probemos el ejemplo siguiente:

PUT /synonym_chaining
{
  "settings": {
    "index": {
      "analysis": {
        "filter": {
          "first_synonyms": {
            "type": "synonym",
            "synonyms": ["a => b", "e => f"]
          },
          "second_synonyms": {
            "type": "synonym",
            "synonyms": ["b => c", "d => e"]
          }
        },
        "analyzer": {
          "synonym_analyzer": {
            "filter": [
              "first_synonyms",
              "second_synonyms"
            ],
            "tokenizer": "whitespace"
          }
        }
      }
    }
  }
}
GET /synonym_chaining/_analyze
{
  "analyzer": "synonym_analyzer",
  "text": "a"
}

El token de salida sería “c”, lo que muestra que se aplican ambos filtros de manera consecutiva: el primero reemplaza “a” con “b”, y el segundo reemplaza esta entrada con “c”. Si en cambio intentas con “d” como entrada, se reemplaza con “e” (no se aplica la primera regla); pero si usas “e” en su lugar, el token se reemplaza con “f” en el primer filtro, dejando al segundo filtro sin nada con que coincidir.

¿Recuerdas que acabamos de hablar sobre las excepciones de volver a escribir con respecto a los filtros de token precedentes? Si el filtro second_synonyms del ejemplo anterior hubiera aplicado las reglas del primer filtro a su conjunto de reglas, hubiera cambiado su propia regla d => e a d => f (porque se hubiera aplicado la regla e => f del filtro precedente). Este comportamiento solía ser una fuente de confusión en las versiones anteriores de Elasticsearch, y es el motivo por el cual los filtros de sinónimos ahora se omiten al procesar las reglas de sinónimos del filtro siguiente. Funcionará como se describe en la versión 6.6 y posteriores.

Volver al futuro

En este breve blog, solo mencionamos lo básico de lo que puedes lograr con los sinónimos e intentamos responder algunas preguntas frecuentes sobre su uso. Los sinónimos son una herramienta poderosa que puede aprovecharse para aumentar la recuperación de tu sistema de búsqueda, pero existen muchas sutilezas que es importante que conozcas y con las cuales debes experimentar, especialmente junto con pruebas de relevancia sistemáticas.

La nueva API para volver a cargar analizadores al momento de la búsqueda que se agregó en Elasticsearch 7.3 hace que este tipo de experimentación sea más fácil dado que no requiere que cierres el índice y lo vuelvas a abrir, como antes, y también proporciona formas de actualizar las reglas de sinónimos que se aplican al momento de la búsqueda sin tener que colocar los índices fuera de línea. Esto, sin embargo, es solo un paso de una serie de mejoras que queremos introducir para que la gestión de sinónimos en un cluster grande sea más sencilla para el usuario. Cuéntanos tu opinión y déjanos comentarios o preguntas en el foro de debate. Hasta entonces, ¡feliz análisis!