AI Shift Tech Blog
💎

Gemini Embedding 2で作るネイティブマルチモーダルRAG

に公開

はじめに

こんにちは、AI Shift AIチームの林崎です。

本記事では先日一般提供となったGemini Embedding 2でシンプルなマルチモーダルRAGを構築し、その実用性や使用時のTipsについてご紹介します。

従来のマルチモーダルRAGでは、画像ならOCR、音声なら文字起こし(ASR)など、メディアごとに異なる前処理やモデルを組み合わせる必要があり、パイプラインが複雑化したり、メディア特有の情報が欠落することが課題でした。Gemini Embedding 2の登場によって、これらの異なるモダリティを同一の埋め込み空間に写像することが可能になりました。

本記事では、この「ネイティブ」なマルチモーダル埋め込みを活用したRAGの構築方法を解説します。

https://docs.cloud.google.com/gemini-enterprise-agent-platform/models/gemini/embedding-2?hl=ja

Gemini Embedding 2 とは?

Gemini Embedding 2は、テキスト・画像・音声・動画といった多様なモダリティを区別することなく、単一のモデルで高次元の埋め込みに変換できるモデルです。OCRを使用することによってPDFファイルにも対応しています。

Gemini Embedding 2の概要
Gemini Embedding 2の概要

Gemini Embedding 2 [1] のテクニカルレポートによると、Amazon Nova MME・Voyage-3.5-multimodal・gemini-embeddingと比較して、マルチモーダル検索タスクにおいて優れた性能を示しています。
マルチモーダルに拡張しながらも、MTEBベンチマークでも従来のGemini Embeddingの性能を上回っている点が特徴的かと思います。

Gemini Embedding 2は、画像・テキスト・動画・ドキュメントといった多様なモダリティにわたるマルチモーダル検索タスクにおいて、優れた性能を発揮している
Gemini Embedding 2は、画像・テキスト・動画・ドキュメントといった多様なモダリティにわたるマルチモーダル検索タスクにおいて、優れた性能を発揮している

Massive Text Embedding Benchmarkにおけるマルチモーダル埋め込みモデルとテキスト専用埋め込みモデルの比較
Massive Text Embedding Benchmarkにおけるマルチモーダル埋め込みモデルとテキスト専用埋め込みモデルの比較

Gemini Embedding 2 [1] は、Geminiから初期化され、ノイズ対照推定(NCE)損失によるファインチューニングを経て構築されます。負例の選択方法については明示されていませんが、MM-Embed [3] のように「意味はそっくりだが、モダリティが異なる」データを負例として選択しているのではないかと推測しています。学習データについても言及はありませんでした。

Gemini Embedding 2を使用する時には、モダリティごとにAPIの入力制限に注意が必要です。主要な注意点は以下の通りです。

モダリティ 注意点
全体 最大入力トークン数は8192までで、すべてのモダリティで共有される
画像 最大16384x16384 px
1回のリクエストで入力できるのは6枚まで
音声 1回のリクエストあたり180秒まで
動画 音声なしの動画は120秒・音声ありの動画は80秒まで
最大120フレーム
ドキュメント (PDFファイル) 最大6ページ (ファイルごとに1ページ推奨)

https://docs.cloud.google.com/gemini-enterprise-agent-platform/models/embeddings/get-multimodal-embeddings?hl=ja#api-usage

料金は入力データごとに課されます。テキストに関しては従来通りトークン単位での請求になりますが、画像は枚数・動画はフレーム・音声は秒数単位での請求となります。動画と音声はフレームや秒数を意識しないと料金が高くなりやすいので注意しましょう。

https://cloud.google.com/gemini-enterprise-agent-platform/generative-ai/pricing?hl=ja&_gl=1*1ct0f4o*_ga*MTIzNzkyMzE2MS4xNzgyMDk4NTE4*_ga_WH2QY8WWF5*czE3ODI3MDMwNjUkbzE1JGcxJHQxNzgyNzA1ODEzJGozNCRsMCRoMA

マルチモーダルRAGの構築

本記事では、Gemini Embedding 2でシンプルなマルチモーダルRAGを構築します。

