Articles de blog

Utiliser la recherche hybride pour la chasse aux gophers avec Elasticsearch et Go

Apprenez à réaliser une recherche hybride en combinant la recherche par mot-clé et la recherche vectorielle à l'aide d'Elasticsearch et du client Elasticsearch Go.

Dans les parties précédentes de cette série, il a été démontré comment utiliser le client Elasticsearch Go pour la recherche traditionnelle par mot-clé et la recherche vectorielle. Cette troisième partie traite de la recherche hybride. Nous partagerons des exemples de la façon dont vous pouvez combiner la recherche vectorielle et la recherche par mot-clé en utilisant Elasticsearch et le client Elasticsearch Go.

Produits requis

Tout comme dans la première partie de cette série, les conditions suivantes sont requises pour cet exemple :

  1. Installation de Go version 1.21 ou ultérieure

  2. Créez votre propre répertoire Go en utilisant la structure recommandée et la gestion des paquets décrite dans la documentation Go.

  3. Création de votre propre cluster Elasticsearch, alimenté par un ensemble de pages sur les rongeurs, y compris notre sympathique Gopher, tiré de Wikipédia :

Page Gopher de Wikipedia

Connexion à Elasticsearch

Pour rappel, dans nos exemples, nous utiliserons l'API typée proposée par le client Go. Pour établir une connexion sécurisée pour n'importe quelle requête, il faut configurer le client en utilisant l'une ou l'autre des méthodes suivantes :

  1. ID du nuage et clé API si vous utilisez Elastic Cloud

  2. URL du cluster, nom d'utilisateur, mot de passe et certificat

La connexion à notre cluster situé sur Elastic Cloud ressemblerait à ceci :

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
}

La connexion client peut alors être utilisée pour la recherche, comme le montrent les sections suivantes.

Renforcement manuel pour la recherche hybride

Lors de la combinaison d'un ensemble d'algorithmes de recherche, l'approche traditionnelle consiste à configurer manuellement des constantes pour stimuler chaque type de requête. Plus précisément, un facteur est spécifié pour chaque requête, et l'ensemble des résultats combinés est comparé à l'ensemble prévu pour déterminer le rappel de la requête. Nous répétons ensuite l'opération pour plusieurs ensembles de facteurs et choisissons celui qui se rapproche le plus de l'état souhaité.

Par exemple, la combinaison d'une requête de recherche textuelle unique augmentée d'un facteur de 0.8 avec une requête knn avec un facteur inférieur de 0.2 peut être réalisée en spécifiant le champ Boost dans les deux types de requêtes, comme le montre l'exemple ci-dessous :

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)
}

Le facteur spécifié dans l'option Boost pour chaque requête est ajouté au score du document. En augmentant le score de notre requête par un facteur plus important que celui de la requête knn, les résultats de la requête par mot-clé sont plus fortement pondérés.

La difficulté du renforcement manuel, en particulier si vous n'êtes pas un expert en recherche, réside dans le fait qu'il nécessite une mise au point pour déterminer les facteurs qui conduiront à l'ensemble de résultats souhaité. Il s'agit simplement d'essayer des valeurs aléatoires pour voir ce qui vous rapproche de l'ensemble des résultats souhaités.

Fusion de rangs réciproques dans la recherche hybride & Go client

Reciprocal Rank Fusion, ou RRF, a été publié en avant-première technique pour la recherche hybride dans Elasticsearch 8.9. Il vise à réduire la courbe d'apprentissage associée à la mise au point et à réduire le temps passé à expérimenter des facteurs pour optimiser l'ensemble des résultats.

Avec la méthode RRF, le score du document est recalculé en combinant les scores par l'algorithme ci-dessous :

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

L'avantage de l'utilisation de RRF est que nous pouvons utiliser les valeurs par défaut d'Elasticsearch. La constante de classement k est remplacée par défaut par 60. Afin de trouver un compromis entre la pertinence des documents renvoyés et les performances de la requête lors de la recherche sur de grands ensembles de données, la taille de l'ensemble de résultats pour chaque requête considérée est limitée à la valeur de window_size, qui est fixée par défaut à 100 comme indiqué dans la documentation.

k et windows_size peuvent également être configurés dans la configuration Rrf dans la méthode Rank dans le client Go, comme dans l'exemple ci-dessous :

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)
}

Conclusion

Nous avons vu ici comment combiner la recherche vectorielle et la recherche par mot-clé dans Elasticsearch à l'aide du client Elasticsearch Go.

Consultez le repo GitHub pour tout le code de cette série. Si vous ne l'avez pas encore fait, consultez les parties 1 et 2 pour connaître tous les codes de cette série.

Bonne chasse aux marmottes !

Ressources

  1. Guide Elasticsearch

  2. Client Elasticsearch Go

  3. Qu'est-ce que la recherche vectorielle ? | Elastique

  4. Fusion de rangs réciproques

Pour aller plus loin

LINQ to Elasticsearch ES|QL : écrire en C#, interroger Elasticsearch

Florian Bernd

Améliorer les capacités des chatbots grâce au NLP et à la recherche vectorielle dans Elasticsearch

Priscilla Parodi

Elasticsearch et OpenSearch : comparatif de performance pour la recherche vectorielle.

Ugo Sangiorgi

Plagiat par l'IA : Détection de plagiat avec Elasticsearch

Priscilla Parodi

Techniques avancées de RAG, partie 2 : Requêtes et tests

Han Xiang Choong

Prêt à créer des expériences de recherche d'exception ?

Une recherche suffisamment avancée ne se fait pas avec les efforts d'une seule personne. Elasticsearch est alimenté par des data scientists, des ML ops, des ingénieurs et bien d'autres qui sont tout aussi passionnés par la recherche que vous. Mettons-nous en relation et travaillons ensemble pour construire l'expérience de recherche magique qui vous permettra d'obtenir les résultats que vous souhaitez.

Jugez-en par vous-même