Engenharia

Igual, só que diferente: como aumentar o poder do Elasticsearch com sinônimos

Sem dúvida, o uso de sinônimos é uma das técnicas mais importantes no repertório de um engenheiro de busca. Embora os novatos às vezes subestimem sua importância, quase nenhum sistema de busca na vida real pode funcionar sem eles. Ao mesmo tempo, algumas complexidades e sutilezas decorrentes de seu uso são ocasionalmente subestimadas até por usuários avançados. Os filtros de sinônimos fazem parte do processo de análise que converte o texto de entrada em termos buscáveis e, embora seja relativamente fácil começar a trabalhar com eles, seu uso pode ser bastante variado e exigir alguma compreensão mais profunda dos conceitos para que possam ser aplicados com sucesso em um cenário do mundo real.

Recentemente, foram feitas algumas melhorias na análise no Elasticsearch. A mais notável é provavelmente a funcionalidade que permite recarregar os analisadores no momento da busca, o que, por sua vez, possibilita que os sinônimos no momento da busca sejam alterados e recarregados. Além de apresentar essa nova API, este blog responderá a algumas dúvidas comuns sobre o uso de sinônimos e apontará algumas ressalvas frequentes em seu uso.

Por que usar sinônimos?

Para entender a utilidade e a flexibilidade dos sinônimos, vamos ver rapidamente como a maioria dos mecanismos de busca atuais funciona internamente. Os documentos e as consultas são analisados e reduzidos às suas menores unidades, geralmente chamadas de tokens, que são essencialmente símbolos abstratos. O processo de correspondência na busca utiliza uma semelhança simples de strings, razão pela qual até mesmo pequenos erros de ortografia (“hous”) ou o uso do plural de uma palavra (“houses”) em uma consulta não gerará uma correspondência com um documento que contiver apenas o singular (“house”). Coisas como stemmers ou consultas difusas resolvem alguns dos problemas mais comuns, mas não preenchem a lacuna entre relacionar conceitos e ideias ou entre o uso de vocabulário ligeiramente diferente nos documentos e consultas.

É aí que os sinônimos brilham. As origens gregas da palavra são o prefixo σύν (syn, “juntos”) e ὄνομα (ónoma, “nome”). A origem do termo já mostra que os sinônimos descrevem palavras diferentes com exatamente ou quase o mesmo significado no mesmo idioma ou domínio. Na prática, isso pode incluir sinônimos gerais (“cansado” x “sonolento”), abreviações (“lb” x “libra”), diferentes variações ortográficas de produtos na busca de comércio eletrônico (“iPod” x “i-Pod”), pequenas diferenças de idioma (como “lift ” do inglês britânico x “elevator” do inglês americano), linguagem especialista x leiga (“canino” x “cachorro”) ou simplesmente denotar o mesmo conceito de duas maneiras (“universo” ou “cosmos”). Ao fornecer regras de sinônimos apropriadas, o engenheiro de busca pode fornecer informações sobre quais palavras em seu domínio significam coisas semelhantes e, portanto, devem ser tratadas de maneira semelhante.

Para um mecanismo de busca, é importante saber quais termos nos documentos e consultas devem corresponder, mesmo que pareçam diferentes. Como isso é altamente específico do domínio, os usuários precisam fornecer as regras apropriadas. Os filtros de sinônimos, que podem ser usados em analisadores personalizados, substituem ou adicionam tokens com base nas regras definidas pelo usuário, seja no momento da indexação para armazenar, por exemplo, ambas as variações de uma palavra em um documento indexado ou no momento da consulta para expandir os termos da consulta e gerar correspondências com documentos mais relevantes. Discutiremos algumas vantagens e desvantagens dessas duas abordagens mais adiante.

Quando ficar atento ao uso de sinônimos

