Articles de blog

RAG (Retrieval Augmented Generation) avec LlamaIndex, Elasticsearch et Mistral

Apprenez à mettre en œuvre un système RAG (Retrieval Augmented Generation) en utilisant LlamaIndex, Elasticsearch et Mistral en local.

Dans ce blog, nous verrons comment mettre en œuvre l'expérience Q&A en utilisant une technique RAG (Retrieval Augmented Generation) avec Elasticsearch comme base de données vectorielle. Nous utiliserons LlamaIndex et un LLM Mistral fonctionnant localement.

Avant de commencer, nous allons examiner quelques termes.

Terminologie

LlamaIndex est un cadre de données de premier plan pour la création d'applications LLM (Large Language Model). LlamaIndex fournit des abstractions pour les différentes étapes de la construction d'une application RAG (Retrieval Augmented Generation). Des structures telles que LlamaIndex et LangChain fournissent des abstractions afin que les applications ne soient pas étroitement liées aux API d'un LLM spécifique.

Elasticsearch est proposé par Elastic. Elastic est un leader de l'industrie avec Elasticsearch, un moteur de recherche et d'analyse qui prend en charge la recherche en texte intégral pour la précision, la recherche vectorielle pour la compréhension sémantique, et la recherche hybride pour le meilleur des deux mondes. Elasticsearch est un magasin de données évolutif et une base de données vectorielle. Les fonctionnalités d'Elasticsearch que nous utilisons dans ce blog sont disponibles dans la version gratuite et ouverte d'Elasticsearch.

Retrieval Augment Generation (RAG) est une technique/un modèle d'IA dans lequel les LLM sont dotés de connaissances externes pour générer des réponses aux requêtes des utilisateurs. Cela permet d'adapter les réponses du programme d'éducation et de formation tout au long de la vie à un contexte spécifique et les réponses sont plus spécifiques.

Mistral propose des modèles LLM open-source et optimisés pour les entreprises. Dans ce tutoriel, nous utiliserons leur modèle open source mistral-7b qui fonctionne sur votre ordinateur portable. Si vous ne souhaitez pas exécuter le modèle sur votre ordinateur portable, vous pouvez utiliser la version en nuage, auquel cas vous devrez modifier le code de ce blog afin d'utiliser les clés et les paquets API appropriés.

Ollama permet d'exécuter des LLM localement sur votre ordinateur portable. Nous utiliserons Ollama pour exécuter localement le modèle open source Mistral-7b.

Les encastrements sont des représentations numériques de la signification d'un texte ou d'un média. Il s'agit de représentations de dimensions inférieures d'informations de dimensions supérieures.

Construire une application RAG avec LlamaIndex, Elasticsearch & Mistral : Aperçu du scénario

Scénario :

Nous disposons d'un échantillon de données (sous forme de fichier JSON) de conversations entre des agents et des clients d'une compagnie d'assurance habitation fictive. Nous allons créer une application RAG simple qui peut répondre à des questions telles que

Give me summary of water related issues.

Débit élevé

Flux RAG

Mistral LLM fonctionne localement à l'aide d'Ollama.

Ensuite, nous chargeons les conversations du fichier JSON en tant que Documents dans le magasin ElasticsearchStore (qui est un magasin VectorStore soutenu par Elasticsearch). Lors du chargement des documents, nous créons des enchâssements à l'aide du modèle Mistral exécuté localement. Nous stockons ces encastrements ainsi que les conversations dans le magasin vectoriel Elasticsearch de LlamaIndex(ElasticsearchStore).

Nous configurons une ligne d'ingestion LlamaIndex et lui fournissons le LLM local que nous utilisons, dans ce cas Mistral fonctionnant via Ollama.

Lorsque nous posons une question telle que "Donnez-moi un résumé des problèmes liés à l'eau", Elasticsearch effectue une recherche sémantique et renvoie les conversations liées aux problèmes de l'eau. Ces conversations, ainsi que la question initiale, sont envoyées au LLM local pour générer une réponse.

Étapes de la création de l'application RAG

Exécuter Mistral localement

Téléchargez et installez Ollama. Après avoir installé Ollama, lancez la commande suivante pour télécharger et exécuter mistral

ollama run mistral

Le téléchargement et l'exécution locale du modèle pour la première fois peuvent prendre quelques minutes. Vérifiez si le mistral fonctionne en posant une question telle que la suivante : "Ecrivez un poème sur les nuages" et vérifiez si le poème vous plaît. Gardez ollama en marche car nous aurons besoin d'interagir avec le modèle mistral plus tard par le biais du code.

Installer Elasticsearch

