Gemini

Gemini Embedding 2 완전 가이드 — 텍스트, 이미지, 비디오, 오디오를 하나의 벡터 공간에

cell-devlog 2026. 5. 6. 15:33
반응형

 

 

지금까지 멀티모달 RAG를 만들려면 텍스트 임베딩 모델, 이미지 임베딩 모델, 비디오 처리기를 따로 연결해야 했습니다. Gemini Embedding 2는 이걸 API 호출 하나로 끝냅니다.

[핵심 요약]
→ 출시: 2026년 3월 10일 (Public Preview), 4월 22일 GA
→ 모델 ID: gemini-embedding-2 (GA), gemini-embedding-2-preview
→ 정체: Google 최초 네이티브 멀티모달 임베딩 모델
→ 지원 입력: 텍스트, 이미지(최대 6개), 비디오(120초), 오디오(180초), PDF(6페이지)
→ 벡터 크기: 3072차원 (기본값), Matryoshka로 축소 가능
→ 언어: 100개+ 지원
→ 핵심: 5가지 모달리티를 단일 임베딩 공간에 매핑
→ 가격: 텍스트 $0.20/1M tokens, 배치 $0.10/1M
→ 주의: gemini-embedding-001(텍스트 전용)과 벡터 공간 비호환 → 전체 재임베딩 필요

 


왜 이게 게임체인저인가

[기존 멀티모달 RAG 파이프라인]

질문: "이 회의 영상에서 예산 관련 내용 찾아줘"

기존 방식:
1. 비디오 → Whisper로 음성 추출 → 텍스트 변환
2. 텍스트 → text-embedding-3-large로 임베딩
3. 이미지 슬라이드 → CLIP으로 별도 임베딩
4. 두 벡터 DB를 각각 검색
5. 결과 재순위화 (reranking)
6. LLM에 전달

문제:
→ 파이프라인 3개 관리
→ 음성→텍스트 변환 시 정보 손실
→ 3개 모달리티 검색 결과 합치기 복잡
→ 레이턴시: 각 변환마다 추가됨

Gemini Embedding 2:
1. 비디오 + 이미지 + 텍스트 → API 한 번
2. 단일 벡터 DB 검색
3. 끝

→ 실제 사례: Sparkonomy 레이턴시 70% 감소
→ 실제 사례: Everlaw 법률 문서 검색 리콜 20% 향상

실전 1 — 기본 설치 및 사용

# pip install google-genai

from google import genai
from google.genai import types

client = genai.Client(api_key="GEMINI_API_KEY")

# ===== 텍스트 임베딩 =====
result = client.models.embed_content(
    model="gemini-embedding-2",
    contents=["파이썬에서 리스트 중복 제거하는 법"],
    config=types.EmbedContentConfig(
        task_type="RETRIEVAL_DOCUMENT",
        output_dimensionality=3072  # 기본값
    )
)
print(result.embeddings[0].values[:5])
# [0.023, -0.041, 0.089, ...]
# ===== 이미지 임베딩 =====
import base64
from pathlib import Path

def embed_image(image_path: str) -> list[float]:
    """이미지 파일 → 임베딩"""
    image_bytes = Path(image_path).read_bytes()
    b64 = base64.b64encode(image_bytes).decode()

    result = client.models.embed_content(
        model="gemini-embedding-2",
        contents=[
            types.Content(parts=[
                types.Part(
                    inline_data=types.Blob(
                        mime_type="image/png",
                        data=b64
                    )
                )
            ])
        ]
    )
    return result.embeddings[0].values

# 사용
img_embedding = embed_image("product_photo.png")
# ===== 비디오 임베딩 (오디오 포함) =====
def embed_video(video_path: str) -> list[float]:
    """비디오 파일 → 임베딩 (음성 변환 없이 직접)"""
    video_bytes = Path(video_path).read_bytes()
    b64 = base64.b64encode(video_bytes).decode()

    result = client.models.embed_content(
        model="gemini-embedding-2",
        contents=[
            types.Content(parts=[
                types.Part(
                    inline_data=types.Blob(
                        mime_type="video/mp4",
                        data=b64
                    )
                )
            ])
        ]
    )
    return result.embeddings[0].values

# 기존 방식 대비:
# 기존: video → Whisper(ASR) → 텍스트 → text-embedding
# 신규: video → gemini-embedding-2 (직접)
# → 정보 손실 없음, 레이턴시 감소
# ===== PDF 임베딩 =====
def embed_pdf(pdf_path: str) -> list[float]:
    """PDF 파일 → 임베딩 (OCR 포함)"""
    pdf_bytes = Path(pdf_path).read_bytes()
    b64 = base64.b64encode(pdf_bytes).decode()

    result = client.models.embed_content(
        model="gemini-embedding-2",
        contents=[
            types.Content(parts=[
                types.Part(
                    inline_data=types.Blob(
                        mime_type="application/pdf",
                        data=b64
                    )
                )
            ])
        ]
    )
    return result.embeddings[0].values