環境構築

今回の実装では、Gemini APIを呼び出すために google-genaiSDKを使用します。
また、ベクトルデータベースにはローカルで手軽に動作するChroma DBを選定しました。Chroma DBはマルチモーダル埋め込みの管理にも対応しており、今回の用途に適していると思います。より大規模なサービス・ワークロードになる場合は他のベクトルデータベースを推奨します。

ベクトルデータベースの比較については、以下の記事がわかりやすいかと思います。

https://zenn.dev/serio/articles/733b53d2b912d1

必要なライブラリは事前にインストールしておきます。

uv add google-genai chromadb python-dotenv tqdm

クライアントの初期化

まずはクライアントの初期化を行います。Google CloudのVertex AI経由で使用するので.envなどで環境変数を設定しておきます。

GOOGLE_CLOUD_PROJECT=YOUR_GCP_PROJECT
GOOGLE_CLOUD_LOCATION=YOUR_LOCATION

google-genaiSDKクライアントの初期化を行います。

import os
from google import genai
from dotenv import load_dotenv

load_dotenv()

PROJECT_ID = os.getenv("GOOGLE_CLOUD_PROJECT", "YOUR_PROJECT_ID")
LOCATION = os.getenv("GOOGLE_CLOUD_LOCATION", "global")

gemini_client = genai.Client(vertexai=True, project=PROJECT_ID, location=LOCATION)

ChromaDBの初期化を行います。CHROMADB_PATHがデータの保存先となります。PersistentClientを使うことでデータの永続化を行うようにします。

import chromadb

CHROMADB_PATH = "./chromadb"

chroma_client = chromadb.PersistentClient(path=CHROMADB_PATH)
collection = chroma_client.get_or_create_collection(name="gemini_embedding_2")

マルチモーダルデータの埋め込みと保存

今回の実装では、ローカルファイルを表す簡易的な LocalFile エンティティを定義しています。Gemini Embedding 2が対応しているモダリティはテキスト・画像・音声・動画・PDFの5種類です。

from dataclasses import dataclass, field
from pathlib import Path
from typing import ClassVar


@dataclass(frozen=True)
class LocalFile:
    path: Path
    bytes: int = field(init=False)
    mime_type: str = field(init=False)

    SUPPORTED_MIME_TYPES: ClassVar[dict[str, str]] = {
        ".txt": "text/plain",
        ".jpg": "image/jpeg",
        ".jpeg": "image/jpeg",
        ".png": "image/png",
        ".wav": "audio/wav",
        ".mp4": "video/mp4",
        ".pdf": "application/pdf",
    }

    def __post_init__(self):
        if not self.path.is_file():
            raise FileNotFoundError(f"File not found: {self.path}")

        mime_type = self.SUPPORTED_MIME_TYPES.get(self.path.suffix.lower())
        if mime_type is None:
            raise ValueError(f"Unsupported file type: {self.path}")

        object.__setattr__(self, "bytes", self.path.stat().st_size)
        object.__setattr__(self, "mime_type", mime_type)

    @property
    def is_text(self) -> bool:
        return self.mime_type == "text/plain"

    def read_text(self) -> str:
        return self.path.read_text(encoding="utf-8")

    def read_bytes(self) -> bytes:
        return self.path.read_bytes()

google-genaiSDKクライアントでリクエストを送る際には、SDKが提供しているContent型に整形する必要があるので、データの変換を担うAdapterを定義しておきます。

from google.genai import types


class GoogleContentAdapter:
    def to_content(self, obj: str | LocalFile) -> types.Content:
        if isinstance(obj, str):
            part = types.Part.from_text(text=obj)

        elif isinstance(obj, LocalFile) and obj.is_text:
            part = types.Part.from_text(text=obj.read_text())

        elif isinstance(obj, LocalFile):
            part = types.Part.from_bytes(
                data=obj.read_bytes(),
                mime_type=obj.mime_type,
            )

        else:
            raise ValueError(f"Unsupported content type: {type(obj)}")

        return types.Content(
            role="user",
            parts=[part],
        )