Faites fonctionner Elasticsearch en créant un déploiement dans le nuage(instructions ici) ou en l'exécutant dans Docker(instructions ici). Vous pouvez également créer un déploiement autonome d'Elasticsearch au niveau de la production en commençant ici.

Si vous utilisez le déploiement dans le nuage, saisissez la clé API et l'ID du nuage pour le déploiement, comme indiqué dans les instructions. Nous les utiliserons plus tard.

Application RAG

Pour référence, le code complet peut être trouvé dans ce dépôt Github. Le clonage de la base de données est facultatif car nous allons parcourir le code ci-dessous.

Dans votre IDE préféré, créez une nouvelle application Python avec les 3 fichiers ci-dessous.

  • index.py où se trouve le code lié à l'indexation des données.

  • query.py où se trouve le code lié à l'interrogation et à l'interaction avec le LLM.

  • .env où se trouvent les propriétés de configuration telles que les clés d'API.

Nous devons installer quelques paquets. Nous commençons par créer un nouvel environnement virtuel python dans le dossier racine de votre application.

python3 -m venv .venv

Activez l'environnement virtuel et installez les paquets requis ci-dessous.

source .venv/bin/activate
pip install llama-index 
pip install llama-index-embeddings-ollama
pip install llama-index-llms-ollama
pip install llama-index-vector-stores-elasticsearch
pip install sentence-transformers
pip install python-dotenv

Indexation des données

Téléchargez le fichier conversations.json qui contient les conversations entre les clients et les agents du centre d'appel de notre compagnie d'assurance habitation fictive. Placez le fichier dans le répertoire racine de l'application avec les 2 fichiers python et le fichier .env. que vous avez créé précédemment. Vous trouverez ci-dessous un exemple du contenu du fichier.

{
    "conversation_id": 103,
    "customer_name": "Sophia Jones",
    "agent_name": "Emily Wilson",
    "policy_number": "JKL0123",
    "conversation": "Customer: Hi, I'm Sophia Jones. My Date of Birth is November 15th, 1985, Address is 303 Cedar St, Miami, FL 33101, and my Policy Number is JKL0123.\nAgent: Hello, Sophia. How may I assist you today?\nCustomer: Hello, Emily. I have a question about my policy.\nCustomer: There's been a break-in at my home, and some valuable items are missing. Are they covered?\nAgent: Let me check your policy for coverage related to theft.\nAgent: Yes, theft of personal belongings is covered under your policy.\nCustomer: That's a relief. I'll need to file a claim for the stolen items.\nAgent: We'll assist you with the claim process, Sophia. Is there anything else I can help you with?\nCustomer: No, that's all for now. Thank you for your assistance, Emily.\nAgent: You're welcome, Sophia. Please feel free to reach out if you have any further questions or concerns.\nCustomer: I will. Have a great day!\nAgent: You too, Sophia. Take care.",
    "summary": "A customer inquires about coverage for stolen items after a break-in at home, and the agent confirms that theft of personal belongings is covered under the policy. The agent offers assistance with the claim process, resulting in the customer expressing relief and gratitude."
}

Nous définissons une fonction appelée get_documents_from_file dans index.py qui lit le fichier json et crée une liste de documents. Les objets document sont l'unité de base de l'information avec laquelle LlamaIndex travaille.

# index.py
import json, os
from llama_index.core import Document, Settings
from llama_index.core.node_parser import SentenceSplitter
from llama_index.core.ingestion import IngestionPipeline
from llama_index.embeddings.ollama import OllamaEmbedding
from llama_index.vector_stores.elasticsearch import ElasticsearchStore
from dotenv import load_dotenv

def get_documents_from_file(file):
   """Reads a json file and returns list of Documents"""

   with open(file=file, mode='rt') as f:
       conversations_dict = json.loads(f.read())
      
   # Build Document objects using fields of interest.
   documents = [Document(text=item['conversation'],
                         metadata={"conversation_id": item['conversation_id']})
                for
                item in conversations_dict]
   return documents

Créer un pipeline d'ingestion

Tout d'abord, ajoutez au fichier .env l'Elasticsearch CloudID et les clés API que vous avez obtenues dans la section Install Elasticsearch. Votre fichier .env devrait ressembler à ce qui suit (avec des valeurs réelles).

ELASTIC_CLOUD_ID=<REPLACE WITH YOUR CLOUD ID>
ELASTIC_API_KEY=<REPLACE WITH YOUR API_KEY>

LlamaIndex IngestionPipeline vous permet de composer un pipeline à l'aide de plusieurs composants. Ajoutez le code ci-dessous au fichier index.py.

# index.py

