【コスト50%オフ】Gemini Batch APIで2万社/日の企業情報抽出パイプラインを作ってみた

【コスト50%オフ】Gemini Batch APIで2万社/日の企業情報抽出パイプラインを作ってみた

はじめに

業務で「URLリストから企業情報を抽出してCSVにまとめる」という、地味だけど数を捌くのが大変なタスクと向き合うことになりました。

ざっくり要件はこんな感じです。

  • インプット: 企業のWebサイトURL(採用ページや会社サイトなど)

  • アウトプット: 会社名・住所・電話番号・業種(日本標準産業分類JSIC)を整形したCSV

  • 目標スループット: 2万社/日

「LLMに投げればいいでしょ」と最初は思ったのですが、普通に同期APIで処理するとまったく目標に届かない。そこで Gemini Batch API + Context Caching を使って組み直したら、コストもスピードもいい感じに収まったので、ハマりどころも含めてまとめておきます。

※ 構成の発想元として、こちらの記事をかなり参考にさせていただきました
Gemini Batch API を試してみた - Qiita

なぜ普通の同期APIだとダメだったのか

最初は素直にこう書いていました。

1リクエスト4秒として、単純計算で 1日 = 86,400秒 ÷ 4秒 ≒ 21,600件……一見いけそうに見えますが、

  • レートリミットに引っかかる

  • リトライ・タイムアウトのハンドリングで実効スループットがガクッと落ちる

  • 並列度を上げるとコストもエラー率も悪化する

実測では 1日1,500件くらいが現実的な上限で、目標の2万社/日には全然届きませんでした。

ということで Batch API の出番です。

Batch APIってそもそも何?

Batch APIは、大量のリクエストをまとめて非同期で処理する仕組みです。

宅配便でたとえると、こんな感じ。


同期API(普通のやり方)

Batch API

配送方法

荷物1個ずつ郵便局に持っていく

全部まとめてトラックに積む

待ち方

1個ずつ「届いた?」と確認

「明日までに頼む」と渡して放置

スループット

1日1,500件くらいが限界

制限なし(トークン量次第)

料金

通常料金

50%オフ

つまり「急がないからその代わり安く・大量に」というトレードオフのAPIです。今回みたいに「夜投げて朝までに結果が揃っていればOK」という用途にはドンピシャ。

JSONL とは

Batch APIに投げるリクエストは JSONL という形式で書きます。

  • JSON = データを構造化して書く形式

  • JSONL = JSONを「1行1レコード」で並べたファイル(L = Lines)

{"key": "company_001", "request": {...A社用のプロンプト...}}
{"key": "company_002", "request": {...B社用のプロンプト...}}
{"key": "company_003", "request": {...C社用のプロンプト...}}

このファイルをGoogleにアップロードすると、「中の各行をまとめて処理しといて」と依頼できる、というイメージです。

システム全体のフロー

実装したパイプラインはこんな構成です。


ディレクトリはこんな感じに整理しました。


ポイント①:固定プロンプト + Context Caching でコスト削減

各企業に投げるプロンプトはこう分解できます。


ここでポイントなのが Context Caching(コンテキストキャッシング)。

Context Caching とは?
「毎回同じ内容を送るなら、サーバー側で覚えておくよ。次からはその分の料金安くするね」という仕組み。
Geminiの場合、キャッシュヒットしたトークンは 約90%オフ になります。

今回のケースでは、固定プレフィックス(抽出ルール + JSICデータ)が 約11,000文字 あったので、ここがキャッシュに乗るかどうかでコストが大きく変わります。

実際の結果がこちら:


約7割がキャッシュからの読み出しになっていて、コスト圧縮にかなり効いています。

ポイント②:Webクロールは別ステップに切り出す

最初は「Batchジョブ内でURLを読みに行く」設計も考えたのですが、

  • サイトによってはレスポンスが遅い・タイムアウトする

  • 失敗したものだけリトライしたいので、ジョブ全体を流し直したくない

という理由で、クロールとLLM呼び出しを完全に分離しました。

