블로그

Elasticsearch로 멀티모달 RAG 시스템 구축하기: 고담시 이야기

텍스트, 오디오, 비디오, 이미지 데이터를 통합하여 더욱 풍부하고 맥락에 맞는 정보 검색을 제공하는 멀티모달 검색 증강 세대(RAG) 시스템을 구축하는 방법을 알아보세요.

이 블로그에서는 Elasticsearch를 사용해 멀티모달 RAG(검색 증강 생성) 파이프라인을 구축하는 방법에 대해 알아보세요. 이미지 바인딩을 활용하여 텍스트, 이미지, 오디오, 심도 맵 등 다양한 데이터 유형에 대한 임베딩을 생성하는 방법을 살펴보겠습니다. 또한 dense_vector와 k-NN 검색을 사용하여 이러한 임베딩을 Elasticsearch에서 효율적으로 저장하고 검색하는 방법도 알아보세요. 마지막으로, 검색된 증거를 분석하고 종합적인 최종 보고서를 생성하기 위해 대규모 언어 모델(LLM)을 통합합니다.

멀티모달 RAG 파이프라인은 어떻게 작동하나요?

  1. 단서 수집하기 → 고담의 범죄 현장에서 이미지, 오디오, 텍스트, 심도 맵을 수집합니다.

  2. 임베딩 생성 → 각 파일은 ImageBind 멀티모달 모델을 사용하여 벡터로 변환됩니다.

  3. Elasticsearch에서 인덱싱 → 효율적인 검색을 위해 벡터가 저장됩니다.

  4. 유사도로 검색 → 새로운 단서가 주어지면 가장 유사한 벡터가 검색됩니다.

  5. LLM이 증거를 분석하고 → GPT-4 모델이 응답을 종합하여 용의자를 식별합니다!

사용된 기술

  • 이미지 바인드 → 다양한 모달리티를 위한 통합 임베딩을 생성합니다.

  • Elasticsearch → 빠르고 효율적인 벡터 검색을 지원합니다.

  • LLM(GPT-4, OpenAI ) → 증거를 분석하고 최종 보고서를 생성합니다.

이 블로그는 누구를 위한 블로그인가요?

  • 멀티모달 벡터 검색에 관심이 있는 Elastic 사용자.

  • 멀티모달 RAG를 실제로 이해하고자 하는 개발자.

  • 여러 소스의 데이터를 분석할 수 있는 확장 가능한 솔루션을 찾고 계신 분이라면 누구나 가능합니다.

멀티모달 RAG를 위한 전제 조건: 환경 설정하기

고담시의 범죄를 해결하려면 기술 환경을 설정해야 합니다. 이 단계별 가이드를 따르세요:

1. 기술 요구 사항

구성 요소

사양

시스템 OS

Linux, macOS 또는 Windows

Python

3.10 이상

RAM

최소 8GB(16GB 권장)

GPU

선택 사항이지만 이미지 바인딩에 권장

2. 프로젝트 설정하기

모든 조사 자료는 GitHub에서 확인할 수 있으며, 이 대화형 범죄 해결 경험을 위해 Jupyter Notebook(Google Colab)을 사용할 예정입니다. 시작하려면 다음 단계를 따르세요:

주피터 노트북(Google Colab)으로 설정하기

1. 노트북에 액세스합니다.

  • 바로 사용할 수 있는 Google Colab 노트북을 열어보세요: Elasticsearch를 사용한 멀티모달 RAG.

  • 이 노트북에는 따라하는 데 필요한 모든 코드와 설명이 포함되어 있습니다.

2. 리포지토리 복제

# 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. 설치 종속성

 # 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. 자격 증명 구성

# 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

참고: ImageBind 모델(~2GB)은 처음 실행할 때 자동으로 다운로드됩니다.

이제 모든 설정이 완료되었으니 세부 사항을 살펴보고 범죄를 해결해 보겠습니다!

소개 고담시의 범죄

