Content
本記事では、2026 年 4 月 22 日 に GA となった Google 初のネイティブマルチモーダルエンベディングモデル Gemini Embedding 2 (gemini-embedding-2) について解説します。

「テンションの上がる曲を聴きたい」「動画に合う BGM を探したい」など、ユーザーの直感的なニーズに対して、エンベディングを使用したアプローチはこれまでも試みられてきました。しかし、今までのエンベディングモデルは、テキスト・画像・動画には対応しているものの、音声には対応していないといったように、真にシームレスなマルチモーダル検索を実現するにはハードルがありました。

このようなハードルを突破するのが、Gemini Embedding 2 です。今回は、Streamlit でアプリを作成し、Gemini Embedding 2 によるマルチモーダル検索を試してみます。
 

Gemini Embedding 2 とは

Gemini Embedding 2 とは、テキスト・画像・動画・音声・PDF ドキュメントの 5 つのモダリティを、ひとつの共通したベクトル空間にマッピングできる、Google 初のネイティブマルチモーダルエンベディングモデル です。

Google はこれまでテキスト専用モデルや、テキスト・画像・動画を同一のベクトル空間で扱えるマルチモーダルエンベディングモデルなど、用途に応じたさまざまなエンベディングモデルを提供してきました。

こうした複数のモデルを統合・発展させたものが Gemini Embedding 2 です。2026 年 3 月 10 日にパブリックプレビュー版のリリースを経て、2026 年 4 月 22 日より GA (一般提供) が開始されています。

Gemini のアーキテクチャを基盤とする本モデルは、以下のような特徴を備えています。

  • 5 つのモダリティを統合: テキスト・画像・動画に加え、新たに音声、PDF ドキュメントにも対応。これらをひとつの共通したベクトル空間にネイティブにマッピングします。
  • 広範な入力サイズ: 1 回の呼び出しで最大 8,192 個のテキストトークン、6 枚の画像、120 秒 (音声ありの場合 80 秒) の動画、180 秒の音声、6 ページの PDF ファイルなど、広範な入力に対応します。
  • インターリーブ入力のサポート: 複数のモダリティ (画像とテキストなど) を 1 つのリクエストとして渡すことが可能になり、より複雑なデータの関係性を正確に捉えられます。
  • 表現力と対応言語の向上: 出力される次元数が 3,072 次元へと大幅にスケールアップし、100 を超える言語のセマンティックな意図を解釈します。

 

マルチモーダル検索アプリを構築する

Google AI Studio で公開されているデモ Multimodal Search with Gemini Embedding 2 – Try in Google AI Studio を参考に、Gemini Embedding 2 の動作を理解するため、Streamlit でアプリを作成し、マルチモーダル検索を試してみます。


 

構成は次のようになります。素材のエンベディングデータは pickle ファイルに保存します。なお、今回はデータが少ないので NumPy を使用して総当たり検索を行います。


 

また、Nano Banana や Lyria で生成した画像や音声を素材として使用します。

テキスト:

  • 「友達に会う。」
  • 「バナナを食べる。」
  • 「今週末も笑えないジョークに付き合わされるなんて、最高の拷問だ。」
  • 「誰もいないはずの 2 階から、ゆっくりと床板が軋む音が聞こえる。」

画像:

  • バナナの画像
  • オレンジの画像
  • チョコレートバナナの画像
  • 暖炉とロッキングチェアが置かれた部屋の画像
  • 子どもが裏路地にいる画像

音声:

  • スキャットの軽快な歌声
  • 安らぎを感じさせるゆったりとしたサウンド
  • 緊張感のあるサスペンスなサウンド

 

初期化と依存関係のインストール: プロジェクトのディレクトリでコマンドを実行し、初期化と依存関係のインストールを行います。

uv init
uv add streamlit numpy google-genai pillow

認証: ADC で認証を行います。ADC については、アプリケーションのデフォルト認証情報を設定する を参考にしてください。

gcloud auth application-default login

アプリケーションの実装: アプリのソースコード app.py を作成します。YOUR_PROJECT_ID はプロジェクト ID に置き換えてください。

import os
import pickle
import uuid

import numpy as np
import streamlit as st
from google import genai
from google.genai import types
from PIL import Image

# =====================================================================
# 初期設定・カスタムCSS・共通関数
# =====================================================================
st.set_page_config(
page_title="マルチモーダル検索", layout="wide", initial_sidebar_state="collapsed"
)

DB_FILE = "local_vector_db.pkl"

st.markdown(
"""
""",
unsafe_allow_html=True,
)


