Google検索によるグラウンディング実装時のTips8選+UI参照実装

2025/03/19に公開されました。
2025/03/19に更新されました。

Gemini 1.xシリーズと2.0シリーズの違いなどにも触れます。


author: Shintaro

はじめに

Google検索によるグラウンディング実装時のTipsを8つ紹介します。
また、UI実装時のサンプルコードもこちらに置いてあります。
※Gemini Developer APIでも共通の部分はありますが、基本的にGoogle CloudのVertex AIを前提としています。

後続の説明を容易にするため、まずGoogle検索によるグラウンディングについて紹介します。
簡単に説明すると、GeminiにツールとしてGoogle検索を持たせることができる機能です。 Pythonでの具体例を以下に掲載します。

import vertexai
from vertexai.generative_models import (
    GenerationConfig,
    GenerativeModel,
    Tool,
    grounding,
)

PROJECT_ID = "project_id"
vertexai.init(project=PROJECT_ID, location="us-central1")

model = GenerativeModel("gemini-1.5-flash-002")
# Use Google Search for grounding
tool = Tool.from_google_search_retrieval(grounding.GoogleSearchRetrieval())
prompt = "〇〇の最新情報を教えてください"

response = model.generate_content(
    prompt,
    tools=[tool],
    generation_config=GenerationConfig(temperature=0.0),
    stream=true
)

なお、Gemini 2.0を使用する場合はモデル名を変えるだけでは動かず、以下のようにGen AI SDKに移行する必要がありそうです。

from google import genai
from google.genai.types import (
    GenerateContentConfig,
    GoogleSearch,
    HttpOptions,
    Tool,
)

client = genai.Client(
    vertexai=True,
    project="project_id",
    location="us-central1",
    http_options=HttpOptions(api_version="v1"),
)

response = client.models.generate_content(
    model="gemini-2.0-flash-001",
    contents="〇〇の最新情報を教えてください",
    config=GenerateContentConfig(
        tools=[
            # Use Google Search Tool
            Tool(google_search=GoogleSearch())
        ],
        temperature=0.0,
    ),
)

そしてレスポンスとしては以下のようなものが返ってきます。
※実際のレスポンスではなく、簡略化したサンプルです
※Gemini 1.5 Flashでストリーミングのパラメータをtrueにした場合のレスポンスです。

candidates {
  content {
    role: "model"
    parts {
      text: "イ"
    }
  }
}
usage_metadata {
}
model_version: "gemini-1.5-flash-002"

candidates {
  content {
    role: "model"
    parts {
      text: "ーロンマスク氏は現在、ドナルド・トランプ大統領によって設立された"
    }
  }
}
model_version: "gemini-1.5-flash-002"

candidates {
  content {
    role: "model"
    parts {
      text: "政府効率化省(DOGE)の長官を務めています\n"
    }
  }
  finish_reason: STOP
  grounding_metadata {
    web_search_queries: "イーロンマスクの政治家としての最新動向"
    search_entry_point {
      rendered_content: "<style>..."
    }
    grounding_chunks {
      web {
        uri: "https://vertexaisearch.cloud.google.com/grounding-api-redirect/..."
        title: "gicloud.co.jp"
      }
    }
    grounding_chunks {
      web {
        uri: "https://vertexaisearch.cloud.google.com/grounding-api-redirect/..."
        title: "gicloud.co.jp"
      }
    }
    grounding_supports {
      segment {
        end_index: 117
        text: "政府効率化省(DOGE)の長官を務めています"
      }
      grounding_chunk_indices: 0
      grounding_chunk_indices: 1
      confidence_scores: 0.853702784
      confidence_scores: 0.853702784
    }
    retrieval_metadata {
    }
  }
}
usage_metadata {
  prompt_token_count: 17
  candidates_token_count: 29
  total_token_count: 46
}
model_version: "gemini-1.5-flash-002"

なお、検索が行われた場合でも、レスポンスとしてgrounding_metadataが含まれない場合があります(公式ドキュメント)。

また、ユーザーへの回答表示時、これらのレスポンスを使用して参照元サイトの情報をUIで示すよう、Googleは推奨しています。
後ほど説明します。

実装時のTips

1. Gemini の利用とは別に料金がかかる

