Places API ランキング取得
概要
| 項目 | 内容 |
|---|---|
| ステータス | 🔵 提案中 |
| Issue | - |
| 担当 | - |
Google Places API(Text Search)を使用して、キーワードごとの検索ランキングを取得する機能。既存のスクレイピング方式と並列運用し、処理対象データは mappy_gbp_locations の data_source で区別する(1 = Places API、2 = Scraping)。
| 項目 | 内容 |
|---|---|
| 方式 | Google Places API (New) — Text Search |
| 費用 | $0(Field Mask: places.id のみ → Basic SKU) |
| 対象 | 全3システム(MAPPY / GCOR / PIPIT) |
| 頻度 | 1日1回(スクレイピングと同タイミング) |
| 実行 | Python スクリプト(main.py) |
提案内容
背景・課題
- 既存のスクレイピング方式はPlaywrightでGoogle検索を実行しており、reCAPTCHA・タイムアウト・アンチ検出対策が複雑
- ヘッドレスブラウザのサーバーリソース消費が大きい
- Google検索のDOM・レイアウト変更により結果が不安定になるリスクがある
- ランキングの信頼性を高めるため、補助的な取得手段が必要
提案するソリューション
Google Places API (New) の Text Search を使用し、place_id でランキングを取得する。スクレイピングと並列運用し、同一 mappy_* テーブルに保存する。
主な特徴:
- シンプルなHTTP POST呼び出し(ブラウザ不要)
- Field Mask を
places.idのみに限定 → Basic SKU($0) raw_jsonに既存のplaceId(metadata.placeId)で正確にマッチング- reCAPTCHA・DOM変更の影響を受けない
機能一覧
| # | 機能名 | 説明 | 優先度 |
|---|---|---|---|
| 1 | placeId 抽出 | mappy_gbp_locations.raw_json の metadata.placeId を取得 | 高 |
| 2 | Text Searchランキング | APIを呼び出し、結果内の place_id 位置を特定 | 高 |
| 3 | ランキング値マッピング | 位置をそのまま保存(1-based)、0=圏外、997=エラー | 高 |
| 4 | マルチAPIキー | 複数APIキー対応。キーごとに --threads スレッド。合計=threads×キー数 | 高 |
| 5 | レート制限 | --min_interval によるAPI呼び出し間隔制御(スレッドごと独立) | 中 |
| 6 | 429リトライ | --max_retries による自動リトライ | 中 |
| 7 | data_source フィルタ | data_source=1(API)のロケーションのみ処理 | 高 |
技術スタック
| 項目 | 技術 | 選定理由 |
|---|---|---|
| 言語 | Python 3.10+ | 既存スクレイピングと同じ言語、requestsで簡潔にAPI呼び出し可能 |
| HTTP | requests | 軽量、同期処理でスレッドと相性が良い |
| DB | SQLAlchemy + PyMySQL | 既存スクレイピングと同じORM、共有DBプール対応 |
| ログアップロード | boto3 | S3へのログファイルアップロード |
| 実行環境 | Docker (Amazon ECS) | 既存スクレイピングと同じインフラ、1 CPU / 2GB RAM |
| スケジューラ | Amazon EventBridge | 日次バッチトリガー(既存と同じ) |
Docker構成
FROM python:3.10-slim-bookworm
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
ENV TZ=Asia/Tokyo
RUN apt-get update && \
apt-get install -y --no-install-recommends tzdata && \
ln -sf /usr/share/zoneinfo/$TZ /etc/localtime && \
echo $TZ > /etc/timezone && \
apt-get clean && rm -rf /var/lib/apt/lists/*
WORKDIR /code
COPY requirements.txt /code/
RUN pip install --upgrade pip && pip install -r requirements.txt
COPY . /code/
CMD ["python", "main.py", "--threads", "4", "--type_keyword", "all", "--min_interval", "500"]# requirements.txt
PyMySQL==1.1.2
SQLAlchemy==2.0.44
SQLAlchemy-Utils==0.42.0
python-dotenv==1.0.1
requests==2.32.5
cryptography==46.0.3
boto3==1.38.0実行例:
# ローカル
docker build -t places-api-ranking .
docker run --env-file .env places-api-ranking
# 引数オーバーライド
docker run --env-file .env places-api-ranking \
python main.py --threads 8 --type_keyword normal
# ECS タスク定義(既存スクレイピングと同構成)
# CPU: 1024 (1 vCPU), Memory: 2048 (2GB)
# Override command: ["python", "main.py", "--threads", "4", "--type_keyword", "all"]AWSインフラ構成
既存のスクレイピングと同じAWSアカウント(881980194724)・リージョン(ap-northeast-1)を使用する。
ECR(コンテナレジストリ)
| 項目 | 値 |
|---|---|
| リポジトリ | python310 (既存) |
| URI | 881980194724.dkr.ecr.ap-northeast-1.amazonaws.com/python310 |
| タグ | :latest |
# ビルド&プッシュ
aws ecr get-login-password --region ap-northeast-1 | docker login --username AWS --password-stdin 881980194724.dkr.ecr.ap-northeast-1.amazonaws.com
docker build -t places-api-ranking .
docker tag places-api-ranking:latest 881980194724.dkr.ecr.ap-northeast-1.amazonaws.com/python310:latest
docker push 881980194724.dkr.ecr.ap-northeast-1.amazonaws.com/python310:latestBatch Job Definition
既存の scraper_def を参考に、新しいジョブ定義を作成する。
| 項目 | 値 |
|---|---|
| 名前 | places_api_def |
| タイプ | container |
| プラットフォーム | Fargate / LINUX / X86_64 |
| イメージ | 881980194724.dkr.ecr.ap-northeast-1.amazonaws.com/python310:latest |
| vCPU | 1.0 |
| メモリ | 2048 MB |
| 実行ロール | ecsTaskExecutionRole (既存と同じ) |
| タイムアウト | 21600秒(6時間) |
| ネットワーク | assignPublicIp: ENABLED |
| コマンド | [] (Lambda側で指定) |
EventBridge(スケジューラ)
| 項目 | 値 |
|---|---|
| ルール名 | places-api-ranking-daily |
| スケジュール | 既存スクレイピングと同タイミング(日次) |
| ターゲット | Lambda関数(sync_database → Batch submit → sync_result の既存フローに統合) |
※ 環境変数(
PLACES_API_KEYS等)はLambda関数からBatchジョブをsubmitする際にcontainerOverridesで渡す(既存スクレイピングと同じ方式)。
構成図
環境変数
| 変数名 | 必須 | 説明 |
|---|---|---|
GOOGLE_PLACES_API_KEYS | Yes | Google Places APIキー(カンマ区切り)。各キーは別GCPプロジェクト推奨 |
GOOGLE_PLACES_API_KEY | Yes* | 単一APIキー(GOOGLE_PLACES_API_KEYS 未設定時のフォールバック) |
PLACES_API_BASE_URL | No | エンドポイントURL上書き(デフォルト: https://places.googleapis.com) |
SCRAPER_MYSQL_HOST | Yes | MySQL ホスト |
SCRAPER_MYSQL_DATABASE | Yes | MySQL データベース名 |
SCRAPER_MYSQL_USER | Yes | MySQL ユーザー |
SCRAPER_MYSQL_PASSWORD | Yes | MySQL パスワード |
SCRAPER_MYSQL_PORT | No | MySQL ポート(デフォルト: 3306) |
SCRAPER_ACCESS_KEY_ID | Yes | S3アップロード用AWSアクセスキー |
SCRAPER_SECRET_ACCESS_KEY | Yes | S3シークレットキー |
SCRAPER_SCRAPING_BUCKET | Yes | ログアップロード先S3バケット |
注意: 各APIキーは別GCPプロジェクトから取得すること。同プロジェクトのキーはQPM制限を共有する(複数キーの効果なし)。
全体フロー
ランキング値マッピング
Text Search APIの結果をそのまま位置(1-based)で保存する。
| 結果 | ランク値 | 意味 |
|---|---|---|
| 位置Nで発見 | N (1〜60) | 実際の順位 |
| 未検出(60件以内) | 0 | 圏外 |
| APIエラー(リトライ後) | 997 | 予期しないエラー |
スクレイピングとの違い: スクレイピングはLocal Pack上位3件に-11/-12/-13を使用。Places APIはLocal Packを区別せず、実際の順位をそのまま保存する。
ランキングステータス
| 値 | 定数 | 意味 |
|---|---|---|
| 1 | STATUS_SUCCESS | 結果内で発見 |
| 2 | STATUS_OUT_OF_RANGE | 未検出(ranking = 0) |
| 3 | STATUS_UNEXPECTED_ERROR | APIエラー(ranking = 997) |
データベース設計
戦略: 既存 mappy_* テーブルを共用
Places APIとスクレイピングは同じテーブルに書き込む。各システムは mappy_gbp_locations.data_source により処理対象のロケーションが分かれているため、競合しない。
| テーブル | 役割 | 備考 |
|---|---|---|
mappy_search_ranking_update_logs | 日次実行ログ | 1レコード/日 |
mappy_search_ranking_processes | プロセス追跡(normal/reverse) | type: 0=normal, 1=reverse |
mappy_temp_search_rankings | ランキング生データ(1レコード/キーワード) | メインデータ |
mappy_search_ranking_exports | エクスポートデータ集約(1レコード/ロケーション) | JSON配列、最大8キーワード |
既存テーブルの拡張
mappy_gbp_locations に data_source カラムを追加し、各システムの処理対象ロケーションを分ける。ランキング結果の区別ではない。
| 値 | 定数 | 意味 |
|---|---|---|
| 1 | DATA_SOURCE_API | Places APIが処理するロケーション |
| 2 | DATA_SOURCE_RANK_SEARCH | スクレイピングが処理するロケーション |
- Places API はキーワードクエリ時に
data_source = 1のロケーションのみ取得 - スクレイピングは
data_source = 2のロケーションのみ取得 - 両者のランキング結果は同じ
mappy_temp_search_rankings、mappy_search_ranking_exportsに保存される
placeId の取得元
mappy_gbp_locations.raw_json の metadata.placeId から取得する。新規カラム追加は不要。
import json
# raw_json からの抽出例
raw = json.loads(raw_json)
placeId = raw["metadata"]["placeId"] # "ChIJg9Jc9EiHGGARCDHlvl19Coo"
lat = raw["latlng"]["latitude"] # 35.6928321
lng = raw["latlng"]["longitude"] # 139.9285475検索座標の決定
優先順位:
- ユーザー設定:
mappy_user_settings.settings→keywordSettings.locations[location_id] - raw_json:
mappy_gbp_locations.raw_json→latlng.latitude/longitude - デフォルト: 東京エリア (35.6441649, 139.347756) — 上記2ソースがない場合のフォールバック
ER図
テーブル定義: mappy_temp_search_rankings
| No | カラム名(論理) | カラム名(物理) | データ型 | NULL | キー | 説明 |
|---|---|---|---|---|---|---|
| 1 | ID | id | int | NO | PK | 自動採番 |
| 2 | 実行ログID | update_log_id | int | NO | IDX | mappy_search_ranking_update_logs.id |
| 3 | プロセスID | process_id | int | NO | IDX | mappy_search_ranking_processes.id |
| 4 | ユーザーID | user_id | int | NO | IDX | mappy_users.id |
| 5 | ロケーションID | gbp_location_id | int | NO | IDX | mappy_gbp_locations.id |
| 6 | キーワードID | keyword_id | int | NO | IDX | mappy_keywords.id |
| 7 | システムID | system_id | int | NO | - | 1:MAPPY / 2:GCOR / 3:PIPIT |
| 8 | ランキング | ranking | int | YES | - | 位置 (1-60)、0=圏外、997=エラー |
| 9 | ランキングステータス | ranking_status | tinyint | YES | - | 1:成功 / 2:圏外 / 3:エラー |
| 10 | ランキング日付 | ranking_at | date | NO | - | 何日分のランクか |
| 11 | API実行日時 | search_at | datetime | NO | - | 実際にAPI呼び出しした日時 |
| 12 | 作成日時 | created_at | datetime | NO | - | レコード作成日時 |
| 13 | 更新日時 | updated_at | datetime | NO | - | レコード更新日時 |
テーブル定義: mappy_search_ranking_exports
エクスポートデータは1ロケーションあたりのJSON配列で、最大8キーワードとtop 3/10/20統計を含む。
| No | カラム名(論理) | カラム名(物理) | データ型 | NULL | キー | 説明 |
|---|---|---|---|---|---|---|
| 1 | ID | id | int | NO | PK | 自動採番 |
| 2 | 実行ログID | update_log_id | int | NO | - | mappy_search_ranking_update_logs.id |
| 3 | プロセスID | process_id | int | NO | IDX | mappy_search_ranking_processes.id |
| 4 | ユーザーID | user_id | int | NO | IDX | mappy_users.id |
| 5 | ロケーションID | gbp_location_id | int | NO | IDX | mappy_gbp_locations.id |
| 6 | システムID | system_id | int | NO | - | 1:MAPPY / 2:GCOR / 3:PIPIT |
| 7 | ランキング日付 | ranking_at | date | NO | - | |
| 8 | データ | data | longtext | NO | - | JSON配列(下記構造参照) |
| 9 | 作成日時 | created_at | datetime | NO | - | |
| 10 | 更新日時 | updated_at | datetime | NO | - |
JSON data 構造:
[
"2026-04-07T10:30:00", // datetime
"user_login_id", // login_id
"store_001", // store_code
123, // gbp_location_id
"1234567890", // location_name_id
"店舗名", // display_name
1, // flag
4, // keyword_count
"kw1", "kw2", "kw3", "kw4", "", "", "", "", // 8 keyword slots
35.6595, // lat
139.7004, // lng
null, // reserved
3, 15, 0, null, null, null, null, null, // 8 ranking slots
null, // reserved
1, 2, // has_top3, top3_count
1, 3, // has_top10, top10_count
1, 4, // has_top20, top20_count
1 // system_id
]1キーワード処理フロー
CLI引数
python main.py [options]| 引数 | 型 | デフォルト | 説明 |
|---|---|---|---|
--threads | int | 4 | APIキーごとのスレッド数。合計=threads×キー数 |
--type_keyword | str | all | all (normal+reverse) / normal / reverse |
--min_interval | int | 500 | APIリクエスト間の最小間隔(ms、スレッドごと独立) |
--max_retries | int | 3 | 429エラー時のリトライ回数 |
--retry_delay | int | 5000 | リトライ前の待機時間(ms) |
--max-groups | int | 0 | 処理グループ数の制限(0=無制限、テスト用) |
重要:
--threadsはキーごとのスレッド数。合計=threads×キー数。例: 3キー +--threads 4= 12スレッド。
Text Search API リクエスト仕様
エンドポイント
POST {PLACES_API_BASE_URL}/v1/places:searchTextデフォルトは PLACES_API_BASE_URL=https://places.googleapis.com。環境変数でモックサーバーURLに上書き可能。
ヘッダー
Content-Type: application/json
X-Goog-Api-Key: {API_KEY}
X-Goog-FieldMask: places.id,nextPageTokenリクエストボディ
{
"textQuery": "渋谷 歯医者",
"languageCode": "ja",
"locationBias": {
"circle": {
"center": {
"latitude": 35.6595,
"longitude": 139.7004
},
"radius": 5000.0
}
}
}レスポンス例
{
"places": [
{ "id": "ChIJ..." },
{ "id": "ChIJ..." },
{ "id": "ChIJ..." }
],
"nextPageToken": "..."
}ランキング算出ロジック
def find_ranking(places, place_id):
"""結果内のplace_id位置を検索。
Returns:
位置 (1-based)。見つからなければ0。
"""
if not places or not place_id:
return 0
for index, place in enumerate(places):
if place.get("id") == place_id:
return index + 1
return 0シーケンス図
エラーハンドリング
| エラー | HTTPステータス | 対応 |
|---|---|---|
| Quota超過 | 429 | retry_delay ms待機後リトライ(最大 max_retries 回)、全リトライ失敗 → ranking=997 + ERRORログ |
| 認証エラー | 401 | PlacesAPIError type=AUTH_ERROR、ranking=997 |
| ブロック | 403 | PlacesAPIError type=BLOCKED、ranking=997 |
| サーバーエラー | 5xx | PlacesAPIError type=UNEXPECTED、ranking=997 |
| タイムアウト | - | PlacesAPIError type=TIMEOUT、ranking=997 |
| 接続エラー | - | PlacesAPIError type=UNEXPECTED、ranking=997 |
DBエラー処理: get_db() コンテキストマネージャがrollback後にre-raise。
マルチAPIキー・スレッド処理方式
スレッドモデル
--threads はキーごとのスレッド数。合計=--threads × キー数。
# 1キー + 4スレッド/キー = 4スレッド合計
GOOGLE_PLACES_API_KEYS=keyA --threads 4
Thread-0 ← keyA Thread-1 ← keyA
Thread-2 ← keyA Thread-3 ← keyA
# 3キー + 4スレッド/キー = 12スレッド合計
GOOGLE_PLACES_API_KEYS=keyA,keyB,keyC --threads 4
chunk 0→keyA chunk 1→keyB chunk 2→keyC chunk 3→keyA
chunk 4→keyB chunk 5→keyC chunk 6→keyA chunk 7→keyB
chunk 8→keyC chunk 9→keyA chunk 10→keyB chunk 11→keyCスケールアップ方法: 環境変数にキーを追加 → 合計スレッド自動増加。コード変更不要。
グループ化と均等分配
全キーワードを一括取得し、user_id × gbp_location_id × system_id でグループ化後、連続チャンクに分割(ラウンドロビンではない)。
合計: 120グループ、12スレッド(3キー × 4スレッド/キー)
chunk_size = ceil(120/12) = 10
Thread-0: group 0-9 (keyA)
Thread-1: group 10-19 (keyB)
Thread-2: group 20-29 (keyC)
Thread-3: group 30-39 (keyA)
...
Thread-11: group 110-119 (keyC)利点:
- グループの分断なし(全件取得→グループ化→分配のためシンプル)
- normalとreverseが連続実行(同一ロケーションで
request_timerを共有) - 各スレッドが独立したrate limiterを持つ
- 1つの共有DBプール(
pool_size=15、max_overflow=10、最大25接続)
レート制限とクォータ
Google Places Text Search API のデフォルトQPMは 600 QPM。ただし実測値から、バースト制限**~10 req/秒**も適用されていることが判明。
実績データ(2026/03/25): 4バッチ合計ピーク約395 req/分(600未満)でも429エラー発生。 原因: 4バッチ × 3.41 req/秒 = 13.65 req/秒 → 10 req/秒のバースト制限を超過。
min_interval の動作
各API呼び出し前(ページネーションも含む):
elapsed = now - request_timer
if elapsed < min_interval:
sleep(min_interval - elapsed)
request_timer = nowrequest_timerは同一グループ内の normal と reverse で共有- 各スレッドは独立した
request_timerを持つ - ページネーション(2〜3ページ目)もレート制限対象
スループット見積もり
| キー数 | --threads | --min_interval | 合計スレッド | 合計 req/s | 合計 QPM | 評価 |
|---|---|---|---|---|---|---|
| 1 | 4 | 500ms | 4 | 8 | 480 | 初期構成・推奨 |
| 2 | 4 | 500ms | 8 | 16 | 960 | 推奨 |
| 3 | 4 | 500ms | 12 | 24 | 1440 | 2GB RAMで安定動作の上限 |
| 1 | 4 | 300ms | 4 | ~13 | ~800 | バースト制限に近い |
429リトライフロー
試行 1 → 429 → retry_delay待機 → 試行 2 → 429 → 待機 → ... → 試行 max_retries+1- リトライごとに
WARNINGログを出力 - 全リトライ失敗時は ranking=997、
ERRORログを出力 sleep(retry_delay)後にrequest_timerをリセット
スループット向上策
APIキーの追加(推奨)
各Google Cloudプロジェクトに個別のレート制限(約10 req/秒)があるため、GOOGLE_PLACES_API_KEYS にキーを追加するだけで効果がある。
# 初期構成(1キー × 4スレッド = 4スレッド)
GOOGLE_PLACES_API_KEYS=keyA --threads 4 # ~8 req/s
# スケールアップ(3キー × 4スレッド = 12スレッド)
GOOGLE_PLACES_API_KEYS=keyA,keyB,keyC --threads 4 # ~24 req/s| キー数 | threads/key | 合計スレッド | レート上限 | 処理時間(49,912 req) | RAM |
|---|---|---|---|---|---|
| 1 | 4 | 4 | 10 req/秒 | 83分 | ~160MB |
| 2 | 4 | 8 | 20 req/秒 | 42分 | ~240MB |
| 3 | 4 | 12 | 30 req/秒 | 28分 | ~320MB |
| 4 | 4 | 16 | 40 req/秒 | 21分 | ~400MB |
レート制限の引き上げリクエスト
Google Cloud Console から Quotas の引き上げをリクエスト可能。IDs Only SKU(無料)はGoogleの収益に影響しないため承認されやすい。
組み合わせ: 3キー × quota引き上げ1,200 QPM/キー = 60 req/秒 → 49,912 req を 14分 で処理可能。
ログ
ログファイル
- ディレクトリ:
log/debug_aws/ - 統合ログ:
{YYYY-MM-DD_HH-MM-SS}_total.log(全スレッド) - キー別ログ:
{YYYY-MM-DD_HH-MM-SS}_key{suffix}.log(キーでフィルタ) - S3アップロード先:
s3://{bucket}/log/apigoogle/{filename}
ログフォーマット
2026-04-07 10:30:18,015 [INFO][T1][...a1b2] [1/1212] location='渋谷歯科', keywords=8
2026-04-07 10:30:18,378 [DEBUG][T1][...a1b2] [uid=1 sys=1 loc=123] [N][1/8] '渋谷 歯医者': rank=1, results=60
2026-04-07 10:30:18,900 [WARNING][T2][...c3d4] [uid=2 sys=1 loc=456] [R][3/8] '歯医者 渋谷': 429 rate limited (attempt 1/4), waiting 5000ms...
2026-04-07 10:30:23,910 [INFO][T2][...c3d4] [uid=2 sys=1 loc=456] [R][3/8] '歯医者 渋谷': retry succeeded (attempt 2/4)| フィールド | 意味 |
|---|---|
[T{n}] | スレッド番号 |
[...{suffix}] | APIキーの末尾4文字 |
[uid=X sys=Y loc=Z] | コンテキスト: user_id, system_id, gbp_location_id |
[N]/[R] | Normal / Reverse |
[i/total] | グループ内の i 番目のキーワード |
attempt X/Y | 合計 Y 回中の X 回目の試行 |
リソース制限(1 CPU / 2GB RAM)
GILとI/Oバウンド
このワークロードは I/Oバウンド のため、GIL下でもスレッドが有効。
各キーワード: ~293ms(実測平均) Google API HTTP レスポンス待ち
~5〜10ms DB 書き込み
< 1ms Python コード(JSON パース、ランク計算)→ CPUは実質約98%の時間アイドル → 複数スレッドで効率的に並列実行可能。
リソース見積もり
| リソース | 見積もり |
|---|---|
| RAM / スレッド | ~15〜20MB |
| RAM ベースプロセス | ~80MB |
| RAM(4スレッド、1キー) | ~80 + 4×20 = ~160MB |
| RAM(12スレッド、3キー) | ~80 + 12×20 = ~320MB |
| 平均 CPU | < 20% |
| DB 接続数 | pool_size=15、max_overflow=10、最大 25 |
推奨構成
| キー数 | --threads | 合計スレッド | RAM | 合計 QPM | 評価 |
|---|---|---|---|---|---|
| 1 | 4 | 4 | ~160MB | 480 | 初期構成・推奨 |
| 2 | 4 | 8 | ~240MB | 960 | 推奨 |
| 3 | 4 | 12 | ~320MB | 1440 | 2GB RAMで安定上限 |
| 4 | 4 | 16 | ~400MB | 1920 | QPM quota引き上げ要 |
初期推奨設定: GOOGLE_PLACES_API_KEYS=key1 + --threads 4 --min_interval 500
ソースコード構造
places_api/
├── main.py # エントリポイント: CLI引数、オーケストレーション、スレッド管理
├── services/
│ └── places_api.py # Google Places APIクライアント (search_text, find_ranking)
├── database/
│ ├── database.py # SQLAlchemyエンジン、セッション、get_db()コンテキストマネージャ
│ ├── models.py # ORMモデル (mappy_* テーブル)
│ └── repositories.py # DBクエリ (CRUD操作)
├── logger.py # 構造化ログ(スレッド別・キー別ファイルハンドラ)
├── migrations/
│ └── gplaces_tables.sql # 新テーブルDDL
├── Dockerfile
├── requirements.txt
└── .env.exampleテスト項目
| # | テスト内容 | 確認方法 |
|---|---|---|
| 1 | placeId 抽出 | raw_json の metadata.placeId から正しく取得できること |
| 2 | ランキング発見 | ranking値が実際の位置(1-based)で保存されること |
| 3 | 圏外 | 60件内に見つからない場合 ranking=0、status=2 になること |
| 4 | APIエラー | max_retries後 → ranking=997、status=3 になること |
| 5 | ページネーション | nextPageToken で最大3ページ取得されること |
| 6 | 429リトライ | 429応答時に max_retries 回 retry_delay でリトライされること |
| 7 | マルチAPIキー | キーがラウンドロビンでチャンクに割り当て(chunk_idx mod キー数) |
| 8 | スレッドモデル | 合計スレッド = --threads × キー数 |
| 9 | レート制限 | min_interval でリクエスト間隔制御(ページネーションも対象) |
| 10 | normal/reverse | 各グループでnormal→reverseが連続実行、request_timer 共有 |
| 11 | チャンク分配 | グループが連続チャンクでスレッドに分配されること |
| 12 | data_source フィルタ | data_source=1 のロケーションのみ処理されること |
| 13 | エクスポートデータ | JSON配列が正しいフォーマット(8 keyword slots、top 3/10/20統計) |
| 14 | S3ログ | 統合ログ + キー別ログがS3にアップロードされること |
| 15 | update_log status | 開始時にRUNNING設定、process がSUCCEEDED/FAILED設定 |
| 16 | --max-groups | テスト時にグループ数が正しく制限されること |
| 17 | get_db() 例外処理 | DBエラーがrollback後にre-raiseされること |
非機能要件
| # | 項目 | 要件 |
|---|---|---|
| 1 | 費用 | $0/月(Field Mask: places.id → Basic SKU 無料) |
| 2 | 処理性能 | 初期: 1キー4スレッド 480 QPM。キー追加でリニアにスケール |
| 3 | リソース | 1 CPU / 2GB RAM — 1キー4スレッド ~160MB、3キー12スレッド ~320MB |
| 4 | 可用性 | Google Places API のSLA(99.9%)に依存 |
| 5 | データ保持 | ランキング履歴は無期限保持 |
| 6 | セキュリティ | APIキーはサーバー側環境変数で管理 |
| 7 | 監視 | ログS3アップロード、キー別ログファイルでデバッグ |
| 8 | スケーラビリティ | 環境変数にキー追加 → 自動的にスレッド・スループット増加 |
前提条件・制約事項
前提条件
- Google Cloud Projectで Places API (New) が有効化されていること
- APIキーが作成済みで、Text Search へのアクセス権限があること(複数プロジェクトのキーを推奨)
mappy_gbp_locations.raw_jsonのmetadata.placeIdに有効な値が含まれていることmappy_gbp_locationsにdata_sourceカラムが追加済み(1=API, 2=scraping)- Python 3.10+ と
requirements.txtのライブラリが利用可能であること - AWS S3 認証情報(ログアップロード用)
制約事項
- Text Search APIではKnowledge Panel(-1 / -2)を判定できない
- 検索結果は最大60件(20件×3ページ)まで — 61位以降は圏外扱い
locationBiasの半径は5000m固定- スクレイピングとPlaces APIの結果は完全一致しない可能性がある(検索エンジンとPlaces APIはランキングロジックが異なる)
- エクスポートデータは最大8キーワード/ロケーション(9番目以降はexportに含まれないが、
mappy_temp_search_rankingsには保存される) - Slack通知は未実装(TODO)
概算工数
体制
| 役割 | 人数 | 担当内容 |
|---|---|---|
| 設計者 | 1名 | 要件確認 → 設計書作成 → レビュー → 製造へ指示 |
| 製造者 | 1名 | ISSUEを元に作成 → コードレビュー → テスト実施 → デプロイ |
工数内訳
| # | 作業項目 | 工数(人日) | 担当 |
|---|---|---|---|
| 1 | 要件確認・設計書作成 | 1.0 | 設計者 |
| 2 | DBマイグレーション(data_source追加・インデックス) | 0.5 | 製造者 |
| 3 | コアロジック: Text Search API+ランキング算出 | 1.5 | 製造者 |
| 4 | マルチAPIキー+スレッド+CLI+レート制限 | 1.0 | 製造者 |
| 5 | エクスポートデータ+エラーハンドリング | 1.0 | 製造者 |
| 6 | ログ出力+S3ログアップロード | 0.5 | 製造者 |
| 7 | 結合テスト | 1.0 | 製造者 |
| 8 | デプロイ・動作確認 | 0.5 | 製造者 |
| 合計 | 7.0 |
スケジュール
| タスク | 担当 | 日数 | 4/7 | 4/8 | 4/9 | 4/10 | 4/11 | 4/12 | 4/13 | 4/14 | 4/15 | 4/16 | 4/17 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 火 | 水 | 木 | 金 | 土 | 日 | 月 | 火 | 水 | 木 | 金 | |||
| 要件確認・設計書作成 | 設計者 | 1d | |||||||||||
| DBマイグレーション(data_source+インデックス) | 製造者 | 1d | |||||||||||
| 設計レビュー・製造指示 | 設計者 | 1d | |||||||||||
| コアロジック: API+ランキング | 製造者 | 2d | |||||||||||
| マルチAPIキー+スレッド+レート制限 | 製造者 | 1d | |||||||||||
| エクスポートデータ+エラー処理 | 製造者 | 1d | |||||||||||
| ログ出力+S3アップロード | 製造者 | 1d | |||||||||||
| 結合テスト | 製造者 | 1d | |||||||||||
| デプロイ・動作確認 | 製造者 | 1d |