# 失敗したURLだけ再取得できるようにしておく
python batch_v2/2a_fetch_pages.py sample_urls.txt --retry-errors

この設計のおかげで、

  • ネットワーク起因の失敗は再クロールだけで解決

  • LLM呼び出しは「pages_cacheにあるものだけ」を対象に、確実に実行できる

という見通しのいいパイプラインになりました。

実際に走らせてみた結果(149件)

テスト用に149社で実行してみました。

Step 2a: Webページ取得


並列クロールしているので、134件取って 40秒弱。ここはサクサク。

Step 2b: JSONL生成 + Batch投入


ファイル投入自体は数秒で終わります。

Step 3: 完了待ち

ここがBatch APIの特性が出る部分。約50分で完了しました。

[09:42:54] JOB_STATE_PENDING | 経過: 0秒
[09:47:55] JOB_STATE_PENDING | 経過: 5分0秒
...
[10:32:57]

Googleの公式は「24時間以内」と言っているので、混雑状況によってかなりブレます。前回別の検証では 7時間39分 かかったこともありました。深夜帯のほうが早い傾向です。

Step 4: CSV変換


ジョブが完了した後の処理は 0.1秒。一度成功した結果は構造化されているので、ここはほぼ瞬殺です。

取れたデータの例

実際に取得できたデータがこちら。

URL

会社名

業種

chie-kenso-recruit.com

And one株式会社

D 建設業

hayaken.co.jp

株式会社千恵建総

D 建設業

aika-job.jp

株式会社愛花

P 医療,福祉

repro.estate

京成タクシーウエスト株式会社

H 運輸業

注目してほしいのは、採用サイトのURLでも、運営している実際の会社名と業種を正しく抽出できていること。「採用」というキーワードに引っ張られて求人サービス事業者になったりせず、ページ内容から実体を読み取ってくれています。

コスト実績

134件のフルパイプラインにかかったコストはこちら。

項目

数字

入力トークン合計

1,199,983

キャッシュヒット

823,122 (68.6%)

出力トークン合計

61,467

合計コスト

$0.1581(約 ¥24)

1件あたり

約 ¥0.18

2万件/日換算

約 ¥3,539/日

Tier 1って何?
Geminiの有料プランの最小レベル。

  • 無料(Free): Batch API 使えない

  • Tier 1(課金あり): Batch API OK ← 今回はここ

  • Tier 2 / 3: より大量処理向け

2万件/日で ¥3,500円台 に収まるのは、業務利用としては全然許容範囲かなと思います。

ハマったポイント / Tips

1. 完了時間がブレる

「24時間以内」とは書かれているものの、実測では 50分〜8時間 くらいの幅がありました。クリティカルパスに置くのは危険なので、バックグラウンドジョブ前提で設計するのが現実的です。

2. 失敗時のリカバリは最初から組み込む

Webクロールで一定数こけるのは確実なので、

  • 失敗URLを fetch_errors.json に書き出す

  • --retry-errors フラグで失敗分のみ再実行

という仕組みを最初から入れておくと、運用がだいぶ楽になります。

3. 固定プレフィックスはなるべく長く

逆説的ですが、**「全リクエストで共通の文字列はなるべく長く詰め込む」**ほうがコスト効率が良くなります(キャッシュヒット率が上がるため)。
今回はJSICの分類データ全部を固定側に入れることで、キャッシュヒット率68.6%を達成しました。

4. JSONLは案外でかい

134件で 約4MB ありました。2万件なら単純計算で 600MB級。File APIにアップロードする時間も意識して、夜間バッチに組み込むなどの工夫が必要です。

まとめ

観点

結果

スループット目標

2万社/日 ✅ 達成可能

成功率

クロール 90% + 抽出 100%(= 全体 ~90%)

コスト

約 ¥3,500 / 2万社

待ち時間

平均 1〜数時間(夜間バッチ向き)

急がない × 大量 × 安く」というユースケースに、Batch APIは本当によくフィットします。同期APIで頑張ってレートリミットと格闘していた頃が嘘みたいに、シンプルなパイプラインに収まりました。