Os filtros de sinônimos são uma ferramenta muito flexível, que leva as pessoas a usá-los de maneira indiscriminada em determinadas situações. Por exemplo, às vezes eles são usados como uma substituto de força bruta para os stemmers, com grandes arquivos de sinônimos contendo variações gramaticais de verbos e substantivos. Embora essa abordagem seja possível, o desempenho geralmente é pior e a manutenção é mais difícil do que quando se usam stemmers ou lematizadores reais. O mesmo vale para corrigir erros de ortografia. Se há apenas alguns erros de ortografia muito comuns, como em uma configuração de comércio eletrônico, às vezes é recomendável tentar corrigi-los usando sinônimos. Mas se o problema é mais geral, o uso de consultas difusas ou técnicas de n-grama de caracteres são abordagens mais sustentáveis. Considere também as alternativas para expansão de sinônimos na cadeia de análise. Às vezes, aprimorar documentos em um pipeline de ingestão ou em algum outro processo no lado do cliente é mais flexível e gerenciável do que usar sinônimos no processo de análise mais restrito. Por exemplo, você pode detectar entidades nomeadas em seus documentos usando frameworks comuns de reconhecimento de entidade nomeada (NER) e codificá-las em identificadores exclusivos no seu pipeline de pré-processamento ou no momento da ingestão. Se aplicar o mesmo processo às consultas do usuário antes de enviá-las ao Elasticsearch, você obterá o mesmo efeito, mas, em geral, ganhará mais controle.

Além disso, é tentador usar sinônimos para outras noções de “semelhança”, como agrupar certas espécies de animais sob um termo comum ou até criar suporte para taxonomia para o seu domínio. É aqui que as coisas ficam realmente interessantes e há muito a explorar, mas lembre-se de que os sinônimos nem sempre são a melhor opção e poderão fazer com que o seu sistema se comporte de maneiras inesperadas se não forem usados com cuidado.

Sinônimos no momento da indexação x momento da busca

Os sinônimos são utilizados em analisadores que podem ser usados no momento da indexação ou no momento da busca. Uma das dúvidas mais frequentes sobre o uso de filtros de sinônimos no Elasticsearch é: “devo usá-los no momento da indexação, no momento da busca ou em ambos?” Vamos dar uma olhada primeiro na aplicação da filtragem de sinônimos no momento da indexação. Isso significa que os termos nos documentos indexados são substituídos ou expandidos de uma vez por todas, e o resultado persiste no índice de busca.

Os sinônimos no momento da indexação têm várias desvantagens:

  • O índice pode ficar maior, porque todos os sinônimos devem ser indexados.
  • A pontuação da busca, que depende das estatísticas dos termos, pode sofrer porque os sinônimos também são contados, e as estatísticas de palavras menos comuns ficam distorcidas.
  • As regras de sinônimos não podem ser alteradas para os documentos existentes sem reindexá-los.

Particularmente, as duas últimas são grandes desvantagens. A única vantagem potencial dos sinônimos no momento da indexação é o desempenho, pois você paga antecipadamente o custo do processo de expansão e não precisa fazer isso toda vez novamente no momento da consulta, resultando potencialmente em mais termos que precisam ser correspondidos. No entanto, isso geralmente não é um problema real na prática.

O uso de sinônimos nos analisadores no momento da busca, por outro lado, não apresenta muitos dos problemas mencionados acima:

  • O tamanho do índice não é afetado.
  • As estatísticas dos termos no corpus permanecem iguais.
  • Alterações nas regras de sinônimos não exigem reindexação dos documentos.

Essas vantagens geralmente superam a única desvantagem de ter de executar a expansão de sinônimos toda vez no momento da consulta e potencialmente ter mais termos a serem correspondidos. Além disso, a expansão de sinônimos no momento da busca possibilita o uso do filtro de token synonym_graph mais sofisticado, que pode manipular sinônimos com várias palavras corretamente e foi projetado para ser usado como parte apenas de um analisador de busca.

Em geral, as vantagens de usar sinônimos no momento da busca geralmente superam qualquer leve ganho de desempenho que você possa obter ao usá-los no momento da indexação.

No entanto, costumava haver outra ressalva ao usar sinônimos no momento da busca. Embora a alteração das regras de sinônimos não exigisse a reindexação dos documentos, para alterá-los, você precisava fechar e reabrir o índice temporariamente. Isso era necessário porque os analisadores são instanciados no momento da criação do índice, quando um nó é reiniciado ou quando um índice fechado é reaberto. Para tornar as alterações em um arquivo de regras de sinônimos visíveis para o índice, primeiro era necessário atualizar o arquivo em todos os nós, depois fechar e reabrir o índice. Mas esse não é mais o caso.

Sinônimos recarregados