# 특징: OCR 자동 처리 → 스캔 문서도 임베딩 가능

실전 2 — Task Prefix로 성능 최적화

Gemini Embedding 2는 task prefix를 지정하면 해당 태스크에 최적화된 임베딩을 생성합니다.

# Task Prefix 종류와 사용법

def embed_with_task(content: str, task: str) -> list[float]:
    """태스크별 최적화 임베딩"""
    # 태스크에 따라 prefix 적용
    prefixed = f"task: {task} | query: {content}"

    result = client.models.embed_content(
        model="gemini-embedding-2",
        contents=[prefixed]
    )
    return result.embeddings[0].values


# 사용 예시별 태스크 타입

# 1. 검색 쿼리 (사용자 질문)
query_embedding = embed_with_task(
    "파이썬 비동기 처리 방법",
    "search result"  # 검색용 쿼리
)

# 2. Q&A (질문에 답변 찾기)
qa_embedding = embed_with_task(
    "FastAPI에서 JWT 토큰은 어떻게 검증하나요?",
    "question answering"
)

# 3. 코드 검색
code_embedding = embed_with_task(
    "JWT token validation middleware",
    "code retrieval"
)

# 4. 팩트 체킹
fact_embedding = embed_with_task(
    "파이썬은 1991년에 출시됐다",
    "fact checking"
)

# 문서 임베딩 (검색 대상)
def embed_document(content: str, title: str = None) -> list[float]:
    if title:
        prefixed = f"title: {title} | text: {content}"
    else:
        prefixed = content

    result = client.models.embed_content(
        model="gemini-embedding-2",
        contents=[prefixed],
        config=types.EmbedContentConfig(
            task_type="RETRIEVAL_DOCUMENT"
        )
    )
    return result.embeddings[0].values
[Task Type 선택 가이드]

검색 쿼리:      "search result" 또는 RETRIEVAL_QUERY
문서 색인:      "search result" 또는 RETRIEVAL_DOCUMENT
질의응답:       "question answering"
코드 검색:      "code retrieval"
팩트 체킹:      "fact checking"

→ task prefix 미지정 vs 지정 시 검색 정확도 차이 있음
→ 특히 긴 문서와 짧은 쿼리 간 비대칭 검색에서 효과적

실전 3 — 멀티모달 RAG 파이프라인

import numpy as np
import anthropic
from pathlib import Path
from dataclasses import dataclass
from typing import Any

@dataclass
class MultimodalChunk:
    """멀티모달 청크"""
    id:        str
    content:   Any           # 텍스트, 이미지 경로, 비디오 경로 등
    mime_type: str           # "text/plain", "image/png", "video/mp4"...
    metadata:  dict
    embedding: list[float] = None


