Engineering

Búsqueda multilingüe con identificación de idioma en Elasticsearch

Nos complace anunciar que además del procesador de ingesta de interferencias de Machine Learning, lanzaremos la identificación de idioma en Elasticsearch 7.6. Con este lanzamiento, quisiéramos aprovechar para describir algunos casos de uso y estrategias para búsqueda en corpus multilingües, y el papel que juega la identificación de idioma. Abarcamos algunos de estos temas previamente, y nos basaremos en eso en algunos de los ejemplos siguientes.

Motivación

En el mundo sumamente interconectado actual, nos encontramos con que los documentos y otras fuentes de información están en diversos idiomas. Esto presenta un problema para muchas aplicaciones de búsqueda. Debemos comprender el idioma de estos documentos lo mejor que podamos para analizarlos adecuadamente y proporcionar la mejor experiencia de búsqueda posible. Aquí es donde entra la identificación de idioma.

La identificación de idioma se usa para mejorar la relevancia de búsqueda general en estos corpus multilingües. Dado un conjunto de documentos de los que aún no conocemos los idiomas que contienen, queremos buscar en ellos de forma eficiente. Los documentos pueden contener un solo idioma o varios. Lo primero es habitual en dominios como la informática en los que el inglés es el idioma de comunicación predominante, mientras que lo segundo es habitual en textos sobre biología y medicina en los que se intercala con frecuencia terminología en latín en el inglés.

A través de la aplicación de análisis específicos según el idioma, podemos mejorar la relevancia (tanto precisión como exhaustividad) asegurándonos de que se comprendan, indexen y busquen adecuadamente los términos del documento. Con un conjunto de analizadores específicos según el idioma en Elasticsearch (tanto integrados como a través de plugins adicionales), podemos proporcionar tokenización, filtrado de tokens y filtrado de términos mejorados:

  • Listas de palabras vacías y sinónimos
  • Normalización de la forma de las palabras: derivación y lematización
  • Descomposición (por ejemplo, alemán, neerlandés, coreano)

Por motivos similares, creemos que la identificación de idioma en pipelines de procesamiento del lenguaje natural (NLP) es uno de los primeros pasos de procesamiento para usar modelos y algoritmos específicos según el idioma y sumamente precisos. Por ejemplo, los modelos de NLP previamente entrenados como BERT y ALBERT de Google o GPT-2 de OpenAI se entrenan por lo común a partir de corpus por idioma o corpus con un idioma predominante, y se ajustan para tareas como clasificación de documentos, análisis de sentimientos, reconocimiento de entidades con nombre (NER), etc.

Para los siguientes ejemplos y estrategias, a menos que se especifique lo contrario, asumiremos que los documentos contienen un solo idioma o uno predominante.

Beneficios del análisis específico según el idioma

Para motivar aún más esto, veamos rápidamente algunos beneficios de los analizadores específicos según el idioma.

Descomposición: en alemán, los sustantivos generalmente se construyen a través de la unión de otros sustantivos para crear palabras compuestas maravillosamente largas y difíciles de leer. Un ejemplo simple es la combinación de “Jahr” (“año”) en otras palabras como “Jahrhunderts” (“centenario”), “Jahreskalender” (“calendario anual”) o “Schuljahr” (“año escolar”). Sin un analizador personalizado que pueda descomponer estas palabras, no podríamos buscar “jahr” y obtener documentos sobre años escolares, “Schuljahr”. Además, el alemán tiene reglas diferentes a otras lenguas latinas con respecto a la forma plural y en dativo, por lo que buscar “jahr” también debería coincidir con “Jahre” (plural) y “Jahren” (plural dativo).

Término común: algunos idiomas también usan terminología común o específica de un dominio. Por ejemplo, “computer” es una palabra que con frecuencia se usa tal cual en otros idiomas. Si deseamos buscar “computer”, quizás también nos interesen documentos que no están en inglés. Poder buscar en un conjunto conocido de idiomas y encontrar coincidencias para términos comunes puede ser un caso de uso interesante. Si tomamos el alemán como ejemplo nuevamente, podemos tener documentos sobre seguridad de las computadoras en varios idiomas. En alemán, se dice “Computersicherheit” (“sicherheit” significa “seguridad”) y, solo con un analizador de alemán, las búsquedas de “computer” encontrarán coincidencias en inglés y alemán.

Alfabetos no latinos: el analizador standard funciona bastante bien para la mayoría de los idiomas con alfabeto latino (idiomas de Europa occidental). Sin embargo, comienza a fallar rápidamente con alfabetos no latinos como cirílico o CJK (chino/japonés/coreano). En una serie de blogs anterior, vimos cómo se forman los idiomas CJK y la necesidad de tener analizadores específicos según el idioma. Por ejemplo, el coreano tiene posposiciones (sufijos que se agregan a los sustantivos y pronombres, y cambian su significado). En ocasiones, con el analizador standard se encuentran coincidencias para los términos de búsqueda, pero no se puntúan bien las coincidencias. Esto significa que puedes recuperar bien los documentos, pero la exhaustividad se ve afectada. En otros casos, el analizador standard no encontrará coincidencias de ningún término y se verán afectadas tanto la precisión como la exhaustividad.

Veamos el ejemplo práctico de las “Olimpiadas de Invierno”. En coreano, se dice “동계올림픽대회는” que está compuesto por “동계” que significa “temporada de invierno”, “올림픽대회” que significa “Olimpiadas” o “competencia olímpica”, y por último “는” que es la posposición del tema: un sufijo que se agrega a la palabra y denota el tema. Buscar ese texto exacto con el analizador standard devuelve una coincidencia perfecta, pero buscar “올림픽대회”, es decir solamente “Olimpiadas”, no muestra resultados. Sin embargo, si se usa el analizador de coreano nori, obtenemos un resultado porque “동계올림픽대회는” (“Olimpiadas de Invierno”) se tokenizó correctamente al momento de la indexación.

