Blog

Creación de un sistema RAG multimodal con Elasticsearch: la historia de Gotham City

Aprenda a crear un sistema multimodal de generación aumentada de recuperación (RAG) que integre datos de texto, audio, video e imagen para proporcionar una recuperación de información más rica y contextualizada.

En este blog, aprenderás a crear un pipeline RAG (Retrieval-Augmented Generation) multimodal con Elasticsearch. Exploraremos cómo aprovechar ImageBind para generar incrustaciones para varios tipos de datos, incluidos texto, imágenes, audio y mapas de profundidad. También descubrirás cómo almacenar y recuperar de manera eficiente estas incrustaciones en Elasticsearch mediante dense_vector y búsqueda k-NN. Finalmente, integraremos un modelo de lenguaje grande (LLM) para analizar la evidencia recuperada y generar un reporte final completo.

¿Cómo funciona el oleoducto multimodal RAG?

  1. Recopilación de pistas → imágenes, audio, textos y mapas de profundidad de la escena del crimen en Gotham.

  2. Generación de incrustaciones → Cada archivo se convierte en un vector mediante el modelo multimodal ImageBind.

  3. Indexación en Elasticsearch → Los vectores se almacenan para una recuperación eficiente.

  4. Buscando por similitud → Dada una nueva pista, se recuperan los vectores más similares.

  5. El LLM analiza la evidencia → ¡Un modelo GPT-4 sintetiza la respuesta e identifica al sospechoso!

Tecnologías empleadas

  • ImageBind → Genera incrustaciones unificadas para varias modalidades.

  • Elasticsearch → Permite una búsqueda vectorial rápida y eficiente.

  • LLM (GPT-4, OpenAI) → Analiza la evidencia y genera un reporte final.

¿A quién va dirigido este blog?

  • Usuarios de Elastic interesados en la búsqueda vectorial multimodal.

  • Desarrolladores que buscan comprender el RAG multimodal en la práctica.

  • Cualquiera que busque soluciones escalables para analizar datos de múltiples fuentes.

Requisitos previos para el RAG multimodal: Configuración del entorno

Para resolver el crimen en Gotham City, debe configurar su entorno tecnológico. Siga esta guía paso a paso:

1. Requisitos técnicos

Componente

Especificación

Sistema operativo

Linux, macOS o Windows

Python

3.10 o posterior

CARNERO

Mínimo 8 GB (se recomiendan 16 GB)

GPU

Opcional pero recomendado para ImageBind

2. Configuración del proyecto

Todos los materiales de investigación están disponibles en GitHub, y usaremos Jupyter Notebook (Google Colab) para esta experiencia interactiva de resolución de crímenes. Siga estos pasos para comenzar:

Configuración con Jupyter Notebook (Google Colab)

1. Accede al cuaderno

  • Abre nuestro notebook de Google Colab listo para usar: RAG multimodal con Elasticsearch.

  • Este cuaderno contiene todo el código y las explicaciones que debe seguir.

2. Clonar el repositorio

# Clone the repository with the multimodal RAG code
!git clone -b https://github.com/elastic/elasticsearch-labs.git

# Navigate to the project directory
cd elasticsearch-labs/supporting-blog-content/building-multimodal-rag-with-elasticsearch-gotham

3. Instalar dependencias

 # Install PyTorch and related libraries
!pip install torch>=2.1.0 torchvision>=0.16.0 torchaudio>=2.1.0

# Install vision processing libraries
!pip install opencv-python-headless pillow numpy

# Install the specific ImageBind fork
!pip install git+https://github.com/hkchengrex/ImageBind.git

# Install Elasticsearch and environment management
!pip install elasticsearch python-dotenv

4. Configurar credenciales

# Input your credentials securely
import getpass

ELASTICSEARCH_URL = input("Enter the Elasticsearch endpoint url: ")
ELASTICSEARCH_API_KEY = getpass.getpass("Enter the Elasticsearch API key: ")
OPENAI_API_KEY = getpass.getpass("Enter the OpenAI API key: ")

# Configure environment variables
import os
os.environ["ELASTICSEARCH_API_KEY"] = ELASTICSEARCH_API_KEY
os.environ["OPENAI_API_KEY"] = OPENAI_API_KEY
os.environ["ELASTICSEARCH_URL"] = ELASTICSEARCH_URL

Nota: El modelo ImageBind (~2 GB) se descargará automáticamente en la primera ejecución.

Ahora que todo está configurado, ¡profundicemos en los detalles y resolvamos el crimen!

Introducción: El crimen en Gotham City

En una noche lluviosa en Gotham City, un crimen impactante sacude la ciudad. El comisionado Gordon necesita su ayuda para desentrañar el misterio. Las pistas están dispersas en diferentes formatos: imágenes borrosas, audio misterioso, textos encriptados e incluso mapas de profundidad. ¿Estás listo para emplear la tecnología de IA más avanzada para resolver el caso?

En este blog, se le guiará paso a paso a través de la construcción de un sistema RAG (Retrieval-Augmented Generation) multimodal que unifica diferentes tipos de datos (imágenes, audio, textos y mapas de profundidad) en un solo espacio de búsqueda. Usaremos ImageBind para generar incrustaciones multimodales, Elasticsearch para almacenar y recuperar estas incrustaciones y un modelo de lenguaje grande (LLM) para analizar la evidencia y generar un reporte final.

