Multilingual search using language identification in Elasticsearch | Elastic Blog
Engineering

Busca multilíngue usando identificação de idioma no Elasticsearch

Temos a satisfação de anunciar que, juntamente com o lançamento do processador de ingestão de inferência de machine learning, estamos lançando a identificação de idioma no Elasticsearch 7.6. Com esse lançamento, gostaríamos de aproveitar a oportunidade para descrever alguns casos de uso e estratégias para fazer buscas em corpora multilíngues e mostrar o papel desempenhado pela identificação de idioma. Abordamos alguns desses tópicos no passado e os desenvolveremos em alguns dos exemplos a seguir.

Motivação

No mundo altamente interconectado de hoje, constatamos que documentos e outras fontes de informação vêm em vários idiomas. Isso representa um problema para muitas aplicações de busca. Precisamos entender o idioma desses documentos da melhor maneira possível para analisá-los adequadamente e proporcionar a melhor experiência de busca. É aí que entra a identificação de idioma.

A identificação de idioma é usada para melhorar a relevância geral da busca para esses corpora multilíngues. Vamos considerar um conjunto de documentos cujos idiomas ainda não conhecemos e nos quais queremos fazer buscas com eficiência. Os documentos podem conter um único idioma ou vários. O primeiro é comum em domínios como a ciência da computação, onde o inglês é a língua predominante da comunicação, enquanto o segundo é comumente encontrado em textos médicos e biológicos, nos quais a terminologia em latim é frequentemente intercalada com o inglês.

Ao aplicar uma análise específica do idioma, podemos melhorar a relevância (precisão e revocação), garantindo que os termos do documento sejam entendidos, indexados e buscados adequadamente. Usando um conjunto de analisadores específicos do idioma no Elasticsearch (tanto integrados quanto por meio de plugins adicionais), podemos fornecer tokenização aprimorada, filtragem de tokens e filtragem de termos:

  • Listas de palavras vazias e sinônimos
  • Normalização da forma da palavra: stemmização e lemmatização
  • Decomposição (por exemplo, alemão, neerlandês, coreano)

Por razões semelhantes, encontramos a identificação de idioma em pipelines de processamento de linguagem natural (PLN) mais gerais como uma das primeiras etapas de processamento para fazer uso de algoritmos e modelos altamente precisos e específicos do idioma. Por exemplo, modelos de PLN pré-treinados, como o BERT e o ALBERT do Google ou o GPT-2 da OpenAI, geralmente são treinados em corpora de cada idioma ou em corpora com um idioma predominante e ajustados para tarefas como classificação de documentos, análise de sentimentos, reconhecimento de entidades nomeadas (NER) etc.

Para os exemplos e estratégias a seguir, a menos que especificado de outra forma, vamos pressupor que os documentos contenham um idioma único ou predominante.

Benefícios da análise específica do idioma

Para ajudar a motivar isso ainda mais, vamos dar uma olhada rápida em alguns benefícios dos analisadores específicos do idioma.

Decomposição: no idioma alemão, os substantivos geralmente são construídos combinando outros substantivos para criar palavras compostas maravilhosamente longas e difíceis de ler. Um exemplo simples é combinar “Jahr” (“ano”) em outras palavras como “Jahrhunderts” (“século”), “Jahreskalender” (“calendário anual”) ou “Schuljahr” (“ano letivo”). Sem um analisador customizado que consiga decompor essas palavras, não poderíamos fazer buscas por “jahr” e obter como resultado documentos sobre anos letivos, “Schuljahr”. Além disso, o alemão tem regras diferentes das outras línguas latinas para as formas de plural e dativo, o que significa que a busca por “jahr” também deve corresponder a “Jahre” (plural) e “Jahren” (dativo plural).

Termo comum: alguns idiomas também usam terminologia comum ou específica de um domínio. Por exemplo, “computer” é uma palavra usada com frequência em outros idiomas assim como está. Se queremos fazer buscas por “computer”, é possível que tenhamos interesse também em documentos que não estejam em inglês. Conseguir fazer buscas em um conjunto conhecido de idiomas e ainda ter correspondência com termos comuns pode ser um caso de uso interessante. Novamente, usando o alemão como exemplo, podemos ter documentos sobre segurança de computadores em vários idiomas. Em alemão, isso é “Computersicherheit” (“sicherheit” significa “segurança” ou “proteção”), e somente com um analisador alemão as buscas por “computer” encontrarão correspondências em inglês e alemão.