Geminiのインプット、アウトプットとは別にお金が掛かります。
1,000リクエストあたり35ドルです。詳しくは公式ドキュメントをご覧ください。

2. Gemini 1.x シリーズと 2.0 シリーズでは仕様が異なる

※前提
Gemini 1.xシリーズは2025年中に順次廃止されます。
これからGoogle検索によるグラウンディングの導入を検討する場合は、
Gemini 2.0シリーズと、用意されている言語の場合はGen AI SDKの使用がおすすめです。

Google検索によるグラウンディングは、以下のモデルでサポートされています。
※2025/3/17時点の情報です。最新情報は公式ドキュメントをご覧ください。
※Gemini 2.0 ProなどのExperimentalなモデルは未調査です。

  • Gemini 1.5 Pro(テキスト入力のみ)
  • Gemini 1.5 Flash(テキスト入力のみ)
  • Gemini 1.0 Pro(テキスト入力のみ)
  • Gemini 2.0 Flash

Gemini 2.0 Flashは画像や動画など、グラウンディングがオンになっている際もマルチモーダルなインプットに対応していますが、
現時点で以下で説明するDynamic Retrievalには未対応です。

3. Dynamic Retrieval の活用有無

Google検索の実行要否をLLMが判断する機能です。 以下のようにして使います。

# 再掲。これを
tool = Tool.from_google_search_retrieval(grounding.GoogleSearchRetrieval())

# こうする
tool = Tool.from_google_search_retrieval(
    grounding.GoogleSearchRetrieval(
        dynamic_retrieval_config=grounding.DynamicRetrievalConfig(
            mode="MODE_DYNAMIC",
            dynamic_threshold=0.5,
        )
    )
)
  • Prediction scoreというスコアに基づいて検索の実行有無が決まる。デフォルトでは0.7であり、それを超えると検索が行われる
  • 前述のように、2025/3/17時点でGemini2.0はDynamic Retrieval未対応。必ずGoogle検索が実行される

なお、このトピックのテーマからは外れますが、
Gemini 2.0は公式ドキュメントに検索とcode executionを組み合わせられるということが書いてあったので以下のように試したのですが、実行できませんでした。

response = client.models.generate_content(
    model="gemini-2.0-flash-001",
    contents="helloと出力するPythonのコード",
    config=GenerateContentConfig(
        tools=[
            # Use Google Search Tool
            Tool(google_search=GoogleSearch()),
            Tool(code_execution=ToolCodeExecution()),
        ],
    ),
)

以下のようなエラーが出ます。

{'error': {'code': 400, 'message': 'At most one tool is supported.', 'status': 'INVALID_ARGUMENT'}}

このあたりは仕様への理解が不足していることが原因だと感じているため、どなたかご存知の方いましたら教えてください。

4. Temperature は 0 が推奨

公式ドキュメントに記載があります。

5. Google の推奨 (一部は必須) UI が存在する

簡単にまとめると、以下の3つです。

  • 参照元サイトのタイトルやファビコンなどを表示させる
  • 回答文中の該当箇所に、上記サイトへの引用数字を表示させる
  • 検索クエリをSuggestionとして表示させる(必須)

Google推奨のUI実装

Google検索によるグラウンディングを使用した際のGeminiからのレスポンスを再掲します。

candidates {
  content {
    role: "model"
    parts {
      text: "イ"
    }
  }
}
usage_metadata {
}
model_version: "gemini-1.5-flash-002"

chunk: candidates {
  content {
    role: "model"
    parts {
      text: "ーロンマスク氏は現在、ドナルド・トランプ大統領によって設立された"
    }
  }
}
model_version: "gemini-1.5-flash-002"

chunk: candidates {
  content {
    role: "model"
    parts {
      text: "政府効率化省(DOGE)の長官を務めています\n"
    }
  }
  finish_reason: STOP
  grounding_metadata {
    web_search_queries: "イーロンマスクの政治家としての最新動向"
    search_entry_point {
      rendered_content: "<style>..."
    }
    grounding_chunks {
      web {
        uri: "https://vertexaisearch.cloud.google.com/grounding-api-redirect/..."
        title: "gicloud.co.jp"
      }
    }
    grounding_chunks {
      web {
        uri: "https://vertexaisearch.cloud.google.com/grounding-api-redirect/..."
        title: "gicloud.co.jp"
      }
    }
    grounding_supports {
      segment {
        end_index: 117
        text: "政府効率化省(DOGE)の長官を務めています"
      }
      grounding_chunk_indices: 0
      grounding_chunk_indices: 1
      confidence_scores: 0.853702784
      confidence_scores: 0.853702784
    }
    retrieval_metadata {
    }
  }
}
usage_metadata {
  prompt_token_count: 17
  candidates_token_count: 29
  total_token_count: 46
}
model_version: "gemini-1.5-flash-002"

