Blog

Busca lexical e semântica com Elasticsearch

Neste blog, exploraremos várias abordagens para recuperar informações usando o Elasticsearch, com foco em busca lexical e semântica.

A busca é o processo de localizar as informações mais relevantes com base na sua consulta de pesquisa ou em consultas combinadas, e os resultados de pesquisa relevantes são os documentos que melhor correspondem a essas consultas. Embora existam diversos desafios e métodos associados à busca, o objetivo final permanece o mesmo: encontrar a melhor resposta possível para sua pergunta.

Considerando esse objetivo, neste post do blog, exploraremos diferentes abordagens para recuperar informações usando o Elasticsearch, com foco específico em busca de texto: busca lexical e busca semântica.

Pré-requisitos

Para isso, forneceremos exemplos em Python que demonstram vários cenários de pesquisa em um conjunto de dados gerado para simular informações de produtos de comércio eletrônico.

Este conjunto de dados contém mais de 2.500 produtos, cada um com uma descrição. Esses produtos estão categorizados em 76 categorias distintas, cada uma contendo um número variável de produtos, conforme mostrado abaixo:

Visualização em mapa de árvore - os 22 principais valores de category.keyword (categorias de produtos)

Para a configuração, você precisará de:

  • Python 3.6 ou posterior

  • O cliente Elastic Python

  • Implantação do Elasticsearch 8.8 ou posterior, com nó de aprendizado de máquina de 8 GB de memória.

  • O modelo Elastic Learned Sparse EncodeR que vem pré-carregado no Elastic instalado e em execução na sua implementação.

Usaremos o Elastic Cloud, e um período de teste gratuito está disponível.

Além das consultas de pesquisa fornecidas nesta postagem do blog, um notebook Python irá guiá-lo pelos seguintes processos:

  • Estabeleça uma conexão com nossa implantação Elastic usando o cliente Python.

  • Carregar um modelo de incorporação de texto no cluster Elasticsearch

  • Crie um índice com mapeamentos para indexar vetores de características e vetores densos.

  • Crie um pipeline de ingestão com processadores de inferência para incorporação e expansão de texto.

Busca lexical - recuperação esparsa

A forma clássica como os documentos são classificados por relevância pelo Elasticsearch com base em uma consulta de texto utiliza a implementação Lucene do modelo BM25 , um modelo esparso para busca lexical. Este método segue a abordagem tradicional para busca de texto, procurando por correspondências exatas dos termos.

Para tornar essa busca possível, o Elasticsearch converte os dados dos campos de texto em um formato pesquisável, realizando uma análise de texto.

A análise de texto é realizada por um analisador, um conjunto de regras que regem o processo de extração de tokens relevantes para a pesquisa. Um analisador deve ter exatamente um tokenizador. O analisador léxico recebe um fluxo de caracteres e o divide em tokens individuais (geralmente palavras individuais), como no exemplo abaixo:

Tokenização de strings para busca lexical

#Performs text analysis on a string and returns the resulting tokens.

# Define the text to be analyzed
text = "Comfortable furniture for a large balcony"

# Define the analyze request
request_body = {
  "analyzer": "standard",
  "text": text
}

# Perform the analyze request
response = client.indices.analyze(analyzer=request_body["analyzer"], text=request_body["text"])

# Extract and display the analyzed tokens
tokens = [token["token"] for token in response["tokens"]]
print("Analyzed Tokens:", tokens)

Saída

Analyzed Tokens: ['comfortable', 'furniture', 'for', 'a', 'large', 'balcony']

Neste exemplo, estamos usando o analisador padrão , que funciona bem para a maioria dos casos de uso, pois fornece tokenização baseada na gramática inglesa. A tokenização permite a correspondência de termos individuais, mas cada token ainda é correspondido literalmente.

Se você deseja personalizar sua experiência de busca, pode escolher um analisador integrado diferente. Por exemplo, ao atualizar o código para usar o analisador de palavras irrelevantes, o texto será dividido em tokens em qualquer caractere que não seja letra, com suporte para a remoção de palavras irrelevantes.