Primeros pasos con la identificación de idioma

Proyecto de demostración

Para ayudar a ilustrar casos de uso y estrategias para la identificación de idioma en la búsqueda, configuramos un proyecto pequeño de demostración. Contiene todos los ejemplos de este blog y algunas herramientas para indexar y buscar en WiLI-2018, un corpus multilingüe, que puedes usar como referencia y ejemplo práctico para experimentar con la búsqueda multilingüe. Para seguir los ejemplos, es útil (aunque no estrictamente necesario) tener listo el proyecto de demostración, con los documentos indexados, en caso de que desees seguir los pasos a nuestra par.

Para estos experimentos, puedes instalar Elasticsearch 7.6 de forma local o activar una prueba gratuita de Elasticsearch Service.

Primeros experimentos

La identificación de idioma es un modelo previamente entrenado que se envía con la distribución predeterminada de Elasticsearch. Se usa junto con el procesador de ingesta de interferencias especificando lang_ident_model_1 como model_id al configurar tu procesador de interferencia en un pipeline de ingesta.

{ 
  "inference": { 
    "model_id": "lang_ident_model_1", 
    "inference_config": {}, 
    "field_mappings": {} 
  } 
}

El resto de la configuración es igual a la de otros modelos, te permite especificar configuraciones como la cantidad de clases principales que se incluirán en la salida, el campo de salida que contendrá la predicción y, lo más importante para nuestro caso de uso, el campo de entrada que se usará. De manera predeterminada, el modelo espera que un campo llamado "text" contenga la entrada. En el ejemplo siguiente, usamos pipeline _simulate API con algunos documentos de un solo campo. Mapea el campo de contenidos de entrada con el campo de texto de la interferencia: este mapeo no afecta a otros procesadores en el pipeline. Después genera la salida con las tres clases principales para inspección.

# simula una configuración de interferencia básica

POST _ingest/pipeline/_simulate
{
  "pipeline": {
    "processors": [
      {
        "inference": {
          "model_id": "lang_ident_model_1",
          "inference_config": {
            "classification": {
              "num_top_classes": 3
            }
          },
          "field_mappings": {
            "contents": "text"
          },
          "target_field": "_ml.lang_ident"
        }
      }
    ]
  },
  "docs": [
    {
      "_source": {
        "contents": "Das Leben ist kein Ponyhof"
      }
    },
    {
      "_source": {
        "contents": "The rain in Spain stays mainly in the plains"
      }
    },
    {
      "_source": {
        "contents": "This is mostly English but has a touch of Latin since we often just say, Carpe diem"
      }
    }
  ]
}

La salida nos muestra cada documento, además de información adicional en el campo _ml.lang_ident. Esto incluye la probabilidad de cada uno de los tres idiomas principales y del idioma principal, que se almacena en _ml.lang_ident.predicted_value.

{
  "docs" : [
    {
      "doc" : {
        "_index" : "_index",
        "_type" : "_doc",
        "_id" : "_id",
        "_source" : {
          "contents" : "Das Leben ist kein Ponyhof",
          "_ml" : {
            "lang_ident" : {
              "top_classes" : [
                {
                  "class_name" : "de",
                  "class_probability" : 0.9996006023972855
                },
                {
                  "class_name" : "el-Latn",
                  "class_probability" : 2.625873919853074E-4
                },
                {
                  "class_name" : "ru-Latn",
                  "class_probability" : 1.130237050226503E-4
                }
              ],
              "predicted_value" : "de",
              "model_id" : "lang_ident_model_1"
            }
          }
        },
        "_ingest" : {
          "timestamp" : "2020-01-21T14:38:13.810179Z"
        }
      }
    },
    {
      "doc" : {
        "_index" : "_index",
        "_type" : "_doc",
        "_id" : "_id",
        "_source" : {
          "contents" : "The rain in Spain stays mainly in the plains",
          "_ml" : {
            "lang_ident" : {
              "top_classes" : [
                {
                  "class_name" : "en",
                  "class_probability" : 0.9988809847231199
                },
                {
                  "class_name" : "ga",
                  "class_probability" : 7.764148026288316E-4
                },
                {
                  "class_name" : "gd",
                  "class_probability" : 7.968926766495827E-5
                }
              ],
              "predicted_value" : "en",
              "model_id" : "lang_ident_model_1"
            }
          }
        },
        "_ingest" : {
          "timestamp" : "2020-01-21T14:38:13.810185Z"
        }
      }
    },
    {
      "doc" : {
        "_index" : "_index",
        "_type" : "_doc",
        "_id" : "_id",
        "_source" : {
          "contents" : "This is mostly English but has a touch of Latin since we often just say, Carpe diem",
          "_ml" : {
            "lang_ident" : {
              "top_classes" : [
                {
                  "class_name" : "en",
                  "class_probability" : 0.9997901768317939
                },
                {
                  "class_name" : "ja",
                  "class_probability" : 8.756250766054857E-5
                },
                {
                  "class_name" : "fil",
                  "class_probability" : 1.6980752372837307E-5
                }
              ],
              "predicted_value" : "en",
              "model_id" : "lang_ident_model_1"
            }
          }
        },
        "_ingest" : {
          "timestamp" : "2020-01-21T14:38:13.810189Z"
        }
      }
    }
  ]
}