似たような「URLリストから情報抽出」「大量ドキュメントの構造化」みたいなタスクを抱えている方の参考になれば嬉しいです。

参考

はじめに

業務で「URLリストから企業情報を抽出してCSVにまとめる」という、地味だけど数を捌くのが大変なタスクと向き合うことになりました。

ざっくり要件はこんな感じです。

  • インプット: 企業のWebサイトURL(採用ページや会社サイトなど)

  • アウトプット: 会社名・住所・電話番号・業種(日本標準産業分類JSIC)を整形したCSV

  • 目標スループット: 2万社/日

「LLMに投げればいいでしょ」と最初は思ったのですが、普通に同期APIで処理するとまったく目標に届かない。そこで Gemini Batch API + Context Caching を使って組み直したら、コストもスピードもいい感じに収まったので、ハマりどころも含めてまとめておきます。

※ 構成の発想元として、こちらの記事をかなり参考にさせていただきました
Gemini Batch API を試してみた - Qiita

なぜ普通の同期APIだとダメだったのか

最初は素直にこう書いていました。

1リクエスト4秒として、単純計算で 1日 = 86,400秒 ÷ 4秒 ≒ 21,600件……一見いけそうに見えますが、

  • レートリミットに引っかかる

  • リトライ・タイムアウトのハンドリングで実効スループットがガクッと落ちる

  • 並列度を上げるとコストもエラー率も悪化する

実測では 1日1,500件くらいが現実的な上限で、目標の2万社/日には全然届きませんでした。

ということで Batch API の出番です。

Batch APIってそもそも何?

Batch APIは、大量のリクエストをまとめて非同期で処理する仕組みです。

宅配便でたとえると、こんな感じ。


同期API(普通のやり方)

Batch API

配送方法

荷物1個ずつ郵便局に持っていく

全部まとめてトラックに積む

待ち方

1個ずつ「届いた?」と確認

「明日までに頼む」と渡して放置

スループット

1日1,500件くらいが限界

制限なし(トークン量次第)

料金

通常料金

50%オフ

つまり「急がないからその代わり安く・大量に」というトレードオフのAPIです。今回みたいに「夜投げて朝までに結果が揃っていればOK」という用途にはドンピシャ。

JSONL とは

Batch APIに投げるリクエストは JSONL という形式で書きます。

  • JSON = データを構造化して書く形式

  • JSONL = JSONを「1行1レコード」で並べたファイル(L = Lines)

{"key": "company_001", "request": {...A社用のプロンプト...}}
{"key": "company_002", "request": {...B社用のプロンプト...}}
{"key": "company_003", "request": {...C社用のプロンプト...}}

このファイルをGoogleにアップロードすると、「中の各行をまとめて処理しといて」と依頼できる、というイメージです。

システム全体のフロー

実装したパイプラインはこんな構成です。


ディレクトリはこんな感じに整理しました。


ポイント①:固定プロンプト + Context Caching でコスト削減

各企業に投げるプロンプトはこう分解できます。


ここでポイントなのが Context Caching(コンテキストキャッシング)。

Context Caching とは?
「毎回同じ内容を送るなら、サーバー側で覚えておくよ。次からはその分の料金安くするね」という仕組み。
Geminiの場合、キャッシュヒットしたトークンは 約90%オフ になります。

今回のケースでは、固定プレフィックス(抽出ルール + JSICデータ)が 約11,000文字 あったので、ここがキャッシュに乗るかどうかでコストが大きく変わります。

実際の結果がこちら:


約7割がキャッシュからの読み出しになっていて、コスト圧縮にかなり効いています。

ポイント②:Webクロールは別ステップに切り出す

最初は「Batchジョブ内でURLを読みに行く」設計も考えたのですが、

  • サイトによってはレスポンスが遅い・タイムアウトする

  • 失敗したものだけリトライしたいので、ジョブ全体を流し直したくない

という理由で、クロールとLLM呼び出しを完全に分離しました。

# 失敗したURLだけ再取得できるようにしておく
python batch_v2/2a_fetch_pages.py sample_urls.txt --retry-errors

