Stocker les Embeddings dans Elasticsearch

Elasticsearch prend entièrement en charge le stockage et la récupération des vecteurs, ce qui en fait une base de données idéale pour travailler avec les embeddings.

Types de champs

Dans le chapitre Recherche plein texte de ce tutoriel, vous avez appris à créer un index avec plusieurs champs. À l'époque, il avait été mentionné qu'Elasticsearch pouvait, en grande partie, déterminer automatiquement le meilleur type à utiliser pour chaque champ en fonction des données elles-mêmes. Même si Elasticsearch 8.11 est capable de mapper automatiquement certains types de vecteurs, dans ce chapitre, vous définirez ce type explicitement afin d'en apprendre davantage sur les mappages de types dans Elasticsearch.

Récupération des correspondances de types

Les types associés à chaque champ d'un index sont déterminés par un processus appelé " mapping", qui peut être dynamique ou explicite. Les mappings créés dans la partie de ce tutoriel consacrée à la recherche plein texte ont tous été générés dynamiquement par Elasticsearch.

Le client Elasticsearch propose une méthode get_mapping, qui renvoie les correspondances de types en vigueur pour un index donné. Si vous souhaitez explorer ces correspondances par vous-même, démarrez un shell Python et entrez le code suivant :

La réponse de la méthode get_mapping() est un dictionnaire contenant des informations sur chaque champ de l'index. Pour votre commodité, vous trouverez ci-dessous une structure joliment formatée de ces informations pour l'index my_documents créé dans la section Recherche en texte intégral du didacticiel :

Vous pouvez ainsi constater que les champs created_on et updated_at ont été automatiquement tapés avec date, tandis que tous les autres champs ont été tapés avec text. Lorsqu'il essaie de choisir un type, Elasticsearch vérifie d'abord le type des données, ce qui l'aide à attribuer des types numériques, booléens et d'objets aux champs. Lorsque les données du champ sont des chaînes de caractères, il essaie également de vérifier si les données correspondent à un modèle de date. La détection des chaînes de caractères basée sur des motifs peut également être activée pour les nombres si vous le souhaitez.

Les champs de texte ont une définition fields avec une entrée keyword. Il s'agit d'un sous-champ, un type alternatif ou secondaire qui peut être utilisé le cas échéant. Dans Elasticsearch, les champs text tapés dynamiquement se voient attribuer un sous-champ keyword. Vous avez déjà utilisé le sous-champ category.keyword pour effectuer une recherche exacte dans une catégorie donnée. Pour éviter l'ajout d'un sous-champ, il est possible d'indiquer une correspondance explicite avec text ou keyword, qui sera alors le type principal et unique.

Ajout d'un champ vectoriel à l'index

Ajoutons un nouveau champ à l'index dans lequel sera stocké un embedding pour chaque document.

La structure d'une correspondance explicite correspond à la clé mappings de la réponse renvoyée par la méthode get_mapping() du client Elasticsearch. Seuls les champs qui doivent être typés explicitement doivent être indiqués, car tous les champs qui ne sont pas inclus dans le mappage continueront à être typés dynamiquement comme auparavant.

Vous pouvez voir ci-dessous une nouvelle version de la méthode create_index() de la classe Search, qui ajoute un champ explicitement typé nommé embedding. Remplacez cette méthode dans search.py :

Comme vous pouvez le constater, le champ embedding est de type dense_vector, ce qui est le type approprié pour stocker des embeddings. Vous apprendrez plus tard à connaître un autre type de vecteur, le sparse_vector, qui est utile dans d'autres types d'applications de recherche sémantique.

Le type dense_vector accepte quelques paramètres, tous facultatifs.

  • dims: la taille des vecteurs qui seront stockés. Depuis la version 8.11, les dimensions sont automatiquement attribuées lors de l'insertion du premier document.
  • index: doit être fixé à True pour indiquer que les vecteurs doivent être indexés pour la recherche. Il s'agit de la valeur par défaut.
  • similarity: la fonction de distance à utiliser pour comparer les vecteurs. Les deux plus courants sont dot_product et cosine. Le produit en points est plus efficace, mais il nécessite la normalisation des vecteurs. La valeur par défaut est cosine.

Ajout d'éléments d'intégration à des documents

Dans la section précédente, vous avez appris à générer des embeddings à l'aide du cadre SentenceTransformers et du modèle all-MiniLM-L6-v2. Il est maintenant temps d'intégrer le modèle dans l'application.

Tout d'abord, le modèle peut être instancié dans le constructeur de la classe Search:

Comme vous vous en souvenez dans la partie de ce tutoriel consacrée à la recherche en texte intégral, la classe Search possède les méthodes insert_document() et insert_documents(), qui permettent d'insérer respectivement un et plusieurs documents dans l'index. Ces deux méthodes doivent à présent générer les encastrements correspondants qui vont avec chaque document.

Le bloc de code suivant présente de nouvelles versions de ces deux méthodes, ainsi qu'une nouvelle méthode d'aide get_embedding() qui renvoie une intégration.

Les méthodes modifiées ajoutent le nouveau champ embedding au document à insérer. L'intégration est générée à partir du champ summary de chaque document. En général, les embeddings sont générés à partir de phrases ou de courts paragraphes, donc dans ce cas le résumé est un champ idéal à utiliser. D'autres options auraient été le champ name, qui contient le titre du document, ou peut-être les premières phrases du document body.

Une fois ces changements mis en place, l'index peut être reconstruit, de sorte qu'il stocke un embedding pour chaque document. Pour reconstruire l'index, utilisez cette commande :

Au cas où vous auriez besoin d'un rappel, la commande flask reindex est implémentée dans la fonction reindex() de app.py. Il appelle la méthode reindex() de la classe Search, qui à son tour invoque create_index() et transmet toutes les données du fichier data.json à insert_documents().

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