¡Se ve bien! Se identificó el alemán en el primer documento y el inglés en el segundo y tercer documento, incluso con los toques de latín en el tercero.

Estrategias para la identificación de idiomas en la búsqueda

Ahora que vimos un ejemplo básico de identificación de idioma, es momento de comenzar a volcar eso en una estrategia de indexación y búsqueda.

Usaremos dos estrategias de indexación básicas: idioma por campo e idioma por índice. En la estrategia idioma por campo, crearemos un solo índice con un conjunto de campos específicos según el idioma y usaremos un analizador adaptado a cada idioma. Al momento de la búsqueda, podemos elegir buscar en un campo de idioma conocido o buscar en los campos de todos los idiomas y seleccionar el que mejor coincida. En la estrategia idioma por índice, crearemos un conjunto de índices específicos según el idioma con dos mapeos diferentes, cuando el campo indexado tenga un analizador para ese idioma. Al momento de la búsqueda, podemos seguir un enfoque similar al de la estrategia de idioma por campo y elegir buscar en un índice de un solo idioma o en varios índices con un patrón de índice en la solicitud de búsqueda.

Contrasta estas dos estrategias con lo que deberías hacer hoy: indexar el mismo texto varias veces, cada una a un campo o índice con un analizador específico según el idioma. Si bien este enfoque puede funcionar, provoca una gran cantidad de duplicación y resulta en búsquedas más lentas y un uso considerablemente mayor al necesario del espacio de almacenamiento.

Indexación

Hagamos un desglose y veamos cada una de las dos estrategias de indexación, dado que estas rigen las estrategias de búsqueda que podemos usar.

Por campo

En la estrategia de idioma por campo, usaremos la salida de la identificación de idioma y una serie de procesadores en un pipeline de ingesta para almacenar el campo de entrada en un campo específico según el idioma. Soportaremos solo un conjunto finito de idiomas (alemán, inglés, coreano, japonés y chino) debido a que necesitamos configurar un analizador específico para cada idioma. Cualquier documento que no se encuentre en uno de los idiomas soportados se indexará en un campo predeterminado con el analizador standard.

El proyecto de demostración contiene una definición del pipeline completo: config/pipelines/lang-per-field.json

Un mapeo para soportar esta estrategia de indexación se vería así:

{
  "settings": {
    "index": {
      "number_of_shards": 1,
      "number_of_replicas": 0
    }
  },
  "mappings": {
    "dynamic": "strict",
    "properties": {
      "contents": {
        "properties": {
          "language": {
            "type": "keyword"
          },
          "supported": {
            "type": "boolean"
          },
          "default": {
            "type": "text",
            "analyzer": "default",
            "fields": {
              "icu": {
                "type": "text",
                "analyzer": "icu_analyzer"
              }
            }
          },
          "en": {
            "type": "text",
            "analyzer": "english"
          },
          "de": {
            "type": "text",
            "analyzer": "german_custom"
          },
          "ja": {
            "type": "text",
            "analyzer": "kuromoji"
          },
          "ko": {
            "type": "text",
            "analyzer": "nori"
          },
          "zh": {
            "type": "text",
            "analyzer": "smartcn"
          }
        }
      }
    }
  }
}

(Ten en cuenta que la configuración del analizador de alemán se omitió del ejemplo anterior por cuestiones de brevedad y que puedes encontrarla en config/mappings/de_analyzer.json).

Como en el ejemplo anterior, usaremos la API _simulate del pipeline para explorar:

# simula un idioma por campo y genera una salida de las tres clases principales de idiomas para inspección

POST _ingest/pipeline/_simulate
{
  "pipeline": {
    "processors": [
      {
        "inference": {
          "model_id": "lang_ident_model_1",
          "inference_config": {
            "classification": {
              "num_top_classes": 3
            }
          },
          "field_mappings": {
            "contents": "text"
          },
          "target_field": "_ml.lang_ident"
        }
      },
      {
        "rename": {
          "field": "contents",
          "target_field": "contents.default"
        }
      },
      {
        "rename": {
          "field": "_ml.lang_ident.predicted_value",
          "target_field": "contents.language"
        }
      },
      {
        "script": {
          "lang": "painless",
          "source": "ctx.contents.supported = (['de', 'en', 'ja', 'ko', 'zh'].contains(ctx.contents.language))"
        }
      },
      {
        "set": {
          "if": "ctx.contents.supported",
          "field": "contents.{{contents.language}}",
          "value": "{{contents.default}}",
          "override": false
        }
      }
    ]
  },
  "docs": [
    {
      "_source": {
        "contents": "Das Leben ist kein Ponyhof"
      }
    },
    {
      "_source": {
        "contents": "The rain in Spain stays mainly in the plains"
      }
    },
    {
      "_source": {
        "contents": "オリンピック大会"
      }
    },
    {
      "_source": {
        "contents": "로마는 하루아침에 이루어진 것이 아니다"
      }
    },
    {
      "_source": {
        "contents": "授人以鱼不如授人以渔"
      }
    },
    {
      "_source": {
        "contents": "Qui court deux lievres a la fois, n’en prend aucun"
      }
    },
    {
      "_source": {
        "contents": "Lupus non timet canem latrantem"
      }
    },
    {
      "_source": {
        "contents": "This is mostly English but has a touch of Latin since we often just say, Carpe diem"
      }
    }
  ]
}

Esta es la salida con idioma por campo:

