Vertex AI Gemini で非構造化データを扱う
Vertex AI Gemini で非構造化データを構造化して BigQuery に格納してみた
はじめに
てつどんです。データエンジニアやってます。
データエンジニアとして、必要な知識をインプット・アウトプットする上で、次はLLMを用いた学習をやってみました。
一般的な「チャットで質問する」使い方ではなく、データパイプラインの中に LLM を組み込むという観点でのインプットです。
一般的な使い方:人間 → LLM → 回答
データエンジニア的な使い方:データ → LLM → 構造化 → BigQuery
今回学んだ内容はこちらです。
- Step 1:Vertex AI Gemini API の基本
- Step 2:構造化出力(JSON 抽出)
- Step 3:複数テキストの一括処理
- Step 4:マルチモーダル対応(PDF からの情報抽出)
- Step 5:BigQuery への連携
学習環境
- GCP(個人アカウント)
- Vertex AI Gemini 2.5 Flash
- Python(ローカル環境)
費用感: Gemini 2.5 Flash は非常に安価で、学習目的で 100 回呼び出しても数円〜数十円程度です。個人アカウントでも気軽に試せます。
Step 1:Vertex AI Gemini API の基本
環境セットアップ
pip install google-cloud-aiplatform vertexai
# GCP 認証
gcloud auth application-default login
最初の API 呼び出し
import vertexai
from vertexai.generative_models import GenerativeModel
PROJECT_ID = "your-project-id"
LOCATION = "asia-northeast1" # 東京リージョン
vertexai.init(project=PROJECT_ID, location=LOCATION)
model = GenerativeModel("gemini-2.5-flash")
response = model.generate_content("データエンジニアとはどんな仕事ですか?3行で教えてください。")
print(response.text)
たったこれだけで LLM が使えます。GCP ネイティブなので IAM による権限管理も BigQuery との連携もシームレスです。
Step 2:構造化出力(JSON 抽出)
テキストから決まった形式でデータを取り出すのが非構造化データ処理の核心です。
import vertexai
import json
from vertexai.generative_models import GenerativeModel, GenerationConfig
vertexai.init(project=PROJECT_ID, location=LOCATION)
model = GenerativeModel("gemini-2.5-flash")
text = """
先月の売上は500万円で、前月比110%でした。
主力商品はノートパソコンで、販売数は200台です。
"""
prompt = f"""
以下のテキストから情報を抽出して、JSON形式で返してください。
JSONのみを返し、説明文は不要です。
抽出する項目:
- sales_amount: 売上金額(数値)
- month_over_month_ratio: 前月比(小数)
- top_product: 主力商品名
- units_sold: 販売数(数値)
テキスト:
{text}
"""
response = model.generate_content(
prompt,
generation_config=GenerationConfig(
response_mime_type="application/json" # JSON形式を強制
)
)
data = json.loads(response.text)
print(f"売上金額: {data['sales_amount']:,}円")
print(f"前月比: {data['month_over_month_ratio'] * 100:.0f}%")
ハマりポイント:コードブロック記号の問題
response_mime_type を指定しないと、Gemini が JSON の前後にコードブロック記号を付けて返すことがあります。
```json ← これが邪魔
{...}
``` ← これも邪魔
解決策:response_mime_type="application/json" を必ず指定する
これで純粋な JSON が返ってくるので後処理が不要になります。
Step 3:複数テキストの一括処理
実務では複数件まとめて処理する場面がほとんどです。1件ずつ API を呼び出すと遅くてコストも高くなるので、まとめて1回のAPIコールで処理するのがおすすめです。
reviews = [
"配送が遅くてがっかりしました。商品自体は良かったです。",
"品質が素晴らしく、また購入したいと思います!",
"説明と違う商品が届きました。返品したいです。",
]
prompt = f"""
以下の複数のレビューをそれぞれ分析して、JSON配列で返してください。
抽出する項目:
- sentiment: 感情(positive / negative / neutral)
- category: カテゴリ(配送 / 品質 / 商品説明 / その他)
- score: 満足度スコア(1〜5の整数)
レビュー一覧:
{json.dumps(reviews, ensure_ascii=False, indent=2)}
"""
response = model.generate_content(
prompt,
generation_config=GenerationConfig(
response_mime_type="application/json"
)
)
results = json.loads(response.text)
for i, (review, result) in enumerate(zip(reviews, results)):
result["review"] = review
print(f"レビュー{i+1}:{result}")
実行結果:
レビュー1:{'sentiment': 'negative', 'category': '配送', 'score': 2, 'review': '配送が遅くて...'}
レビュー2:{'sentiment': 'positive', 'category': '品質', 'score': 5, 'review': '品質が素晴らしく...'}
レビュー3:{'sentiment': 'negative', 'category': '商品説明', 'score': 1, 'review': '説明と違う...'}
Gemini がレビューの文脈をちゃんと読み取っているのがわかります。「商品自体は良かった」と書いてあっても配送の問題で negative・スコア 2 と判断しているのが的確です。
2つのアプローチの比較:
| 1件ずつ処理 | まとめて処理 | |
|---|---|---|
| API呼び出し回数 | 件数分 | 1回 |
| 速度 | 遅い | 速い |
| コスト | 高め | 安め |
| 向いているケース | 細かいエラーハンドリングが必要 | 件数が多い・速度重視 |
Step 4:マルチモーダル対応(PDF からの情報抽出)
Gemini はテキストだけでなく PDF も理解できます。Part.from_data() で PDF をバイナリとして渡すだけです。
from vertexai.generative_models import GenerativeModel, GenerationConfig, Part
with open("sample_invoice.pdf", "rb") as f:
pdf_data = f.read()
pdf_part = Part.from_data(
data=pdf_data,
mime_type="application/pdf"
)
prompt = """
この請求書PDFから情報を抽出して、JSON形式で返してください。
抽出する項目:
- company: 会社名
- invoice_no: 請求書番号
- date: 請求日
- payment_due: 支払期限
- items: 明細リスト(各itemはname・quantity・unit_price・totalを含む)
- subtotal: 小計
- tax: 税額
- total: 合計金額
"""
# テキストのときと違い、リストで渡す
response = model.generate_content(
[pdf_part, prompt],
generation_config=GenerationConfig(
response_mime_type="application/json"
)
)
data = json.loads(response.text)
print(f"会社名: {data['company']}")
print(f"合計: ${data['total']}")
テキストと PDF の渡し方の違い:
# テキストのとき
response = model.generate_content(prompt)
# PDF のとき(リストで渡す)
response = model.generate_content([pdf_part, prompt])
従来の OCR との違い
従来の OCR ツールは「文字を認識するだけ」でしたが、Gemini は文脈を理解して構造化まで一気にやってくれます。
従来の OCR:文字を認識 → 後から自分でパースが必要
Gemini:内容を理解して構造化まで一気にやってくれる
請求書・契約書・レポートなど、PDF からの情報抽出が必要なケースで非常に強力です。
ハマりポイント:壊れた JSON が返ってくることがある
response_mime_type="application/json" を指定しても稀に壊れた JSON が返ってくることがあります。LLM のレスポンスは確率的なので、リトライ処理を入れておくのが実務的な書き方です。
for attempt in range(3):
response = model.generate_content(
[pdf_part, prompt],
generation_config=GenerationConfig(
response_mime_type="application/json"
)
)
try:
data = json.loads(response.text)
print("抽出成功!")
break
except json.JSONDecodeError as e:
print(f"試行{attempt + 1}回目失敗: {e}")
if attempt == 2:
raise Exception("JSONパースに3回失敗しました")
Step 5:BigQuery への連携
テーブル作成(bq コマンド)
# データセット作成
bq mk --dataset \
--location=asia-northeast1 \
your-project-id:llm_output
# テーブル作成
bq mk --table \
your-project-id:llm_output.invoices \
company:STRING,invoice_no:STRING,date:DATE,payment_due:DATE,subtotal:FLOAT,tax:FLOAT,total:FLOAT,inserted_at:TIMESTAMP
PDF → Gemini → BigQuery の一連のパイプライン
import vertexai
import json
from datetime import datetime
from vertexai.generative_models import GenerativeModel, GenerationConfig, Part
from google.cloud import bigquery
PROJECT_ID = "your-project-id"
LOCATION = "asia-northeast1"
vertexai.init(project=PROJECT_ID, location=LOCATION)
model = GenerativeModel("gemini-2.5-flash")
bq_client = bigquery.Client(project=PROJECT_ID)
# PDF を読み込む
with open("sample_invoice.pdf", "rb") as f:
pdf_data = f.read()
pdf_part = Part.from_data(data=pdf_data, mime_type="application/pdf")
prompt = """
この請求書PDFから情報を抽出して、JSON形式で返してください。
抽出する項目:
- company, invoice_no, date(YYYY-MM-DD), payment_due(YYYY-MM-DD)
- subtotal, tax, total(数値)
"""
# Gemini で抽出(リトライあり)
for attempt in range(3):
response = model.generate_content(
[pdf_part, prompt],
generation_config=GenerationConfig(
response_mime_type="application/json"
)
)
try:
data = json.loads(response.text)
break
except json.JSONDecodeError:
if attempt == 2:
raise Exception("JSONパースに3回失敗しました")
# BigQuery に書き込む
row = {
"company": data["company"],
"invoice_no": data["invoice_no"],
"date": data["date"],
"payment_due": data["payment_due"],
"subtotal": float(data["subtotal"]),
"tax": float(data["tax"]),
"total": float(data["total"]),
"inserted_at": datetime.utcnow().isoformat()
}
table_id = f"{PROJECT_ID}.llm_output.invoices"
errors = bq_client.insert_rows_json(table_id, [row])
if errors:
print(f"エラー: {errors}")
else:
print(f"BigQuery への書き込み成功!テーブル: {table_id}")
全体の流れ:
sample_invoice.pdf
↓ Part.from_data() で読み込む
Gemini 2.5 Flash
↓ JSON で構造化(リトライあり)
{company, invoice_no, date, ...}
↓ insert_rows_json() で書き込む
BigQuery llm_output.invoices
まとめ
今回学んだことを整理します。
| Step | 内容 | 重要なポイント |
|---|---|---|
| 1 | API 基本 |
vertexai.init() + generate_content() だけで使える |
| 2 | JSON 抽出 |
response_mime_type="application/json" は必須 |
| 3 | 一括処理 | 複数件はまとめて1回のAPIコールが速くて安い |
| 4 | PDF 対応 |
Part.from_data() でリスト形式で渡す |
| 5 | BQ 連携 |
insert_rows_json() で Python から直接書き込める |
共通のハマりポイント:
-
response_mime_typeを指定しないとコードブロック記号が混入する - LLM のレスポンスは確率的なのでリトライ処理は必須
- PDF はリスト形式
[pdf_part, prompt]で渡す
まだまだ、必要な知識をインプットしていきたいと思います!
Discussion