# Load .env file contents into env
# ELASTIC_CLOUD_ID and ELASTIC_API_KEY are expected to be in the .env file.
load_dotenv('.env')

# ElasticsearchStore is a VectorStore that
# takes care of ES Index and Data management.
es_vector_store = ElasticsearchStore(index_name="calls",
                                     vector_field='conversation_vector',
                                     text_field='conversation',
                                     es_cloud_id=os.getenv("ELASTIC_CLOUD_ID"),
                                     es_api_key=os.getenv("ELASTIC_API_KEY"))


def main():
    # Embedding Model to do local embedding using Ollama.
    ollama_embedding = OllamaEmbedding("mistral")

    # LlamaIndex Pipeline configured to take care of chunking, embedding
    # and storing the embeddings in the vector store.
    pipeline = IngestionPipeline(
        transformations=[
            SentenceSplitter(chunk_size=350, chunk_overlap=50),
            ollama_embedding,
        ],
        vector_store=es_vector_store
    )

    # Load data from a json file into a list of LlamaIndex Documents
    documents = get_documents_from_file(file="conversations.json")

    pipeline.run(documents=documents)
    print(".....Done running pipeline.....\n")


if __name__ == "__main__":
    main()

Comme nous l'avons déjà mentionné, la LlamaIndex IngestPipeline peut être composée de plusieurs éléments. Nous ajoutons 3 composants au pipeline dans la ligne pipeline = IngestionPipeline(....

  • SentenceSplitter: Comme on peut le voir dans la définition de get_documents_from_file(), chaque document possède un champ texte qui contient la conversation trouvée dans le fichier json. Ce champ de texte est un long texte. Pour que la recherche sémantique fonctionne bien, elle doit être décomposée en morceaux de textes plus petits. La classe SentenceSplitter s'en charge pour nous. Ces morceaux sont appelés nœuds dans la terminologie de LlamaIndex. Les nœuds contiennent des métadonnées qui renvoient au document auquel ils appartiennent. Vous pouvez également utiliser Elasticsearch Ingestpipeline pour le découpage, comme indiqué dans ce blog.

  • OllamaEmbedding: Les modèles d'incorporation convertissent un texte en nombres (également appelés vecteurs). La représentation numérique nous permet d'effectuer une recherche sémantique où les résultats de la recherche correspondent à la signification du mot plutôt que d'effectuer une simple recherche textuelle. Nous fournissons à l'IngestionPipeline le site OllamaEmbedding("mistral"). Les morceaux que nous découpons à l'aide de SentenceSplitter sont envoyés au modèle Mistral qui s'exécute sur votre machine locale via Ollama, mistral crée alors des embeddings pour les morceaux.

  • ElasticsearchStore: Le magasin vectoriel ElasticsearchStore de LlamaIndex sauvegarde les embeddings créés dans un index Elasticsearch. ElasticsearchStore se charge de créer et d'alimenter le contenu de l'index Elasticsearch spécifié. Lors de la création de l'ElasticsearchStore (référencé par es_vector_store), nous fournissons le nom de l'index Elasticsearch que nous voulons créer (calls dans notre cas), le champ de l'index dans lequel nous voulons stocker les embeddings (conversation_vector dans notre cas) et le champ dans lequel nous voulons stocker le texte (conversation dans notre cas). En résumé, selon notre configuration, ElasticsearchStore crée un nouvel index dans Elasticsearch avec conversation_vector et conversation comme champs (parmi d'autres champs créés automatiquement).

Pour relier le tout, nous exécutons le pipeline en appelant pipeline.run(documents=documents).

Lancez le script index.py pour exécuter le pipeline d'ingestion :

python index.py

Une fois l'exécution du pipeline terminée, nous devrions voir un nouvel index dans Elasticsearch appelé calls. En exécutant une simple requête elasticsearch à l'aide de la Dev Console, vous devriez être en mesure de voir les données chargées avec les embeddings.

GET calls/_search?size=1

Pour résumer ce que nous avons fait jusqu'à présent, nous avons créé des documents à partir d'un fichier JSON, nous les avons divisés en morceaux, nous avons créé des embeddings pour ces morceaux et nous avons stocké les embeddings (et la conversation textuelle) dans un magasin vectoriel (ElasticsearchStore).

Interrogation

Le VectorStoreIndex llamaIndex vous permet de retrouver des documents pertinents et d'interroger des données. Par défaut, VectorStoreIndex stocke les intégrations en mémoire dans un SimpleVectorStore. Cependant, des entrepôts de vecteurs externes (comme ElasticsearchStore) peuvent être utilisés à la place pour rendre les enregistrements persistants.

Ouvrez le site query.py et collez le code ci-dessous

