Google MapsレビューAPI:適切な入口を選び、店舗のレビューを記録する
Business Profile、Places API、EveryInfraによる店舗レビュー取得の違いを整理し、店舗の同一性、言語、期間フィルターを検証し、評価や店舗返信の変更履歴を保存して、サンプル・保存・表示の範囲を明確にします。
「Google Mapsのレビューを取得したい」という言葉は、3つの異なるタスクを指すことがあります。自社店舗のレビュー管理、プロダクト内での店舗詳細の表示、複数店舗にわたるユーザーの声のリサーチです。いずれもレビューを扱いますが、権限、エンドポイント、保存ルールは共通ではありません。利用経路の選択を誤ると、後からリクエスト量を増やしたりパース方法を変えたりしても解決できません。
このガイドでは、まずワークフローの選び方を説明し、その後EveryInfraのgoogle_maps_reviews.reviewsを使って、店舗の検証、小規模なリクエストの組み立て、評価と店舗返信の扱い、継続的な観測の整理方法を解説します。重点は、任意の店舗の過去のレビューをすべてエクスポートできると約束することではなく、各データレコードが正しい店舗に対応づけられることを保証し、サンプルの欠落を説明できるようにすることです。
インデックスと実装の確認日は2026-09-04です。本文で触れている有料リクエストは今回は実行しておらず、コード内の合成レコードはフィールド変化の検証を示すためだけに使用しています。本番環境でこのテンプレートを使用するのは、具体的なデータ取得、処理、保存、表示の各ユースケースについて認可を確認してからにしてください。
利用経路の選択:店舗管理、店舗情報の表示、調査目的のレビュー取得
運営権限のある店舗を管理する場合は、まずGoogle Business Profile APIを確認してください。公式のレビューガイドでは、読み取りと店舗返信の操作がアカウントとロケーション単位で整理されており、連携にはアプリの登録とOAuth認証情報が必要です。ユーザーのレビューに返信する際は、任意の地図リンクに汎用的な書き込みリクエストを送るのではなく、適切な管理権限のもとでアクションを実行してください。
公式ガイドのdeleteReply関数が削除するのは店舗返信であり、ユーザーの元のレビューではありません。この違いは運用ワークフロー上重要です。自分が書いた誤った返信を取り消すタスクなのか、不適切なユーザーレビューに対処するタスクなのかによって、DELETEの例を流用せず、それぞれのプロセスに従ってください。
プロダクト内で場所の詳細を表示する場合は、Google Places APIのPlace Detailsエンドポイントを参照してください。場所IDで照会し、FieldMaskで必要なフィールドを選択します。この場所表示用の経路は、過去のすべてのレビューを一括エクスポートするインターフェースとしてそのまま使うことはできません。また、Placesのコンテンツを利用する際は、キャッシュ、保存、帰属表示に関する要件を遵守する必要があります。
EveryInfraでは、これらを読み取る別の方法を提供しています。プラットフォームはgoogle_maps_reviews、アクションはreviews、入力は場所のURLです。以下ではこの取得経路のみを説明し、店舗プロフィールの変更、返信、削除については扱いません。また、これをGoogleの公式APIとは呼びません。
サードパーティのインターフェースを選んでも、データ利用の制限がなくなるわけではありません。Google Maps追加利用規約には複製、再配布、一括ダウンロードに関する制限が含まれており、Places APIにも独自のコンテンツルールがあります。1回のリクエストでデータが返されたからといって、長期間の蓄積、大量の再販売、任意の再表示が許可されるわけではありません。予定している用途が許可された範囲を超える場合は、技術でライセンス上の判断を回避するのではなく、計画を見直してください。
公式の店舗管理経路で具体的なパラメータが必要な場合は、reviews.listを直接確認してください。アカウントと場所を指定して、レビュー一覧、全体の平均評価、レビュー総数、次ページのトークンを返します。この構造から、「このページで何件取得したか」と「その場所にレビューが合計何件あるか」を区別する必要がある理由がわかります。ただし、そのフィールドやページネーションのルールをEveryInfraの保証として持ち込まないでください。
ステップ1:店舗名だけでなく場所の同一性を確認
店舗名は安定した一意の識別子ではありません。同じブランドが複数の支店を運営していることもあれば、同じ商業エリアに似た名前の店舗が並ぶこともあります。店舗が名称を変更しても、古い記録がそのまま新しい店舗のものになるとは限りません。レビューのリクエストを開始する前に、業務目的のチェックリストで、場所の参照情報、住所、運営主体を確認してください。
リクエストに使用した完全な場所URLと、確実に取得したplace_idを保持してください。データの統合は同一性が確認できたものに限り、地理的な近さ、同じ名前、文字列の類似性だけを根拠に2つの店舗のレビューを結合しないでください。未確認の対象は確認用チェックリストに入れ、誤って特定した店舗のデータが後続の分析で苦情の傾向として扱われないようにします。
現在の取得実装ではURLに含まれる識別可能な場所情報を利用しますが、対象の指定に適しているかどうかは地図URLの形式によって異なります。店舗名と地図の表示範囲を示す座標しか含まれないリンクを、それだけで確認済みの場所IDとして扱わないでください。地図ページのように見えるプレースホルダーURLを組み立てるのではなく、検証済みの完全な参照元リンクを保持することを優先してください。
ここにもインターフェースの境界があります。実装内部では一部の場所の詳細を認識しますが、公開リクエストでは任意のplaceIdやplace_idパラメータを追加できません。現在、reviewsの必須パラメータはurlであり、クライアントは公開ドキュメントに従ってパラメータを渡す必要があります。
ステップ2:言語、並び順、期間条件の確認
curl -fsS --max-time 30 \
'https://api.everyinfra.com/api/v1/social/catalog?platform=google_maps_reviews' \
| jq -e '.capabilities[] | select(.action == "reviews") | {
platform, action, required_params, optional_params, param_meanings,
mode, returns_list, default_limit, max_limit, response_fields
}'このディレクトリではurlが必須で、結果は同期的にリストで返されます。デフォルトの件数は20件、上限は100件で、lang、language、since、sortなどの任意パラメータに対応しています。正確なプラットフォーム識別子はgoogle_maps_reviewsです。記事のタイトルを理由に別のプラットフォームに省略しないでください。
newestとrelevanceは異なる並び替えの意図に対応します。前者は最新のコンテンツ、後者は関連性の高いサンプルを対象とし、互いに置き換え可能なページのデータセットではありません。店舗を比較する際は選択基準を統一し、実際に取得したレビューIDを保存してください。並び順を切り替えると、サンプルの構成が変わる可能性があります。
言語フィールドは、投稿者の国籍や居住地で絞り込むものではありません。langに指定できる値はインターフェースの列挙値に従い、現在はen、zhcn、zhtwなどがあります。大まかな説明をもとに任意の言語コードを入れないでください。langとlanguageを安易に両方指定することも避けてください。現在のマッピングではlangがlanguageの設定より優先されるため、最初の実装では明示的にサポートされている値を1つだけ選んでください。
sinceはレビュー投稿日の下限を表し、ディレクトリではYYYY-MM-DD形式が推奨されています。店舗の開業日、レビューの更新日時、処理の進捗を示す信頼できるカーソルではありません。必要に応じて絞り込み条件として使用し、境界となる日付は実際の結果と照らし合わせて個別に検証してください。
ステップ3:許可された場所から得た少数の結果をテスト
サーバーにEVERYINFRA_API_KEYと確認済みのEVERYINFRA_MAPS_PLACE_URLが設定されていることを前提とします。サンプルは1回だけ送信し、limitはparamsの中に指定してください。リクエストのルートレベルに置いても件数は制御されません。
: "${EVERYINFRA_API_KEY:?set the API Key first}"
: "${EVERYINFRA_MAPS_PLACE_URL:?set the authorized full place URL first}"
jq -cn --arg url "${EVERYINFRA_MAPS_PLACE_URL}" '{
platform: "google_maps_reviews",
action: "reviews",
params: {url: $url, sort: "newest", limit: 3}
}' | curl --silent --show-error --include --max-time 180 \
'https://api.everyinfra.com/api/v1/social' \
-H "Authorization: Bearer ${EVERYINFRA_API_KEY}" \
-H 'Content-Type: application/json' --data-binary @-limit: 3はテスト用の少数サンプルの設定であり、ちょうど3件が返ることを保証するものでも、その場所にレビューが3件しかないことを意味するものでもありません。180秒はクライアント側の待機時間の一例で、サービスの保証ではありません。まずHTTPステータスとエラーを確認し、次に結果が想定どおりのリストか、対象の場所に対応しているかを確認してください。
空の配列を、そのまま「この店舗にはレビューがない」と判断することはできません。今回の呼び出しで取得に失敗した可能性や、対象、フィルター、アクセス可否を確認する必要がある可能性があります。実際の理由が不明な場合は、状態を不明のまま保持してください。クライアントがタイムアウトした場合も、前回の試行が実行されなかった、または課金されなかったと仮定してPOSTリクエストを自動で再試行しないでください。
ステップ4:場所全体の評価、個別の評価、返信の分離
現在のレコードのフィールドには、id、text、rating、rating_scale、posted_at、place_id、place_name、owner_response、owner_response_atがあります。場所の詳細にある平均評価と個々のレビューの評価は別のものです。空の値を埋めるために、店舗の平均スコアを各レビューにコピーしないでください。
curl -fsS --max-time 30 \
'https://api.everyinfra.com/api/v1/social/fields?platform=google_maps_reviews&action=reviews' \
| jq '{fields, undocumented}'評価と本文も分けて処理する必要があります。評価のみで本文のないレコードは評価のサンプルとしては役立ちますが、テキストのトピック分類にそのまま投入することはできません。ratingが欠けている場合は不明の状態を保持してください。プラットフォーム間で比較する際は評価とあわせてrating_scaleも確認し、異なるスケールの数値を平均しないでください。
detailed_ratingが返された場合はサブ項目のスコアを含みますが、すべての場所やレビューに同じ項目があるとは想定しないでください。is_local_guideや過去のレビュー数を、そのまま「実在ユーザー」の認証や信頼性スコアとして扱わないでください。これらはプラットフォームが提供する属性であり、個々のレビュー内容が事実として正確であることを検証したものではありません。
店舗からの返信は別に保存します。owner_responseがないことは、今回の実行でこの値を取得できなかったことを示すだけで、店舗が一度も返信していないことの証明にはなりません。また、後から返信を取得できたとしても、ユーザーの元のレビューが新たに投稿されたことを意味するわけではありません。両方を新しいレビューとして扱うと、意味のない新規追加の監視アラートが発生します。
ステップ5:識別情報、観測、変更ごとのレコード作成
- 場所の識別情報:確認済みのplace_id、ソースの参照情報、事業の所有者情報。重複する店舗は、名称だけで大まかに統合せず、まず識別情報に基づいて確認します。
- レビューの識別情報:場所IDとレビューIDの組み合わせ。信頼できるIDがないレコードは別に処理し、投稿者のニックネームを一意のキーとして使わないでください。
- 観測ログ:リクエストID、実際の収集日時、並び順と言語・日付の条件、取得したフィールド、欠けている要素。
- 変更ログ:同じレビューについて観測できた本文、評価、店舗の返信の差分。不明なフィールドを削除と解釈しないでください。
比較に必要なフィールドは、許可された範囲と期間内でのみ保存してください。本文を保存できない場合は記録を許可された最小限の状態に調整し、「監査しやすいから」という理由で無期限に保持しないでください。次のセクションでは、架空の場所、レビュー、本文を使って、値の変更、初めて取得したフィールド、未取得のデータを区別する方法を示します。
function compareReview(before, after) {
const identity = row => {
if (typeof row.place_id !== "string" || !row.place_id
|| typeof row.id !== "string" || !row.id) {
throw new Error("place and review ids required");
}
return JSON.stringify([row.place_id, row.id]);
};
if (identity(before) !== identity(after)) throw new Error("different review");
const t0 = Date.parse(before.observed_at);
const t1 = Date.parse(after.observed_at);
if (!Number.isFinite(t0) || !Number.isFinite(t1) || t1 <= t0) {
throw new Error("observations must have increasing times");
}
const changed = [], firstObserved = [], notObserved = [];
for (const field of ["text", "rating", "owner_response", "owner_response_at"]) {
if (!Object.hasOwn(after, field) || after[field] == null) {
notObserved.push(field);
} else if (!Object.hasOwn(before, field) || before[field] == null) {
firstObserved.push(field);
} else if (before[field] !== after[field]) {
changed.push(field);
}
}
return {changed, firstObserved, notObserved};
}
console.log(compareReview(
{place_id: "synthetic-place", id: "synthetic-review",
text: "synthetic review", rating: 3, observed_at: "2026-09-01T00:00:00Z"},
{place_id: "synthetic-place", id: "synthetic-review",
text: "synthetic review", rating: 3, owner_response: "synthetic reply",
observed_at: "2026-09-02T00:00:00Z"}
));この例では、owner_responseを新しいレビューではなくfirstObservedとして記録し、返信日時がない場合はnotObservedとして記録します。changedにテキストが含まれていても、それは2回の取得で得られたテキストが異なっていたことを示すだけで、翻訳や表記の変化による可能性もあります。裏付けとなる根拠なしに、ユーザーが原文を編集したと断定しないでください。
時系列チェックにより、古い観測ログが新しい観測を上書きすることは防げますが、サーバー側のデータがリアルタイムで最新であることを証明するものではありません。記録したobserved_atはレスポンスを確認した時点を表すもので、プラットフォームのフィールドの更新日時ではありません。
タイムゾーンをまたいで観測レコードを保存する場合は、オフセットを含むRFC 3339形式の時刻を使い、元の値は別途保持してください。形式を統一すると特定時点での比較がしやすくなりますが、観測時刻がレビューの更新日時になるわけでも、今回の取得のカバレッジが広がるわけでもありません。
ステップ6:単なる日付の繰り上げではなく、増分観測のカバレッジを明示
継続的なモニタリングでは、許可された条件のもとで固定した場所の集合を繰り返し観測し、日付フィルターの範囲を適切に重複させたうえで、レビューの識別情報に基づいて重複を排除できます。重複期間の長さは、遅れて現れたレコードの観測状況とタスクの実行頻度によって決まります。小規模なサンプルから、あらゆる場合に通用する日数を直接導くことはできません。
ただし、重複期間を設けても、すべての取りこぼしを解消できるわけではありません。1回の取得で返される件数には上限があり、古いレビューに後から店舗の返信が付くこともあり、並び順によって返されるサンプルも変わります。特に、sinceで更新された古いレビューをすべて取得できるとは考えないでください。sinceは投稿日時を絞り込むものであり、完全な変更ストリームではありません。
実行ごとに、確認予定の場所、レコードを取得できた場所、取得に失敗した場所または結果が不明な場所、および場所ごとの返却件数を記録してください。カバレッジを論じるには、収集範囲、並び順、取得結果が打ち切られる条件を把握している必要があります。返却件数を不明な総数で割り、一見精密なカバレッジ率を作り出すことは妥当ではありません。
店舗間の比較にも同様の慎重さが必要です。最近の少数のレビューから算出した平均スコアは、マップ上の場所の総合評価と同じではありません。また、サンプルの入れ替わりだけでスコアの低下を確認することはできません。問題の種類を比較する場合は、まず分類基準を定義し、そのうえで店舗ごとのサンプル取得時期と欠落要素を示してください。
ステップ7:分析、内部処理、公開表示に必要な権限の違い
言語の指定によってコンテンツの表現が変わることはありますが、返されるすべてのレビューがその言語で書かれた原文であるとは限りません。翻訳文と原文を、独立した2件のユーザー意見として扱ってはいけません。分析結果には適用した言語条件を記録し、言語間の差異については人手による確認を継続してください。
公開表示にPlaces APIを使う場合は、テキストを自社ページにコピーするのではなく、現行のポリシーに従って、コンテンツの帰属表示、投稿者情報、提供元への導線に関する要件に対応してください。データが別の経路から取得されたものであれば、適用されるライセンスを事前に確認し、Placesの要件を自動的に適用したり免除したりしないでください。このドキュメントのインメモリの例は、保存や再配布を許可するものではありません。
内部分析では、タスクに不可欠なフィールドのみを処理することを優先し、閲覧できる人と保持期間を制限してください。モデルによる分類や緊急度レベルには、元データから導いた判断であることを明示する必要があります。システムで人によるレビュー用のキューを整理することはできますが、低評価をきっかけに自動返信したり、公の場で投稿者に連絡したり、店舗の営業ステータスを変更したりしないでください。
ワークフロー拡大前の単一ロケーションの完全な検証
最初の統合を終えた時点で、次の点を説明できる状態にしてください。このデータセットを処理する権限がある理由、対象とした店舗、選択した並び順と言語のオプション、裏付けのあるフィールド、返信の変化を識別する方法、今回の実行で欠落した要素です。また、出所不明のレビューファイルを残すのではなく、問題をリクエストIDまでさかのぼって追跡し、実際の課金を確認できる必要があります。
これらの問いに答えられたら、ロケーションリストを拡大するか、定期実行をスケジュールしてください。持続可能なレビューワークフローとは、データを正常に読み取れるだけでなく、ロケーションの識別情報、ユースケース、サンプル範囲、変更記録を常に明確に保てるものです。
参考資料 LF-05