@st.cache_resource
def get_client():
return genai.Client(
vertexai=True,
project="YOUR_PROJECT_ID",
location="us",
)


def load_db():
return pickle.load(open(DB_FILE, "rb")) if os.path.exists(DB_FILE) else []


def save_db(db_data):
pickle.dump(db_data, open(DB_FILE, "wb"))


def cosine_similarity(v1, v2):
return np.dot(v1, v2) / (np.linalg.norm(v1) * np.linalg.norm(v2))


if "db" not in st.session_state:
st.session_state.db = load_db()

client = get_client()


def add_to_db(item_type, content_label, api_content, **kwargs):
with st.spinner("処理中..."):
res = client.models.embed_content(
model="gemini-embedding-2",
contents=api_content,
)
st.session_state.db.append(
{
"id": str(uuid.uuid4()),
"type": item_type,
"content": content_label,
"vector": res.embeddings[0].values,
**kwargs,
}
)
save_db(st.session_state.db)
st.success("処理完了")
st.rerun()


# =====================================================================
# データ追加ダイアログ
# =====================================================================
@st.dialog("データを追加")
def data_management_dialog():
reg_type = st.selectbox("追加するデータの種類", ["テキスト", "画像", "音声"])

if reg_type == "テキスト":
val = st.text_area("テキスト:")
elif reg_type == "画像":
val = st.file_uploader("画像:", type=["jpg", "jpeg", "png"])
elif reg_type == "音声":
val = st.file_uploader("音声:", type=["wav", "mp3", "m4a"])

if st.button("追加", type="primary", width="stretch") and val:
if reg_type == "テキスト":
add_to_db("text", val, val)
elif reg_type == "画像":
img = Image.open(val)
add_to_db("image", val.name, img, image=img)
elif reg_type == "音声":
audio_bytes = val.read()
add_to_db(
"audio",
val.name,
types.Part.from_bytes(data=audio_bytes, mime_type=val.type),
audio_bytes=audio_bytes,
mime_type=val.type,
)

st.divider()

if st.button(
"すべて削除",
type="secondary",
icon=":material/delete:",
width="stretch",
):
st.session_state.db.clear()
save_db([])
st.rerun()


# =====================================================================
# メイン UI (ヘッダー・検索バー)
# =====================================================================
head_left, head_right = st.columns([5, 1])

with head_left:
st.subheader("マルチモーダル検索")
st.caption("Gemini Embedding 2 検証用アプリ")
with head_right:
st.write("")
st.write("")
if st.button("追加", icon=":material/add:", width="stretch"):
data_management_dialog()

st.write("")

with st.container(border=True):
query_text = st.text_input(
"テキスト入力",
placeholder="テキストを入力",
label_visibility="collapsed",
)
query_file = st.file_uploader(
"メディア添付",
type=["jpg", "png", "jpeg", "wav", "mp3"],
label_visibility="collapsed",
)

c_btn1, _ = st.columns([1, 5])

with c_btn1:
search_clicked = st.button(
"検索",
icon=":material/search:",
type="primary",
width="stretch",
)

st.write("")

# =====================================================================
# 検索処理
# =====================================================================
display_data = [{"item": item, "match": None} for item in st.session_state.db]

if search_clicked:
if not st.session_state.db:
st.error("データを追加してください。")
elif not query_text and not query_file:
st.error("検索条件を入力してください。")
else:
with st.spinner("検索中..."):
contents = []

if query_text:
contents.append(query_text)
if query_file:
contents.append(
Image.open(query_file)
if query_file.type.startswith("image/")
else types.Part.from_bytes(
data=query_file.read(),
mime_type=query_file.type,
)
)

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

for d in display_data:
raw_sim = cosine_similarity(q_vec, d["item"]["vector"])
d["match"] = max(0.0, raw_sim) * 100

display_data.sort(key=lambda x: x["match"], reverse=True)


# =====================================================================
# 結果描画ロジック (カードUI)
# =====================================================================
def render_result_card(r):
item = r["item"]
match_str = f"{r['match']:.2f}% " if r["match"] is not None else "- %"

with st.container(border=True):
c_match, c_del = st.columns([6, 1])
with c_match:
st.markdown(
f"類似度: {match_str}",
unsafe_allow_html=True,
)
with c_del:
if st.button(
"",
key=f"del_{item['id']}",
icon=":material/delete:",
type="tertiary",
):
st.session_state.db = [
x for x in st.session_state.db if x["id"] != item["id"]
]
save_db(st.session_state.db)
st.rerun()