./dataディレクトリのファイルを走査し、対象となるファイルがあった場合にgemini-embedding-2で埋め込みを取得するようリクエストを送信します。
取得した埋め込みはChroma DBに保存します。ここではidsmetadatasプロパティを使用して、元のファイル名やファイル形式の情報を保持するようにしています。

from google.genai import types
from tqdm import tqdm

content_adapter = GoogleContentAdapter()
for path in tqdm(Path("./data").glob("*")):
    try:
        file = LocalFile(path)
    except ValueError:
        continue

    embedding = gemini_client.models.embed_content(
        model="gemini-embedding-2",
        contents=[content_adapter.to_content(file)],
        config=types.EmbedContentConfig(output_dimensionality=768),
    )

    collection.add(
        ids=[f"{file.path.stem}_{file.path.suffix.lower().lstrip('.')}"],
        embeddings=[embedding.embeddings[0].values],
        documents=[file.path.stem],
        metadatas=[
            {
                "path": str(file.path),
                "mime_type": file.mime_type,
            }
        ],
    )

検索と回答の生成

ユーザークエリから埋め込みを生成し、Chroma DBで類似度検索をすることによって上位3件のファイルを取得します。
取得したファイルはそれぞれプロンプトとContent型に変換し、マルチモーダルな入力に対応しているgemini-3.5-flashにユーザークエリと一緒に入力します。
最後のLLMによる回答生成のときも、動画やPDFをテキストに変換することなくそのまま LLM に入力できるため、パイプライン全体で一切の前処理が不要になっています。


def generate(query: str) -> str:
    content_adapter = GoogleContentAdapter()
    query_embeddings = gemini_client.models.embed_content(
        model="gemini-embedding-2",
        contents=[
            content_adapter.to_content(
                f"task: question answering | query: {query}"
            )
        ],
        config=types.EmbedContentConfig(output_dimensionality=768),
    )

    retrieved = collection.query(
        query_embeddings=[query_embeddings.embeddings[0].values],
        n_results=3,
        include=["documents", "metadatas", "distances"],
    )

    contexts = []
    parts = []

    for metadata, distance, document in zip(
        retrieved["metadatas"][0],
        retrieved["distances"][0],
        retrieved["documents"][0],
        strict=True,
    ):
        source_file = LocalFile(Path(metadata["path"]))

        contexts.append(f"distance={distance}\n{document}")
        parts.extend(content_adapter.to_content(source_file).parts)

        print(f"hit: {document} ({source_file.mime_type})")

    prompt = f"""
以下の検索結果と添付ファイルを根拠に、日本語で簡潔に回答してください。
分からない場合は、分からないと答えてください。

# 質問
{query}

# 検索結果
{"\n".join(contexts)}
""".strip()

    response = gemini_client.models.generate_content(
        model="gemini-3.5-flash",
        contents=[
            types.Content(
                role="user",
                parts=[types.Part.from_text(text=prompt), *parts],
            )
        ],
    )

    return response.text or ""

検証

構築したパイプラインが、テキスト以外の各モダリティから正しく情報を検索し、回答を生成できるか検証を行います。データにはFineVideoデータセットの一部動画 (33件; train-00000-of-01357.parquetに含まれるデータ) と、それを音声・画像・テキストに変換したもの、およびOCR Task JAデータセットの一部PDFファイル (5件) を使用しています。

PDFファイルの情報検索

複雑なレイアウトや表組みを含むPDFドキュメントに対する検索精度を検証します。
今回はOCR Task JAデータセットに含まれる以下の図表の情報を正しく取得・認識できるか確認します。

PDFデータ
日本銀行金融研究所/金融研究/2001.1 「技術革新と銀行業・金融政策―電子決済技術と金融政運営との関連を考えるフォーラム」報告書より

query = "B to C 電子商取引の市場規模は1998年から2004年にかけてどのように変化していますか?"
answer = generate(query)
print(answer)

結果