...
# Define the analyze request
request_body = {
  "analyzer": "stop",
  "text": text
}
...

Saída

Analyzed Tokens: ['comfortable', 'furniture', 'large', 'balcony']

Quando os analisadores integrados não atendem às suas necessidades, você pode criar um analisador personalizado, que utiliza a combinação apropriada de zero ou mais filtros de caracteres, um analisador léxico e zero ou mais filtros de tokens.

"analyzer":  {

  "my_analyzer": {

    "type": "custom", #For custom analyzers, use a type of custom or omit the type parameter.

    "tokenizer": "standard", #Built-in or customized tokenizer

    "filter": ["lowercase", "synonym"] #Built-in or customized token filters
  }
}

No exemplo acima, que combina um analisador léxico e filtros de tokens, o texto será convertido para minúsculas pelo filtro de minúsculas antes de ser processado pelo filtro de tokens de sinônimos.

Correspondência lexical

O BM25 medirá a relevância dos documentos para uma determinada consulta de pesquisa com base na frequência dos termos e na sua importância.

O código abaixo realiza uma consulta de correspondência , buscando até dois documentos considerando os valores do campo "description" do índice "ecommerce-search" e a consulta de pesquisa "Móveis confortáveis para uma varanda grande".

Refinar os critérios para que um documento seja considerado uma correspondência para esta consulta pode melhorar a precisão. No entanto, resultados mais específicos têm como contrapartida uma menor tolerância a variações.

# BM25

response = client.search(size=2,
index="ecommerce-search",
query= {
  "match": {
    "description" : {  
      "query": "Comfortable furniture for a large balcony",
      "analyzer": "stop"
    }
  }
}
)

hits = response['hits']['hits']

if not hits:
  print("No matches found")

else:
  for hit in hits:
    score = hit['_score']
    product = hit['_source']['product']
    category = hit['_source']['category']
    description = hit['_source']['description']
    print(f"\nScore: {score}\nProduct: {product}\nCategory: {category}\nDescription: {description}\n")

Saída

Score: 15.607948
Product: Barbie Dreamhouse
Category: Toys
Description: is a classic Barbie playset with multiple rooms, furniture, a large balcony, a pool, and accessories. It allows kids to create their dream Barbie world.

Score: 9.137739
Product: Comfortable Rocking Chair
Category: Indoor Furniture
Description: enjoy relaxing moments with this comfortable rocking chair. Its smooth motion and cushioned seat make it an ideal piece of furniture for unwinding.

Ao analisar os resultados, o mais relevante é o produto "Casa dos Sonhos da Barbie", na categoria "Brinquedos", cuja descrição é altamente relevante por incluir os termos "móveis", "grande" e "varanda". Este é o único produto com três termos na descrição que correspondem à consulta de pesquisa, sendo também o único com o termo "varanda" na descrição.

O segundo produto mais relevante é uma "Cadeira de Balanço Confortável", categorizada como "Móveis para Interiores", cuja descrição inclui os termos "confortável" e "móveis". Apenas 3 produtos no conjunto de dados correspondem a pelo menos 2 termos desta consulta de pesquisa, e este produto é um deles.

"Confortável" aparece na descrição de 105 produtos e "móveis" na descrição de 4 produtos com 4 categorias diferentes: Brinquedos, Móveis para Interior, Móveis para Exterior e 'Suprimentos e Brinquedos para Cães e Gatos'.

Como você pôde ver, o produto mais relevante considerando a consulta é um brinquedo e o segundo produto mais relevante são móveis para interiores. Se você deseja informações detalhadas sobre o cálculo da pontuação para saber por que esses documentos correspondem, pode definir o parâmetro explain __query como verdadeiro.

Apesar de ambos os resultados serem os mais relevantes, considerando tanto o número de documentos quanto a ocorrência de termos neste conjunto de dados, a intenção por trás da consulta "Móveis confortáveis para uma varanda grande" é buscar móveis para uma varanda grande de fato, excluindo, entre outros, brinquedos e móveis de interior.