비 내리는 고담시의 어느 날 밤, 충격적인 범죄가 도시를 뒤흔듭니다. 고든 위원장이 수수께끼를 풀기 위해 여러분의 도움이 필요합니다. 단서는 다양한 형식으로 흩어져 있습니다: 흐릿한 이미지, 의문의 오디오, 암호화된 텍스트, 심지어 심도 지도까지. 최첨단 AI 기술을 사용하여 사건을 해결할 준비가 되셨나요?

이 블로그에서는 다양한 유형의 데이터(이미지,오디오, 텍스트, 심도 맵) 를 단일 검색 공간으로 통합하는 멀티모달 RAG(검색 증강 생성) 시스템을 구축하는 과정을 단계별로 안내합니다. ImageBind를 사용해 멀티모달 임베딩을 생성하고, Elasticsearch를 사용해 이러한 임베딩을 저장 및 검색하며, 대규모 언어 모델(LLM) 을 사용해 증거를 분석하고 최종 보고서를 생성합니다.

기본 사항: 멀티모달 RAG 아키텍처

멀티모달 RAG란 무엇인가요?

검색 증강 세대(RAG) 멀티모달의 등장으로 AI 모델과 상호 작용하는 방식이 혁신적으로 변화하고 있습니다. 전통적으로 RAG 시스템은 응답을 생성하기 전에 데이터베이스에서 관련 정보를 검색하는 텍스트 전용으로 작동합니다. 하지만 세상은 텍스트에만 국한되지 않습니다. 이미지, 동영상, 오디오에도 귀중한 지식이 담겨 있습니다. 그렇기 때문에 AI 시스템이 다양한 형식의 정보를 결합하여 더욱 풍부하고 정확한 응답을 제공하는 멀티모달 아키텍처가 각광받고 있습니다.

멀티모달 RAG를 위한 세 가지 주요 접근 방식

멀티모달 RAG를 구현하기 위해 일반적으로 세 가지 전략이 사용됩니다. 각 접근 방식에는 사용 사례에 따라 고유한 장점과 한계가 있습니다:

1. 공유 벡터 공간

서로 다른 양식의 데이터는 ImageBind와 같은 멀티모달 모델을 사용하여 공통 벡터 공간에 매핑됩니다. 이를 통해 텍스트 쿼리는 명시적인 형식 변환 없이 이미지, 동영상, 오디오를 검색할 수 있습니다.

장점:

  • 명시적인 형식 변환 없이 크로스 모달 검색을 지원합니다.

  • 텍스트, 이미지, 오디오 및 비디오에서 직접 검색할 수 있도록 서로 다른 양식 간의 유연한 통합을 제공합니다.

  • 다양한 데이터 유형에 맞게 확장할 수 있어 대규모 검색 애플리케이션에 유용합니다.

단점:

  • 교육에는 대규모 멀티모달 데이터 세트가 필요한데, 항상 사용할 수 있는 것은 아닙니다.

  • 공유 임베딩 공간은 양식 간의 관계가 완벽하게 보존되지 않는 의미론적 드리프트가 발생할 수 있습니다.

  • 멀티모달 모델의 편향은 데이터 세트 분포에 따라 검색 정확도에 영향을 미칠 수 있습니다.

2. 단일 접지 방식

모든 양식은 검색 전에 단일 형식(일반적으로 텍스트)으로 변환됩니다. 예를 들어, 이미지는 자동으로 생성된 캡션을 통해 설명되고 오디오는 텍스트로 변환됩니다.

장점:

  • 모든 것이 균일한 텍스트 표현으로변환되므로 검색이 간편해집니다.

  • 기존 텍스트 기반 검색 엔진과 잘 작동하므로 특수한 멀티모달 인프라가 필요하지 않습니다.

  • 검색된 결과가 사람이 읽을 수 있는 형식으로 제공되므로 해석 가능성을 높일 수 있습니다.