Scripts não latinos: o analisador standard funciona muito bem para a maioria dos idiomas de scripts latinos (idiomas da Europa Ocidental). No entanto, ele deixa de funcionar rapidamente com scripts não latinos, como cirílico ou CJK (chinês/japonês/coreano). Em uma série anterior do blog, vimos como os idiomas CJK são formados e a necessidade de ter analisadores específicos do idioma. Por exemplo, o coreano tem posposições — sufixos adicionados a substantivos e pronomes que alteram seu significado. Às vezes, o uso do analisador standard gera correspondências para os termos de busca, mas não faz um bom trabalho ao pontuar as correspondências. Isso significa que você pode ter uma boa revocação nos documentos, mas sua precisão é sofrível. Em outros casos, o analisador standard não encontra correspondência com nenhum termo, e tanto a precisão quanto a revocação são sofríveis.

Vejamos o exemplo de trabalho para “Winter Olympics”. Em coreano, é “동계 올림픽 대회 는”, que é composto por “동계”, que significa “temporada de inverno”, “올림픽 대회”, que significa “Olimpíadas” ou “competição olímpica” e, finalmente, “는”, que é a posposição do tópico — um sufixo adicionado à palavra que denota o tópico. A busca por essa string exata com o analisador standard produz uma correspondência perfeita, mas a busca por “올림픽 대회”, significando apenas “Olimpíadas”, não retorna resultados. No entanto, usando o analisador coreano nori, obtemos uma correspondência porque “동계올림픽대회는”/“Winter Olympics” foi tokenizado corretamente no momento do índice.

Começar a trabalhar com identificação de idioma

Projeto de demonstração

Para ajudar a ilustrar casos de uso e estratégias para identificação de idioma na busca, configuramos um pequeno projeto de demonstração. Ele contém todos os exemplos deste post do blog, além de algumas ferramentas para indexar e buscar no WiLI-2018, um corpus multilíngue, que você pode usar como referência e exemplo de trabalho para realizar experiências com a busca multilíngue. Para seguir os exemplos, será útil (mas não estritamente necessário) ter o projeto de demonstração em funcionamento, com documentos indexados, caso você queira acompanhar.

Para essas experiências, você pode instalar o Elasticsearch 7.6 localmente ou iniciar uma avaliação gratuita do Elasticsearch Service.

Primeiras experiências

A identificação de idioma é um modelo pré-treinado que vem na distribuição padrão do Elasticsearch. É usado em conjunto com o processador de ingestão de inferência, especificando o lang_ident_model_1 como o model_id ao configurar o seu processador de inferência em um pipeline de ingestão.

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

O restante da configuração é igual à de outros modelos, permitindo especificar configurações como o número de principais classes para saída, o campo de saída que conterá a previsão e, o mais importante para os nossos casos de uso, o campo de entrada a ser usado. Por padrão, o modelo espera que um campo chamado “text” contenha a entrada. No exemplo a seguir, usamos a API pipeline _simulate com alguns documentos de campo único. Ela mapeia o campo de conteúdo de entrada para o campo “text” para inferência — esse mapeamento não afeta outros processadores no pipeline. Em seguida, gera as três principais classes para inspeção.

# simular uma configuração básica de inferência

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"
      }
    }
  ]
}

A saída nos mostra cada documento, além de algumas informações extras no campo _ml.lang_ident. Isso inclui a probabilidade de cada um dos três principais idiomas e do idioma principal, que é armazenado em _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"
        }
      }
    }
  ]
}

Parece bom! Identificamos alemão para o primeiro documento e inglês para o segundo e terceiro documentos, mesmo com um pouco de latim no terceiro documento.

Estratégias para identificação de idioma na busca

Agora que vimos um exemplo básico de identificação de idioma, é hora de começar a colocar isso em uma estratégia para indexação e busca.

Existem duas estratégias básicas de indexação que usaremos: idioma por campo e idioma por índice. Na estratégia de idioma por campo, criaremos um único índice com um conjunto de campos específicos do idioma e usaremos um analisador ajustado para cada idioma. No momento da busca, podemos optar por buscar em um campo do idioma conhecido ou em todos os campos do idioma e escolher o campo de melhor correspondência. Na estratégia de idioma por índice, criaremos um conjunto de índices específicos do idioma com mapeamentos diferentes, nos quais o campo indexado tem um analisador para esse idioma. No momento da busca, podemos adotar uma abordagem semelhante à de idioma por campo e optar por buscar em um único índice do idioma ou em vários índices com um padrão de indexação na solicitação de busca.