Fundamentos: Arquitectura RAG multimodal

¿Qué es un RAG multimodal?

El auge de la generación aumentada de recuperación (RAG) multimodal está revolucionando la forma en que interactuamos con los modelos de IA. Tradicionalmente, los sistemas RAG funcionan exclusivamente con texto, recuperando información relevante de las bases de datos antes de generar respuestas. Sin embargo, el mundo no se limita al texto: las imágenes, los videos y el audio también contienen conocimientos valiosos. Es por eso que las arquitecturas multimodales están ganando protagonismo, lo que permite que los sistemas de IA combinen información de diferentes formatos para obtener respuestas más ricas y precisas.

Tres enfoques principales para el GAR multimodal

Para implementar un GAR multimodal, se emplean comúnmente tres estrategias. Cada enfoque tiene sus propios beneficios y limitaciones, según el caso de uso:

1. Espacio vectorial compartido

Los datos de diferentes modalidades se asignan a un espacio vectorial común empleando modelos multimodales como ImageBind. Esto permite que las consultas de texto recuperen imágenes, videos y audio sin conversión de formato explícita.

Beneficios:

  • Permite la recuperación multimodal sin necesidad de conversión de formato explícita.

  • Proporciona una integración fluida entre diferentes modalidades, lo que permite la recuperación directa a través de texto, imagen, audio y video.

  • Escalable para diversos tipos de datos, lo que lo hace útil para aplicaciones de recuperación a gran escala.

Desventajas:

  • La capacitación requiere grandes conjuntos de datos multimodales, que pueden no estar siempre disponibles.

  • El espacio de incrustación compartido puede introducir una deriva semántica, donde las relaciones entre modalidades no se conservan perfectamente.

  • El sesgo en los modelos multimodales puede afectar la precisión de la recuperación, según la distribución del conjunto de datos.

2. Modalidad de conexión a tierra única

Todas las modalidades se convierten a un solo formato, generalmente texto, antes de la recuperación. Por ejemplo, las imágenes se describen a través de subtítulos generados automáticamente y el audio se transcribe en texto.

Beneficios:

  • Simplifica la recuperación, ya que todo se convierte en una representación de texto uniforme.

  • Funciona bien con los motores de búsqueda basados en texto existentes, eliminando la necesidad de una infraestructura multimodal especializada.

  • Puede mejorar la interpretabilidad ya que los resultados recuperados están en un formato legible por humanos.

Desventajas:

  • Pérdida de información: Es posible que ciertos detalles (por ejemplo, relaciones espaciales en imágenes, tono en audio) no se capturen completamente en las descripciones de texto.

  • Depende de la calidad de los subtítulos/transcripciones: los errores en las anotaciones automáticas pueden reducir la eficacia de la recuperación.

  • No es óptimo para consultas puramente visuales o auditivas , ya que el proceso de conversión podría eliminar el contexto esencial.

3. Recuperación separada

Mantiene modelos distintos para cada modalidad. El sistema realiza búsquedas separadas para cada tipo de datos y luego combina los resultados.

Beneficios:

  • Permite la optimización personalizada por modalidad, mejorando la precisión de la recuperación para cada tipo de datos.

  • Menos dependencia de modelos multimodales complejos, lo que facilita la integración de los sistemas de recuperación existentes.

  • Proporciona un control detallado sobre la clasificación y la reclasificación, ya que los resultados de diferentes modalidades se pueden combinar dinámicamente.

Desventajas:

  • Requiere fusión de resultados, lo que hace que el proceso de recuperación y clasificación sea más complejo.

  • Puede generar respuestas inconsistentes si diferentes modalidades devuelven información contradictoria.

  • Mayor costo computacional ya que se realizan búsquedas independientes para cada modalidad, aumentando el tiempo de procesamiento.

Nuestra elección: espacio vectorial compartido con ImageBind

Entre estos enfoques, elegimos el espacio vectorial compartido, una estrategia que se alinea perfectamente con la necesidad de búsquedas multimodales eficientes. Nuestra implementación se basa en ImageBind, un modelo capaz de representar múltiples modalidades (texto, imagen, audio y video) en un espacio vectorial común. Esto nos permite:

  • Realice búsquedas multimodales entre diferentes formatos de medios sin necesidad de convertir todo en texto.

  • Emplee incrustaciones altamente expresivas para capturar relaciones entre diferentes modalidades.

  • Garantiza la escalabilidad y la eficiencia, almacenando incrustaciones optimizadas para una recuperación rápida en Elasticsearch.

Al adoptar este enfoque, creamos una estable canalización de búsqueda multimodal, donde una consulta de texto puede recuperar directamente imágenes o audio sin procesamiento previo adicional. Este método amplía las aplicaciones prácticas desde la búsqueda inteligente en grandes repositorios hasta los sistemas avanzados de recomendación multimodal.

En la ilustración siguiente se muestra el flujo de datos dentro de la canalización RAG multimodal, destacando el proceso de indexación, recuperación y generación de respuestas basado en datos multimodales:

Flujo de datos multimodal

¿Cómo funciona el espacio de incrustación?