A busca lexical é relativamente simples e rápida, mas tem limitações, já que nem sempre é possível conhecer todos os termos e sinônimos possíveis sem necessariamente conhecer a intenção e as consultas do usuário. Um fenômeno comum no uso da linguagem natural é a incompatibilidade de vocabulário. Pesquisas mostram que, em média, 80% das vezes, pessoas diferentes (especialistas na mesma área) nomeiam a mesma coisa de maneiras diferentes.

Essas limitações nos motivam a buscar outros modelos de pontuação que incorporem conhecimento semântico. Os modelos baseados em Transformers, que se destacam no processamento de tokens de entrada sequenciais, como a linguagem natural, capturam o significado subjacente da sua pesquisa considerando representações matemáticas tanto dos documentos quanto das consultas. Isso permite uma representação vetorial densa e contextualizada do texto, impulsionando a Busca Semântica, uma maneira refinada de encontrar conteúdo relevante.

Busca semântica - recuperação densa

Nesse contexto, após converter seus dados em valores vetoriais significativos, o algoritmo de busca k-vizinhos mais próximos (kNN) é utilizado para encontrar representações vetoriais em um conjunto de dados que sejam mais semelhantes a um vetor de consulta. O Elasticsearch suporta dois métodos para busca kNN: kNN exato por força bruta e kNN aproximado, também conhecido como ANN.

O kNN de força bruta garante resultados precisos, mas não escala bem com grandes conjuntos de dados. O algoritmo kNN aproximado encontra de forma eficiente os vizinhos mais próximos aproximados, sacrificando um pouco de precisão em troca de um melhor desempenho.

Com o suporte do Lucene para busca kNN e índices vetoriais densos, o Elasticsearch aproveita o algoritmo Hierarchical Navigable Small World (HNSW), que demonstra um forte desempenho de busca em diversos conjuntos de dados de benchmark de redes neurais. Uma busca aproximada por kNN pode ser realizada em Python usando o código de exemplo abaixo.

Busca semântica com kNN aproximado

# KNN - approximate kNN

response = client.search(index='ecommerce-search', size=2,
knn={
  "field": "description_vector.predicted_value",
  "k": 50, # Number of nearest neighbors to return as top hits.
#The optimal value of k is dependent on the data. It can vary in different scenarios.

  "num_candidates": 500, # Number of nearest neighbor candidates to consider per shard.

#Increasing num_candidates tends to improve the accuracy of the final k results.

  "query_vector_builder": { # Object indicating how to build a query_vector. kNN search enables you to perform semantic search by using a previously deployed text embedding model, the steps for this process are demonstrated in the Python notebook.
    "text_embedding": { 
      "model_id": "sentence-transformers__all-mpnet-base-v2", # Text embedding model id
      "model_text": "Comfortable furniture for a large balcony" # Query
    }
  }
}
)

for hit in response['hits']['hits']:
        
  score = hit['_score']
  product = hit['_source']['product']
  category = hit['_source']['category']
  description = hit['_source']['description']
  print(f"\nScore: {score}\nProduct: {product}\nCategory: {category}\nDescription: {description}\n")

Este bloco de código usa o kNN do Elasticsearch para retornar até dois produtos com uma descrição semelhante à consulta vetorizada (query_vector_build) de "Móveis confortáveis para uma varanda grande", considerando os embeddings do campo "descrição " no conjunto de dados de produtos.

Os embeddings dos produtos foram gerados previamente em um pipeline de ingestão com um processador de inferência contendo o modelo de embedding de texto "all-mpnet-base-v2" para inferir com base nos dados que estavam sendo ingeridos no pipeline.

