🤖

[BQML] Gemini の Grounding (Google検索) で欠損値を補完する

に公開

はじめに

こんにちは、クラウドエース株式会社 第四開発部の多賀です。

データ分析や活用において、頻繁に課題となるのが 「データの欠損」 です。
「顧客リストの電話番号が抜けている」「住所が入っていない」といった不完全なデータに対し、Web 検索等を用いて情報を補完しようとする場合、これまでは外部スクリプトを作成して API を利用したり、あるいは手作業で検索してデータを更新するといった対応が必要でした。

しかし、BigQuery ML (BQML) と Gemini、そして Grounding with Google Search を組み合わせることで、このプロセスをSQLだけで完結させることが可能になりました。

本記事では、不完全な企業データを、「既知の情報をもとに Gemini に Google 検索を行わせ、欠損部分を埋める」 というアプローチで補完する方法をご紹介します。

対象読者

  • BigQuery を日常的に利用しているデータアナリストやエンジニア
  • データの欠損や不備の補完作業に課題を感じている方
  • Python などの外部スクリプトを書かずに、SQL だけで生成 AI (Gemini) を活用したい方

検証

アーキテクチャとアプローチ

今回のアプローチは非常にシンプルです。BigQuery 上にある欠損を含むデータに対し、以下のフローで処理を行います。

  1. プロンプト生成: 欠損している項目を特定し、「〇〇(既知の情報)をもとに、××(欠損項目)を調べて」という命令文を動的に作成。
  2. Grounding 実行: Gemini が Google 検索を実行し、最新かつ正確な情報を取得。
  3. データ補完: 検索結果から必要な情報を抽出し、テーブルの NULL を埋める。

前提設定

BQML と Gemini の連携設定(Cloud リソース接続の作成や権限付与)については、以下の公式ドキュメントなどを参考に設定を行ってください。

参考: Gemini モデルと AI.GENERATE_TEXT 関数を使用してテキストを生成する

接続設定が完了している前提で、まずはデータセットと Gemini のリモートモデルを作成します。

CREATE SCHEMA IF NOT EXISTS `[PROJECT_ID].demo_dataset`
OPTIONS (
  location = 'asia-northeast1',
  description = 'デモ作成用のデータセット'
);

CREATE OR REPLACE MODEL `demo_dataset.gemini`
  REMOTE WITH CONNECTION `projects/[PROJECT_ID]/locations/asia-northeast1/connections/[CONNECTION_ID]`
  OPTIONS (ENDPOINT = 'gemini-2.5-pro');

検証用データの準備

検証のために、「虫食い状態」のテーブルを作成します。
企業名、電話番号、郵便番号、住所のカラムを用意し、いくつかのパターンで NULL を混入させます。

入力データ (demo_dataset.missing_values_table)

id company_name phone_number post_code address
1 クラウドエース株式会社 0362805939 1000004 東京都千代田区大手町1丁目7−2 東京サンケイビル 26階
2 NULL 0362805939 1000004 東京都千代田区大手町1丁目7−2 東京サンケイビル 26階
3 クラウドエース株式会社 NULL 1000004 東京都千代田区大手町1丁目7−2 東京サンケイビル 26階
4 クラウドエース株式会社 0362805939 NULL 東京都千代田区大手町1丁目7−2 東京サンケイビル 26階
5 クラウドエース株式会社 0362805939 1000004 NULL
6 クラウドエース株式会社 NULL NULL NULL

実行クエリの解説

SQL だけで「動的なプロンプト作成」から「検索結果の反映」までを一気通貫で行います。

ポイント

  1. 動的なプロンプト生成 (prompt_components)
    全ての行に対して同じプロンプトを投げるのではなく、CASE WHEN 句を使用して 「何が欠けているか(何を調べるべきか)」「何がわかっているか(検索の手がかり)」 を行ごとに組み立てています。
  2. コスト効率を考慮した処理フロー
    欠損がない行に対してGeminiを呼び出すと不要なコストが発生するため、WHERE missing_labels != '' でフィルタリングし、必要な行だけ推論を実行しています。
  3. Grounding の有効化
    ML.GENERATE_TEXT 関数の引数で ground_with_google_search: TRUE を指定することで、Gemini が学習データだけでなく、リアルタイムな Google 検索結果を利用できるようになります。

実際のクエリ

WITH prompt_components AS (
  SELECT
    *,
    -- 既知の情報を連結して「検索の手がかり」を作成
    CONCAT(
      IF(company_name IS NOT NULL, CONCAT('\n企業名: ', company_name), ''),
      IF(post_code IS NOT NULL, CONCAT('\n郵便番号: ', post_code), ''),
      IF(address IS NOT NULL, CONCAT('\n住所: ', address), ''),
      IF(phone_number IS NOT NULL, CONCAT('\n電話番号: ', phone_number), '')
    ) AS known_info,
    -- 欠損している項目をリストアップして「調査対象」を明示
    ARRAY_TO_STRING(
      [
        CASE WHEN company_name IS NULL THEN '「企業名」' END,
        CASE WHEN post_code IS NULL THEN '「郵便番号」' END,
        CASE WHEN address IS NULL THEN '「住所」' END,
        CASE WHEN phone_number IS NULL THEN '「代表電話番号」' END
      ], 'と'
    ) AS missing_labels,
    -- JSON出力用のキー指定
    ARRAY_TO_STRING(
      [
        CASE WHEN company_name IS NULL THEN '"company_name"' END,
        CASE WHEN post_code IS NULL THEN '"post_code"' END,
        CASE WHEN address IS NULL THEN '"address"' END,
        CASE WHEN phone_number IS NULL THEN '"phone_number"' END
      ], ', '
    ) AS missing_keys
  FROM
    `[PROJECT_ID].demo_dataset.missing_values_table`
),