Tradicionalmente, las incrustaciones de texto provienen de modelos de lenguaje (por ejemplo, BERT, GPT). Ahora, con modelos multimodales nativos como ImageBind de Meta AI, tenemos una columna vertebral que genera vectores para múltiples modalidades:

  • Texto: Las oraciones y los párrafos se transforman en vectores de la misma dimensión.

  • Imágenes (visión): los pixeles se asignan al mismo espacio dimensional empleado para el texto.

  • Audio: Las señales de sonido se convierten en incrustaciones comparables a imágenes y texto.

  • Mapas de profundidad: Los datos de profundidad se procesan y también dan como resultado vectores.

Por lo tanto, cualquier pista (texto, imagen, audio, profundidad) se puede comparar con cualquier otra empleando métricas de similitud vectorial como la similitud de coseno. Si una muestra de audio riendo y una imagen de la cara de un sospechoso están "cerca" en este espacio, podemos inferir alguna correlación (por ejemplo, la misma identidad).

Etapa 1 - Recopilación de pistas de la escena del crimen

Antes de analizar la evidencia, necesitamos recopilarla. El crimen en Gotham dejó rastros que pueden estar ocultos en imágenes, audio, textos e incluso datos de profundidad. Organicemos estas pistas para alimentar nuestro sistema.

¿Qué tenemos?

El Comisionado Gordon nos envió los siguientes archivos que contienen evidencia recopilada de la escena del crimen en cuatro modalidades diferentes:

Descripción y modalidad de la pista

a) Imágenes (2 fotos)

  • crime_scene1.jpg, crime_scene2.jpg → Fotos tomadas de la escena del crimen. Muestra rastros sospechosos en el suelo.

  • suspect_spotted.jpg → Imagen de la cámara de seguridad que muestra una silueta huyendo de la escena.

b) Audio (1 grabación)

  • joker_laugh.wav → Un micrófono cerca de la escena del crimen capturó una risa siniestra.

c) Texto (1 mensaje)

  • Riddle.txt, note2.txt → Se encontraron algunas notas misteriosas en el lugar, posiblemente dejadas por el criminal.

d) Profundidad (1 mapa de profundidad)

  • depth_suspect.png → Una cámara de seguridad con un sensor de profundidad capturó a un sospechoso en un callejón cercano.

  • jdancing-depth.png → Una cámara de seguridad con un sensor de profundidad capturó a un sospechoso bajando por la estación de metro.

Estas pruebas están en diferentes formatos y no pueden analizar directamente de la misma manera. Necesitamos transformarlos en incrustaciones, vectores numéricos que permitan la comparación intermodal.

Organización de archivos

Antes de comenzar a procesar, debemos cerciorarnos de que todas las pistas estén organizadas correctamente en el directorio data/ para que la canalización funcione sin problemas.

Estructura de directorios esperada:

data/
├── images/
│   ├── crime_scene1.jpg
│   ├── suspect_spotted.jpg
│   ...
├── audios/
│   ├── joker_laugh.wav
│   ...
├── texts/
│   ├── riddle.txt
│   ... 
├── depths/
│   ├── depth_suspect.png

Código para verificar la organización de pistas

Antes de continuar, cerciorémonos de que todos los archivos necesarios estén en la ubicación correcta.

import os

# Base directory for clues
data_dir = "data"