{
  "docs" : [
    {
      "doc" : {
        "_index" : "_index",
        "_type" : "_doc",
        "_id" : "_id",
        "_source" : {
          "contents" : {
            "de" : "Das Leben ist kein Ponyhof",
            "default" : "Das Leben ist kein Ponyhof",
            "language" : "de",
            "supported" : true
          },
          "_ml" : {
            "lang_ident" : {
              "top_classes" : [
                {
                  "class_name" : "de",
                  "class_probability" : 0.9996006023972855
                },
                {
                  "class_name" : "el-Latn",
                  "class_probability" : 2.625873919853074E-4
                },
                {
                  "class_name" : "ru-Latn",
                  "class_probability" : 1.130237050226503E-4
                }
              ],
              "model_id" : "lang_ident_model_1"
            }
          }
        },
        "_ingest" : {
          "timestamp" : "2020-01-22T12:40:03.218641Z"
        }
      }
    },
    {
      "doc" : {
        "_index" : "_index",
        "_type" : "_doc",
        "_id" : "_id",
        "_source" : {
          "contents" : {
            "en" : "The rain in Spain stays mainly in the plains",
            "default" : "The rain in Spain stays mainly in the plains",
            "language" : "en",
            "supported" : true
          },
          "_ml" : {
            "lang_ident" : {
              "top_classes" : [
                {
                  "class_name" : "en",
                  "class_probability" : 0.9988809847231199
                },
                {
                  "class_name" : "ga",
                  "class_probability" : 7.764148026288316E-4
                },
                {
                  "class_name" : "gd",
                  "class_probability" : 7.968926766495827E-5
                }
              ],
              "model_id" : "lang_ident_model_1"
            }
          }
        },
        "_ingest" : {
          "timestamp" : "2020-01-22T12:40:03.218646Z"
        }
      }
    },
    {
      "doc" : {
        "_index" : "_index",
        "_type" : "_doc",
        "_id" : "_id",
        "_source" : {
          "contents" : {
            "default" : "オリンピック大会",
            "language" : "ja",
            "ja" : "オリンピック大会",
            "supported" : true
          },
          "_ml" : {
            "lang_ident" : {
              "top_classes" : [
                {
                  "class_name" : "ja",
                  "class_probability" : 0.9993823252841599
                },
                {
                  "class_name" : "el",
                  "class_probability" : 2.6448654791599055E-4
                },
                {
                  "class_name" : "sd",
                  "class_probability" : 1.4846805271384584E-4
                }
              ],
              "model_id" : "lang_ident_model_1"
            }
          }
        },
        "_ingest" : {
          "timestamp" : "2020-01-22T12:40:03.218648Z"
        }
      }
    },
    {
      "doc" : {
        "_index" : "_index",
        "_type" : "_doc",
        "_id" : "_id",
        "_source" : {
          "contents" : {
            "default" : "로마는 하루아침에 이루어진 것이 아니다",
            "language" : "ko",
            "ko" : "로마는 하루아침에 이루어진 것이 아니다",
            "supported" : true
          },
          "_ml" : {
            "lang_ident" : {
              "top_classes" : [
                {
                  "class_name" : "ko",
                  "class_probability" : 0.9999939196272863
                },
                {
                  "class_name" : "ka",
                  "class_probability" : 3.0431805047662344E-6
                },
                {
                  "class_name" : "am",
                  "class_probability" : 1.710514725818281E-6
                }
              ],
              "model_id" : "lang_ident_model_1"
            }
          }
        },
        "_ingest" : {
          "timestamp" : "2020-01-22T12:40:03.218649Z"
        }
      }
    },
    {
      "doc" : {
        "_index" : "_index",
        "_type" : "_doc",
        "_id" : "_id",
        "_source" : {
          "contents" : {
            "default" : "授人以鱼不如授人以渔",
            "language" : "zh",
            "zh" : "授人以鱼不如授人以渔",
            "supported" : true
          },
          "_ml" : {
            "lang_ident" : {
              "top_classes" : [
                {
                  "class_name" : "zh",
                  "class_probability" : 0.9999810103320087
                },
                {
                  "class_name" : "ja",
                  "class_probability" : 1.0390454083183788E-5
                },
                {
                  "class_name" : "ka",
                  "class_probability" : 2.6302271562335787E-6
                }
              ],
              "model_id" : "lang_ident_model_1"
            }
          }
        },
        "_ingest" : {
          "timestamp" : "2020-01-22T12:40:03.21865Z"
        }
      }
    },
    {
      "doc" : {
        "_index" : "_index",
        "_type" : "_doc",
        "_id" : "_id",
        "_source" : {
          "contents" : {
            "default" : "Qui court deux lievres a la fois, n’en prend aucun",
            "language" : "fr",
            "supported" : false
          },
          "_ml" : {
            "lang_ident" : {
              "top_classes" : [
                {
                  "class_name" : "fr",
                  "class_probability" : 0.9999669852240882
                },
                {
                  "class_name" : "gd",
                  "class_probability" : 2.3485226102079597E-5
                },
                {
                  "class_name" : "ht",
                  "class_probability" : 3.536708810360631E-6
                }
              ],
              "model_id" : "lang_ident_model_1"
            }
          }
        },
        "_ingest" : {
          "timestamp" : "2020-01-22T12:40:03.218652Z"
        }
      }
    },
    {
      "doc" : {
        "_index" : "_index",
        "_type" : "_doc",
        "_id" : "_id",
        "_source" : {
          "contents" : {
            "default" : "Lupus non timet canem latrantem",
            "language" : "la",
            "supported" : false
          },
          "_ml" : {
            "lang_ident" : {
              "top_classes" : [
                {
                  "class_name" : "la",
                  "class_probability" : 0.614050940088811
                },
                {
                  "class_name" : "fr",
                  "class_probability" : 0.32530021315840363
                },
                {
                  "class_name" : "sq",
                  "class_probability" : 0.03353817054854559
                }
              ],
              "model_id" : "lang_ident_model_1"
            }
          }
        },
        "_ingest" : {
          "timestamp" : "2020-01-22T12:40:03.218653Z"
        }
      }
    },
    {
      "doc" : {
        "_index" : "_index",
        "_type" : "_doc",
        "_id" : "_id",
        "_source" : {
          "contents" : {
            "en" : "This is mostly English but has a touch of Latin since we often just say, Carpe diem",
            "default" : "This is mostly English but has a touch of Latin since we often just say, Carpe diem",
            "language" : "en",
            "supported" : true
          },
          "_ml" : {
            "lang_ident" : {
              "top_classes" : [
                {
                  "class_name" : "en",
                  "class_probability" : 0.9997901768317939
                },
                {
                  "class_name" : "ja",
                  "class_probability" : 8.756250766054857E-5
                },
                {
                  "class_name" : "fil",
                  "class_probability" : 1.6980752372837307E-5
                }
              ],
              "model_id" : "lang_ident_model_1"
            }
          }
        },
        "_ingest" : {
          "timestamp" : "2020-01-22T12:40:03.218654Z"
        }
      }
    }
  ]
}

