Blog

Usando busca híbrida para caça de gopher com Elasticsearch e Go

Aprenda como realizar buscas híbridas combinando buscas por palavra-chave e buscas vetoriais usando o Elasticsearch e o cliente Elasticsearch Go.

Nas partes anteriores desta série, foi demonstrado como usar o cliente Elasticsearch Go para pesquisa tradicional por palavras-chave e pesquisa vetorial. Esta terceira parte aborda a busca híbrida. Vamos compartilhar exemplos de como você pode combinar a pesquisa vetorial e a pesquisa por palavra-chave usando o Elasticsearch e o cliente Elasticsearch Go.

Pré-requisitos

Assim como na primeira parte desta série, os seguintes pré-requisitos são necessários para este exemplo:

  1. Instalação do Go versão 1.21 ou posterior

  2. Crie seu próprio repositório Go usando a estrutura recomendada e o gerenciamento de pacotes abordados na documentação do Go.

  3. Criando seu próprio cluster Elasticsearch, preenchido com um conjunto de páginas baseadas em roedores, incluindo para o nosso amigável Gopher, da Wikipédia:

Página do Gopher na Wikipédia

Conectando-se ao Elasticsearch

Lembrando que, em nossos exemplos, utilizaremos a API tipada oferecida pelo cliente Go. Para estabelecer uma conexão segura para qualquer consulta, é necessário configurar o cliente usando um dos seguintes métodos:

  1. ID da nuvem e chave de API, caso esteja utilizando o Elastic Cloud.

  2. URL do cluster, nome de usuário, senha e certificado

A conexão com nosso cluster localizado no Elastic Cloud seria feita da seguinte forma:

func GetElasticsearchClient() (*elasticsearch.TypedClient, error) {
	var cloudID = os.Getenv("ELASTIC_CLOUD_ID")
	var apiKey = os.Getenv("ELASTIC_API_KEY")

	var es, err = elasticsearch.NewTypedClient(elasticsearch.Config{
		CloudID: cloudID,
		APIKey:  apiKey,
		Logger:  &elastictransport.ColorLogger{os.Stdout, true, true},
	})

	if err != nil {
		return nil, fmt.Errorf("unable to connect: %w", err)
	}

	return es, nil
}

A conexão client pode então ser usada para pesquisa, como demonstrado nas seções subsequentes.

Impulsionamento manual para pesquisa híbrida

Ao combinar qualquer conjunto de algoritmos de busca, a abordagem tradicional tem sido configurar manualmente constantes para otimizar cada tipo de consulta. Especificamente, um fator é definido para cada consulta, e o conjunto de resultados combinados é comparado ao conjunto esperado para determinar a recuperação da consulta. Em seguida, repetimos o processo para vários conjuntos de fatores e escolhemos aquele que mais se aproxima do estado desejado.

Por exemplo, combinar uma consulta de pesquisa de texto simples, ampliada por um fator de 0.8 , com uma consulta KNN com um fator menor de 0.2 pode ser feito especificando o campo Boost em ambos os tipos de consulta, como mostrado no exemplo abaixo:

func HybridSearchWithBoost(client *elasticsearch.TypedClient, term string) ([]Rodent, error) {
	var k = 10
	var numCandidates = 10
	var knnBoost float32 = 0.2
	var queryBoost float32 = 0.8

	res, err := client.Search().
		Index("vector-search-rodents").
		Knn(types.KnnSearch{
			Field:         "text_embedding.predicted_value",
			Boost:         &knnBoost,
			K:             &k,
			NumCandidates: &numCandidates,
			QueryVectorBuilder: &types.QueryVectorBuilder{
				TextEmbedding: &types.TextEmbedding{
					ModelId:   "sentence-transformers__msmarco-minilm-l-12-v3",
					ModelText: term,
				},
			}}).
		Query(&types.Query{
			Match: map[string]types.MatchQuery{
				"title": {
					Query: term,
					Boost: &queryBoost,
				},
			},
		}).
		Do(context.Background())

	if err != nil {
		return nil, err
	}

	return getRodents(res.Hits.Hits)
}

O fator especificado na opção Boost para cada consulta é adicionado à pontuação do documento. Ao aumentar a pontuação da nossa consulta de correspondência por um fator maior do que a consulta knn, os resultados da consulta por palavra-chave recebem um peso maior.

O desafio da otimização manual, especialmente se você não for um especialista em buscas, é que ela exige ajustes para descobrir os fatores que levarão ao conjunto de resultados desejado. Trata-se simplesmente de experimentar valores aleatórios para ver o que o aproxima do resultado desejado.

Fusão de classificação recíproca em pesquisa híbrida e cliente Go

O Reciprocal Rank Fusion, ou RRF, foi lançado em versão de pré-visualização técnica para busca híbrida no Elasticsearch 8.9. Tem como objetivo reduzir a curva de aprendizado associada ao ajuste fino e diminuir o tempo gasto experimentando diferentes fatores para otimizar o conjunto de resultados.

Com o RRF, a pontuação do documento é recalculada combinando as pontuações pelo algoritmo abaixo:

score := 0.0
// q is a query in the set of queries (vector and keyword search)
for _, q := range queries {
    // result(q) is the results 
    if document in result(q) {
        // k is a ranking constant (default 60)
        // rank(result(q), d) is the document's rank within result(q) 
        // range from 1 to the window_size (default 100)
        score +=  1.0 / (k + rank(result(q), d))
    }
}

return score

A vantagem de usar o RRF é que podemos aproveitar os valores padrão adequados do Elasticsearch. A constante de classificação k assume o valor padrão 60. Para proporcionar um equilíbrio entre a relevância dos documentos retornados e o desempenho da consulta ao pesquisar em grandes conjuntos de dados, o tamanho do conjunto de resultados para cada consulta considerada é limitado ao valor de window_size, que por padrão é 100 conforme descrito na documentação.

k e windows_size também pode ser configurado dentro da configuração Rrf dentro do método Rank no cliente Go, conforme o exemplo abaixo:

func HybridSearchWithRRF(client *elasticsearch.TypedClient, term string) ([]Rodent, error) {
	var k = 10
	var numCandidates = 10

	// Minimum required window size for the default result size of 10
	var windowSize int64 = 10
	var rankConstant int64 = 42

	res, err := client.Search().
		Index("vector-search-rodents").
		Knn(types.KnnSearch{
			Field:         "text_embedding.predicted_value",
			K:             &k,
			NumCandidates: &numCandidates,
			QueryVectorBuilder: &types.QueryVectorBuilder{
				TextEmbedding: &types.TextEmbedding{
					ModelId:   "sentence-transformers__msmarco-minilm-l-12-v3",
					ModelText: term,
				},
			}}).
		Query(&types.Query{
			Match: map[string]types.MatchQuery{
				"title": {Query: term},
			},
		}).
		Rank(&types.RankContainer{
			Rrf: &types.RrfRank{
				WindowSize:   &windowSize,
				RankConstant: &rankConstant,
			},
		}).
		Do(context.Background())

	if err != nil {
		return nil, err
	}

	return getRodents(res.Hits.Hits)
}

Conclusão

Aqui discutimos como combinar a pesquisa vetorial e a pesquisa por palavra-chave no Elasticsearch usando o cliente Elasticsearch Go.

Confira o repositório do GitHub para acessar todo o código desta série. Caso ainda não tenha feito, confira a parte 1 e a parte 2 para ver todo o código desta série.

Boa caçada aos esquilos!

Recursos

  1. Guia do Elasticsearch

  2. Cliente Go do Elasticsearch

  3. O que é busca vetorial? | Elástico

  4. Fusão de Classificação Recíproca

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)