class MultimodalRAG:
    """Gemini Embedding 2 기반 멀티모달 RAG"""

    def __init__(self, gemini_client, llm_client):
        self.gemini  = gemini_client
        self.llm     = llm_client
        self.chunks: list[MultimodalChunk] = []

    def embed_chunk(self, chunk: MultimodalChunk) -> list[float]:
        """단일 청크 임베딩 (모달리티 자동 처리)"""
        if chunk.mime_type == "text/plain":
            # 텍스트: 직접 임베딩
            result = self.gemini.models.embed_content(
                model="gemini-embedding-2",
                contents=[f"title: {chunk.metadata.get('title', 'none')} | text: {chunk.content}"],
                config=types.EmbedContentConfig(
                    task_type="RETRIEVAL_DOCUMENT"
                )
            )
        else:
            # 이미지/비디오/오디오/PDF: 바이너리 임베딩
            file_bytes = Path(chunk.content).read_bytes()
            b64        = base64.b64encode(file_bytes).decode()

            result = self.gemini.models.embed_content(
                model="gemini-embedding-2",
                contents=[
                    types.Content(parts=[
                        types.Part(
                            inline_data=types.Blob(
                                mime_type=chunk.mime_type,
                                data=b64
                            )
                        )
                    ])
                ]
            )

        return result.embeddings[0].values

    def add_chunks(self, chunks: list[MultimodalChunk]):
        """청크 추가 + 임베딩"""
        for chunk in chunks:
            chunk.embedding = self.embed_chunk(chunk)
            self.chunks.append(chunk)
        print(f"총 {len(self.chunks)}개 청크 저장됨")

    def search(self, query: str, top_k: int = 5) -> list[MultimodalChunk]:
        """쿼리 → 관련 청크 검색"""
        # 쿼리 임베딩
        query_result = self.gemini.models.embed_content(
            model="gemini-embedding-2",
            contents=[f"task: question answering | query: {query}"],
            config=types.EmbedContentConfig(
                task_type="RETRIEVAL_QUERY"
            )
        )
        query_vec = np.array(query_result.embeddings[0].values)

        # 코사인 유사도 계산
        scores = []
        for chunk in self.chunks:
            chunk_vec  = np.array(chunk.embedding)
            similarity = float(
                np.dot(query_vec, chunk_vec) /
                (np.linalg.norm(query_vec) * np.linalg.norm(chunk_vec))
            )
            scores.append((chunk, similarity))

        # 상위 k개 반환
        scores.sort(key=lambda x: x[1], reverse=True)
        return [chunk for chunk, _ in scores[:top_k]]

    def query(self, question: str) -> str:
        """검색 + 생성"""
        retrieved = self.search(question)

        # 컨텍스트 구성 (멀티모달)
        context_parts = []
        for i, chunk in enumerate(retrieved, 1):
            if chunk.mime_type == "text/plain":
                context_parts.append(f"[{i}] {chunk.content}")
            else:
                context_parts.append(
                    f"[{i}] {chunk.mime_type} 파일: {chunk.metadata.get('title', chunk.content)}"
                )

        context = "\n".join(context_parts)

        # LLM으로 답변 생성
        response = self.llm.messages.create(
            model="claude-sonnet-4-6",
            max_tokens=1024,
            system=f"다음 자료를 참고하여 질문에 답변하세요:\n\n{context}",
            messages=[{"role": "user", "content": question}]
        )
        return response.content[0].text


# 사용 예시
rag = MultimodalRAG(
    gemini_client=client,
    llm_client=anthropic.Anthropic()
)

# 다양한 모달리티 청크 추가
rag.add_chunks([
    MultimodalChunk(
        id="doc1",
        content="FastAPI는 Python 기반 고성능 웹 프레임워크입니다...",
        mime_type="text/plain",
        metadata={"title": "FastAPI 소개"}
    ),
    MultimodalChunk(
        id="img1",
        content="architecture_diagram.png",
        mime_type="image/png",
        metadata={"title": "시스템 아키텍처 다이어그램"}
    ),
    MultimodalChunk(
        id="vid1",
        content="api_tutorial.mp4",
        mime_type="video/mp4",
        metadata={"title": "API 개발 튜토리얼 영상"}
    ),
    MultimodalChunk(
        id="pdf1",
        content="api_spec.pdf",
        mime_type="application/pdf",
        metadata={"title": "API 명세서"}
    ),
])

# 질문 → 멀티모달 검색 + 생성
answer = rag.query("FastAPI 인증 미들웨어 구현 방법은?")
print(answer)

실전 4 — Matryoshka 차원 축소

# 차원별 트레이드오프
dimension_guide = {
    3072: "최고 품질 — 프로덕션 고정밀 검색",
    1536: "우수 품질 — 대부분 케이스 충분, 저장 50% 절감",
    768:  "양호 품질 — 비용/속도 최적화 필요 시",
}

# 차원 축소 사용법
result = client.models.embed_content(
    model="gemini-embedding-2",
    contents=["검색할 텍스트"],
    config=types.EmbedContentConfig(
        output_dimensionality=768  # 3072 → 768으로 축소
    )
)

embedding_768 = result.embeddings[0].values
print(f"차원: {len(embedding_768)}")  # 768

실전 5 — 크로스 모달 검색

# 이미지로 텍스트 검색, 텍스트로 이미지 검색 — 같은 벡터 공간이라 가능

