RAG (Generación aumentada por recuperación) con LlamaIndex, Elasticsearch y Mistral
Aprende a implementar un sistema RAG (Generación Aumentada por Recuperación) usando LlamaIndex, Elasticsearch y Mistral ejecutando localmente.
En este blog discutiremos cómo implementar la experiencia de preguntas frecuentes empleando una técnica RAG (Generación Aumentada de Recuperación) con Elasticsearch como base de datos vectorial. Emplearemos LlamaIndex y un LLM Mistral que se ejecuta localmente.
Antes de empezar, veremos algo de terminología.
Terminología
LlamaIndex es un marco de datos líder para construir aplicaciones LLM (Grandes Modelos de Lenguaje). LlamaIndex proporciona abstracciones para las distintas etapas de la construcción de una aplicación RAG (Generación Aumentada por Recuperación). Frameworks como LlamaIndex y LangChain proporcionan abstracciones para que las aplicaciones no se acoplen estrechamente a las APIs de ningún LLM específico.
Elasticsearch es ofrecido por Elastic. Elastic es líder en el sector detrás de Elasticsearch, un motor de búsqueda y análisis que soporta búsqueda en texto completo para mayor precisión, búsqueda vectorial para comprensión semántica y búsqueda híbrida para lo mejor de ambos mundos. Elasticsearch es un almacén de datos escalable y una base de datos vectorial. Las capacidades de elasticsearch que empleamos en este blog están disponibles en la versión gratis y abierta de Elasticsearch.
La Generación de Aumentos por Recuperación (RAG) es una técnica/patrón de IA en la que los LLMs reciben conocimientos externos para generar respuestas a las consultas de los usuarios. Esto permite adaptar las respuestas de los LLM a un contexto específico y que las respuestas sean más específicas.
Mistral ofrece tanto modelos LLM de código abierto como optimizados de nivel empresarial. En este tutorial, usaremos su modelo de código abierto mistral-7b que funciona en tu portátil. Si no quieres ejecutar el modelo en tu portátil, alternativamente podrías usar su versión en la nube, en cuyo caso tendrás que modificar el código de este blog para usar las claves y paquetes de API adecuados.
Ollama ayuda a ejecutar LLMs localmente en tu portátil. Usaremos Ollama para ejecutar localmente el modelo de código abierto Mistral-7b.
Las incrustaciones son representaciones numéricas del significado de texto/medios. Son representaciones de información de alta dimensión en dimensiones inferiores.
Construcción de una aplicación RAG con LlamaIndex, Elasticsearch y Mistral: Resumen del escenario
Escenario:
Disponemos de un conjunto de datos de ejemplo (como archivo JSON) de conversaciones en centros de llamadas entre agentes y clientes de una compañía de seguros de hogar ficticia. Construiremos una aplicación RAG sencilla que podrá responder preguntas como
Give me summary of water related issues.
Caudal de alto nivel