Como esperábamos, los campos de alemán se almacenan en contents.de, los de inglés en contents.en, los de coreano en contents.ko y etc. Ten en cuenta que también agregamos un par de ejemplos de idiomas no soportados: francés y latín. Vemos que no tienen un indicador que señale que se soportan y solo se puede buscar en ellos en el campo predeterminado. Echa un vistazo también a las clases previstas principales en el ejemplo de latín. Parece que el modelo cree que es latín, lo cual es correcto, pero el modelo no está seguro y prevé un segundo puesto firme para el francés.

Este es solo un ejemplo básico de un pipeline de ingesta con identificación de idioma, pero esperamos que te dé una idea de lo que es posible. Con la flexibilidad de los pipelines de ingesta, podemos lograr muchas situaciones diferentes. Veremos algunas alternativas al final de este blog. Algunos de los pasos en este ejemplo podrían combinarse u omitirse en un pipeline de producción, pero recuerda que un buen pipeline de procesamiento de datos es aquel que se puede leer y comprender con facilidad, no el que tiene la menor cantidad posible de líneas.

Por índice

Nuestra estrategia de idioma por índice usa los mismos elementos esenciales básicos que el pipeline para idioma por campo. La gran diferencia es que en lugar de almacenar en un campo específico según el idioma, usamos un índice diferente. Esto es posible gracias a que al momento de la ingesta podemos configurar el campo _index de un documento, lo que nos permite anular el valor predeterminado y configurarlo con el nombre de un índice específico según el idioma. Si no soportamos el idioma, omitimos ese paso y el documento se indexará en el índice predeterminado. Simple.

El proyecto de demostración contiene una definición del pipeline completo: config/pipelines/lang-per-index.json

Un mapeo para soportar esta estrategia de indexación se vería de la siguiente manera.

{
  "settings": {
    "index": {
      "number_of_shards": 1,
      "number_of_replicas": 0
    }
  },
  "mappings": {
    "dynamic": "strict",
    "properties": {
      "contents": {
        "properties": {
          "language": {
            "type": "keyword"
          },
          "text": {
            "type": "text",
            "analyzer": "default"
          }
        }
      }
    }
  }
}

Ten en cuenta que en ese mapeo no especificamos un analizador personalizado y usamos, en cambio, este archivo como plantilla. Cuando creamos cada índice específico según el idioma, configuramos el analizador para dicho idioma.

Simulación de este pipeline:

# simula un idioma por índice y genera una salida de las tres clases principales de idiomas para inspección

POST _ingest/pipeline/_simulate
{
  "pipeline": {
    "processors": [
      {
        "inference": {
          "model_id": "lang_ident_model_1",
          "inference_config": {
            "classification": {
              "num_top_classes": 3
            }
          },
          "field_mappings": {
            "contents": "text"
          },
          "target_field": "_ml.lang_ident"
        }
      },
      {
        "rename": {
          "field": "contents",
          "target_field": "contents.text"
        }
      },
      {
        "rename": {
          "field": "_ml.lang_ident.predicted_value",
          "target_field": "contents.language"
        }
      },
      {
        "set": {
          "if": "['de', 'en', 'ja', 'ko', 'zh'].contains(ctx.contents.language)",
          "field": "_index",
          "value": "{{_index}}_{{contents.language}}",
          "override": true
        }
      }
    ]
  },
  "docs": [
    {
      "_source": {
        "contents": "Das Leben ist kein Ponyhof"
      }
    },
    {
      "_source": {
        "contents": "The rain in Spain stays mainly in the plains"
      }
    },
    {
      "_source": {
        "contents": "オリンピック大会"
      }
    },
    {
      "_source": {
        "contents": "로마는 하루아침에 이루어진 것이 아니다"
      }
    },
    {
      "_source": {
        "contents": "授人以鱼不如授人以渔"
      }
    },
    {
      "_source": {
        "contents": "Qui court deux lievres a la fois, n’en prend aucun"
      }
    },
    {
      "_source": {
        "contents": "Lupus non timet canem latrantem"
      }
    },
    {
      "_source": {
        "contents": "This is mostly English but has a touch of Latin since we often just say, Carpe diem"
      }
    }
  ]
}

Esta es una salida con idioma por índice:

