「夕暮れの海を紫がかったトーンで仕上げた壁紙、あれはどのフォルダだったか」。先週、運営中の壁紙アプリの特集ページを組んでいて、この一枚を探すのに10分以上かけてしまいました。カテゴリは合っている。タグも辿った。それでも見つからない。原因は単純で、その画像は「海」カテゴリではなく「空」カテゴリに入っていたのです。
私の手元には、個人開発で運営しているアプリ群の壁紙アセットが数千枚あります。これまでは Gemini Vision による30カテゴリの自動分類で整理しており、その経緯はGemini Vision で壁紙アプリのカテゴリ自動分類を試した記録に書きました。分類の精度には満足しております。ただ、分類とは「画像を一つの箱に入れる」ことです。箱をまたぐニュアンス――「夕暮れ」で「海」で「紫」――は、箱の構造ではすくえません。
そこに、File Search が gemini-embedding-2 で画像のネイティブ埋め込み・検索に対応したという知らせが届きました。自然言語のクエリで画像そのものを意味検索できるなら、この「箱をまたぐ探しもの」が解決するかもしれない。まずは300枚で検証した記録です。
カテゴリ分類で困っていなかったのに、なぜ検索が欲しくなったか
正直に書くと、エンドユーザー向けの機能としてはカテゴリ分類で十分に回っております。困っていたのは運営側の作業です。
特集ページの編成、App Store 用スクリーンショットの素材選び、季節キャンペーンの差し替え。こうした作業では「カテゴリ」ではなく「印象」で画像を探します。「静かで寒色系の朝」「視線が抜ける構図の都市夜景」。この語彙はカテゴリ30個では表現できず、結局サムネイル一覧を目視で流すことになります。1回あたり5〜15分。月に何度も発生します。
タグを増やせば解決する、とは考えませんでした。タグ体系を細かくするほど付与の一貫性が崩れ、メンテナンスコストが検索の便益を食いつぶす。これは数千枚を管理してきた中での実感です。欲しいのは「タグを設計しない検索」でした。
300枚をストアに入れて検索できるようにするまで
検証には Python SDK(google-genai)を使いました。やることは三つだけです。ストアを作る、画像を入れる、検索する。
まずストアの作成と画像のアップロードです。後で手元の資産管理と突き合わせられるよう、custom_metadata にアセットIDを必ず入れておきます。
import pathlib
from google import genai
client = genai.Client() # 環境変数 GEMINI_API_KEY を参照
# 検証用の File Search ストアを作成
store = client.file_search_stores.create(
config={"display_name": "wallpaper-assets-trial"}
)
# 2026年6月入庫分の300枚をインポート
for path in sorted(pathlib.Path("./assets/2026-06").glob("*.jpg")):
client.file_search_stores.upload_to_file_search_store(
file_search_store_name=store.name,
file=str(path),
config={
"display_name": path.stem,
"custom_metadata": [
{"key": "asset_id", "string_value": path.stem},
],
},
)このコードのポイントは custom_metadata です。検索結果はチャンク(取得元)として返ってくるため、メタデータにIDがないと「どのファイルがヒットしたのか」をアプリ側の資産DBと照合する手段がなくなります。最初の数十枚をID無しで入れてしまい、入れ直す羽目になりました。
検索は、通常の generate_content に File Search をツールとして渡すだけです。
from google.genai import types
response = client.models.generate_content(
model="gemini-3.5-flash",
contents="夕暮れの海を写した、紫がかったトーンの壁紙を探してください",
config=types.GenerateContentConfig(
tools=[
types.Tool(
file_search=types.FileSearch(
file_search_store_names=[store.name]
)
)
]
),
)
# 回答本文より、根拠として返るチャンクの方が本命
meta = response.candidates[0].grounding_metadata
for chunk in meta.grounding_chunks:
print(chunk.retrieved_context.title) # → custom_metadata と照合運営ツールとして使う場合、欲しいのはモデルの文章ではなくヒットした画像の一覧です。なので grounding_metadata 側を主役として扱い、回答テキストは添え物にする。この割り切りで実装がだいぶ素直になりました。
ぶつかった壁は三つ
一つ目は、インデックス反映のラグです。アップロード直後に検索しても、その画像はヒットしません。300枚の検証では、全件が検索に乗るまで体感で数分、長いときで7分ほどかかりました。バッチで入庫して翌朝使う運用なら問題ありませんが、「入れた直後に確認」という手作業の流れには合いません。入庫スクリプトの最後に、代表クエリを投げて新規分がヒットするか確かめる待ち合わせ処理を足しました。
二つ目は、厳密な条件との混在です。意味検索は「紫っぽい夕暮れの海」には強いのですが、「アスペクト比 19.5:9 以上」「2024年以前に入庫」のような確定条件は埋め込み検索の守備範囲ではありません。ここは無理をさせず、File Search で意味の候補を出し、解像度や入庫日は手元の資産DBで絞る二段構えにしました。メタデータフィルタも併用できますが、数値レンジの絞り込みは自前DBの方が確実です。
三つ目は、コストの考え方です。File Search は検索クエリ時の埋め込み生成には課金されず、インデックス作成時の埋め込み処理に課金される設計です。つまり「入庫時に一度払い、検索は何度でも軽い」。300枚の検証ではインデックス費用は誤差の範囲でしたが、数千枚を全量投入するなら、二度と検索しない死蔵アセットまで入れるかは考えどころです。私は「現役アセットのみ投入、引退分はストアから削除」という運用に倒すつもりです。
カテゴリ分類は捨てない ― 役割が違った
検証前は「意味検索が使えるならカテゴリ分類は引退かもしれない」と内心思っておりました。20本のクエリで試した結果、17本で意図した画像が上位に入り、精度には確かな手応えがあります。それでも結論は「両方残す」です。役割が違いました。
- カテゴリ分類が向くもの: エンドユーザー向けの一覧導線。「箱」が安定していること自体に価値があり、毎日開く画面では予測可能性が信頼になります
- マルチモーダル検索が向くもの: 運営側の探しもの。語彙が毎回変わる、一回性の高いクエリ。タグ設計のメンテナンスコストをゼロにできるのが効きます
- 両者の接続点: 検索でよくヒットする語彙(「夕暮れ」「寒色」など)は、次のカテゴリ再編やアプリ内検索機能の語彙設計のヒントになります
冒頭の「紫がかった夕暮れの海」は、File Search では最初のクエリで1位に返ってきました。10分の目視が数秒になった瞬間で、静かに胸が熱くなりました。
次のステップ
全量投入はまだ行いません。まず新規入庫分だけをストアに流すパイプラインを組み、月次で「検索が実作業を何分置き換えたか」を記録してから判断します。私自身、意味検索の便利さは体感的なぶん、効果を数字にしておかないと、ただの維持費になりかねないからです。
もし手元に「タグでは表現しきれない画像資産」をお持ちでしたら、まずは数百枚の小さなストアで試すことをおすすめします。インデックスのラグとメタデータ設計さえ押さえれば、検証自体は半日で終わります。同じ課題に取り組んでいる方の参考になれば幸いです。