Este modelo foi escolhido com base na avaliação de modelos pré-treinados usando "sentence_transformers.evaluation". onde diferentes classes são usadas para avaliar um modelo durante o treinamento. O modelo "all-mpnet-base-v2" demonstrou o melhor desempenho médio de acordo com o ranking do Sentence-Transformers e também garantiu uma posição favorável no ranking do Massive Text Embedding Benchmark (MTEB) . O modelo, pré-treinado a partir do modelo microsoft/mpnet-base e ajustado em um conjunto de dados de 1 bilhão de pares de frases, mapeia as frases para um espaço vetorial denso de 768 dimensões.

Alternativamente, existem muitos outros modelos disponíveis que podem ser utilizados, especialmente aqueles ajustados especificamente para os dados do seu domínio.

Saída

Score: 0.79207325
Product: Patio Sofa Set with Ottoman
Category: Outdoor Furniture
Description: is a versatile and comfortable patio sofa set, including a sofa, ottoman, and coffee table, great for outdoor lounging.

Score: 0.7836937
Product: Patio Sofa Set with Canopy
Category: Outdoor Furniture
Description: is a luxurious and comfortable patio sofa set with a canopy, providing shade and style for outdoor lounging.

O resultado pode variar dependendo do modelo escolhido, dos filtros e do ajuste aproximado do kNN.

Os resultados da pesquisa kNN estão ambos na categoria "Móveis para Exterior", embora a palavra "exterior" não tenha sido mencionada explicitamente na consulta, o que destaca a importância da compreensão semântica no contexto.

A busca vetorial densa oferece diversas vantagens:

  • Habilitar a pesquisa semântica

  • Escalabilidade para lidar com conjuntos de dados muito grandes

  • Flexibilidade para lidar com uma ampla variedade de tipos de dados.

No entanto, a busca vetorial densa também apresenta seus próprios desafios:

  • Selecionando o modelo de incorporação correto para o seu caso de uso.

  • Uma vez escolhido o modelo, pode ser necessário ajustá-lo para otimizar o desempenho em um conjunto de dados específico do domínio, um processo que exige o envolvimento de especialistas na área.

  • Além disso, a indexação de vetores de alta dimensão pode ser computacionalmente dispendiosa.

Busca semântica - recuperação esparsa aprendida

Vamos explorar uma abordagem alternativa: recuperação esparsa aprendida, outra maneira de realizar buscas semânticas.

Como um modelo esparso, ele utiliza o índice invertido baseado em Lucene do Elasticsearch, que se beneficia de décadas de otimizações. No entanto, essa abordagem vai além da simples adição de sinônimos com funções de pontuação lexical como o BM25. Em vez disso, incorpora associações aprendidas usando um conhecimento mais profundo em escala linguística para otimizar a relevância.

Ao expandir as consultas de pesquisa para incluir termos relevantes que não estão presentes na consulta original, o Elastic Learned Sparse Encoder aprimora os embeddings de vetores esparsos, como você pode ver no exemplo abaixo.

Busca vetorial esparsa com codificador esparso de aprendizado elástico

# Elastic Learned Sparse Encoder

response = client.search(index='ecommerce-search', size=2,
query={
  "text_expansion": {
    "ml.tokens": {
      "model_id":"elser_model",
      "model_text":"Comfortable furniture for a large balcony"                
    }
  }
}
)

for hit in response['hits']['hits']:

  score = hit['_score']
  product = hit['_source']['product']
  category = hit['_source']['category']
  description = hit['_source']['description']
  print(f"\nScore: {score}\nProduct: {product}\nCategory: {category}\nDescription: {description}\n")

Saída

Score: 14.405318
Product: Garden Lounge Set with Side Table
Category: Garden Furniture
Description: is a comfortable and stylish garden lounge set, including a sofa, chairs, and a side table for outdoor relaxation.

Score: 14.281318
Product: Rattan Patio Conversation Set
Category: Outdoor Furniture
Description: is a stylish and comfortable outdoor furniture set, including a sofa, two chairs, and a coffee table, all made of durable rattan material.

Os resultados, neste caso, incluem a categoria "Móveis de Jardim", que oferece produtos bastante semelhantes aos de "Móveis para Exterior".