단점:

  • 정보 손실: 특정 세부 정보(예: 이미지의 공간 관계, 오디오의 음색)가 텍스트 설명에 완전히 포착되지 않을 수 있습니다.

  • 캡션/전사 품질에 따라 달라질 수 있습니다: 자동 주석의 오류로 인해 검색 효율성이 떨어질 수 있습니다.

  • 변환 과정에서 필수 컨텍스트가 제거될 수 있으므로 순수하게 시각적 또는 청각적 쿼리에는 적합하지 않습니다.

3. 별도 검색

각 양식에 대해 고유한 모델을 유지합니다. 시스템은 각 데이터 유형에 대해 개별 검색을 수행한 후 나중에 결과를 병합합니다.

장점:

  • 양식별 맞춤 최적화를 통해 각 데이터 유형에 대한 검색 정확도를 개선할 수 있습니다.

  • 복잡한 멀티모달 모델에 대한 의존도가 낮아져 기존 검색 시스템을 더 쉽게 통합할 수 있습니다.

  • 다양한 양식의 결과를 동적으로 결합할 수 있으므로 순위 및 재순위를 세밀하게 제어할 수 있습니다.

단점:

  • 결과를 융합해야 하므로 검색 및 순위 지정 프로세스가 더 복잡해집니다.

  • 서로 다른 양식에서 상충되는 정보를 반환하는 경우 일관되지 않은 응답이 생성될 수 있습니다.

  • 각 양식에 대해 독립적인 검색이 수행되므로 계산 비용이 높아져 처리 시간이 늘어납니다.

우리의 선택: ImageBind를 통한 벡터 공간 공유

이러한 접근 방식 중에서 효율적인 멀티모달 검색의 필요성에 완벽하게 부합하는 전략인 공유 벡터 공간을 선택했습니다. 구현은 공통 벡터 공간에서여러 양식(텍스트, 이미지, 오디오, 비디오) 을 표현할 수 있는 모델인 ImageBind를 기반으로 합니다. 이를 통해 가능합니다:

  • 모든 것을 텍스트로 변환할 필요 없이 다양한 미디어 형식 간에 교차 모드 검색을 수행하세요.

  • 표현력이 뛰어난 임베딩을 사용하여 다양한 양식 간의 관계를 포착하세요.

  • 확장성과 효율성을 보장하여 Elasticsearch에서 빠르게 검색할 수 있도록 최적화된 임베딩을 저장합니다.

이 접근 방식을 채택하여 텍스트 쿼리가 추가 전처리 없이 이미지나 오디오를 직접 검색할 수 있는 강력한 멀티모달 검색 파이프라인을 구축했습니다. 이 방법은 대규모 저장소의 지능형 검색에서 고급 멀티모달 추천 시스템에 이르기까지 실제 적용 범위를 확장합니다.

다음 그림은 멀티모달 RAG 파이프라인 내의 데이터 흐름을 보여주며, 멀티모달 데이터를 기반으로 한 인덱싱, 검색 및 응답 생성 프로세스를 강조합니다:

멀티모달 래그 데이터 흐름

임베딩 공간은 어떻게 작동하나요?

전통적으로 텍스트 임베딩은 언어 모델(예: BERT, GPT)에서 제공됩니다. 이제 메타 AI의 ImageBind와 같은 네이티브 멀티모달 모델을 통해 여러 모달리티에 대한 벡터를 생성하는 백본을 갖추게 되었습니다:

  • 텍스트: 문장과 단락이 같은 차원의 벡터로 변환됩니다.

  • 이미지(시각): 픽셀은 텍스트에 사용되는 것과 동일한 차원 공간에 매핑됩니다.

  • 오디오: 사운드 신호는 이미지 및 텍스트와 유사한 임베딩으로 변환됩니다.

  • 뎁스 맵: 심도 데이터가 처리되어 벡터로 생성됩니다.

따라서 코사인 유사도와 같은 벡터 유사도 메트릭을 사용하여 모든단서(텍스트, 이미지, 오디오, 깊이)를 다른 모든 단서와 비교할 수 있습니다. 이 공간에서 웃는 오디오 샘플과 용의자 얼굴 이미지가 '비슷하다'면 상관관계(예: 동일한 신원)를 추론할 수 있습니다.