{
  "docs" : [
    {
      "doc" : {
        "_index" : "_index_de",
        "_type" : "_doc",
        "_id" : "_id",
        "_source" : {
          "contents" : {
            "language" : "de",
            "text" : "Das Leben ist kein Ponyhof"
          },
          "_ml" : {
            "lang_ident" : {
              "top_classes" : [
                {
                  "class_name" : "de",
                  "class_probability" : 0.9996006023972855
                },
                {
                  "class_name" : "el-Latn",
                  "class_probability" : 2.625873919853074E-4
                },
                {
                  "class_name" : "ru-Latn",
                  "class_probability" : 1.130237050226503E-4
                }
              ],
              "model_id" : "lang_ident_model_1"
            }
          }
        },
        "_ingest" : {
          "timestamp" : "2020-01-21T14:41:48.486009Z"
        }
      }
    },
    {
      "doc" : {
        "_index" : "_index_en",
        "_type" : "_doc",
        "_id" : "_id",
        "_source" : {
          "contents" : {
            "language" : "en",
            "text" : "The rain in Spain stays mainly in the plains"
          },
          "_ml" : {
            "lang_ident" : {
              "top_classes" : [
                {
                  "class_name" : "en",
                  "class_probability" : 0.9988809847231199
                },
                {
                  "class_name" : "ga",
                  "class_probability" : 7.764148026288316E-4
                },
                {
                  "class_name" : "gd",
                  "class_probability" : 7.968926766495827E-5
                }
              ],
              "model_id" : "lang_ident_model_1"
            }
          }
        },
        "_ingest" : {
          "timestamp" : "2020-01-21T14:41:48.486037Z"
        }
      }
    },
    {
      "doc" : {
        "_index" : "_index_ja",
        "_type" : "_doc",
        "_id" : "_id",
        "_source" : {
          "contents" : {
            "language" : "ja",
            "text" : "オリンピック大会"
          },
          "_ml" : {
            "lang_ident" : {
              "top_classes" : [
                {
                  "class_name" : "ja",
                  "class_probability" : 0.9993823252841599
                },
                {
                  "class_name" : "el",
                  "class_probability" : 2.6448654791599055E-4
                },
                {
                  "class_name" : "sd",
                  "class_probability" : 1.4846805271384584E-4
                }
              ],
              "model_id" : "lang_ident_model_1"
            }
          }
        },
        "_ingest" : {
          "timestamp" : "2020-01-21T14:41:48.486039Z"
        }
      }
    },
    {
      "doc" : {
        "_index" : "_index_ko",
        "_type" : "_doc",
        "_id" : "_id",
        "_source" : {
          "contents" : {
            "language" : "ko",
            "text" : "로마는 하루아침에 이루어진 것이 아니다"
          },
          "_ml" : {
            "lang_ident" : {
              "top_classes" : [
                {
                  "class_name" : "ko",
                  "class_probability" : 0.9999939196272863
                },
                {
                  "class_name" : "ka",
                  "class_probability" : 3.0431805047662344E-6
                },
                {
                  "class_name" : "am",
                  "class_probability" : 1.710514725818281E-6
                }
              ],
              "model_id" : "lang_ident_model_1"
            }
          }
        },
        "_ingest" : {
          "timestamp" : "2020-01-21T14:41:48.486041Z"
        }
      }
    },
    {
      "doc" : {
        "_index" : "_index_zh",
        "_type" : "_doc",
        "_id" : "_id",
        "_source" : {
          "contents" : {
            "language" : "zh",
            "text" : "授人以鱼不如授人以渔"
          },
          "_ml" : {
            "lang_ident" : {
              "top_classes" : [
                {
                  "class_name" : "zh",
                  "class_probability" : 0.9999810103320087
                },
                {
                  "class_name" : "ja",
                  "class_probability" : 1.0390454083183788E-5
                },
                {
                  "class_name" : "ka",
                  "class_probability" : 2.6302271562335787E-6
                }
              ],
              "model_id" : "lang_ident_model_1"
            }
          }
        },
        "_ingest" : {
          "timestamp" : "2020-01-21T14:41:48.486043Z"
        }
      }
    },
    {
      "doc" : {
        "_index" : "_index",
        "_type" : "_doc",
        "_id" : "_id",
        "_source" : {
          "contents" : {
            "language" : "fr",
            "text" : "Qui court deux lievres a la fois, n’en prend aucun"
          },
          "_ml" : {
            "lang_ident" : {
              "top_classes" : [
                {
                  "class_name" : "fr",
                  "class_probability" : 0.9999669852240882
                },
                {
                  "class_name" : "gd",
                  "class_probability" : 2.3485226102079597E-5
                },
                {
                  "class_name" : "ht",
                  "class_probability" : 3.536708810360631E-6
                }
              ],
              "model_id" : "lang_ident_model_1"
            }
          }
        },
        "_ingest" : {
          "timestamp" : "2020-01-21T14:41:48.486044Z"
        }
      }
    },
    {
      "doc" : {
        "_index" : "_index",
        "_type" : "_doc",
        "_id" : "_id",
        "_source" : {
          "contents" : {
            "language" : "la",
            "text" : "Lupus non timet canem latrantem"
          },
          "_ml" : {
            "lang_ident" : {
              "top_classes" : [
                {
                  "class_name" : "la",
                  "class_probability" : 0.614050940088811
                },
                {
                  "class_name" : "fr",
                  "class_probability" : 0.32530021315840363
                },
                {
                  "class_name" : "sq",
                  "class_probability" : 0.03353817054854559
                }
              ],
              "model_id" : "lang_ident_model_1"
            }
          }
        },
        "_ingest" : {
          "timestamp" : "2020-01-21T14:41:48.486046Z"
        }
      }
    },
    {
      "doc" : {
        "_index" : "_index_en",
        "_type" : "_doc",
        "_id" : "_id",
        "_source" : {
          "contents" : {
            "language" : "en",
            "text" : "This is mostly English but has a touch of Latin since we often just say, Carpe diem"
          },
          "_ml" : {
            "lang_ident" : {
              "top_classes" : [
                {
                  "class_name" : "en",
                  "class_probability" : 0.9997901768317939
                },
                {
                  "class_name" : "ja",
                  "class_probability" : 8.756250766054857E-5
                },
                {
                  "class_name" : "fil",
                  "class_probability" : 1.6980752372837307E-5
                }
              ],
              "model_id" : "lang_ident_model_1"
            }
          }
        },
        "_ingest" : {
          "timestamp" : "2020-01-21T14:41:48.48605Z"
        }
      }
    }
  ]
}