参照元サイトのタイトルやファビコンなどを表示させる

以下部分のuriをfetchする必要があります。

grounding_chunks {
  web {
    uri: "https://vertexaisearch.cloud.google.com/grounding-api-redirect/..."
    title: "gicloud.co.jp"
  }
}

この部分はGeminiが回答を作成するために使ったWebサイトのURIとドメインです。
URIはhttps://vertexai...となっていますが、記事用のダミーではなく実際にこのような形式になっています。

なおファビコンについては、取得率的には以下のAPIを使うのが良さそうでした。

http://www.google.com/s2/favicons?domain=

回答文中の該当箇所に、参照元サイトへの引用数字を表示させる

以下の部分を使います。

grounding_chunks {
  web {
    uri: "https://vertexaisearch.cloud.google.com/grounding-api-redirect/..."
    title: "gicloud.co.jp"
  }
}
grounding_chunks {
  web {
    uri: "https://vertexaisearch.cloud.google.com/grounding-api-redirect/..."
    title: "gicloud.co.jp"
  }
}

grounding_supports {
  segment {
    end_index: 117
    text: "政府効率化省(DOGE)の長官を務めています"
  }
  grounding_chunk_indices: 0
  grounding_chunk_indices: 1
  confidence_scores: 0.853702784
  confidence_scores: 0.853702784
}

grounding_supports.grounding_chunk_indicesgrounding_chunksの出現順に対応しています。
つまり上記の例で言うと「政府効率化省(DOGE)の長官を務めています」の部分は
grounding_chunksの1つめと2つめ、両方のサイトを根拠として出力した回答ということです。

参照実装ではファビコンなどの取得を経つつ、
上記のレスポンスを整形して以下のような型のリストとしてまとめています。

export type GroundingSource = {
  title: string;
  domain: string;
  uri: string;
  faviconUrl: string;
};

export type GroundingCitation = {
  referenceNumber: number[];
  correspondingText: string;
};

const groundingCitations: GroundingCitation[] = [];
const groundingSources: GroundingSource[] = [];

これらを、回答文として出力されたテキストと一緒に以下のような関数に渡します。
該当部分に対して上付き文字のリンクを追加し、リプレイスしています。

export const addCitations = (
  responseText: string,
  sources: GroundingSource[],
  citations: GroundingCitation[],
): string => {
  // 文字列の複数回置換を避けるためのマップを作成
  const replacementMap = new Map<string, string>();

  for (const citation of citations) {
    const citationText = citation.correspondingText.replace(
      /(?<=[^\s\p{P}])\s+(?=[^\s\p{P}])/gu,
      "",
    );

    if (!replacementMap.has(citationText)) {
      let links = "";

      // 重複するインデックス番号を削除
      const uniqueIndices = [...new Set(citation.referenceNumber)].sort(
        (a, b) => a - b,
      );

      for (const index of uniqueIndices) {
        if (sources[index]) {
          links += `<a href="${
            sources[index].uri
          }" target="_blank" rel="noopener noreferrer" class="align-super text-xs no-underline">${
            index + 1
          }</a>`;
        }
      }

      if (links) {
        replacementMap.set(citationText, `${citationText}${links}`);
      }
    }
  }

  // 一度にすべての置換を実行
  let newResponseText = responseText;
  for (const [original, replacement] of replacementMap.entries()) {
    // 正規表現を使わず単純な文字列置換を使用
    newResponseText = newResponseText.split(original).join(replacement);
  }

  return newResponseText;
};

これまでの例で具体的なアウトプットを示すと、まず以下のレスポンスをGeminiから受け取ったとします。

# Geminiのレスポンス(テキスト部分をすべてつなげたもの)
イーロンマスク氏は現在、ドナルド・トランプ大統領によって設立された政府効率化省(DOGE)の長官を務めています