if item["type"] == "text":
st.write(item["content"])
elif item["type"] == "image":
st.image(item["image"], width="stretch")
st.write(item["content"])
elif item["type"] == "audio":
st.write(f"**{item['content']}**")
st.audio(item["audio_bytes"], format=item["mime_type"])


columns = st.columns(3)
column_configs = [
("text", "description", "テキスト"),
("image", "image", "画像"),
("audio", "music_note", "音声"),
]

for col, (m_type, icon, title) in zip(columns, column_configs):
with col:
st.markdown(f"**:material/{icon}: {title}**")
for r in display_data:
if r["item"]["type"] == m_type:
render_result_card(r)

注目すべきは、embed_content メソッドを使用してエンベディングを生成している箇所です。model には gemini-embedding-2 を指定します。

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

また、エンベディングを生成する際、タスクの指示を明記するとパフォーマンスが向上します。タスクの指示については、タスクの指示を指定してパフォーマンスを改善する を参考にしてください。

add_to_db("text", val, val)  # タスクの指示なし
add_to_db("text", val, f"title: none | text: {val}") # タスクの指示あり

アプリケーションの実行: コマンドを実行し、アプリを実行します。

uv run streamlit run app.py

 

マルチモーダル検索を試す

アプリを構築したので、マルチモーダル検索を試してみます。アプリを構築していない方は、Google AI Studio で公開されているデモ Multimodal Search with Gemini Embedding 2 – Try in Google AI Studio で同じような内容を試すことができます。

 

「怖い」で検索する

シンプルなテキストクエリを入力します。

検索結果として類似度が高かったものは、「誰もいないはずの 2 階から、ゆっくりと床板が軋む音が聞こえる。」というテキストや緊張感のあるサスペンスなサウンド、子どもが裏路地にいる画像でした。

メタ情報を付与していないにもかかわらず、多くの人が怖さを感じるコンテンツに高い類似度を出していることに驚きます。モデルが「怖い」というニュアンスを理解していることがうかがえます。

 

バナナの画像で検索する

次に、画像での検索を試してみます。検索に使用する画像は、素材に使用しているバナナの画像です。

同じ画像を使用したのでバナナの画像との類似度は 100% でした。他にも「バナナを食べる。」というテキストが高い類似度を出しており、音声など関連性がないものは、類似度がほぼ同等の結果となりました。

画像では、バナナの画像に続き、オレンジの画像、チョコレートバナナの画像という順になりました。チョコレートバナナの画像が 2 番目になると予想していたため、とても興味深い結果でした。

 

バナナの画像 +「バナナ」で検索する

Gemini Embedding 2 の特徴のひとつであるインターリーブ入力を試してみます。バナナの画像と、テキスト「バナナ」を組み合わせて再度検索を行います。

今度は、バナナの画像に続き、チョコレートバナナの画像が 2 番目になりました。テキスト「バナナ」を加えたことでクエリの意味が補強され、バナナとの関連性がより強調された結果になったと推測できます。

他にも、「皮肉」というクエリで「今週末も笑えないジョークに付き合わされるなんて、最高の拷問だ。」というテキストが高い類似度を出したり、「雨」というクエリで子どもが裏路地にいる画像の類似度が高かったりという結果が得られました。

 

まとめ

本記事では、Gemini Embedding 2 について解説しました。

Gemini Embedding 2 は、テキスト・画像・動画・音声・PDF ドキュメントを共通したベクトル空間にネイティブにマッピングします。これにより、よりシームレスなマルチモーダル検索が実現するだけでなく、複数のモデルを単一モデルに統合できるため、システム構成をシンプルに保てるという開発者にとってのメリットもあります。

サービスでの直感的なコンテンツ検索や社内コンテンツの横断検索など、活用シーンは幅広く考えられます。ぜひさまざまなクエリを入力し、Gemini Embedding 2 のマルチモーダル検索を体感してみてください。

 

参考資料

 


システムサポートでは、Google Cloud の最新かつ高度なソリューションをお客さまに提供し続けることで、お客さまの DX 推進を強力にサポートしてまいります。AI Agent、データアナリティクス/データ分析基盤、クラウドマイグレーション/モダナイゼーションなど、広範囲でご支援が可能です。ぜひ、以下のボタンからお問い合わせください。
 

お問い合わせはこちら

2026年5月28日 Gemini Embedding 2 でマルチモーダル検索を試す

Category Google Cloud

ご意見・ご相談・料金のお見積もりなど、
お気軽にお問い合わせください。

お問い合わせはこちら