Ao analisar "ml.tokens", Ao analisar o campo "rank_features" que contém os tokens gerados pela Recuperação Esparsa Aprendida, torna-se evidente que, entre os vários tokens gerados, existem termos que, embora não façam parte da consulta de pesquisa, ainda são relevantes em significado, como "relaxar" (confortável), "sofá" (móveis) e "exterior" (varanda).

A imagem abaixo destaca alguns desses termos juntamente com a consulta, tanto com quanto sem expansão de termos.

Conforme observado, este modelo proporciona uma busca sensível ao contexto e ajuda a mitigar o problema de incompatibilidade de vocabulário, ao mesmo tempo que oferece resultados mais interpretáveis. Ele pode até superar modelos vetoriais densos quando nenhum re-treinamento específico do domínio é aplicado.

Busca híbrida: resultados relevantes combinando busca lexical e semântica.

Quando se trata de pesquisa, não existe uma solução universal. Cada um desses métodos de recuperação tem seus pontos fortes, mas também seus desafios. Dependendo do caso de uso, a melhor opção pode mudar. Muitas vezes, os melhores resultados obtidos com diferentes métodos de recuperação de informação podem ser complementares. Portanto, para melhorar a relevância, vamos analisar a possibilidade de combinar os pontos fortes de cada método.

Existem várias maneiras de implementar uma busca híbrida, incluindo combinação linear, atribuição de um peso a cada pontuação e fusão de classificação recíproca (RRF), onde especificar um peso não é necessário.

Elasticsearch: o melhor dos dois mundos com busca lexical e semântica

# BM25 + Elastic Learned Sparse Encoder (Linear Combination)

response = client.search(index='ecommerce-search', size=2,

query= {
  "bool": {
    "should": [
    {
      "match": {
        "description" : {  
          "query": "A dining table and comfortable chairs for a large balcony",
          "boost": 1
        }
      }
    },                   
    {
      "text_expansion": {
        "ml.tokens": {
          "model_id": "elser_model",
          "model_text": "A dining table and comfortable chairs for a large balcony",
          "boost": 1
        }
      }
     }
    ]
  }
}
)

# The boost value is 1 for the text expansion and match query. This means that the relevance score of the results of these queries are not boosted. You can specify a boost value to give a weight to each score in the sum. The scores will be calculated as: score = boost value * match_score + boost value * text_expansion_score

for hit in response['hits']['hits']:

  score = hit['_score']
  product = hit['_source']['product']
  category = hit['_source']['category']
  description = hit['_source']['description']
  print(f"\nScore: {score}\nProduct: {product}\nCategory: {category}\nDescription: {description}\n")

Neste código, realizamos uma busca híbrida com duas consultas que tinham o valor "Uma mesa de jantar e cadeiras confortáveis para uma varanda grande". Em vez de usar "móveis" como termo de busca, estamos especificando o que procuramos, e ambas as buscas consideram os mesmos valores de campo, "descrição". A classificação é determinada por uma combinação linear com pesos iguais para as pontuações BM25 e ELSER.

Saída

Score: 31.628141
Product: Garden Dining Set with Swivel Rockers
Category: Garden Furniture
Description: is a functional and comfortable garden dining set, including a table and chairs with swivel rockers for easy movement.

Score: 31.334227
Product: Garden Dining Set with Swivel Chairs
Category: Garden Furniture
Description: is a functional and comfortable garden dining set, including a table and chairs with swivel seats for convenience.

No código abaixo, usaremos o mesmo valor para a consulta, mas combinaremos as pontuações de BM25 (parâmetro de consulta) e kNN (parâmetro knn) usando o método de fusão de classificação recíproca para combinar e classificar os documentos.

# BM25 + KNN (RRF)