1단계 - 범죄 현장 단서 수집

증거를 분석하기 전에 먼저 증거를 수집해야 합니다. 고담에서 발생한 범죄는 이미지, 오디오, 텍스트, 심지어 심도 데이터에 숨겨져 있을 수 있는 흔적을 남겼습니다. 이러한 단서들을 정리하여 시스템에 입력해 보겠습니다.

뭐가 있나요?

고든 커미셔너는 범죄 현장에서 수집한 증거가 담긴 다음 파일을 네 가지 방식으로 보내주었습니다:

트랙 설명 및 양식

a) 이미지(사진 2장)

  • crime_scene1.jpg, crime_scene2.jpg → 범죄 현장에서 찍은 사진. 지상에 의심스러운 흔적을 표시합니다.

  • suspect_spotted.jpg → 현장에서 도망가는 실루엣을 보여주는 보안 카메라 이미지.

b) 오디오(녹음 1건)

  • joker_laugh.wav → 범죄 현장 근처의 마이크에 불길한 웃음이 포착되었습니다.

c) 문자(메시지 1건)

  • Riddle.txt, note2.txt → 범인이 남긴 것으로 추정되는 의문의 메모가 발견되었습니다.

d) 깊이(깊이 맵 1개)

  • depth_suspect.png → 심도 센서가 장착된 보안 카메라가 인근 골목에서 용의자를 포착했습니다.

  • jdancing-depth.png → 깊이 센서가 장착된 보안 카메라가 지하철역에서 내려가는 용의자를 포착했습니다.

이러한 증거는 서로 다른 형식이며 동일한 방식으로 직접 분석할 수 없습니다. 이를 임베딩, 즉 모달 간 비교가 가능한 숫자 벡터로 변환해야 합니다.

파일 정리

처리를 시작하기 전에 파이프라인이 원활하게 실행되도록 모든 단서가 데이터/디렉토리에 제대로 정리되어 있는지 확인해야 합니다.

예상 디렉토리 구조:

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

단서 구성 확인 코드

계속 진행하기 전에 필요한 모든 파일이 올바른 위치에 있는지 확인합니다.

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!")

파일 실행

python  stages/01-stage/files_check.py

예상 출력(모든 파일이 올바른 경우):

All files are correctly organized!

예상 출력(누락된 파일이 있는 경우):

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

이 스크립트는 임베딩을 생성하고 Elasticsearch로 색인하기 전에 오류를 방지하는 데 도움이 됩니다.

2단계 - 증거 정리

이미지 바인딩으로 임베딩 생성하기

단서를 통합하려면 각 양식의 의미를 포착하는 임베딩, 즉 벡터 표현으로 변환해야 합니다. 공유 벡터 공간 내에서 다양한 데이터 유형(이미지,오디오, 텍스트, 심도 맵) 에 대한 임베딩을 생성하는 Meta AI의 모델인 ImageBind를 사용할 것입니다.

이미지 바인딩으로 임베딩 생성하기

ImageBind는 어떻게 작동하나요?

다양한 유형의 증거(이미지, 오디오, 텍스트, 심도 맵)를 비교하려면 ImageBind를 사용하여 이를 숫자 벡터로 변환해야 합니다. 이 모델을 사용하면 모든 유형의 입력을 동일한 임베딩 형식으로 변환할 수 있으므로 모달리티 간에 교차 모달 검색이 가능합니다.

아래는 각 양식에 적합한 프로세서를 사용하여 모든 유형의 입력에 대한 임베딩을 생성하는 최적화된 코드(src/embedding_generator.py)입니다:

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

텐서는 머신러닝과 딥러닝의 기본 데이터 구조로, 특히 ImageBind와 같은 모델로 작업할 때 유용합니다. 우리 상황에서는:

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