Tenemos Mistral LLM funcionando localmente usando Ollama.
A continuación, cargamos las conversaciones del archivo JSON como Documents en ElasticsearchStore (que es un VectorStore respaldado por Elasticsearch). Al cargar los documentos creamos incrustaciones usando el modelo Mistral que se ejecuta localmente. Almacenamos estos embeddings junto con las conversaciones en LlamaIndex Elasticsearch vector Store (ElasticsearchStore).
Configuramos un LlamaIndex IngestionPipeline y lo suministramos con el LLM local que usamos, en este caso Mistral que funciona a través de Ollama.
Cuando hacemos una pregunta como "Dame un resumen de los problemas relacionados con el agua.", Elasticsearch realiza una búsqueda semántica y devuelve conversaciones relacionadas con problemas relacionados con el agua. Estas conversaciones , junto con la pregunta original, se envían al LLM que se ejecuta localmente para generar una respuesta.
Pasos para construir la aplicación RAG
Ejecutar Mistral localmente
Descarga e instala Ollama. Luego de instalar Ollama, ejecuta este comando para descargar y ejecutar mistral
ollama run mistralPuede que tardes unos minutos en descargar y ejecutar el modelo localmente por primera vez. Verifica si el mistral está funcionando haciendo una pregunta como la siguiente: "Escribe un poema sobre nubes" y verifica si el poema te gusta. Mantén a Ollama funcionando, ya que tendremos que interactuar con el modelo mistral más adelante mediante código.
Instalar Elasticsearch
Pon en marcha Elasticsearch ya sea creando un despliegue en la nube (instrucciones aquí) o ejecutándolo en docker (instrucciones aquí). También puedes crear un despliegue autoalojado de Elasticsearch de calidad producción empezando por aquí.
Suponiendo que usas el despliegue en la nube, descarga la clave API y el ID de la nube para el despliegue tal y como se indica en las instrucciones. Los usaremos más tarde.
Aplicación RAG
Como referencia, se puede encontrar el código completo en este repositorio de Github. Clonar el repositorio es opcional, ya que repasaremos el código a continuación.
En tu IDE favorito, crea una nueva aplicación en Python con los siguientes 3 archivos.
index.pydonde va el código relacionado con la indexación de datos.query.pydonde va el código relacionado con la consulta y la interacción con LLM..envdonde van propiedades de configuración como las claves de API.
Necesitamos instalar algunos paquetes. Comenzamos creando un nuevo entorno virtual de Python en la carpeta raíz de tu aplicación.
python3 -m venv .venvActiva el entorno virtual e instala los paquetes requeridos a continuación.
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-dotenvDatos de indexación
Descarga el archivo conversations.json que contiene conversaciones entre clientes y agentes de centros de llamadas de nuestra compañía de seguros de hogar ficticia. Coloca el archivo en el directorio raíz de la aplicación junto a los 2 archivos python y el archivo .env Archivo que creaste antes. A continuación se muestra un ejemplo del contenido del archivo.
{
"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."
}Definimos una función llamada get_documents_from_file en index.py que lee el archivo json y crea una lista de Documentos. Los objetos documento son la unidad básica de información con la que trabaja LlamaIndex.
# 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 documentsCreate IngestPipeline
En primer lugar, agrega las claves de CloudID y API de Elasticsearch que obtuviste en la sección Install Elasticsearch al archivo .env . Tu archivo .env debería parecer a lo siguiente (con valores reales).
ELASTIC_CLOUD_ID=<REPLACE WITH YOUR CLOUD ID>
ELASTIC_API_KEY=<REPLACE WITH YOUR API_KEY>LlamaIndex IngestionPipeline te permite componer una tubería usando múltiples componentes. Agrega el código de abajo al archivo 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()Como se mencionó anteriormente, el LlamaIndex IngestPipeline puede estar compuesto por múltiples componentes. Estamos agregando 3 componentes a la tubería en la línea pipeline = IngestionPipeline(....
Fragmentador de Sentences: Como se puede ver en la definición de
get_documents_from_file(), cada documento tiene un campo de texto que contiene la conversación encontrada en el archivo json. Este campo de texto es un texto largo. Para que la búsqueda semántica funcione bien, debe dividir en fragmentos de textos más pequeños. La clase SentenceSplitter hace esto por nosotros. Estos bloques se denominan nodos en la terminología de LlamaIndex. Hay metadatos en los nodos que apuntan al documento al que pertenecen. Alternativamente, podrías usar Elasticsearch Ingestpipeline para el chunking, como se muestra en este blog.OllamaEmbedding: Los modelos de incrustación convierten un fragmento de texto en números (también llamados vectores). Tener representación numérica nos permite realizar búsquedas semánticas donde los resultados coinciden con el significado de la palabra en lugar de simplemente hacer una búsqueda de texto. Suministramos a IngestionPipeline
OllamaEmbedding("mistral"). Los chunks que dividimos usando SentenceSplitter se envían al modelo Mistral que se ejecuta en tu máquina local mediante Ollama, luego Mistral crea embeddings para los chunks.ElasticsearchStore: El almacén vectorial LlamaIndex ElasticsearchStore respalda las incrustaciones que se están creando en un Índice Elasticsearch. ElasticsearchStore se encarga de crear y poblar el contenido del índice Elasticsearch especificado. Al crear el ElasticsearchStore (referenciado por
es_vector_store) proporcionamos el nombre del índice Elasticsearch que queremos crear (callsen nuestro caso), el campo en el índice donde queremos almacenar las incrustaciones (conversation_vectoren nuestro caso) y el campo donde queremos almacenar el texto (conversationen nuestro caso). En resumen, basar en nuestra configuraciónElasticsearchStorecrea un nuevo índice en Elasticsearch conconversation_vectoryconversationcomo campos (entre otros campos creados automáticamente).
Para unirlo todo, gestionamos la tubería llamando a pipeline.run(documents=documents).
Ejecuta el script index.py para ejecutar la tubería de ingest:
python index.pyUna vez completada la ejecución de la pipeline, deberíamos ver un nuevo índice en Elasticsearch llamado calls. Ejecutando una consulta sencilla de elasticsearch usando la Dev Console deberías poder ver los datos cargados junto con las incrustaciones.
GET calls/_search?size=1Para resumir lo que hicimos hasta ahora, creamos Documents a partir de un archivo JSON, los dividimos en bloques, creamos incrustaciones para esos fragmentos y almacenamos las incrustaciones (y la conversación de texto) en un almacén vectorial (ElasticsearchStore).
Consulta
El llamaIndex VectorStoreIndex te permite recuperar documentos relevantes y datos de consulta. Por defecto, VectorStoreIndex almacena incrustaciones en memoria en un SimpleVectorStore. Sin embargo, se pueden usar almacenes vectoriales externos (como ElasticsearchStore) para hacer que las incrustaciones sean persistentes.
Abre el query.py y pega el código de abajo
# 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)Definimos un LLM local (local_llm) para apuntar al modelo Mistral que se ejecuta en Ollama. A continuación, creamos un VectorStoreIndex (index) a partir de la tienda vectorial ElasticssearchStore que creamos antes y luego obtenemos un motor de consulta desde el índice. Al crear el motor de consultas, hacemos referencia al LLM local que debe usar para responder, también proporcionamos (similarity_top_k=10) para configurar el número de documentos que deben recuperar del almacén vectorial y enviar al LLM para obtener una respuesta.
Ejecuta el script query.py para ejecutar el flujo RAG:
python query.pyEnviamos el Give me summary of water related issues de consulta (no dudes en personalizar el query) y la respuesta del LLM, que se proporciona con los documentos relacionados, debería ser algo así como el siguiente.
En el contexto proporcionado, vemos varios casos en los que los clientes consultaron por la cobertura por daños relacionados con el agua. En dos casos, las inundaciones causaron daños en los sótanos y las filtraciones en el tejado fueron el problema en otro caso. Los agentes confirmaron que ambos tipos de daños por agua están cubiertos por sus respectivas pólizas. Por lo tanto, los problemas relacionados con el agua, como inundaciones y filtraciones en el tejado, suelen estar cubiertos por las pólizas de seguro de hogar.
Algunas advertencias:
Esta entrada de blog es una introducción para principiantes a la técnica RAG con Elasticsearch y, por tanto, omite la configuración de las características que te permitirán llevar este punto de partida a producción. Al construir para casos de uso en producción, querrás considerar aspectos más sofisticados como poder proteger tus datos con Seguridad a Nivel de Documento, fragmentar tus datos como parte de una pipeline de Ingest de Elasticsearch o incluso ejecutar otros trabajos de ML con los mismos datos que se usan para casos de uso de GenAI/Chat/Q&A.
También podrías considerar buscar datos y crear incrustaciones desde diversas fuentes externas (por ejemplo, Azure Blob Storage, Dropbox, Gmail, etc.) usando Elastic Connectors.
Elastic hace posible todo lo anterior y más posible, y ofrece una solución integral de nivel empresarial para casos de uso de GenAI y más allá.
¿Qué sigue?
Quizá notaste que estamos enviando 10 conversaciones relacionadas junto con la pregunta del usuario al LLM para formular una respuesta. Estas conversaciones pueden contener información personal identificable (PII) como nombre, fecha de nacimiento, dirección, etc. En nuestro caso, el LLM es local, así que la fuga de datos no es un problema. Sin embargo, cuando quieres usar un LLM que se ejecuta en la nube (por ejemplo, OpenAI) no es deseable enviar textos que contengan información de PII. En un blog de seguimiento veremos cómo lograr información de PII de Enmascaramiento antes de enviarla a LLMs externos en el flujo RAG.
En esta publicación empleamos un LLM local; en la próxima publicación sobre Enmascarar datos de PII en RAG, veremos cómo podemos cambiar fácilmente de un LLM local a uno público.
Preguntas frecuentes
¿Qué es LlamaIndex?
LlamaIndex es un marco de datos líder para construir aplicaciones LLM (Grandes Modelos de Lenguaje).
¿Qué es Mistral?
Mistral es una compañía que ofrece tanto modelos LLM de código abierto como modelos optimizados de nivel empresarial.