Como se esperaría, los resultados de identificación de idiomas son los mismos que con la estrategia por campo, la única diferencia es la forma en que usamos esa información en el pipeline para enrutar un documento al índice correcto.

Búsqueda

Dadas las dos estrategias de indexación, ¿cuál es la mejor forma de buscar? Como ya mencionamos, hay un par de opciones para cada una de las estrategias de indexación. Una pregunta común es: ¿cómo especificamos un analizador específico según el idioma para el texto de búsqueda de modo que busque la coincidencia con el campo indexado? No te preocupes, no necesitas especificar un analizador especial al momento de la búsqueda. A menos que especifiques un search_analyzer en tu DSL, el texto de búsqueda se analizará con el mismo analizador que el campo con el que se busca la coincidencia. Como en los ejemplos de idioma por campo, si tienes los campos en y de, el texto de búsqueda se analizará con el analizador english cuando encuentre coincidencias en el campo en y con el analizador german_custom cuando encuentre coincidencias en el campo de.

Idioma de búsqueda

Antes de adentrarnos en estrategias de búsqueda, es importante establecer primero algo de contexto sobre la identificación de idioma en el texto de búsqueda en sí del usuario. Quizás pienses: “bien, ahora que conocemos el idioma (predominante) de los documentos indexados, ¿por qué no simplemente hacer la identificación de idioma en el texto de búsqueda y realizar una búsqueda normal en el campo o índice correspondiente?”. Desafortunadamente, las consultas de búsqueda suelen ser breves. ¡Muy breves! En 2001, un estudio [1] del viejo y buen motor de búsqueda web Excite mostró que la búsqueda promedio de los usuarios contenía solo 2.4 términos. Eso fue hace mucho tiempo y, aunque las cosas han cambiado mucho con la búsqueda conversacional y de lenguaje natural (por ejemplo, “cómo uso Elasticsearch para buscar en corpus multilingües”), las consultas de búsqueda siguen siendo demasiado breves para usarlas en la identificación del idioma. Muchos algoritmos de identificación de idioma funcionan mejor con más de 50 caracteres [2]. Además de este problema, por lo general tenemos consultas de búsqueda que son sustantivos propios, nombres de entidades o nombres científicos como “Justin Trudeau”, “Foo Fighters” o “fascitis plantar”, respectivamente. El usuario puede querer documentos de un idioma arbitrario, pero no es posible saberlo con solo analizar estos tipos de texto de búsqueda.

Por lo tanto, no recomendamos usar la identificación de idioma (de ningún tipo) en textos de búsqueda solamente. Si deseas usar el idioma de búsqueda del usuario para seleccionar el índice o campo de búsqueda, es mejor considerar otros enfoques que usen información implícita o explícita sobre el usuario. Por ejemplo, el contexto implícito puede ser usar el dominio del sitio web (como .com o .de) o la configuración regional de la tienda de apps desde la cual se descargó la app (como tienda de EE. UU. o tienda de Alemania). Sin embargo, en la mayoría de los casos lo mejor es simplemente preguntarle al usuario. Muchos sitios tienen selección de configuración regional cuando un usuario nuevo visita el sitio por primera vez. También puedes considerar usar el facetado (con agregación de términos) en los idiomas del documento para ayudar al usuario a guiarte hacia los idiomas que le interesan.

Por campo

Con la estrategia por campo, tenemos subcampos de varios idiomas, por lo que necesitamos buscar en todos ellos al mismo tiempo y elegir el campo con mejor puntuación. Esto es relativamente sencillo porque en el pipeline de indexación configuramos solo un campo de idioma. Si bien estamos buscando en varios campos, solo uno de ellos realmente tiene contenido. Para hacerlo, usaremos una búsqueda multi_match con el tipo best_fields (predeterminado). Esta combinación se ejecuta como una búsqueda dis_max, y la usamos porque nos interesan todos los términos que coincidan en un solo campo y no en todos ellos.

GET lang-per-field/_search 
{ 
  "query": { 
    "multi_match": { 
      "query": "jahr", 
      "type": "best_fields", 
      "fields": [ 
        "contents.de", 
        "contents.en", 
        "contents.ja", 
        "contents.ko", 
        "contents.zh" 
      ] 
    } 
  } 
}

Si deseamos buscar en todos los idiomas, también podemos agregar el campo contents.default en la búsqueda multi_match. Una ventaja de la estrategia por campo también es poder usar el idioma identificado para ayudar a realzar los documentos, por ejemplo aquellos que coinciden con el idioma o la configuración regional del usuario como mencionamos. Esto puede proporcionar una mejora tanto en la precisión como en la exhaustividad debido a que se puede usar directamente para influenciar la relevancia. Del mismo modo, si queremos buscar en un solo idioma, como cuando conocemos el idioma de búsqueda del usuario, simplemente podemos usar una búsqueda match en el campo de idioma para dicho idioma, por ejemplo contents.de.