def cross_modal_search():
    """크로스 모달 검색 예시"""

    # 텍스트 문서들 색인
    text_chunks = [
        "고양이는 독립적인 성격의 반려동물입니다",
        "강아지는 사람을 좋아하는 사회적인 동물입니다",
        "파이썬 코딩 튜토리얼 시작하기"
    ]
    text_embeddings = []
    for text in text_chunks:
        result = client.models.embed_content(
            model="gemini-embedding-2",
            contents=[text]
        )
        text_embeddings.append(np.array(result.embeddings[0].values))

    # 이미지로 관련 텍스트 검색 (크로스 모달!)
    cat_image_bytes = Path("cat_photo.jpg").read_bytes()
    b64 = base64.b64encode(cat_image_bytes).decode()

    image_result = client.models.embed_content(
        model="gemini-embedding-2",
        contents=[
            types.Content(parts=[
                types.Part(
                    inline_data=types.Blob(
                        mime_type="image/jpeg",
                        data=b64
                    )
                )
            ])
        ]
    )
    image_vec = np.array(image_result.embeddings[0].values)

    # 코사인 유사도로 가장 관련 있는 텍스트 찾기
    similarities = [
        float(np.dot(image_vec, text_vec) /
              (np.linalg.norm(image_vec) * np.linalg.norm(text_vec)))
        for text_vec in text_embeddings
    ]

    best_match_idx = np.argmax(similarities)
    print(f"고양이 이미지와 가장 관련된 텍스트:")
    print(f"→ '{text_chunks[best_match_idx]}' (유사도: {similarities[best_match_idx]:.3f})")
    # → '고양이는 독립적인 성격의 반려동물입니다' (유사도: 0.847)

cross_modal_search()

Gemini Embedding 2 vs 기존 모델 비교

  

항목 gemini-embedding-001 Gemini Embedding 2 text-embedding-3-large
텍스트
이미지 ✅ (최대 6개)
비디오 ✅ (120초)
오디오 ✅ (180초)
PDF ✅ (6페이지)
차원 768 3072 (축소 가능) 3072 (축소 가능)
언어 다국어 100개+ 영어 중심
가격 (텍스트) $0.00002/1K $0.20/1M $0.13/1M
상태 GA GA (4/22~) GA
벡터 호환 ❌ (별도 공간) ❌ (별도 공간) -
[선택 기준]
텍스트만, 비용 최소화:  gemini-embedding-001 또는 text-embedding-3-small
텍스트만, 최고 성능:    text-embedding-3-large (영어) 또는 Qwen3-Embedding
멀티모달 필요:          Gemini Embedding 2 (현재 유일한 상업 옵션)
비디오/오디오 네이티브: Gemini Embedding 2 (경쟁자 없음)

 


마무리

✅ Gemini Embedding 2 써야 할 때
→ 텍스트 + 이미지 + 비디오 동시 검색 필요
→ 비디오/오디오를 텍스트 변환 없이 직접 임베딩
→ 스캔 PDF + 이미지 문서 검색 (OCR 자동 처리)
→ 크로스 모달 검색 (이미지로 텍스트 검색, 반대도)
→ 기존 3개 파이프라인 → 1개로 단순화

❌ 기존 텍스트 임베딩이 나은 경우
→ 텍스트 전용 RAG (Qwen3-Embedding-8B 또는 text-embedding-3-large가 더 저렴)
→ 순수 한국어 텍스트 (bge-m3가 한국어 특화)
→ 대규모 배치 처리 비용 최소화
→ 기존 gemini-embedding-001 사용 중 (재임베딩 비용 고려)

[주의: 마이그레이션]
gemini-embedding-001 → Gemini Embedding 2:
→ 벡터 공간 완전 비호환
→ 전체 데이터셋 재임베딩 필수
→ 벡터 DB 인덱스 재생성 필요
→ 프로덕션 마이그레이션 계획 신중하게

 


관련 글:

 

https://cell-devlog.tistory.com/158

 

임베딩 모델 완전 가이드 — text-embedding 선택과 RAG 적용

RAG를 만들었는데 검색 품질이 나쁩니다. 청킹도 바꿔보고 프롬프트도 바꿨는데 여전합니다. 임베딩 모델이 문제일 수 있습니다. 선택 기준부터 실전 적용까지 정리했습니다.[핵심 요약]→ 임베

cell-devlog.tistory.com

https://cell-devlog.tistory.com/136

 

Gemini 3.1 Flash TTS 완전 가이드 — 자연어로 AI 목소리를 연출하는 법

"긴장감 있게 읽어줘", "여기서 잠깐 멈춰", "속삭이듯이". 이제 이 말 한 마디로 AI 목소리를 연출할 수 있습니다.[핵심 요약]→ 출시: 2026년 4월 15일 (Google, 프리뷰)→ 핵심: SSML 없이 자연어로 음성

cell-devlog.tistory.com

https://cell-devlog.tistory.com/24

 

쿼리 재작성, 반복 검색, 멀티소스 라우팅 — Agentic RAG 동작 원리와 동적 검색 전략 완전 정리

RAG 시스템을 만들고 나면 이런 한계가 생겨요."단순한 질문은 잘 답하는데, '2024년 실적을 바탕으로 2025년 전략을 분석해줘' 같은 복잡한 질문은 엉뚱한 답이 나온다."이건 일반 RAG의 구조적 한계

cell-devlog.tistory.com

 

반응형