この設計のおかげで、

  • ネットワーク起因の失敗は再クロールだけで解決

  • LLM呼び出しは「pages_cacheにあるものだけ」を対象に、確実に実行できる

という見通しのいいパイプラインになりました。

実際に走らせてみた結果(149件)

テスト用に149社で実行してみました。

Step 2a: Webページ取得


並列クロールしているので、134件取って 40秒弱。ここはサクサク。

Step 2b: JSONL生成 + Batch投入


ファイル投入自体は数秒で終わります。

Step 3: 完了待ち

ここがBatch APIの特性が出る部分。約50分で完了しました。

[09:42:54] JOB_STATE_PENDING | 経過: 0秒
[09:47:55] JOB_STATE_PENDING | 経過: 5分0秒
...
[10:32:57]

Googleの公式は「24時間以内」と言っているので、混雑状況によってかなりブレます。前回別の検証では 7時間39分 かかったこともありました。深夜帯のほうが早い傾向です。

Step 4: CSV変換


ジョブが完了した後の処理は 0.1秒。一度成功した結果は構造化されているので、ここはほぼ瞬殺です。

取れたデータの例

実際に取得できたデータがこちら。

URL

会社名

業種

chie-kenso-recruit.com

And one株式会社

D 建設業

hayaken.co.jp

株式会社千恵建総

D 建設業

aika-job.jp

株式会社愛花

P 医療,福祉

repro.estate

京成タクシーウエスト株式会社

H 運輸業

注目してほしいのは、採用サイトのURLでも、運営している実際の会社名と業種を正しく抽出できていること。「採用」というキーワードに引っ張られて求人サービス事業者になったりせず、ページ内容から実体を読み取ってくれています。

コスト実績

134件のフルパイプラインにかかったコストはこちら。

項目

数字

入力トークン合計

1,199,983

キャッシュヒット

823,122 (68.6%)

出力トークン合計

61,467

合計コスト

$0.1581(約 ¥24)

1件あたり

約 ¥0.18

2万件/日換算

約 ¥3,539/日

Tier 1って何?
Geminiの有料プランの最小レベル。

  • 無料(Free): Batch API 使えない

  • Tier 1(課金あり): Batch API OK ← 今回はここ

  • Tier 2 / 3: より大量処理向け

2万件/日で ¥3,500円台 に収まるのは、業務利用としては全然許容範囲かなと思います。

ハマったポイント / Tips

1. 完了時間がブレる

「24時間以内」とは書かれているものの、実測では 50分〜8時間 くらいの幅がありました。クリティカルパスに置くのは危険なので、バックグラウンドジョブ前提で設計するのが現実的です。

2. 失敗時のリカバリは最初から組み込む

Webクロールで一定数こけるのは確実なので、

  • 失敗URLを fetch_errors.json に書き出す

  • --retry-errors フラグで失敗分のみ再実行

という仕組みを最初から入れておくと、運用がだいぶ楽になります。

3. 固定プレフィックスはなるべく長く

逆説的ですが、**「全リクエストで共通の文字列はなるべく長く詰め込む」**ほうがコスト効率が良くなります(キャッシュヒット率が上がるため)。
今回はJSICの分類データ全部を固定側に入れることで、キャッシュヒット率68.6%を達成しました。

4. JSONLは案外でかい

134件で 約4MB ありました。2万件なら単純計算で 600MB級。File APIにアップロードする時間も意識して、夜間バッチに組み込むなどの工夫が必要です。

まとめ

観点

結果

スループット目標

2万社/日 ✅ 達成可能

成功率

クロール 90% + 抽出 100%(= 全体 ~90%)

コスト

約 ¥3,500 / 2万社

待ち時間

平均 1〜数時間(夜間バッチ向き)

急がない × 大量 × 安く」というユースケースに、Batch APIは本当によくフィットします。同期APIで頑張ってレートリミットと格闘していた頃が嘘みたいに、シンプルなパイプラインに収まりました。

似たような「URLリストから情報抽出」「大量ドキュメントの構造化」みたいなタスクを抱えている方の参考になれば嬉しいです。

参考