# query.py
from llama_index.core import VectorStoreIndex, QueryBundle, Response, Settings
from llama_index.embeddings.ollama import OllamaEmbedding
from llama_index.llms.ollama import Ollama
from index import es_vector_store

# Local LLM to send user query to
local_llm = Ollama(model="mistral")
Settings.embed_model= OllamaEmbedding("mistral")

index = VectorStoreIndex.from_vector_store(es_vector_store)
query_engine = index.as_query_engine(local_llm, similarity_top_k=10)

query="Give me summary of water related issues"
bundle = QueryBundle(query, embedding=Settings.embed_model.get_query_embedding(query))
result = query_engine.query(bundle)
print(result)

Nous définissons un LLM local (local_llm) pour indiquer le modèle Mistral fonctionnant sur Ollama. Ensuite, nous créons un VectorStoreIndex (index) à partir du magasin de vecteurs ElasticssearchStore que nous avons créé précédemment, puis nous obtenons un moteur de requête à partir de l'index. Lors de la création du moteur de requête, nous référençons le LLM local qui doit être utilisé pour répondre, nous fournissons également (similarity_top_k=10) pour configurer le nombre de documents qui doivent être récupérés dans le magasin vectoriel et envoyés au LLM pour obtenir une réponse.

Lancez le script query.py pour exécuter le flux RAG :

python query.py

Nous envoyons la demande à Give me summary of water related issues (n'hésitez pas à personnaliser query) et la réponse du LLM, qui est accompagnée des documents correspondants, devrait ressembler à ce qui suit.

Dans le contexte fourni, nous voyons plusieurs cas où les clients se sont renseignés sur la couverture des dommages liés à l'eau. Dans deux cas, les inondations ont causé des dommages aux sous-sols et les fuites de toiture ont été à l'origine d'un autre cas. Les agents ont confirmé que les deux types de dégâts des eaux sont couverts par leurs polices respectives. Par conséquent, les problèmes liés à l'eau, tels que les inondations et les fuites de toit, sont généralement couverts par les polices d'assurance habitation.

Quelques mises en garde :

Cet article de blog est une introduction pour débutants à la technique RAG avec Elasticsearch et omet donc la configuration des fonctionnalités qui vous permettront de passer de ce point de départ à la production. Lorsque vous construisez pour des cas d'utilisation en production, vous voudrez prendre en compte des aspects plus sophistiqués comme la possibilité de protéger vos données avec Document Level Security, le découpage de vos données dans le cadre d'un pipeline d'ingestion Elasticsearch ou même l'exécution d'autres tâches de ML sur les mêmes données que celles utilisées pour les cas d'utilisation GenAI/Chat/Q&A.

Vous pouvez également envisager d'obtenir des données et de créer des embeddings à partir de diverses sources externes (par exemple Azure Blob Storage, Dropbox, Gmail, etc.) à l'aide d'Elastic Connectors.

Elastic rend possible tout ce qui précède et plus encore, et fournit une solution d'entreprise complète pour les cas d'utilisation de la GenAI et au-delà.

Quelle est la suite ?

  • Vous avez peut-être remarqué que nous envoyons 10 conversations connexes avec la question de l'utilisateur au LLM pour formuler une réponse. Ces conversations peuvent contenir des IPI (informations personnelles identifiables) telles que le nom, la date de naissance, l'adresse, etc. Dans notre cas, le LLM est local, la fuite de données n'est donc pas un problème. Cependant, lorsque vous souhaitez utiliser un LLM fonctionnant dans le nuage (par exemple OpenAI), il n'est pas souhaitable d'envoyer des textes contenant des informations PII. Dans un blog ultérieur, nous verrons comment masquer les informations PII avant de les envoyer aux LLM externes dans le flux RAG.

  • Dans ce billet, nous avons utilisé un LLM local. Dans le prochain billet sur le masquage des données PII dans RAG, nous verrons comment passer facilement d'un LLM local à un LLM public.

Questions fréquentes

Qu'est-ce que le LlamaIndex ?

LlamaIndex est un cadre de données de premier plan pour la création d'applications LLM (Large Language Model).

Qu'est-ce que le mistral ?

Mistral est une société qui propose des modèles LLM à code source ouvert et optimisés pour les entreprises.

Pour aller plus loin

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

Han Xiang Choong

Résolution d'entités avec Elasticsearch, partie 4 : le défi ultime

Jessica Moszkowicz

Automatisation de l'analyse des logs dans Streams avec le ML

Nastia Havriushenko

Construire un agent d'IA pour les RH avec Elastic Agent Builder et GPT-OSS

Tomás Murúa

Techniques avancées de RAG partie 1 : Traitement des données

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