gemini_results AS (
  SELECT
    id, -- 後で結合するためにIDを取得
    -- Geminiの出力からJSON部分のみを抽出・整形
    REGEXP_REPLACE(
      JSON_VALUE(ml_generate_text_result['candidates'][0]['content'], '$.parts[0].text'),
      r'^```json\s*|\s*```$', '' 
    ) AS gemini_json_cleaned,
    -- 実際にGeminiが実行した検索クエリ(デバッグ用)
    ml_generate_text_result['candidates'][0]['grounding_metadata']['web_search_queries'] AS used_search_query,
  FROM
    ML.GENERATE_TEXT(
      MODEL `demo_dataset.gemini`,
      (
        SELECT
          *,
          -- プロンプトの最終組み立て
          CONCAT(
            '以下の情報をもとに、Google検索を用いて', missing_labels, 'を調査してください。',
            '\n結果はJSON形式で出力してください。',
            '\nJSONのキーは ', missing_keys, ' としてください。',
            '\n余計な文章は含めず、JSONのみを返してください。',
            '\n検索時は引用符を用いないでください。',
            '\n\n--- 既知の情報 ---',
            known_info
          ) AS prompt
        FROM
          prompt_components
        WHERE
          missing_labels != '' -- 欠損がない行はGeminiに投げない
      ),
      STRUCT(
        TRUE AS ground_with_google_search,
        0.0 AS temperature
      )
    )
)

SELECT
  -- baseテーブルの全カラムを取得しつつ、指定したカラムだけロジック適用後の値に置き換える
  base.* REPLACE (
    -- 企業名:元があれば元、なければGemini
    COALESCE(base.company_name, JSON_VALUE(gemini.gemini_json_cleaned, '$.company_name')) AS company_name,
    
    -- 郵便番号:クレンジング付きで置換
    REGEXP_REPLACE(
      COALESCE(base.post_code, JSON_VALUE(gemini.gemini_json_cleaned, '$.post_code')),
      r'[^0-9]', ''
    ) AS post_code,
    
    -- 住所
    COALESCE(base.address, JSON_VALUE(gemini.gemini_json_cleaned, '$.address')) AS address,
    
    -- 電話番号:クレンジング付きで置換
    REGEXP_REPLACE(
      COALESCE(base.phone_number, JSON_VALUE(gemini.gemini_json_cleaned, '$.phone_number')),
      r'[^0-9]', ''
    ) AS phone_number
  ),
  
  -- Gemini側のデバッグ情報
  gemini.used_search_query

FROM
  [PROJECT_ID].demo_dataset.missing_values_table AS base
LEFT JOIN
  gemini_results AS gemini
ON
  base.id = gemini.id
;

プロンプトの生成例

上記のクエリによって、実際に Gemini に投げられるプロンプトは以下のようになります。
例えば、ID: 2(社名のみ欠損している行) の場合、以下のようなテキストが動的に構成されます。

以下の情報をもとに、Google検索を用いて「企業名」を調査してください。
結果はJSON形式で出力してください。
JSONのキーは "company_name" としてください。
余計な文章は含めず、JSONのみを返してください。
検索時は引用符を用いないでください。

--- 既知の情報 ---
郵便番号: 1000004
住所: 東京都千代田区大手町1丁目7−2 東京サンケイビル 26階
電話番号: 0362805939

このように、行ごとに「何を知りたいか」と「何を知っているか」がモデルに入力されます。

実行結果と考察

上記のクエリを実行すると、NULL だった箇所が補完されたテーブルが生成されます。

実行結果

特に注目すべきは以下の挙動です。

  1. 逆引き検索の成功:
    company_name が欠損していた行(ID:2)において、電話番号や住所をキーにした検索が行われ、そこから「クラウドエース株式会社」を正しく特定できています。
  2. 自律的な検索キーワード生成:
    used_search_query を見ると、Gemini が「今、何を知っているか」「何を知りたいか」を理解し、最適なキーワードを自分で考えて検索していることがわかります。

まとめ

BQML と Gemini の Grounding 機能を活用することで、これまで手作業や外部スクリプトで行っていた「Web 検索を伴うデータ補完」を SQL 内で完結させることができました。

生成 AI なので 100% の精度を保証するものではありませんが、大量のデータに対する一次補完としては強力なソリューションになります。
「データが汚くて使い物にならない」と諦める前に、ぜひ BQML と Gemini でデータの掃除を試してみてください。

Discussion