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:
Instalação do Go versão 1.21 ou posterior
Crie seu próprio repositório Go usando a estrutura recomendada e o gerenciamento de pacotes abordados na documentação do Go.
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:

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:
ID da nuvem e chave de API, caso esteja utilizando o Elastic Cloud.
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 scoreA 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!