# List of expected files
evidences = {
    "images": ["crime_scene1.jpg","crime_scene1.jpg", "joker_alley.jpg"],
    "audios": ["joker_laugh.wav"],
    "texts": ["riddle.txt", "note2.txt”],
    "depths": ["depth_suspect.png", "jdancing-depth.png"]
}

# Create directories if they don't exist
for category, files in evidences.items():
    category_path = os.path.join(data_dir, category)
    os.makedirs(category_path, exist_ok=True)

    for file in files:
        file_path = os.path.join(category_path, file)
        if not os.path.exists(file_path):
            print(f"Warning: {file} not found in {category_path}.")

print("All files are correctly organized!")

Ejecución del archivo

python  stages/01-stage/files_check.py

Salida esperada (si todos los archivos son correctos):

All files are correctly organized!

Salida esperada (si falta algún archivo):

Warning: joker_laugh.wav not found in data/audios/
Warning: depth_suspect.png not found in data/depths/

Este script ayuda a prevenir errores antes de comenzar a generar incrustaciones e indexarlas en Elasticsearch.

Etapa 2 - Organización de la evidencia

Generación de incrustaciones con ImageBind

Para unificar las pistas, necesitamos transformarlas en incrustaciones, representaciones vectoriales que capturen el significado de cada modalidad. Usaremos ImageBind, un modelo de Meta AI que genera incrustaciones para diferentes tipos de datos (imágenes, audio, texto y mapas de profundidad) dentro de un espacio vectorial compartido.

Generación de incrustaciones con ImageBind

¿Cómo funciona ImageBind?

Para comparar diferentes tipos de evidencia (imágenes, audio, texto y mapas de profundidad), necesitamos transformarlos en vectores numéricos usando ImageBind. Este modelo permite convertir cualquier tipo de entrada al mismo formato de incrustación, lo que permite búsquedas multimodales entre modalidades.

A continuación se muestra un código optimizado (src/embedding_generator.py) para generar incrustaciones para cualquier tipo de entrada empleando los procesadores apropiados para cada modalidad:

class EmbeddingGenerator:
    """Class for generating multimodal embeddings using ImageBind."""
    
    def __init__(self):
        self.device = "cuda" if torch.cuda.is_available() else "cpu"
        self.model = self._load_model()

    def _load_model(self):
        """Loads the ImageBind model and sets it to inference mode."""
        model = imagebind_model.imagebind_huge(pretrained=True)
        model.eval()
        model.to(self.device)
        return model

    def generate_embedding(self, input_data, modality):
        """Generates embedding for different modalities"""
        processors = {
            "vision": lambda x: data.load_and_transform_vision_data(x, self.device),
            "audio": lambda x: data.load_and_transform_audio_data(x, self.device),
            "text": lambda x: data.load_and_transform_text(x, self.device),
            "depth": self.process_depth
        }
        
        try:
            # Input type verification
            if not isinstance(input_data, list):
                raise ValueError(f"Input data must be a list. Received: {type(input_data)}")
                
            # Convert input data to a tensor format that the model can process
            # For images: [batch_size, channels, height, width] 
            # For audio: [batch_size, channels, time] 
            # For text: [batch_size, sequence_length]
            inputs = {modality: processors[modality](input_data)}
            with torch.no_grad():
                embedding = self.model(inputs)[modality]
            return embedding.squeeze(0).cpu().numpy()
        except Exception as e:
            logger.error(f"Error generating {modality} embedding: {str(e)}", exc_info=True)
            raise

Un tensor es una estructura de datos fundamental en el aprendizaje automático y el aprendizaje profundo, especialmente cuando se trabaja con modelos como ImageBind. En nuestro contexto:

input_tensor = processors[modality]([input_data], self.device)

Aquí, el tensor representa los datos de entrada (imagen, audio o texto) convertidos a un formato matemático que el modelo puede procesar. Específicamente:

  • Para imágenes: El tensor representa la imagen como una matriz multidimensional de valores numéricos (pixeles organizados por canales de altura, anchura y color).

  • Para audio: El tensor representa las ondas sonoras como una secuencia de amplitudes a lo largo del tiempo.

  • Para texto: El tensor representa palabras o tokens como vectores numéricos.

Probar la generación de incrustación:

Probemos nuestra generación de incrustación con el siguiente código. Almacénelo en 02-stage/test_embedding_generation.py y ejecútelo con este comando:

python stages/02-stage/test_embedding_generation.py
generator = EmbeddingGenerator()
image_embedding = generator.generate_embedding("data/images/crime_scene1.jpg","vision")

print(image_embedding.shape)

Resultado esperado:

(1024,)

Ahora, la imagen se transformó en un vector de 1024 dimensiones.

Etapa 3: Almacenamiento y búsqueda en Elasticsearch

Ahora que generamos las incrustaciones para la evidencia, necesitamos almacenarlas en una base de datos vectorial para permitir búsquedas eficientes. Para ello, emplearemos Elasticsearch, que soporta vectores densos (dense_vector) y permite búsquedas de similitud.

Este paso consta de dos procesos principales:

  • Indexación de las incrustaciones → Almacena los vectores generados en Elasticsearch.

  • Búsqueda de similitudes → Recupera los registros más similares a una nueva evidencia.

Indexación de la evidencia en Elasticsearch

Cada pieza de evidencia procesada por ImageBind (imagen, audio, texto o profundidad) se convierte en un vector de 1024 dimensiones. Necesitamos almacenar estos vectores en Elasticsearch para habilitar futuras búsquedas.

El siguiente código (src/elastic_manager.py) crea un índice en Elasticsearch y configura la asignación para almacenar las incrustaciones.

from elasticsearch import Elasticsearch, helpers
...

class ElasticsearchManager:
    """Manages multimodal operations in Elasticsearch"""
    
    def __init__(self):
        load_dotenv()  # Load variables from .env
        self.es = self._connect_elastic()
        self.index_name = "multimodal_content"
        self._setup_index()
    
    def _connect_elastic(self):
        """Connects to Elasticsearch"""
        return Elasticsearch(
            os.getenv("ELASTICSEARCH_URL"),  # Elasticsearch endpoint
            api_key=os.getenv("ELASTICSEARCH_API_KEY")
        )
    
    def _setup_index(self):
        """Sets up the index if it doesn't exist"""
        if not self.es.indices.exists(index=self.index_name):
            mapping = {
                "mappings": {
                    "properties": {
                        "embedding": {
                            "type": "dense_vector",
                            "dims": 1024,
                            "index": True,
                            "similarity": "cosine"
                        },
                        "modality": {"type": "keyword"},
                        "content": {"type": "binary"},
                        "description": {"type": "text"},
                        "metadata": {"type": "object"},
                        "content_path": {"type": "text"}
                    }
                }
            }
            self.es.indices.create(index=self.index_name, body=mapping)
    
    def index_content(self, embedding, modality, content=None, description="", metadata=None, content_path=None):
        """Indexes multimodal content"""
        doc = {
            "embedding": embedding.tolist(),
            "modality": modality,
            "description": description,
            "metadata": metadata or {},
            "content_path": content_path
        }
        
        if content:
            doc["content"] = base64.b64encode(content).decode() if isinstance(content, bytes) else content
        
        return self.es.index(index=self.index_name, document=doc)
    
    def search_similar(self, query_embedding, modality=None, k=5):
        """Searches for similar contents"""
        query = {
            "knn": {
                "field": "embedding",
                "query_vector": query_embedding.tolist(),
                "k": k,
                "num_candidates": 100,
                "filter": [{"term": {"modality": modality}}] if modality else []
            }
        }
        
        try:
            response = self.es.search(
                index=self.index_name,
                query=query,
                size=k            
            )
            
            # Return both source data and score for each hit
            return [{
                **hit["_source"],
                "score": hit["_score"]
            } for hit in response["hits"]["hits"]]
        
        except Exception as e:
            print(f"Error: processing search_evidence: {str(e)}")
            return "Error generating search evidence"

Ejecución de la indexación

Ahora, indexemos una evidencia para probar el proceso.

# Example: Indexing an image from the crime scene
generator = EmbeddingGenerator()
es_manager = ElasticsearchManager(cloud_id="YOUR_CLOUD_ID", api_key="YOUR_API_KEY")

image_embedding = generator.generate_embedding("data/images/crime_scene1.jpg", "vision")

response = es_manager.index_content(
    embedding=image_embedding,
    modality="vision",
    description="Photo of the crime scene with suspicious traces",
    content_path="data/images/crime_scene1.jpg"
)
print(json.dumps(response, indent=2))

Salida esperada en Elasticsearch (resumen del documento indexado):

{
    "embedding": [0.12, -0.53, 0.89, ...],  
    "modality": "vision",  
    "description": "Photo of the crime scene with suspicious traces",  
    "content_path": "data/images/crime_scene1.jpg"  
}

Para indexar todas las pruebas multimodales, ejecute el siguiente comando de Python:

python stages/03-stage/index_all_modalities.py

Ahora, la evidencia se almacena en Elasticsearch y está lista para recuperar cuando sea necesario.

Verificación del proceso de indexación

Luego de ejecutar el script de indexación, verifiquemos si toda nuestra evidencia se almacenó correctamente en Elasticsearch. Puedes usar las herramientas de desarrollo de Kibana para ejecutar algunas consultas de verificación:

1. Primero, verifique si se creó el índice:

GET _cat/indices/multimodal_content?v

2. Luego, verifique el recuento de documentos por modalidad:

GET multimodal_content/_search
{
  "size": 0,
  "aggs": {
    "modalities": {
      "terms": {
        "field": "modality.keyword"
      }
    }
  }
}

3. Finalmente, examine la estructura del documento indexado:

GET multimodal_content/_search
{
  "size": 1,
  "query": {
    "match_all": {}
  }
}

Resultados esperados:

  • Debe existir un índice denominado 'multimodal_content'.

  • Alrededor de 7 documentos distribuidos en diferentes modalidades (visión, audio, texto, profundidad).

  • Cada documento debe contener: incrustación, modalidad, descripción, metadatos y campos content_path.

Este paso de verificación garantiza que nuestra base de datos de evidencia esté configurada correctamente antes de proceder con las búsquedas de similitud.

Búsqueda de evidencia similar en Elasticsearch

Ahora que la evidencia fue indexada, podemos realizar búsquedas para encontrar los registros más similares a una nueva pista. Esta búsqueda emplea la similitud vectorial para devolver los registros más cercanos en el espacio de incrustación.

El código siguiente realiza esta búsqueda.

def search_similar_evidence(self, query_embedding, k=5, modality=None):
    """Performs a kNN search to find the most similar clues."""
    
    knn_query = {
        "field": "embedding",
        "query_vector": query_embedding.tolist(),
        "k": k,
        "num_candidates": 100
    }

    query_body = {"knn": knn_query}
    if modality:
        query_body = {
            "bool": {
                "must": [
                    query_body, 
                    {"term": {"modality": modality}}
                ]
            }
        }

    try:
      results = self.es.search(
        index=self.index_name,
        query=query_body,
        _source_includes=["description", "modality", "content_path"],
        size=k
      )
    except Exception as e:
            print(f"Error processing search_evidence: {str(e)}")
            return "Error generating search evidence”

    return results["hits"]["hits"]

Prueba de la búsqueda: uso de audio como consulta para resultados multimodales

Ahora, probemos la búsqueda de evidencia empleando un archivo de audio sospechoso. Necesitamos generar una incrustación para el archivo de la misma manera y buscar incrustaciones similares:

python stages/03-stage/search_by_audio.py
# Initialize classes
generator = EmbeddingGenerator()
es_manager = ElasticsearchManager(cloud_id="YOUR_CLOUD_ID", api_key="YOUR_API_KEY")

# Generate embedding for a suspicious audio
audio_embedding = generator.generate_embedding("data/audios/mysterious_laugh.wav", "audio")

# Search for similar evidence in Elasticsearch
similar_evidences = es_manager.search_similar_evidence(audio_embedding, k=3)

# Display the retrieved results
print("\n🔎 Similar evidence found:\n")
for i, evidence in enumerate(similar_evidences, start=1):
    description = evidence['_source']['description']
    modality = evidence['_source']['modality']
    score = evidence['_score']
    content_path = evidence['_source'].get('content_path', 'N/A')

    print(f"{i}. {description} ({modality})")
    print(f"   Similarity: {score:.4f}")
    print(f"   File path: {content_path}\n")

Salida esperada en el terminal:

🔎 Similar evidence found:

1. A sinister laugh captured near the crime scene (audio)
   Similarity: 0.9985
   File path: data/audios/joker_laugh.wav

2. The Joker with green hair, white face paint, and a sinister smile in an urban night setting. (vision)
   Similarity: 0.6068
   File path: data/images/joker_laughing.png

3. Suspect dancing (vision)
   Similarity: 0.5591
   File path: data/images/jdancing.png

Ahora, podemos analizar la evidencia recuperada y determinar su relevancia para el caso.

Más allá del audio: exploración de búsquedas multimodales

Invertir los roles: cualquier modalidad puede ser una "pregunta"

En nuestro sistema RAG multimodal , cada modalidad es una consulta de búsqueda potencial. Vayamos más allá del ejemplo de audio y exploremos cómo otros tipos de datos pueden iniciar investigaciones.

1. Búsqueda por texto (descifrar la nota del criminal)

Escenario: Encontró un mensaje de texto cifrado y desea encontrar evidencia relacionada.

python stages/03-stage/search_by_text.py
# Generate embedding from text
text = "Why so serious?"
embedding_text = generator.generate_embedding([text], "text")

# Search for related evidence
similar_evidences = es_manager.search_similar(
    query_embedding=embedding_text,
    k=3
)

Resultados esperados:

🔎 Similar evidence found:

1. Mysterious note found at the location (text)
   Similarity: 0.7639
   File path: data/texts/riddle.txt

2. The Joker with green hair, white face paint, and a sinister smile in an urban night setting. (vision)
   Similarity: 0.7161
   File path: data/images/joker_laughing.png

3. Why so serious (text)
   Similarity: 0.7132
   File path: data/texts/note2.txt

2. Búsqueda de imágenes (seguimiento de la escena del crimen sospechosa)

Escenario: Una nueva escena del crimen (crime_scene2.jpg) debe comparar con otras pruebas.

python stages/03-stage/search_by_image.py
# Generate embedding for a suspicious image
vision_embedding = generator.generate_embedding(["data/images/crime_scene2.jpg"], "vision")

# Search for similar evidence in Elasticsearch
similar_evidences = es_manager.search_similar(
    query_embedding=vision_embedding,
    k=3
)

Salida:

🔎 Similar evidence found:

1. Photo of the crime scene: A dark, rain-soaked alley is filled with playing cards, while a sinister graffiti of the Joker laughing stands out on the brick wall. (vision)
   Similarity: 0.8258
   File path: data/images/crime_scene1.jpg

2. The Joker with green hair, white face paint, and a sinister smile in an urban night setting. (vision)
   Similarity: 0.6897
   File path: data/images/joker_laughing.png

3. Suspect dancing (vision)
   Similarity: 0.6588
   File path: data/images/jdancing.png

3. Búsqueda de mapas de profundidad (búsqueda 3D)

Escenario: Un mapa jdancing-depth.png de profundidad () revela patrones de escape de imágenes.

python stages/03-stage/search_by_depth.py
# Generate embedding for a suspicious depth map
vision_embedding = generator.generate_embedding(["data/depths/jdancing-depth.png"], "depth")

# Search for similar evidence in Elasticsearch
similar_evidences = es_manager.search_similar(
    query_embedding=vision_embedding,
    modality="vision",
    k=3
)

Salida

🔎 Similar evidence found:

1. The Joker with green hair, white face paint, and a sinister smile in an urban night setting. (vision)
   Similarity: 0.5329
   File path: data/images/joker_laughing.png

2. Photo of the crime scene: A dark, rain-soaked alley is filled with playing cards, while a sinister graffiti of the Joker laughing stands out on the brick wall. (vision)
   Similarity: 0.5053
   File path: data/images/crime_scene1.jpg

3. Suspect dancing (vision)
   Similarity: 0.4859
   File path: data/images/jdancing.png

¿Por qué importa esto?

Cada modalidad revela conexiones únicas:

  • Texto → Patrones lingüísticos del sospechoso.

  • Imágenes → Reconocimiento de ubicaciones y objetos.

  • Reconstrucción de escenas en profundidad → 3D .

Ahora, tenemos una base de datos de evidencia estructurada en Elasticsearch, lo que nos permite almacenar y recuperar evidencia multimodal de manera eficiente.

Resumen de lo que hicimos:

  • Incrustaciones multimodales almacenadas en Elasticsearch.

  • Realizó búsquedas de similitud, encontrando evidencia relacionada con nuevas pistas.

  • Probó la búsqueda empleando un archivo de audio sospechoso, cerciorar de que el sistema funcione correctamente.

Siguiente paso: Emplearemos un LLM (Large Language Model) para analizar la evidencia recuperada y generar un reporte final.

Etapa 4 - Conectando los puntos con el LLM

Ahora que la evidencia fue indexada en Elasticsearch y se puede recuperar por similitud, necesitamos un LLM (Large Language Model) para analizarla y generar un reporte final para enviar al Comisionado Gordon. El LLM será responsable de identificar patrones, conectar pistas y sugerir un posible sospechoso en función de la evidencia recuperada.

Para esta tarea, usaremos GPT-4 Turbo, formulando un aviso detallado para que el modelo pueda interpretar los resultados de manera eficiente.

Integración de LLM

Para integrar el LLM en nuestro sistema, creamos la clase LLMAnalyzer (src/llm_analyzer.py), que recibe la evidencia recuperada de Elasticsearch y genera un informe forense empleando esta evidencia como contexto de solicitud.

import os
from openai import OpenAI
import logging
from dotenv import load_dotenv

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

class LLMAnalyzer:
    """Evidence analyzer using GPT-4"""
    
    def __init__(self):
        load_dotenv()
        self.client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
    
    def analyze_evidence(self, evidence_results):
        """
        Analyzes multimodal search results and generates a report
        
        Args:
            evidence_results: Dict with results by modality
            {
                'vision': [...],
                'audio': [...],
                'text': [...],
                'depth': [...]
            }
        """
        # Format evidence for the prompt
        evidence_summary = self._format_evidence(evidence_results)

        # final prompt
        prompt = f"""
You are a highly experienced forensic detective specializing in multimodal evidence analysis. Your task is to analyze the collected evidence (audio, images, text, depth maps) and conclusively determine the **prime suspect** responsible for the Gotham Central Bank case.

---

### **Collected Evidence:**
{evidence_summary}

### **Task:**
1. **Analyze all the evidence** and identify cross-modal connections.
2. **Determine the exact identity of the criminal** based on behavioral patterns, visual/auditory/textual clues, and symbolic markers.
3. **Justify your conclusion** by explaining why this suspect is definitively responsible.
4. **Assign a confidence score (0-100%)** to your conclusion.

---

### **Final Output Format (Strictly Follow This Format):**
- **Prime Suspect:** [Full Name or Alias]
- **Evidence Supporting Conclusion:** [Detailed breakdown of visual, auditory, textual, and behavioral evidence]
- **Behavioral Patterns:** [Key actions, motives, and criminal signature]
- **Confidence Level:** [0-100%]
- **Next Steps (if any):** [What additional evidence would further confirm the identity? If none, state "No further evidence required."]

If there is **insufficient evidence**, specify exactly what is missing and suggest what additional data would be needed for a conclusive identification.

This report must be **direct and definitive**--avoid speculation and provide a final, actionable determination of the suspect's identity.
"""
        try:
            response = self.client.chat.completions.create(
                model="gpt-4-turbo-preview",
                messages=[
                    {
                        "role": "system",
                        "content": "You are a forensic detective specialized in multimodal evidence analysis."
                    },
                    {"role": "user", "content": prompt_01}
                ],
                temperature=0.5,
                max_tokens=1000
            )
            
            report = response.choices[0].message.content
            logger.info("\n📋 Forensic Report Generated:")
            logger.info("=" * 50)
            logger.info(report)
            logger.info("=" * 50)
            
            return report
            
        except Exception as e:
            logger.error(f"Error generating report: {str(e)}")
            return None

Ajuste de temperatura en el análisis LLM:

Para nuestro sistema de análisis forense, empleamos una temperatura moderada de 0,5. Se eligió esta configuración equilibrada porque:

  • Representa un término medio entre las salidas deterministas (demasiado rígidas) y las altamente aleatorias;

  • A 0,5, el modelo mantiene suficiente estructura para proporcionar conclusiones forenses lógicas y justificables;

  • Esta configuración permite que el modelo identifique patrones y establezca conexiones mientras se mantiene dentro de parámetros razonables de análisis forense;

  • Equilibra la necesidad de resultados consistentes y confiables con la capacidad de generar análisis perspicaces.

Este ajuste moderado de temperatura ayuda a garantizar que nuestro análisis forense sea confiable y perspicaz, evitando conclusiones demasiado rígidas y demasiado especulativas.

Ejecución del análisis de evidencia

Ahora que tenemos la integración de LLM, necesitamos un script que conecte todos los componentes del sistema. Este script:

  • Busca evidencia similar en Elasticsearch.

  • Analice la evidencia recuperada empleando el LLM para generar un reporte final.

Código: Script de análisis de evidencia

python stages/04-stage/rag_crime_analyze.py
import sys
import os
sys.path.append(os.path.join(os.path.dirname(os.path.dirname(__file__)), 'src'))

from embedding_generator import EmbeddingGenerator
from elastic_manager import ElasticsearchManager
from llm_analyzer import LLMAnalyzer

import json
import logging
from dotenv import load_dotenv

# Setup logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

# Load environment variables
load_dotenv()

# Initialize classes
generator = EmbeddingGenerator()
es_manager = ElasticsearchManager()

llm = LLMAnalyzer()
logger.info("✅ All components initialized successfully")
    
try:
    evidence_data = {}
    
    # Get data for each modality
    test_files = {
        'vision': 'data/images/crime_scene2.jpg',
        'audio': 'data/audios/joker_laugh.wav',
        'text': 'Why so serious?',
        'depth': 'data/depths/jdancing-depth.png'
    }
    
    logger.info("🔍 Collecting evidence...")
    for modality, test_input in test_files.items():
        try:
            if modality == 'text':
                embedding = generator.generate_embedding([test_input], modality)
            else:
                embedding = generator.generate_embedding([str(test_input)], modality)
            
            results = es_manager.search_similar(embedding, k=2)
            if results:
                evidence_data[modality] = results
                logger.info(f"✅ Data retrieved for {modality}: {len(results)} results")
            else:
                logger.warning(f"⚠️ No results found for {modality}")
                
        except Exception as e:
            logger.error(f"❌ Error retrieving {modality} data: {str(e)}")
    
    if not evidence_data:
        raise ValueError("No evidence data found in Elasticsearch!")
    
    # Test forensic report generation
    logger.info("\n📝 Generating forensic report...")
    report = llm.analyze_evidence(evidence_data)
    
    if report:
        logger.info("✅ Forensic report generated successfully")
        logger.info("\n📊 Report Preview:")
        logger.info("+" * 50)
        logger.info(report)
        logger.info("+" * 50)
    else:
        raise ValueError("Failed to generate forensic report")
        
except Exception as e:
    logger.error(f"❌ Error in analysis : {str(e)}")

Salida esperada de LLM

**Prime Suspect:** The Joker

**Evidence Supporting Conclusion:**

- **Visual Evidence:**
  - The photo of the crime scene with playing cards scattered around and the graffiti of the Joker laughing matches the Joker's known calling cards and thematic elements. The similarity score of 0.83 indicates a high likelihood that these elements are directly associated with the Joker.
  - The image of the Joker with green hair, white face paint, and a sinister smile in an urban night setting, although with a lower similarity score of 0.69, still supports the presence or recent activity of the Joker in areas consistent with the crime scene's characteristics.

- **Auditory Evidence:**
  - The captured sinister laugh with a similarity score of 1.00 perfectly matches known audio profiles of the Joker, making it a direct auditory signature of his presence at or near the crime scene.
  - Despite the lower similarity score of 0.61, the second audio piece further corroborates the Joker's involvement through thematic consistency.

- **Textual Evidence:**
  - The mysterious note found at the location, with a similarity score of 0.76, likely contains thematic or direct references to the Joker's modus operandi or signature phrases, further implicating him in the crime.
  - The similarity score of 0.72 for the Joker's description in textual evidence reinforces the thematic connection to the crime scene.

- **Depth Evidence:**
  - Depth sensor capture of the suspect with a similarity score of 0.77 suggests a physical presence matching the Joker's known dimensions or characteristic movements.
  - The lower similarity score of 0.53 in the second depth evidence still contributes to the overall pattern of evidence pointing towards the Joker, albeit with less certainty.

**Behavioral Patterns:**
- The Joker is known for his theatrical crimes, often leaving behind a signature trail of chaos, including playing cards, sinister laughter, and thematic graffiti. These elements are not only consistent with his known criminal signature but also directly observed at the crime scene.
- His motives often include creating chaos, drawing attention to his acts, and challenging his arch-nemesis, Batman, making a high-profile bank heist fitting within his behavioral patterns.

**Confidence Level:** 95%

**Next Steps:** No further evidence required.

The combination of visual, auditory, textual, and depth evidence strongly points to the Joker as the prime suspect. The thematic consistency across multiple modes of evidence, combined with known behavioral patterns and criminal signature, leaves little doubt regarding his involvement. While there is always a small margin of uncertainty in forensic analysis, the evidence at hand provides a compelling case against the Joker with a high degree of confidence.

Conclusión: Caso resuelto

Con todas las pistas recopiladas y analizadas, el sistema RAG multimodal identificó a un sospechoso: The Joker.

Al combinar imágenes, audio, texto y mapas de profundidad en un espacio vectorial compartido usando ImageBind, el sistema pudo detectar conexiones que fueron imposibles de identificar manualmente. Elasticsearch garantizó búsquedas rápidas y eficientes, mientras que el LLM sintetizó la evidencia en un reporte claro y concluyente.

Sin embargo, el verdadero poder de este sistema va más allá de Gotham City. La arquitectura RAG multimodal abre las puertas a numerosas aplicaciones del mundo real:

  • Vigilancia urbana: Identificación de sospechosos en función de imágenes, audio y datos de sensores.

  • Análisis forense: Correlacionar evidencia de múltiples fuentes para resolver crímenes complejos.

  • Recomendación multimedia: Crear sistemas de recomendación que comprendan contextos multimodales (por ejemplo, sugerir música basada en imágenes o texto).

  • Tendencias en redes sociales: Detección de temas de tendencia en diferentes formatos de datos.

Ahora que aprendió a construir un sistema RAG multimodal, ¿por qué no probarlo con sus propias pistas?

¡Comparte tus descubrimientos con nosotros y ayuda a la comunidad a avanzar en el campo de la IA multimodal!

Agradecimientos especiales

Me gustaría agradecer a Adrian Cole por su valiosa contribución y revisión durante el proceso de definición de la arquitectura de implementación de este código.

Referencias

Preguntas frecuentes

¿Qué es un RAG multimodal?

El RAG multimodal es un método que permite a los sistemas de IA combinar información de diferentes formatos (como imágenes, videos y audio) para obtener respuestas más ricas y precisas.

¿Cómo implementar el GAR multimodal?

Para implementar un RAG multimodal, se emplean comúnmente tres estrategias: espacio vectorial compartido, modalidad de conexión a tierra única y recuperación separada

Contenido relacionado

Técnicas avanzadas de RAG parte 2: Consultas y pruebas

Han Xiang Choong

Resolución de entidades con Elasticsearch, parte 4: el desafío final

Jessica Moszkowicz

Automatización del análisis de logs en Streams con ML.

Nastia Havriushenko

Construir un agente de IA para RRHH con Elastic Agent Builder y GPT-OSS

Tomás Murúa

Técnicas avanzadas de RAG parte 1: Procesamiento de datos

Han Xiang Choong

¿Estás listo para crear experiencias de búsqueda de última generación?

No se logra una búsqueda suficientemente avanzada con los esfuerzos de uno. Elasticsearch está impulsado por científicos de datos, operaciones de ML, ingenieros y muchos más que son tan apasionados por la búsqueda como tú. Conectemos y trabajemos juntos para crear la experiencia mágica de búsqueda que te dará los resultados que deseas.

Pruébalo tú mismo