Compare essas duas estratégias com o que você teria de fazer hoje — indexar a mesma string várias vezes, cada uma para um campo ou indexar com um analisador específico do idioma. Embora essa abordagem possa funcionar, ela causa muita duplicação, gerando lentidão nas consultas e usando significativamente mais espaço de armazenamento do que o necessário.

Indexação

Vamos dividir isso e dar uma olhada em cada uma das duas estratégias de indexação, pois elas determinam as estratégias de busca que podemos usar.

Por campo

Na estratégia de idioma por campo, usaremos a saída de identificação de idioma e uma série de processadores em um pipeline de ingestão para armazenar o campo de entrada em um campo específico do idioma. Daremos suporte apenas para um conjunto finito de idiomas (alemão, inglês, coreano, japonês e chinês), pois precisamos configurar um analisador específico para cada idioma. Quaisquer documentos que não estejam em um dos nossos idiomas com suporte serão indexados em um campo padrão com o analisador standard.

Uma definição completa do pipeline pode ser encontrada no projeto de demonstração: config/pipelines/lang-per-field.json

Um mapeamento para dar suporte a essa estratégia de indexação ficaria assim:

{
  "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"
          }
        }
      }
    }
  }
}

(Observe que a configuração do analisador do alemão foi eliminada do exemplo acima por questões de brevidade e pode ser encontrada em: config/mappings/de_analyzer.json.)

Como no exemplo anterior, usaremos a API _simulate do pipeline para explorar:

# simular um idioma por campo e gerar as 3 principais classes de idioma para inspeção

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"
      }
    }
  ]
}

E aqui está a saída com um 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 esperado, temos campos em alemão armazenados em contents.de, em inglês em contents.en, em coreano em contents.ko e assim por diante. Observe que também misturamos alguns exemplos de idiomas sem suporte, como francês e latim. Vemos que eles não recebem um sinalizador de suporte e estão disponíveis para busca apenas no campo padrão. Confira também as principais classes previstas para o exemplo em latim. Parece que o modelo acha que é latim, o que é correto, mas não tem certeza e prevê um forte segundo lugar para o francês.

Esse é apenas um exemplo básico de um pipeline de ingestão com identificação de idioma, mas espero que ele tenha lhe dado uma ideia do que é possível. Com a flexibilidade dos pipelines de ingestão, podemos realizar muitos cenários diferentes. Exploraremos algumas alternativas no final do post. Algumas das etapas desse exemplo poderiam ser combinadas ou omitidas em um pipeline de produção, mas lembre-se de que um bom pipeline de processamento de dados é aquele que pode ser facilmente lido e compreendido, e não aquele com o menor número possível de linhas.

Por índice

Nossa estratégia de idioma por índice usa os mesmos componentes básicos do pipeline de idioma por campo. A grande diferença é que, em vez de armazenar em um campo específico do idioma, usamos um índice diferente. Isso é possível porque, no momento da ingestão, podemos definir o campo _index de um documento, o que nos permite substituir o valor padrão e defini-lo com um nome de índice específico do idioma. Se não dermos suporte ao idioma, pularemos essa etapa, e o documento será indexado no índice padrão. É simples.

Uma definição completa do pipeline pode ser encontrada no projeto de demonstração: config/pipelines/lang-per-index.json

Um mapeamento para dar suporte a essa estratégia de indexação ficaria assim:

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

Observe que nesse mapeamento não especificamos um analisador customizado. Em vez disso, usamos esse arquivo como modelo. Quando criamos cada índice específico do idioma, definimos o analisador para esse idioma.

Simulando esse pipeline:

# simular um idioma por índice e gerar as 3 principais classes de idioma para inspeção

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"
      }
    }
  ]
}

E aqui está a saída com um 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 seria de se esperar, os resultados de identificação de idioma são os mesmos da estratégia por campo; a única diferença é como usamos essas informações no pipeline para direcionar um documento para o índice correto.

Busca

Considerando as duas estratégias de indexação, qual é a melhor maneira de fazer buscas? Como mencionado acima, temos algumas opções para cada uma das estratégias de indexação. Uma pergunta comum é: como especificamos um analisador específico do idioma para a string de consulta, para que ela tenha correspondência no campo indexado? Não se preocupe, você não precisa especificar um analisador especial no momento da busca. A menos que você especifique um search_analyzer na DSL da consulta, a string de consulta será analisada pelo mesmo analisador do campo que está sendo usado para correspondência. Como nos exemplos de idioma por campo, se você tiver campos “en” e “de”, a string de consulta será analisada com o analisador de inglês ao fazer a correspondência no campo “en” e com o analisador german_custom ao fazer a correspondência no campo “de”.