Desde o Elasticsearch 7.3, essa reabertura de índices para ver alterações nos arquivos de sinônimos não é mais necessária. Nós adicionamos um novo endpoint que torna possível disparar o recarregamento dos recursos do analisador sob demanda. A chamada desse novo endpoint recarregará todos os analisadores de um índice que tiverem componentes marcados como atualizáveis. Isso, por sua vez, torna esses componentes apenas utilizáveis no momento da busca.

Para os filtros de sinônimos, quando você os marca como atualizáveis e chama a API de recarregamento, são feitas alterações no arquivo de configuração de sinônimos em cada nó visível para o processo de análise. A atualização das regras de sinônimos que fazem parte da definição do filtro (por meio do parâmetro synonyms) não é possível, mas essas regras devem ser usadas mais para fins de testes ad hoc. De qualquer forma, configurar sinônimos usando um arquivo de configuração tem várias vantagens:

  • Eles são mais fáceis de gerenciar! Em um sistema de produção, pode haver muitas regras de sinônimos e, como elas afetam muito a relevância da busca, devem ser tratadas como parte integrante da configuração que precisa ter controle de versão e ser testada com qualquer atualização.
  • Os sinônimos geralmente são derivados de outras fontes ou criados por um algoritmo em execução nos seus dados. A leitura a partir dos arquivos ignora a necessidade de colocá-los na configuração do filtro.
  • O mesmo arquivo de sinônimos pode ser usado em filtros diferentes.
  • Conjuntos de regras de sinônimos maiores consomem muita memória no estado do cluster do Elasticsearch que armazena metainformações sobre configurações do índice. Para não aumentar o tamanho do cluster desnecessariamente, é recomendável armazenar conjuntos de regras de sinônimos maiores em arquivos de configuração.

Para fins de demonstração, vamos supor que você coloque um arquivo my_synonyms.txt inicial contendo a regra única a seguir no diretório config dos seus nós do Elasticsearch. Digamos que o arquivo inicialmente contenha apenas a seguinte regra:

universe, cosmos

Em seguida, precisamos definir um analisador que referencie esse arquivo em um filtro de sinônimos:

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

Observe que marcamos o filtro de sinônimos como updateable. Isso é importante porque somente os filtros atualizáveis são recarregados quando chamamos o novo endpoint de recarregamento, mas isso também tem como efeito colateral o fato de os analisadores contendo filtros atualizáveis não poderem mais ser usados no momento da indexação. Mas vamos verificar primeiro se os sinônimos são aplicados corretamente executando um teste rápido por meio do endpoint _analyze:

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

Isso deve retornar dois tokens, sendo um também “universe”, conforme o esperado. Vamos adicionar outra regra ao arquivo synonyms.txt adicionando uma segunda linha:

lift, elevator

Esse é o ponto no qual, anteriormente, você precisava fechar e reabrir o índice para que essas alterações aparecessem. Agora você pode simplesmente chamar o novo endpoint:

POST /synonym_test/_reload_search_analyzers

A solicitação não requer um corpo, mas pode ser restrita a um ou mais índices que usem os padrões típicos de caracteres curinga do índice. A resposta inclui informações sobre quais analisadores foram recarregados e quais nós foram afetados:

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

A execução da solicitação _analyze acima no termo “lift” agora também retorna “elevator” como um segundo token de sinônimo.

No entanto, há algumas coisas a serem observadas. Como mencionamos anteriormente, um filtro marcado como updateable deve ser usado no momento da busca; portanto, a maneira correta de usar o analisador de sinônimos que definimos acima em um campo seria:

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

Além disso, o recarregamento funciona apenas para sinônimos carregados de arquivos — não há compatibilidade para a alteração dos sinônimos definidos por meio das configurações em um filtro. Por fim, na prática, você deve aplicar atualizações dos arquivos de sinônimos em todos os nós do seu cluster. Se o analisador em alguns nós vir versões diferentes do arquivo, você poderá obter resultados de busca diferentes, dependendo de qual nó for usado em uma busca. Se isso acontecer em relação a um sinônimo, a primeira coisa a verificar é se os seus arquivos de sinônimos são os mesmos em cada nó; em seguida, volte a disparar o recarregamento.

