Armazenar incorporações no Elasticsearch

O Elasticsearch oferece suporte completo para armazenar e recuperar vetores, o que o torna um banco de dados ideal para trabalhar com embeddings.

Tipos de Campo

No capítulo sobre Pesquisa de Texto Completo deste tutorial, você aprendeu como criar um índice com vários campos. Naquela ocasião, foi mencionado que o Elasticsearch consegue, na maioria dos casos, determinar automaticamente o melhor tipo a ser usado para cada campo com base nos próprios dados. Embora o Elasticsearch 8.11 seja capaz de mapear automaticamente alguns tipos de vetores, neste capítulo você definirá esse tipo explicitamente como uma oportunidade para aprender mais sobre mapeamentos de tipos no Elasticsearch.

Recuperando mapeamentos de tipo

Os tipos associados a cada campo em um índice são determinados em um processo chamado mapeamento, que pode ser dinâmico ou explícito. Os mapeamentos criados na seção de Busca de Texto Completo deste tutorial foram todos gerados dinamicamente pelo Elasticsearch.

O cliente Elasticsearch oferece um método get_mapping , que retorna os mapeamentos de tipo que estão em vigor para um determinado índice. Se você quiser explorar esses mapeamentos por conta própria, inicie um shell do Python e digite o seguinte código:

A resposta do método get_mapping() é um dicionário com informações sobre cada campo no índice. Para sua conveniência, segue abaixo uma estrutura bem formatada dessas informações para o índice my_documents criado na seção de Busca de Texto Completo do tutorial:

A partir disso, você pode ver que os campos created_on e updated_at foram digitados automaticamente com date, enquanto todos os outros campos receberam text como tipo. Ao tentar decidir sobre um tipo, o Elasticsearch primeiro verifica o tipo dos dados, o que o ajuda a atribuir tipos numéricos, booleanos e de objeto aos campos. Quando os dados do campo são uma string, o sistema também tenta verificar se os dados correspondem a um padrão de data. A detecção de padrões em sequências de caracteres também pode ser ativada para números, se desejado.

Os campos de texto têm uma definição fields com uma entrada keyword . Isso é chamado de subcampo, um tipo alternativo ou secundário que está disponível para uso quando apropriado. No Elasticsearch, campos text tipados dinamicamente recebem um subcampo keyword . Você já usou o subcampo category.keyword para realizar uma pesquisa exata de uma determinada categoria. Para evitar a adição do subcampo, pode-se fornecer um mapeamento explícito de text ou keyword , e então este será o tipo principal e único.

Adicionando um campo vetorial ao índice

Vamos adicionar um novo campo ao índice onde será armazenado o conteúdo incorporado de cada documento.

A estrutura de um mapeamento explícito corresponde à chave mappings da resposta retornada pelo método get_mapping() do cliente Elasticsearch. Somente os campos que precisam ser tipados explicitamente precisam ser fornecidos, pois quaisquer campos que não estejam incluídos no mapeamento continuarão a ser tipados dinamicamente como antes.

Abaixo você pode ver uma nova versão do método create_index() da classe Search , adicionando um campo explicitamente tipado chamado embedding. Substitua este método em search.py:

Como você pode ver, o campo embedding recebe o tipo dense_vector, que é o tipo apropriado ao armazenar embeddings. Mais tarde você aprenderá sobre outro tipo de vetor, o sparse_vector, que é útil em outros tipos de aplicações de busca semântica.

O tipo dense_vector aceita alguns parâmetros, todos opcionais.

  • dims: o tamanho dos vetores que serão armazenados. Desde a versão 8.11, as dimensões são atribuídas automaticamente quando o primeiro documento é inserido.
  • index: deve ser definido como True para indicar que os vetores devem ser indexados para pesquisa. Esta é a configuração padrão.
  • similarity: a função de distância a ser usada na comparação de vetores. Os dois mais comuns são dot_product e cosine. O produto escalar é mais eficiente, mas exige que os vetores sejam normalizados. O valor padrão é cosine.

Adicionando elementos incorporados a documentos

Na seção anterior, você aprendeu como gerar embeddings usando a estrutura SentenceTransformers e o modelo all-MiniLM-L6-v2 . Agora é hora de integrar o modelo ao aplicativo.

Primeiramente, o modelo pode ser instanciado no construtor da classe Search :

Como você deve se lembrar da parte de pesquisa de texto completo deste tutorial, a classe Search tem métodos insert_document() e insert_documents() , para inserir documentos únicos e múltiplos no índice, respectivamente. Agora, esses dois métodos precisam gerar os respectivos embeddings para cada documento.

O próximo bloco de código mostra novas versões desses dois métodos, juntamente com um novo método auxiliar get_embedding() que retorna um embedding.

Os métodos modificados adicionam o novo campo embedding ao documento a ser inserido. O embedding é gerado a partir do campo summary de cada documento. Em geral, os embeddings são gerados a partir de frases ou parágrafos curtos, portanto, neste caso, o resumo é um campo ideal para usar. Outras opções teriam sido o campo name , que contém o título do documento, ou talvez as primeiras frases do body do documento.

Com essas alterações implementadas, o índice pode ser reconstruído, de forma a armazenar uma representação vetorial para cada documento. Para reconstruir o índice, use este comando:

Caso você precise de um lembrete, o comando flask reindex está implementado na função reindex() em app.py. Ele chama o método reindex() da classe Search , que por sua vez invoca create_index() e então passa todos os dados do arquivo data.json para insert_documents().

Pronto para criar buscas de última geração?

Uma pesquisa suficientemente avançada não se consegue apenas com o esforço de uma só pessoa. O Elasticsearch é impulsionado por cientistas de dados, especialistas em operações de aprendizado de máquina, engenheiros e muitos outros que são tão apaixonados por buscas quanto você. Vamos nos conectar e trabalhar juntos para construir a experiência de busca mágica que lhe trará os resultados desejados.

Experimente você mesmo(a)