# Geminiのレスポンス(grounding_metadataの抜粋)
grounding_chunks {
  web {
    uri: "https://vertexaisearch.cloud.google.com/grounding-api-redirect/response1..."
    title: "gicloud.co.jp"
  }
}
grounding_chunks {
  web {
    uri: "https://vertexaisearch.cloud.google.com/grounding-api-redirect/response2...."
    title: "gicloud.co.jp"
  }
}

grounding_supports {
  segment {
    end_index: 117
    text: "政府効率化省(DOGE)の長官を務めています"
  }
  grounding_chunk_indices: 0
  grounding_chunk_indices: 1
  confidence_scores: 0.853702784
  confidence_scores: 0.853702784
}

「政府効率化省(DOGE)の長官を務めています」の部分は、2つのサイトを参照して回答されたことがわかります。
そのため、この部分のテキストにaddCitations関数で以下のようにaタグを加えます。

政府効率化省(DOGE)の長官を務めています<a href="https://vertexaisearch..../response1" target="_blank" rel="noopener noreferrer" class="align-super text-xs no-underline">1</a><a href="https://vertexaisearch..../response2" target="_blank" ...>2</a>

実例として、以下のように表示されます。
※リンクであることがかなりわかり辛かったり、リンク同士の距離が近かったりとUI/UX的にかなりお粗末ですがご容赦ください。 回答文への引用リンクの実例

検索クエリをSuggestionとして表示させる

以下のrendered_contentの部分です。(本当はすごく長いのですが、省略しています)

grounding_metadata {
  web_search_queries: "イーロンマスクの政治家としての最新動向"
  search_entry_point {
    rendered_content: "<style>..."
  }

これをhtmlとして出力すると、以下画像のようになります。

Google Search Suggestion

ブラウザがダークモードになっていると黒に変わります。

6. グラウンディングされた回答は、保存して良い内容と期間に制限がある

Service Termsに以下のような表現があります。

  • Customer may store the text of the Grounded Results (excluding Links):
    (1) that were displayed by Customer for up to thirty (30) days only to evaluate and optimize the display of the Grounded Results in the Customer Application;
    (2) in the chat history of an End User of the Customer Application for up to six (6) months only for the purpose of allowing that End User to view their chat history.

要約すると以下の通りです。

  • グラウンディングが行われたレスポンスについては、以下条件において、その回答テキストのみ保存可能
    • 評価および最適化の目的においては30日間
    • アプリのユーザーが自身のチャット履歴を閲覧できるようにする目的においては6か月

つまり、前述の参照元サイトのリンクや、Suggestionなどの保存はNGです。

7. ユーザーが入力したクエリはデバッグ、テストの目的でGoogle社内にのみ保存される

※Googleサポートに問い合わせて得た内容ですが、判断に責任は持てません。参考程度にご認識お願いします。

  • 検索に利用したワードは30日間保存され、Google社内でのテスト・デバッグに利用される
  • 一定期間データは残るが、第三者に推測可能な形で利用されることはない

8. 検索されるサイトに制限をかけることはできない

例えば検索実行前にWebプロキシをかませるといった機能はないため、
セキュリティ的に問題がある場合は、取得した回答に対しての対応が必要です。
そもそも、よろしくないサイトが検索結果に表示されてしまうようなクエリを投げないためのリテラシー教育なども重要です。

実装例

コードはこちらに置いておきます。 (記事内ではサンプルとしてPythonを出してきましたが、コードはTypeScriptです)

おわりに

Google検索によるグラウンディングを使用する場合は、Gemini 1.5シリーズから2.0へのマイグレーションが必要で、ちょっと大変ですね。
Web検索関連の余談ですが、直近ではgpt-4oシリーズも検索できるようになったりしていますね。

※本記事は、ジーアイクラウド株式会社の見解を述べたものであり、必要な調査・検討は行っているものの必ずしもその正確性や真実性を保証するものではありません。

※リンクを利用する際には、必ず出典がGIC dryaki-blogであることを明記してください。
リンクの利用によりトラブルが発生した場合、リンクを設置した方ご自身の責任で対応してください。
ジーアイクラウド株式会社はユーザーによるリンクの利用につき、如何なる責任を負うものではありません。