Em resumo, o novo endpoint _reload_search_analyzer permite revisar e alterar rapidamente sinônimos no momento da consulta sem a necessidade de reabrir seus índices. Por exemplo, ao examinar seus logs de consulta, você pode determinar se os usuários buscam termos diferentes dos existentes nos documentos indexados e aplicar essas adições sem interromper as operações. Porém, a adição de sinônimos pode ter efeitos colaterais inesperados na pontuação de relevância; assim, é aconselhável executar algum tipo de teste (seja um teste A/B ou algo como a API de avaliação de classificação) antes de aplicar diretamente as alterações na produção.

Como é fazer parte dessa cadeia (de análise)

Outra pergunta frequente sobre os filtros de sinônimos é o comportamento deles em cadeias de análise mais complexas. Na maioria dos cenários, você coloca alguns filtros de caracteres ou de tokens comuns na frente do seu filtro de sinônimos, como um filtro lowercase. Isso significa que todos os tokens que passarem na cadeia de análise ficarão em minúsculas antes da aplicação do filtro de sinônimos. Isso significa que os sinônimos de entrada nas suas regras de sinônimos também precisam estar em minúsculas para gerar uma correspondência? Vamos tentar este exemplo simples:

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

Você pode verificar que o texto de entrada em minúsculas é expandido para três tokens no exemplo acima, o que mostra que as letras minúsculas também são aplicadas às regras do filtro de sinônimos. Além disso, o lado direito das regras de substituição, como a regra “Cosmos => Universe” acima, é reescrito, como você pode ver pela saída em minúsculas de:

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

Em geral, os filtros de sinônimos reescrevem suas entradas para o tokenizador e os filtros usados na cadeia de análise anterior. No entanto, existem algumas exceções notáveis para isso: Vários filtros que geram tokens empilhados (como common_grams ou o filtro phonetic) não têm permissão para preceder os filtros de sinônimos e gerarão erros se você tentar fazer isso. Outros, como os filtros compostos de palavras ou os próprios filtros de sinônimos, são ignorados quando precedem outro filtro de sinônimos na cadeia. A última regra é importante para possibilitar o encadeamento de filtros de sinônimos. Veremos isso em ação no exemplo a seguir.

Então, o que acontece se você coloca dois ou mais filtros de sinônimos em uma linha? A saída do primeiro será a entrada do segundo, tornando o encadeamento de filtros de sinônimos uma operação transitiva? Vamos tentar o seguinte exemplo:

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

O token de saída seria “c”, que mostra que ambos os filtros são aplicados em ordem consecutiva, com o primeiro filtro substituindo “a” por “b” e o segundo substituindo essa entrada por “c”. Se você tentar “d” como entrada, ele será substituído por “e” (a primeira regra não será aplicada), mas se você usar “e”, o token será substituído por “f” no primeiro filtro, deixando o segundo filtro sem nenhuma correspondência.

Lembra-se de que acabamos de falar sobre as exceções à reescrita nos filtros de token precedentes? Se o filtro second_synonyms no exemplo acima tivesse aplicado as regras do primeiro filtro a seu conjunto de regras, ele teria alterado sua própria regra d => e para d => f (porque a regra e => f do filtro precedente teria sido aplicada). Esse comportamento costumava ser uma fonte de confusão nas versões anteriores do Elasticsearch e é o motivo pelo qual os filtros de sinônimos agora são ignorados ao processar as regras de sinônimos do filtro seguinte. Isso funcionará conforme descrito na versão 6.6 e posterior.

De volta para o futuro

Neste pequeno blog, apenas falamos superficialmente sobre o que você pode realizar usando sinônimos e tentamos responder a algumas dúvidas frequentes sobre o uso deles. Os sinônimos são uma ferramenta poderosa que pode ser utilizada para aumentar a lembrança do seu sistema de busca, mas existem muitas sutilezas importantes para conhecer e experimentar, especialmente em conjunto com o teste de relevância sistemático.

A nova API para recarregar os analisadores no momento da busca que foi adicionada no Elasticsearch 7.3 facilita esse tipo de experimentação ao não exigir que você feche e reabra o índice como no passado. Além disso, ela também fornece maneiras de atualizar as regras de sinônimos que são aplicadas no momento da busca, sem precisar colocar seus índices offline. No entanto, essa é apenas uma etapa de uma série de melhorias que queremos introduzir para tornar o gerenciamento de sinônimos em um grande cluster mais amigável para os usuários. Expresse sua opinião e deixe o seu feedback ou algumas perguntas em nosso fórum de discussão. Até lá, boas análises para você!