response = client.search(index='ecommerce-search', size=2,
query={
  "bool": {
    "should": [
    {
      "match": {
        "description": {
        "query": "A dining table and comfortable chairs for a large balcony"
        }
      }
    }
    ]
  }
},
knn={
  "field": "description_vector.predicted_value",
  "k": 50,
  "num_candidates": 500,
  "query_vector_builder": {
    "text_embedding": {
      "model_id": "sentence-transformers__all-mpnet-base-v2",
      "model_text": "A dining table and comfortable chairs for a large balcony"
    }
  }
},
rank={
  "rrf": { # Reciprocal rank fusion
    "window_size": 50, # This value determines the size of the individual result sets per query.
    "rank_constant": 20 # This value determines how much influence documents in individual result sets per query have over the final ranked result set.
  }
}
)

for hit in response['hits']['hits']:
        
  rank = hit['_rank']
  category = hit['_source']['category']
  product = hit['_source']['product']
  description = hit['_source']['description']
  print(f"\nRank: {rank}\nProduct: {product}\nCategory: {category}\nDescription: {description}\n")

A funcionalidade RRF está em fase de pré-visualização técnica. A sintaxe provavelmente mudará antes do lançamento oficial.

Saída

Rank: 1
Product: Patio Dining Set with Bench
Category: Outdoor Furniture
Description: is a spacious and functional patio dining set, including a dining table, chairs, and a bench for additional seating.

Rank: 2
Product: Garden Dining Set with Swivel Chairs
Category: Garden Furniture
Description: is a functional and comfortable garden dining set, including a table and chairs with swivel seats for convenience.

Aqui também poderíamos usar campos e valores diferentes; alguns desses exemplos estão disponíveis no notebook Python.

Como você pode ver, com o Elasticsearch você tem o melhor dos dois mundos: a busca lexical tradicional e a busca vetorial, seja ela esparsa ou densa, para atingir seu objetivo e encontrar a melhor resposta possível para sua pergunta.

Se você deseja continuar aprendendo sobre as abordagens mencionadas aqui, estes blogs podem ser úteis:

O Elasticsearch fornece um banco de dados vetorial, juntamente com todas as ferramentas necessárias para criar pesquisas vetoriais:

Conclusão

Neste artigo, exploramos diversas abordagens para recuperar informações usando o Elasticsearch, com foco específico em busca textual, lexical e semântica. Para demonstrar isso, fornecemos exemplos em Python que mostram diferentes cenários de pesquisa usando um conjunto de dados contendo informações de produtos de comércio eletrônico.

Analisamos a busca lexical clássica com BM25 e discutimos seus benefícios e desafios, como a incompatibilidade de vocabulário. Enfatizamos a importância de incorporar conhecimento semântico para superar esse problema. Além disso, discutimos a busca vetorial densa, que possibilita a busca semântica, e abordamos os desafios associados a esse método de recuperação, incluindo o custo computacional na indexação de vetores de alta dimensão.

Por outro lado, mencionamos que vetores esparsos se comprimem excepcionalmente bem. Assim, discutimos o Learned Sparse Encoder da Elastic, que expande as consultas de pesquisa para incluir termos relevantes não presentes na consulta original.

Não existe uma solução única que sirva para todos quando se trata de pesquisa. Cada método de recuperação tem seus pontos fortes e desafios. Portanto, também discutimos o conceito de busca híbrida.

Como você pôde ver, com o Elasticsearch, você pode ter o melhor dos dois mundos: busca lexical tradicional e busca vetorial!

Pronto para começar? Confira o notebook Python disponível e inicie um teste gratuito do Elastic Cloud.

Conteúdo relacionado

LINQ para Elasticsearch ES|QL: escreva Consultas em C# e Consulte o Elasticsearch

Florian Bernd

Aprimorando as capacidades de chatbots com PNL e busca vetorial no Elasticsearch.

Priscilla Parodi

Elasticsearch x OpenSearch: comparação de desempenho da busca vetorial

Ugo Sangiorgi

Detecção de plágio por IA: usando o Elasticsearch

Priscilla Parodi

Técnicas avançadas de RAG, parte 2: Consultas e testes

Han Xiang Choong

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)