여기서 텐서는 입력 데이터(이미지, 오디오 또는 텍스트)를 모델이 처리할 수 있는 수학적 형식으로 변환한 것을 나타냅니다. 구체적으로

  • 이미지의 경우: 텐서는 이미지를 숫자 값(높이, 너비, 색상 채널별로 구성된 픽셀)의 다차원 행렬로 표현합니다.

  • 오디오의 경우: 텐서는 음파를 시간에 따른 진폭의 시퀀스로 표현합니다.

  • 텍스트의 경우: 텐서는 단어 또는 토큰을 숫자 벡터로 표현합니다.

임베딩 생성 테스트:

다음 코드를 사용하여 임베딩 생성을 테스트해 보겠습니다. 02-stage/test_embedding_generation.py에 저장하고 다음 명령으로 실행합니다:

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)

예상 출력:

(1024,)

이제 이미지가 1024차원 벡터로 변환되었습니다.

3단계 - Elasticsearch의 저장 및 검색

이제 증거를 위한 임베딩을 생성했으므로 효율적인 검색을 위해 벡터 데이터베이스에 저장해야 합니다. 이를 위해 고밀도 벡터(dense_vector)를 지원하고 유사도 검색이 가능한 Elasticsearch를 사용하겠습니다.

이 단계는 두 가지 주요 프로세스로 구성됩니다:

  • 임베딩 인덱싱 → 생성된 벡터를 Elasticsearch에 저장합니다.

  • 유사성 검색 → 새 증거 자료와 가장 유사한 기록을 검색합니다.

Elasticsearch에서 증거 색인 생성

이미지 바인드로 처리된 각 증거(이미지, 오디오, 텍스트 또는 깊이)는 1024차원 벡터로 변환됩니다. 향후 검색을 가능하게 하려면 이러한 벡터를 Elasticsearch에 저장해야 합니다.

다음 코드(src/elastic_manager.py)는 Elasticsearch에서 인덱스를 생성하고 임베딩을 저장하도록 매핑을 구성합니다.

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"

인덱싱 실행

이제 프로세스를 테스트하기 위해 증거를 색인화해 보겠습니다.

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

Elasticsearch의 예상 출력(색인된 문서의 요약):

{
    "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"  
}

모든 멀티모달 증거를 색인하려면 다음 Python 명령을 실행하세요:

python stages/03-stage/index_all_modalities.py

이제 증거는 Elasticsearch에 저장되며 필요할 때 바로 검색할 수 있습니다.

인덱싱 프로세스 확인

인덱싱 스크립트를 실행한 후, 모든 증거가 Elasticsearch에 올바르게 저장되었는지 확인해 보겠습니다. Kibana의 개발 도구를 사용하여 몇 가지 확인 쿼리를 실행할 수 있습니다:

1. 먼저 인덱스가 생성되었는지 확인합니다:

GET _cat/indices/multimodal_content?v

2. 그런 다음 양식별 문서 수를 확인합니다:

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

3. 마지막으로 색인된 문서 구조를 살펴봅니다:

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

