Gemini APIで外部情報を取得して構造化したい。この2つの要件を同時に満たすのは、つい最近まで不可能でした。Grounding with Google Searchを有効にするとStructured Outputが使えず、逆もまた然り。Gemini 3のAPIアップデートで、ようやくこの制約が解消されました。
Web検索の結果を型定義どおりのJSONで返す。この組み合わせが使えるようになったことで、「最新の為替レートを取得してJSON配列で返す」「競合製品の価格を検索して比較テーブル用のデータにする」といった処理が、1回のAPI呼び出しで完結します。
Grounding × Structured Outputが解決する問題
従来のアプローチでは、2段階のパイプラインが必要でした。
1回目のAPI呼び出しでGroundingを使ってWeb情報を取得し、テキスト応答を受け取ります。2回目の呼び出しで、そのテキストをStructured Outputで構造化します。コストは2倍、レイテンシも2倍、そしてテキスト応答を再入力する際に情報が欠落するリスクがありました。
新しい統合APIでは、toolsにGoogle Searchを指定しつつ、response_mime_typeでJSONスキーマを定義できます。モデルが検索を実行し、結果を理解し、指定した型に変換するまでを1ステップで処理します。
from google import genai
from google.genai import types
import json
client = genai.Client()
# Grounding + Structured Outputの統合リクエスト
response = client.models.generate_content(
model = "gemini-2.5-pro" ,
contents = "2026年4月時点のGemini APIの料金体系を調べて、モデルごとに整理してください" ,
config = types.GenerateContentConfig(
tools = [types.Tool( google_search = types.GoogleSearch())],
response_mime_type = "application/json" ,
response_schema = {
"type" : "object" ,
"properties" : {
"pricing_data" : {
"type" : "array" ,
"items" : {
"type" : "object" ,
"properties" : {
"model_name" : { "type" : "string" },
"input_price_per_million" : { "type" : "string" },
"output_price_per_million" : { "type" : "string" },
"context_window" : { "type" : "string" },
},
"required" : [
"model_name" ,
"input_price_per_million" ,
"output_price_per_million"
]
}
},
"last_updated" : { "type" : "string" },
"source_urls" : {
"type" : "array" ,
"items" : { "type" : "string" }
}
},
"required" : [ "pricing_data" , "last_updated" ]
}
)
)
# 型安全なJSONが直接返る
data = json.loads(response.text)
for model in data[ "pricing_data" ]:
print ( f " { model[ 'model_name' ] } : 入力 { model[ 'input_price_per_million' ] } /1M tokens" )
# Groundingメタデータで出典を確認
if response.candidates[ 0 ].grounding_metadata:
for chunk in response.candidates[ 0 ].grounding_metadata.grounding_chunks:
print ( f " 出典: { chunk.web.title } — { chunk.web.uri } " )
このコードが返すのは、Google検索で取得した最新情報を元に構造化されたJSON。スキーマに沿わないフィールドは含まれず、必須フィールドが欠けることもありません。
レスポンスの構造を理解する
統合リクエストのレスポンスには、通常のStructured Outputに加えてgrounding_metadataが付与されます。ここに検索クエリ、参照元URL、引用情報が格納されています。
candidate = response.candidates[ 0 ]
# 1. 構造化されたJSONテキスト
structured_data = json.loads(candidate.content.parts[ 0 ].text)
# 2. Groundingメタデータ
metadata = candidate.grounding_metadata
# 検索に使われたクエリ
print ( "検索クエリ:" , metadata.web_search_queries)
# → ['Gemini API pricing 2026', 'Gemini 2.5 Pro API cost per token']
# 参照元のWebページ
for chunk in metadata.grounding_chunks:
print ( f "参照: { chunk.web.title } " )
print ( f " URL: { chunk.web.uri } " )
# 引用サポート(レスポンスのどの部分がどの出典に基づくか)
for support in metadata.grounding_supports:
print ( f "テキスト: { support.segment.text } " )
print ( f " 出典インデックス: { support.grounding_chunk_indices } " )
print ( f " 信頼度: { support.confidence_scores } " )
注意すべきなのは、grounding_supportsの挙動です。Structured Outputと組み合わせた場合、テキスト応答がJSON形式になるため、grounding_supportsのsegment.textがJSON断片を指すことがあります。出典の検証にはgrounding_chunksのURL一覧を使うほうが確実です。
実用パターン:競合価格モニタリング
実際のプロダクトで使えるパターンを示す。ECサイトの競合価格を定期的に取得し、構造化データとしてデータベースに格納するケースです。
from google import genai
from google.genai import types
import json
from datetime import datetime
client = genai.Client()
PRICE_SCHEMA = {
"type" : "object" ,
"properties" : {
"products" : {
"type" : "array" ,
"items" : {
"type" : "object" ,
"properties" : {
"product_name" : { "type" : "string" },
"vendor" : { "type" : "string" },
"price" : { "type" : "number" },
"currency" : { "type" : "string" },
"in_stock" : { "type" : "boolean" },
"url" : { "type" : "string" }
},
"required" : [ "product_name" , "vendor" , "price" , "currency" ]
}
},
"search_timestamp" : { "type" : "string" },
"confidence_note" : { "type" : "string" }
},
"required" : [ "products" , "search_timestamp" ]
}
def fetch_competitor_prices (product_query: str ) -> dict :
"""競合価格をリアルタイム検索して構造化データで返す"""
try :
response = client.models.generate_content(
model = "gemini-2.5-flash" ,
contents = (
f "以下の製品について、主要なオンラインストアでの"
f "現在の販売価格を調べてください。"
f "価格は税込み表示で、在庫状況も含めてください。 \n\n "
f "対象製品: { product_query }\n\n "
f "検索対象ストア: Amazon.co.jp, 楽天市場, Yahoo\!ショッピング"
),
config = types.GenerateContentConfig(
tools = [types.Tool( google_search = types.GoogleSearch())],
response_mime_type = "application/json" ,
response_schema = PRICE_SCHEMA ,
temperature = 0.1 # 事実ベースのデータには低温度を推奨
)
)
data = json.loads(response.text)
data[ "search_timestamp" ] = datetime.now().isoformat()
# 出典URLをメタデータから補完
if response.candidates[ 0 ].grounding_metadata:
source_urls = [
chunk.web.uri
for chunk in response.candidates[ 0 ].grounding_metadata.grounding_chunks
]
data[ "source_urls" ] = source_urls
return data
except Exception as e:
return {
"products" : [],
"search_timestamp" : datetime.now().isoformat(),
"confidence_note" : f "検索エラー: { str (e) } "
}
# 実行
result = fetch_competitor_prices( "Sony WH-1000XM6 ワイヤレスヘッドホン" )
print (json.dumps(result, ensure_ascii = False , indent = 2 ))
# 出力例:
# {
# "products": [
# {
# "product_name": "Sony WH-1000XM6",
# "vendor": "Amazon.co.jp",
# "price": 49500,
# "currency": "JPY",
# "in_stock": true,
# "url": "https://www.amazon.co.jp/dp/..."
# }
# ],
# "search_timestamp": "2026-04-12T15:30:00",
# "source_urls": ["https://..."]
# }
temperatureを0.1に設定しているのは意図的です。価格データのように事実を正確に抽出する用途では、創造性を抑えたほうが安定します。0にしないのは、検索結果の解釈に最低限の柔軟性を残すためです。
スキーマ設計で精度を上げるコツ
Grounding × Structured Outputの組み合わせでは、スキーマの設計がレスポンス品質に直結します。普通のStructured Outputと違い、モデルは「検索結果から情報を抽出してスキーマに当てはめる」という二重のタスクを実行するためです。
フィールド数は最小限に保ちます。 スキーマに20以上のフィールドを定義すると、モデルが検索結果から全フィールドを埋めようとして、確証のないデータを推測で補完するケースが増えます。1リクエストあたり10フィールド以下に抑え、足りなければ複数回に分けるほうが精度が高いです。
requiredの使い方を工夫します。 Web検索の結果に含まれない情報もあります。在庫状況や発売日など、確実に取得できるか分からないフィールドはrequiredから外します。モデルに「分からなければ省略してよい」余地を与えることで、ハルシネーションを減らせます。
descriptionをスキーマに追加します。 JSON Schemaのdescriptionフィールドは、モデルがフィールドの意味を理解する手がかりになります。
schema = {
"type" : "object" ,
"properties" : {
"price" : {
"type" : "number" ,
"description" : "税込み価格(日本円)。見つからない場合は0"
},
"last_confirmed" : {
"type" : "string" ,
"description" : "価格が確認された日付。YYYY-MM-DD形式。不明ならnull"
}
}
}
descriptionがあるかないかで、特に曖昧な数値フィールドの精度が変わります。税込み・税抜きの区別、通貨単位、日付フォーマットなど、人間にとって「当然」の文脈をモデルに明示する点が肝心です。
URL Contextとの併用
Google Searchに加えて、URL Contextツールも同時に使えます。特定のWebページの内容を直接読み取り、その情報を構造化データに変換するパターンです。
response = client.models.generate_content(
model = "gemini-2.5-pro" ,
contents = "このページに掲載されているイベント情報を抽出してください" ,
config = types.GenerateContentConfig(
tools = [
types.Tool( url_context = types.UrlContext(
urls = [ "https://example.com/events/2026-spring" ]
)),
types.Tool( google_search = types.GoogleSearch())
],
response_mime_type = "application/json" ,
response_schema = {
"type" : "object" ,
"properties" : {
"events" : {
"type" : "array" ,
"items" : {
"type" : "object" ,
"properties" : {
"event_name" : { "type" : "string" },
"date" : { "type" : "string" },
"location" : { "type" : "string" },
"ticket_price" : { "type" : "string" }
},
"required" : [ "event_name" , "date" ]
}
}
}
}
)
)
URL Contextで指定ページの情報を読み取りつつ、Google Searchで補足情報(会場の住所、交通アクセスなど)を自動で調べてくれます。ただし、ツールを2つ同時に指定するとレスポンスが遅くなる傾向があります。レイテンシが問題になるなら、用途に応じてどちらか一方に絞ることも検討すべきです。
抽出データを信頼してよいか — 出典の有無でレコードを隔離する検証ゲート
ここまでで「検索結果を型どおりのJSONにする」ところまでは到達しました。ですが、抽出できたからといって、その一行一行をそのまま本番データベースに書き込んでよいわけではありません。Groundingが付いていても、モデルが文脈から推測で埋めたフィールドと、実際に検索結果に裏付けられたフィールドが、同じJSONの中に混在することがあるためです。
幸い、grounding_metadataには「どの出力がどの出典に支えられているか」という情報が含まれています。これを使えば、出典が取れた行だけを本番へ流し、裏付けの取れない行は隔離(quarantine)して人間のレビューに回す、という検証ゲートを組めます。
from google import genai
from google.genai import types
import json
client = genai.Client()
MIN_SUPPORT = 1 # 1件以上の出典が必要
CONF_THRESHOLD = 0.6 # 信頼度スコアの下限(提供される場合)
def extract_with_provenance (prompt: str , schema: dict ):
resp = client.models.generate_content(
model = "gemini-2.5-flash" ,
contents = prompt,
config = types.GenerateContentConfig(
tools = [types.Tool( google_search = types.GoogleSearch())],
response_mime_type = "application/json" ,
response_schema = schema,
),
)
cand = resp.candidates[ 0 ]
data = json.loads(cand.content.parts[ 0 ].text)
meta = cand.grounding_metadata
# 出典 URL の集合(空なら「検索が走らなかった」とみなす)
sources = []
if meta and meta.grounding_chunks:
sources = [c.web.uri for c in meta.grounding_chunks if c.web]
# 出力テキストのうち、出典に支えられた区間の信頼度を集計
supports = meta.grounding_supports if (meta and meta.grounding_supports) else []
backed = len (sources) >= MIN_SUPPORT
confident = True
for sup in supports:
scores = getattr (sup, "confidence_scores" , None ) or []
if scores and max (scores) < CONF_THRESHOLD :
confident = False
break
verdict = "accepted" if (backed and confident) else "quarantined"
return {
"verdict" : verdict,
"data" : data,
"sources" : sources,
"reason" : (
"ok" if verdict == "accepted"
else ( "no_grounding_source" if not backed else "low_confidence" )
),
}
schema = {
"type" : "object" ,
"properties" : {
"price" : { "type" : "number" , "description" : "税込み価格(日本円)。不明なら0" },
"currency" : { "type" : "string" },
},
"required" : [ "price" , "currency" ],
}
result = extract_with_provenance(
"現在の某クラウドストレージ 1TB プランの月額を調べてJSONで返してください" ,
schema,
)
if result[ "verdict" ] == "accepted" :
save_to_production(result[ "data" ], result[ "sources" ]) # 本番テーブルへ
else :
save_to_review_queue(result) # 隔離キューへ
print ( "隔離:" , result[ "reason" ])
ポイントは「拒否」ではなく「隔離」にしていることです。出典が取れなかった行を例外として捨ててしまうと、後から「なぜこのデータが無いのか」を追えなくなります。判定理由(no_grounding_source / low_confidence)を添えてレビューキューに残しておくと、閾値が厳しすぎるのか、そもそも検索が走っていないのかを後から切り分けられます。
confidence_scoresは常に返るとは限りません。返らない環境では「出典 URL が1件以上あるか」だけでも十分に効きます。まずは出典の有無で隔離し、信頼度スコアが取れるなら閾値を足す、という順で育てるのが現実的です。
個人開発で複数サイトの運用データを自動収集していると、検索由来の数値をそのまま本番テーブルへ書き込んだ結果、あとから出典をたどれずに原因調査で詰まったことが何度かありました。それ以来、私自身は「出典が取れない行は本番に入れない」を抽出パイプラインの最初のルールにしています。隔離キューを一段挟むだけで、誤った数値が静かに広がる事故がはっきり減りました。
コスト構造を把握する
この統合機能のコスト構造は、3つの要素で成り立っています。
入出力トークン料金 — 通常のGemini API利用料金。Gemini 2.5 Flashなら入力$0.15/100万トークン、出力$0.60/100万トークン(2026年4月時点)。
Google Search料金 — $14/1,000クエリ。1リクエストにつきモデルが1〜3クエリを実行するのが一般的です。
Structured Outputのオーバーヘッド — スキーマ定義自体がトークンを消費します。複雑なスキーマほどコストが上がりますが、2回のAPI呼び出しを1回にまとめる分、差し引きでは有利になることが多いです。
1リクエストあたりの実コストをざっくり計算すると、Gemini 2.5 Flashの場合で約$0.015〜$0.03。2段階パイプラインでは$0.02〜$0.05程度かかっていたので、統合により40〜60%のコスト削減が見込めます。最新の料金はGemini API公式の料金ページ で確認してみてください。
本番運用で見落としがちな3つの注意点
この機能を本番で使い始めると、公式ドキュメントだけでは分からない挙動に出会います。
レート制限はGrounding側が先に来ます。 Google Search APIのクォータは、通常のgenerateContentのRPMとは別に管理されています。大量リクエストを投げると、トークン制限よりも先にGroundingのクォータに引っかかることがあります。バッチ処理では意図的に間隔を空けるか、キャッシュ可能な結果はデータベースに保存して再利用するのが定石です。
grounding_metadataが空で返ることがあります。 モデルが検索不要と判断した場合、Groundingツールを呼び出さずにJSON応答を返します。これはバグではなく仕様です。「最新の」「現在の」「2026年4月時点」など、時間軸を明示するキーワードをプロンプトに含めると、検索が実行される確率が上がります。
ストリーミングとの併用は限定的。 stream=TrueでGrounding + Structured Outputを使うと、JSONの途中断片がチャンクとして返ってきます。パース処理が複雑になるうえ、grounding_metadataはストリーム完了後にしか取得できません。リアルタイム表示が不要なら、非ストリーミングのほうが扱いやすいです。
次に試すこと
まずは最初のコード例をそのまま動かしてみてほしい。google-genaiパッケージ(pip install google-genai)と有効なAPIキーがあれば動く。スキーマを書き換えるだけで、ニュース記事の構造化収集、求人情報のデータ化、技術トレンドの定点観測など、さまざまなリアルタイムデータ抽出に応用できます。
Groundingの基本を復習したい場合は「Grounding with Google Search — Geminiの検索連携で回答精度を上げる 」、構造化出力を本番で信用する設計を深掘りするなら「Gemini の構造化出力を本番で信用するために 」を参照してみてください。