hit: kk20-1-1 (application/pdf)
...
「技術革新と銀行業・金融政策―電子決済技術と金融政策運営との関連を考えるフォーラム」報告書の図表4(a)によると、日本のB to C 電子商取引の市場規模は、1998年の0.06兆円から2004年の6.66兆円へと、一貫して右肩上がりに急拡大(増加)しています。

従来の手法では、テキスト抽出 (PyPDFなど) やOCRでテキストに変換してからベクトル化する煩雑な処理が必要でした。対してGemini Embedding 2ではPDF全体をそのままベクトル化することができ、開発者体験が大きく向上します。

ただし、推論の処理は重くなるので、キャッシュを用意するなどの工夫をするとより良いかと思います。

動画ファイルの情報検索

視覚情報と音声情報が混在する動画ファイル (MP4) に対する検証です。

動画ファイル

query = "地球がズームアウトしている動画のパスと対応する区間を教えて"
answer = generate(query)
print(answer)

結果

hit: finevideo_028_ed1jhrcny_w (video/mp4)
...
地球がズームアウトしている動画のパスと対応する区間は以下の通りです。

* **パス:** `finevideo_028_ed1jhrcny_w`
* **区間:** 0:00〜0:09

動画ファイルを扱うには、事前にフレーム画像に分解したり、キャプションを生成するなどの煩雑な処理が必要となります。Gemini Embedding 2を使えば、複雑で重いパイプラインを構築することなくベクトル検索を機能させることができます。

Gemini Embedding 2のTips

Gemini Embedding 2を使用するにあたり、役に立ちそうなTipsを紹介します。

タスク文字列を指定して精度を上げる

Gemini Embedding 2の学習は、質問応答 (question answering) やコード検索 (code retrieval) などの複数のタスクを同時に学習するマルチタスク学習の設定で行われています [1]
私たちがこのモデルを使う時も、ユーザークエリに適切なタスク文字列を付与することによって、出力される埋め込みを最適化できるとされています。

具体的には、ユーザークエリの形式を以下のような形に変更することで適用できます。

f"task: {タスク文字列} | query: {コンテンツ}"

公式では、タスク文字列は以下の7つが紹介されています。

ユースケース タスク文字列
検索クエリ search result
質問応答 question answering
ファクト チェック fact checking
コードの取得 code retrieval
分類 classification
クラスタリング clustering
意味的類似度 sentence similarity

https://docs.cloud.google.com/gemini-enterprise-agent-platform/models/embeddings/get-multimodal-embeddings?hl=ja#specify-task-instructions

埋め込み次元を最適化する

Gemini Embedding 2はMatryoshka Representation Learning [2]を使用して学習されているため、複数の埋め込み次元を選択することができます (本ブログの実装でも使用しました)。

必要な精度に合わせて適切な埋め込み次元をoutput_dimensionalityから選択することにより、埋め込みの保存に必要なストレージ費用を節約することができます。

response = client.models.embed_content(
    model="gemini-embedding-2",
    contents=[content],
    config=types.EmbedContentConfig(output_dimensionality=768),
)

テクニカルレポート[1] では76815363072の3種類の次元に対して最適化されているとの記述がありましたので、まずはこちらから試してみるのが良いと思います。

https://docs.cloud.google.com/gemini-enterprise-agent-platform/models/embeddings/get-multimodal-embeddings?hl=ja#low-dimension

まとめ

本記事ではGemini Embedding 2でシンプルなマルチモーダルRAGを構築し、その実用性や使用時のTIPについてご紹介しました。

マルチモーダルデータをネイティブに扱うことができるGemini Embedding 2のおかげで処理が非常に楽になったとはいえ、チャンク化やチューニングの部分で工夫の余地はまだまだ残されている印象です。また、精度検証は十分に行われているとはいえ、実際に運用した場合にどのような課題が発生しうるかエラー分析をしっかり実施していきたいところです。

ご覧いただきありがとうございました!

参考文献

[1] Gemini Embedding 2: A Native Multimodal Embedding Model from Gemini
[2] Matryoshka Representation Learning
[3] MM-Embed: Universal Multimodal Retrieval with Multimodal LLMs

AI Shift Tech Blog
AI Shift Tech Blog

Discussion