예상 결과:

  • 멀티모달 콘텐츠`라는 인덱스가 존재해야 합니다.

  • 다양한 모달리티(시각, 오디오, 텍스트, 깊이)에 분산된 약 7개의 문서.

  • 각 문서에는 임베딩, 모달리티, 설명, 메타데이터, 콘텐츠_경로 필드가 포함되어야 합니다.

이 검증 단계는 유사도 검색을 진행하기 전에 증거 데이터베이스가 제대로 설정되었는지 확인하는 단계입니다.

Elasticsearch에서 유사한 증거 검색하기

이제 증거가 색인되었으므로 검색을 수행하여 새로운 단서와 가장 유사한 기록을 찾을 수 있습니다. 이 검색은 벡터 유사성을 사용하여 임베딩 공간에서 가장 가까운 레코드를 반환합니다.

다음 코드는 이 검색을 수행합니다.

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"]

검색 테스트 - 멀티모달 결과에 대한 쿼리로 오디오 사용하기

이제 의심스러운 오디오 파일을 사용하여 증거 검색을 테스트해 보겠습니다. 같은 방법으로 파일에 대한 임베딩을 생성하고 유사한 임베딩을 검색해야 합니다:

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

터미널의 예상 출력입니다:

🔎 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

이제 검색된 증거를 분석하여 사건과의 관련성을 판단할 수 있습니다.

오디오를 넘어서 - 멀티모달 검색 살펴보기

역할 바꾸기: 모든 양식은 "질문이 될 수 있습니다."

멀티모달 RAG 시스템에서 모든 모달리티는 잠재적인 검색 쿼리입니다. 오디오 예제에서 더 나아가 다른 데이터 유형으로 조사를 시작하는 방법을 살펴보겠습니다.

1. 텍스트로 검색(범죄자의 메모 해독)

시나리오: 암호화된 문자 메시지를 발견하고 관련 증거를 찾고자 합니다.

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
)

예상 결과:

🔎 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. 이미지 검색(의심스러운 범죄 현장 추적)

시나리오: 새로운 범죄 현장 (crime_scene2.jpg)을 다른 증거와 비교해야 합니다.

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
)

출력:

🔎 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. 심도 지도 검색(3D 추적)

시나리오: 뎁스 맵 (jdancing-depth.png)을 통해 이미지 이스케이프 패턴을 파악합니다.

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
)

출력

🔎 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

이것이 왜 중요한가요?

각 양식은 고유한 연결성을 드러냅니다:

  • 텍스트 → 용의자의 언어 패턴.

  • 이미지위치 및 물체인식.

  • 깊이 → 3D 장면 재구성.

이제 Elasticsearch에 구조화된 증거 데이터베이스를 확보하여 멀티모달 증거를 효율적으로 저장하고 검색 할 수 있습니다.

그 동안의 성과를 요약합니다:

  • Elasticsearch에 저장된 멀티모달 임베딩.

  • 유사도 검색을 수행하여 새로운 단서와 관련된 증거를 찾습니다.

  • 의심스러운 오디오 파일을 사용하여 검색을 테스트하여 시스템이 올바르게 작동하는지 확인했습니다.

다음 단계: LLM (대규모 언어 모델)을 사용하여 검색된 증거를 분석하고 최종 보고서를 생성합니다.

4단계 - LLM으로 점 연결하기

이제 증거가 Elasticsearch에 색인되어 유사성을 기준으로 검색할 수 있게 되었으므로, 이를 분석하고 최종 보고서를 생성하여 Gordon 위원에게 보낼 LLM(대규모 언어 모델) 이 필요합니다. LLM은 패턴을 식별하고 단서를 연결하며 검색된 증거를 바탕으로 용의자를 제안하는 역할을 담당합니다.

이 작업에서는 모델이 결과를 효율적으로 해석할 수 있도록 세부 프롬프트를 공식화하여 GPT-4 Turbo를 사용합니다.

LLM 통합

LLM을 우리 시스템에 통합하기 위해,우리는 src/llm_analyzer.py Elasticsearch에서 검색된 증거를 수신하고 이 증거를 프롬프트 컨텍스트로 사용하여 포렌식 보고서를 생성하는 LLMAnalyzer 클래스()를 만들었습니다.

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

LLM 분석의 온도 설정:

포렌식 분석 시스템에서는 0.5의 적당한 온도를 사용합니다. 이 균형 잡힌 설정이 선택된 이유는 다음과 같습니다:

  • 이는 결정론적(너무 엄격함)인 결과와 매우 무작위적인 결과 사이의 중간 지점을 나타냅니다;

  • 0.5에서 이 모델은 논리적이고 정당한 포렌식 결론을 내릴 수 있는 충분한 구조를 유지합니다;

  • 이 설정을 사용하면 모델이 합리적인 포렌식 분석 매개변수 내에서 패턴을 식별하고 연결할 수 있습니다;

  • 일관되고 신뢰할 수 있는 결과물에 대한 요구와 통찰력 있는 분석을 생성하는 기능의 균형을 맞추고 있습니다.

이 적당한 온도 설정은 포렌식 분석이 지나치게 엄격하거나 지나치게 추측적인 결론을 내리지 않도록 하여 신뢰성과 통찰력을 모두 확보하는 데 도움이 됩니다.

증거 분석 실행

이제 LLM 통합이 완료되었으므로 모든 시스템 구성 요소를 연결하는 스크립트가 필요합니다. 이 스크립트는

  • Elasticsearch에서유사한 증거를 검색하세요.

  • 검색된 증거를 LLM을 사용하여 분석하여 최종 보고서를 생성합니다.

코드: 증거 분석 스크립트

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

예상 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.

결론 결론: 사례 해결

모든 단서를 수집하고 분석한 결과, 멀티모달 RAG 시스템이 용의자를 찾아냈습니다: 조커입니다.

이미지 바인드를 사용하여 이미지, 오디오, 텍스트, 심도 맵을 공유 벡터 공간에 결합함으로써 시스템은 수동으로 식별할 수 없었던 연결을 감지할 수 있었습니다. Elasticsearch는 빠르고 효율적인 검색을 보장했고, LLM은 증거를 명확하고 결정적인 보고서로 종합했습니다.

하지만 이 시스템의 진정한 힘은 고담시를 넘어선 곳에 있습니다. 멀티모달 RAG 아키텍처는 수많은 실제 애플리케이션에 대한 문을 열어줍니다:

  • 도시 감시: 이미지, 오디오, 센서 데이터를 기반으로 용의자를 식별합니다.

  • 포렌식 분석: 여러 출처의 증거를 상호 연관시켜 복잡한 범죄를 해결합니다.

  • 멀티미디어 추천: 멀티모달 컨텍스트를 이해하는 추천 시스템 만들기(예: 이미지 또는 텍스트를 기반으로 음악 제안).

  • 소셜 미디어 트렌드: 다양한 데이터 형식에서 인기 있는 토픽을 감지합니다.

이제 멀티모달 RAG 시스템을 구축하는 방법을 배웠으니 자신만의 단서를 가지고 테스트해 보는 것은 어떨까요?

여러분의 발견을 저희와 공유하고 커뮤니티가 멀티모달 AI 분야에서 발전할 수 있도록 도와주세요!

특별한 감사

이 코드의 배포 아키텍처를 정의하는 과정에서 귀중한 기여와 검토를 해주신 Adrian Cole에게 감사의 말씀을 전하고 싶습니다.

참고 자료

자주 묻는 질문

멀티모달 RAG란 무엇인가요?

멀티모달 RAG는 AI 시스템이 이미지, 동영상, 오디오 등 다양한 형식의 정보를 결합하여 보다 풍부하고 정확한 응답을 제공하는 방법입니다.

멀티모달 RAG를 구현하는 방법은 무엇인가요?

멀티모달 RAG를 구현하기 위해 일반적으로 공유 벡터 공간, 단일 접지 방식, 별도 검색의 세 가지 전략이 사용됩니다.

관련 콘텐츠

고급 RAG 기술 2부: 쿼리 및 테스트

Han Xiang Choong

Elasticsearch를 통한 엔티티 해석, 4부: 최종 과제

Jessica Moszkowicz

ML을 활용하여 Streams에서 로그 구문 분석 자동화하기

Nastia Havriushenko

Elastic 에이전트 빌더와 GPT-OSS로 HR용 AI 에이전트 구축하기

Tomás Murúa

고급 RAG 기술 1부: 데이터 처리

Han Xiang Choong

최첨단 검색 환경을 구축할 준비가 되셨나요?

충분히 고급화된 검색은 한 사람의 노력만으로는 달성할 수 없습니다. Elasticsearch는 여러분과 마찬가지로 검색에 대한 열정을 가진 데이터 과학자, ML 운영팀, 엔지니어 등 많은 사람들이 지원합니다. 서로 연결하고 협력하여 원하는 결과를 얻을 수 있는 마법 같은 검색 환경을 구축해 보세요.

직접 사용해 보세요