Por índice

Con la estrategia por índice, tenemos varios índices de idioma, pero cada índice tiene los mismos nombres de campo. Eso significa que podemos usar una sola búsqueda simple y únicamente especificar un patrón de índice al hacer la solicitud de búsqueda:

GET lang-per-index_*/_search 
{ 
  "query": { 
    "match": { 
      "contents.text": "jahr" 
    } 
  } 
}

Si deseamos buscar en todos los idiomas, usamos un patrón de índice que también coincida con el índice predeterminado: lang-per-index* (observa que no hay guion bajo). Si deseamos buscar en un solo idioma, simplemente podemos usar el índice de dicho idioma, por ejemplo lang-per-index_de.

Ejemplos

Usando los mismos ejemplos que describimos en la sección “Motivación”, podemos intentar buscar en nuestro corpus WiLI-2018. Prueba estos comandos con el proyecto de demostración y ve lo que sucede.

Descomposición:

# solo buscar coincidencias exactas con el término "jahr" 
bin/search --strategy default jahr
# coincidencias: "jahr", "jahre", "jahren", "jahrhunderts", etc. 
bin/search --strategy per-field jahr

Término común:

# solo buscar coincidencias exactas con el término "computer", varios idiomas en los resultados 
bin/search --strategy default computer
# también se encuentran coincidencias con palabras compuestas en alemán: "Computersicherheit" (seguridad de las computadoras) 
bin/search --strategy per-field computer

Alfabetos no latinos:

# el analizador estándar obtiene mala precisión y devuelve resultados irrelevantes o que no coinciden con "network"/"internet": "网络" 
bin/search --strategy default 网络
# el análisis de ICU y específico según el idioma hace las cosas bien, pero observa las diferentes puntuaciones 
bin/search --strategy icu 网络 
bin/search --strategy per-field 网络

Comparación

Sobre la base de las dos estrategias, ¿cuál deberías usar en realidad? Bueno, depende. Estas son algunas ventajas y desventajas de cada enfoque para ayudarte a decidir.

VentajasDesventajas
Por campo
  • Fácil para gestionar un solo índice
  • Soporta varios idiomas por documento
  • Única fuente de la verdad incluso cuando los documentos tienen varios idiomas
  • Permite realzar los documentos según el idioma
  • Mapeos y búsquedas más complejos
  • Rendimiento más lento a medida que aumenta la cantidad de campos e idiomas soportados
Por índice
  • Búsqueda simple
  • Rápido para buscar dado que cada índice recibe una búsqueda
  • Puede escalar índices individualmente según el uso del idioma
  • Varios índices para gestionar
  • No soporta de forma sencilla la indexación de un solo documento en varios índices si hay un uso de idiomas combinados en el documento

Si aún no puedes decidir, te recomendamos probar ambas estrategias y ver cómo se aplica cada una en tu set de datos. Si tienes un set de datos de etiquetas de relevancia, también puedes usar la API de evaluación de clasificación para ver si hay diferencias en la relevancia entre las diversas estrategias.

Enfoques adicionales

Vimos dos estrategias básicas para usar la identificación de idioma e indexar y buscar en un corpus multilingüe. Con el poder de los pipelines de ingesta, podemos lograr una amplia variedad de enfoques adicionales y modificaciones. Estos son algunos ejemplos para explorar:

  • Mapea idiomas que tengan el alfabeto en común a un solo campo, por ejemplo, mapea chino, japonés y coreano a un campo cjk y usa el analizador cjk, y mapea en y fr a un campo latin con el analizador standard (ve: examples/olympics.txt).
  • Mapea idiomas desconocidos o con un alfabeto distinto al latino a un campo icu y usa el analizador icu (ve: config/mappings/lang-per-field.json).
  • Con un procesador condicional o procesador de script, configura varios idiomas principales sobre un umbral en un campo (para facetado o filtrado).
  • Concatena varios campos del documento en un solo campo para identificar el idioma y úsalo opcionalmente para buscar (por ejemplo, un campo all_contents) o solo continúa siguiendo la estrategia "idioma por campo" después de identificar el idioma (ve: examples/simulate-concatenation.txt y examples/simulate-concatenation.out.json).
  • Con un procesador de script, selecciona el idioma predominante solo si la clase principal supera un umbral (por ejemplo, 60 % o 50 %) o es significativamente superior a la segunda clase prevista (por ejemplo, es superior al 50 % y está más del 10 % por encima de la segunda clase).

Resumen

Esperamos que este blog te sirva como punto de partida y te dé ideas sobre cómo usar la identificación de idioma correctamente para la búsqueda multilingüe. Nos encantaría recibir noticias tuyas, no seas tímido y únete a nuestro foro de debate. Cuéntanos si tienes éxito con la identificación de idioma o si encuentras algún problema.

Referencias

  1. Amanda Spink, Dietmar Wolfram, Major B. J. Jansen, Tefko Saracevic. 2001. Searching the Web: The Public and Their Queries (Búsqueda en la web: el público y sus búsquedas). Journal of the American Society for Information Science and Technology. Volumen 52, Número 3, páginas 226-234.
  2. A. Putsma. 2001. Applying Monte Carlo Techniques to Language Identification (Aplicación de las técnicas Monte Carlo a la identificación de idioma).