Idioma da consulta

Antes de nos aprofundarmos nas estratégias de busca, é importante primeiro definir um contexto sobre a identificação de idioma na própria string de consulta do usuário. Você pode estar pensando: “OK, agora que sabemos o idioma (predominante) dos documentos indexados, por que não simplesmente fazer a identificação de idioma na string de consulta e realizar uma busca normal no campo ou índice correspondente?” Infelizmente, as consultas de busca tendem a ser curtas. Muito, muito curtas. Nos idos de 2001, um estudo [1] do bom e velho mecanismo de busca da Web Excite mostrou que a consulta média do usuário continha apenas 2,4 termos! Isso foi um tempo atrás e, embora as coisas tenham mudado muito com a busca conversacional e a consulta em linguagem natural (por exemplo, “como uso o Elasticsearch para buscar em corpora multilíngues”), as consultas de busca ainda tendem a ser curtas demais para serem usadas na identificação do idioma. Muitos algoritmos de identificação de idioma funcionam melhor com mais de 50 caracteres [2]. Para piorar, muitas vezes temos consultas de busca que são nomes próprios, nomes de entidades ou nomes científicos, como “Justin Trudeau”, “Foo Fighters” ou “fascite plantar”, respectivamente. O usuário pode querer documentos de um idioma arbitrário, mas não é possível saber isso simplesmente analisando esses tipos de strings de consulta.

Por isso, não recomendamos o uso de identificação de idioma (de qualquer tipo) somente nas strings de consulta. Se você realmente quer usar o idioma de consulta do usuário para selecionar o campo ou o índice de busca, é melhor considerar outras abordagens que façam uso de informações implícitas ou explícitas sobre o usuário. Por exemplo, o contexto implícito pode estar usando o domínio do website (por exemplo, .com ou .de) ou a localidade da loja de apps de onde o seu app foi baixado (por exemplo, loja dos EUA ou loja alemã). Na maioria dos casos, no entanto, a melhor coisa a fazer é perguntar ao usuário! Muitos websites têm seleção de localidade quando um novo usuário faz a primeira visita. Você também pode considerar o uso de facetas (com uma agregação de termos) nos idiomas dos documentos para ajudar o usuário a guiar você para os idiomas nos quais ele está interessado.

Por campo

Com a estratégia por campo, temos vários subcampos de idioma; portanto, precisamos buscar em todos eles ao mesmo tempo e escolher o campo de pontuação mais alta. Isso é relativamente simples, já que, no pipeline de indexação, definimos apenas um único campo de idioma. Assim, embora estejamos buscando em vários campos, apenas um deles é realmente preenchido. Para fazer isso, usaremos uma consulta multi_match com o tipo best_fields (padrão). Essa combinação é executada como uma consulta dis_max, e usamos essa combinação porque estamos interessados em todos os termos com correspondência em um único campo, e não entre campos.

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

Se quisermos buscar em todos os idiomas, também poderemos adicionar o campo contents.default à consulta multi_match. Uma vantagem da estratégia por campo é também poder usar o idioma identificado para ajudar a aprimorar documentos, como aqueles que correspondem ao idioma ou à localidade do usuário, conforme mencionamos anteriormente. Isso pode fornecer uma melhoria na precisão e na revocação, pois pode ser usado diretamente para influenciar a relevância. Da mesma forma, se quisermos buscar em um único idioma, como quando sabemos o idioma de consulta do usuário, poderemos simplesmente usar uma consulta match no campo de idioma, por exemplo, contents.de.

Por índice

Com a estratégia por índice, temos vários índices de idioma, mas cada índice tem os mesmos nomes de campos. Isso significa que podemos usar uma única e simples consulta, e apenas especificar um padrão de indexação ao fazer a solicitação de busca:

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

Se quisermos buscar em todos os idiomas, usaremos um padrão de indexação que também corresponda ao índice padrão: lang-per-index* (note a ausência do sublinhado). Se quisermos buscar em um único idioma, poderemos simplesmente usar o índice desse idioma, por exemplo, lang-per-index_de.

Exemplos

Usando os mesmos exemplos que descrevemos na seção “Motivação”, podemos tentar buscar em nosso corpus WiLI-2018. Experimente estes comandos com o projeto de demonstração e veja o que acontece.

Decomposição:

# apenas corresponder exatamente ao termo "jahr" 
bin/search --strategy default jahr
# correspondências: "jahr", "jahre", "jahren", "jahrhunderts" etc. 
bin/search --strategy per-field jahr

Termo comum:

# apenas corresponder exatamente ao termo "computer"; há vários idiomas nos resultados 
bin/search --strategy default computer
# corresponde também a palavras compostas em alemão: "Computersicherheit" (segurança de computadores) 
bin/search --strategy per-field computer

Scripts não latinos:

# o analisador standard obtém baixa precisão e retorna resultados irrelevantes/sem correspondência com "network"/"internet": "网络" 
bin/search --strategy default 网络
# ICU e a análise específica de idioma fazem a coisa do jeito certo, mas note as diferentes pontuações 
bin/search --strategy icu 网络 
bin/search --strategy per-field 网络

Comparação

Com base nas duas estratégias, qual delas você deveria efetivamente usar? Bem, depende. Aqui estão alguns prós e contras de cada abordagem para ajudar você a decidir.

PrósContras
Por campo
  • Fácil de gerenciar um único índice
  • Dá suporte para vários idiomas por documento
  • Fonte única de verdade, mesmo quando os documentos contêm vários idiomas
  • Permite aprimorar documentos no idioma
  • Mapeamentos e consultas mais complexos
  • O desempenho cai à medida que o número de idiomas e campos com suporte aumenta
Por índice
  • Consulta simples
  • Busca rápida, pois cada índice recebe uma consulta
  • Pode ampliar os índices individualmente com base no uso do idioma
  • Vários índices para gerenciar
  • Não dá suporte facilmente para a indexação de um único documento em vários índices se há uso de idiomas mistos no documento

Se ainda não consegue decidir, recomendamos tentar as duas e ver como fica cada estratégia com o seu conjunto de dados. Se você tiver um conjunto de dados de rótulos de relevância, também poderá usar a API de avaliação de classificação para verificar se há diferenças na relevância entre as várias estratégias.

Abordagens adicionais

Vimos duas estratégias básicas para usar a identificação e o índice de idioma, bem como para fazer buscas em um corpus multilíngue. Com o poder dos pipelines de ingestão, podemos obter uma ampla variedade de abordagens e modificações adicionais. Aqui estão alguns exemplos para explorar:

  • Mapeie idiomas de script comum em um único campo, por exemplo, mapeando chinês, japonês e coreano para um campo cjk, e use o analisador cjk, e mapeie en e fr em um campo latin com o analisador standard (consulte: examples/olympics.txt).
  • Mapeie idiomas desconhecidos ou scripts não latinos para um campo icu e use o analisador icu (consulte: config/mappings/lang-per-field.json).
  • Usando uma condicional do processador ou um processador de script, defina vários idiomas principais acima de um limite em um campo (para facetas/filtragem).
  • Concatene vários campos do documento em um único campo para identificar o idioma e, opcionalmente, use-o para fazer buscas (por exemplo, em um campo all_contents) ou simplesmente continue a seguir a estratégia de “idioma por campo” após identificar o idioma (consulte: examples/simulate-concatenation.txt e examples/simulate-concatenation.out.json).
  • Usando um processador de script, escolha o idioma predominante apenas se a principal classe estiver acima de um limite (por exemplo, 60% ou 50%) ou for significativamente maior que a segunda classe prevista (por exemplo, acima de 50% e mais de 10% superior à segunda classe).

Resumo

Esperamos que este post do blog lhe dê um ponto de partida e algumas ideias de como usar a identificação de idioma para buscas multilíngues. Gostaríamos muito de ouvir a sua opinião. Por isso, não seja tímido(a) e participe do nosso fórum de discussão. Conte para nós se você está usando a identificação de idioma com sucesso ou se encontrou algum problema.

Referências

  1. Amanda Spink, Dietmar Wolfram, Major B. J. Jansen, Tefko Saracevic. 2001. Searching the Web: The Public and Their Queries (Fazer buscas na Web: o público e suas consultas). Journal of the American Society for Information Science and Technology. Volume 52, Edição 3, pp. 226–234.
  2. A. Putsma. 2001. Applying Monte Carlo Techniques to Language Identification (Aplicação de técnicas de Monte Carlo à identificação de idioma).