当社独自のアクセス統計は常に有効です。Google Analyticsは、公開ページ(ログイン後のワークスペースは対象外)の利用状況を把握するためのもので、許可をいただいた場合のみ有効になります。詳しくは プライバシーポリシーをご覧ください。

マルチプラットフォームの評判モニタリング設計:ネガティブな変化を評価する前に範囲を決める

監視対象リスト、コメントの識別子、増分カーソルを起点に、追跡可能な評判モニタリングのワークフローを設計します。オフラインのサンプルで重複排除と取り込み漏れを示しつつ、人によるレビューを維持します。

昨日はネガティブなコメントが多く今日は少なかったとしても、それがそのままブランドの評判改善を意味するわけではありません。今日の取り込みで一部の拠点が漏れていた、あるプラットフォームで障害が発生していた、同じコメントが二重にカウントされていた、といった場合、2日間の数値は比較できません。

評判モニタリングでは、どの課題に対応が必要かを判断する前に、まず何を観測し、何を取りこぼしたのかを明らかにする必要があります。本記事では、承認済みの監視対象から始め、コメントの識別子と収集状態を保持し、根拠のある課題を人によるレビューに回すワークフローの設計図を紹介します。既製のモニタリング製品の機能説明や顧客事例ではありません。データ構造、ルール、合成サンプルは設計例であり、実際のブランドの成果やモデルの精度を示すものではありません。

全面的な網羅の約束ではなく、ビジネス課題の選定

「ブランドのネガティブな評判を監視する」では範囲が広すぎて、完了を定義できません。より対応につなげやすい課題は、指定した拠点の最近のコメントに同じサービス上の問題が繰り返し現れているか、あるいは新商品のレビューに確認が必要な品質に関するフィードバックが現れているかを判断することです。課題が定まれば、どの対象を監視し、どの証拠を保持し、誰がレビューを担当するかを決められます。

watch_targetリストを管理し、各行にプラットフォーム、オブジェクトの種類、取得元URL、取得元オブジェクトID、地域または言語、監視目的、許可された収集範囲、担当者、保持期間を含めます。対象の変更は別途バージョン管理してください。店舗の追加、商品の一時停止、地域の調整によって、統計上のベースラインが変わる可能性があります。

公開されていることは、無制限に収集・利用してよいことを意味しません。連携の前に、対象プラットフォームのポリシー、データの利用目的、必要な許可を確認してください。正規化フィールドのためだけに、投稿者のプロフィールや連絡先を追加で収集しないでください。本記事は、いかなるプラットフォームへのアクセス権限も付与するものではなく、特定地域における法令遵守について結論を示すものでもありません。

アクセス権限のある店舗のレビューを管理する場合は、Google Business Profileの公式レビュー管理ドキュメントを確認し、要件が公式のワークフローに合致するかを判断してください。このクロスプラットフォーム監視の設計図は、そのAPIとは別の製品です。EveryInfraにおけるロケーションレビューのパラメータはGoogle MapsレビューAPIページを、連携方法はEveryInfra APIドキュメントを参照してください。

カタログによる機能選定と納品証明との区別

以下の例では公開されている機能のみを照会し、コメントの読み取りやAPIキーの使用は行いません。curlとjqを使ってターミナルで実行してください。pipefailにより、リクエストの失敗が後続のJSON処理で隠されるのを防ぎます。

Bash・コピー可能なサンプル
set -o pipefail
curl -fsS --max-time 20 \
  'https://api.everyinfra.com/api/v1/social/catalog?platform=google_maps_reviews' \
  | jq '.capabilities[] | select(.action == "reviews") |
      {platform, action, required_params, optional_params, response_fields}'
Bash・コピー可能なサンプル
set -o pipefail
curl -fsS --max-time 20 \
  'https://api.everyinfra.com/api/v1/social/catalog?platform=xiaohongshu' \
  | jq '.capabilities[] | select(.action == "comments") |
      {platform, action, required_params, optional_params, response_fields}'

2026年9月4日時点のカタログ確認では、google_maps_reviews.reviewsとxiaohongshu.commentsのいずれもurlが必須です。前者にはsinceが記載されていますが、後者のアクションにはこのパラメータが記載されていないため、「前回のタイムスタンプから続行する」という汎用的なリクエストを両方に適用することはできません。前者は評価と評価スケールのフィールドを宣言していますが、後者は評価を宣言していないため、コメントの感情タグを評価列に入れることはできません。ロケーションレビューのカタログ、Xiaohongshuコメントのカタログ

これらから確認できるのは、カタログの宣言内容だけです。実際のレスポンスに各フィールドが含まれるか、タイムスタンプの形式、ページネーションの扱い、空の結果の表現は、許可されたサンプルで別途検証する必要があります。ここでは未検証の汎用APIは提供しておらず、両プラットフォームの収集が完了したとも主張していません。

3つのタイムスタンプと2つの状態の列分け

コメントには少なくとも3つのタイムスタンプがあります。取得元が提供する投稿日時posted_at、本システムが初めて観測した日時first_seen_at、直近の観測日時last_seen_atです。取得元のタイムスタンプが不明な場合は空のままにし、クロール日時を投稿日時の代わりに使わないでください。日単位のウィンドウを扱う場合は、元のタイムゾーンと変換ルールを記録します。「たった今」や「昨日」などの相対表現は確実に解析できないため、日をまたぐ厳密な統計からは除外する必要があります。

収集の実行ごとにも記録を残す必要があります。run_id、対象リストのバージョン、リクエスト参照、リクエストウィンドウ、開始・終了時刻、実際の納品件数、収集ステータスです。業務システムでは次の状態を区別してください。

  • succeeded:現在のリクエスト範囲内で納品が完了した状態。過去分すべての取得完了を意味するものではありません。
  • empty:リクエストは成功し、空のセットが返された状態。対象にコメントがないことを意味するものではありません。
  • partial:一部の対象またはページのみ完了した状態。受信済みの結果と欠落部分を保持します。
  • failed:合意した納品を達成できなかった状態。トラブルシューティングに使えるエラー分類を保持します。
  • unknown:タイムアウトまたは結果が未確認の状態。成功、空、未課金のいずれとしても恣意的に計上できません。

これらのステータスは本設計図における業務上の設計であり、APIが同名のフィールドを返すことを示すものではありません。収集ステータスと業務上の判断は分けてください。収集の失敗はパイプラインの課題として起票し、根拠が不足しているコンテンツは自動的に「ネガティブなし」と判定せず、保留としてマークします。

タイムスタンプの形式は、日付、時刻、UTCオフセットに関するRFC 3339の定義を参照できます。これは保存時の表現の統一に役立ちますが、posted_at、first_seen_at、last_seen_atを1つの業務上の意味にまとめることはできず、不明な取得元時刻のタイムゾーンを推定で補うこともできません。

コメント識別の安定化と可変コンテンツのバージョン管理

取得元が安定したコメントIDを提供する場合、推奨される識別キーは(platform, source_object_id, source_review_id)です。プラットフォームやオブジェクトが異なると同じ値が生成される可能性があるため、コメントIDだけに頼らないでください。同様に、2人のユーザーが同じ短いフレーズを書いても同じコメントとは限らないため、テキストハッシュだけを使うことも避けてください。

同じ識別キーが再び現れた場合は、内容のバージョンを評価します。テキストや評価が変わった場合は、新しいコメントとして数えずに、新しい観測バージョンを作成できます。バージョンの概要は初回観測日時・直近観測日時とは別に記録し、レビュー担当者が内容の差分を確認できるようにしつつ、増分計算で二重計上が起きないようにします。差分は言語や変換ルールの変更から生じる場合もあるため、検証せずに投稿者が原文を編集したと断定しないでください。安定した取得元IDがないレコードはレビューキューに入れます。近似フィンガープリントは「識別不確実」としてマークし、厳密な重複排除に黙って使用しないでください。

正規化レイヤーには、現在の実行で検証済みの値とフィールドの取得元のみを保持します。評価とスケールはペアで処理し、空の評価を0とみなさないでください。owner_responseがないことは、店舗が一度も返信していないことを意味しません。元のレスポンス構造と内部の変換構造は分けて保持し、変換ルールをバージョン管理して、インターフェースの更新後に古いフィールドが誤って解釈されるのを防ぎます。

こうしたバージョン間の関連付けは、W3C PROVの来歴、処理アクティビティ、派生関係を用いて整理できます。元の観測、正規化済みレコード、モデルによるタグ、アラートのそれぞれが出所を保持します。この設計は追跡可能な手法を採用していますが、出力データがW3C PROV仕様に厳密に準拠していると主張するものではありません。

データ確定後のみのカーソル更新

増分収集には、気づきにくい欠落のリスクがあります。コメントを保存する前にカーソルを今日の日付に更新すると、失敗した場合に後続の再実行でそのコンテンツが飛ばされることがあります。カーソルは、対象の納品、重複排除、永続化、必要なチェックが完了してから進めてください。複数対象のバッチでは対象ごとに進捗を独立して管理し、ある対象の成功によって他の対象が先に進まないようにします。

検証済みのページネーショントークンがある場合は、対応する対象、リクエスト条件、完了済みのページと一緒に保存します。時間でフィルタするクエリでは、ウィンドウが重なるように設計し、識別キーで重複排除を行います。ただし、ウィンドウの長さは業務上の選択であり、欠落ゼロを保証するものではないことを明確にしてください。未検証の増分パラメータをカタログに追加しないでください。監視目的を評価する前に、検証済みの読み取り範囲を使用してください。

1回の実行の上限に達した場合、それはサンプリングの境界に達したことを示しており、過去分をすべて取得したことにはなりません。並び順、地域、言語を変更すると、古いカーソルを再利用できなくなる場合があります。安全な運用として、リクエスト条件を進捗のバージョンに含め、カバー範囲を再検証し、バックフィルのタスクを維持してください。

1回の実行でコメントが観測されなかったからといって、自動的に削除済みとしてマークしないでください。並び順の変更、未到達のページ、一時的な非表示などが要因になり得ます。削除ステータスは、サンプルにないことから推測せず、明示的に検証する必要があります。

オフラインでの重複排除と欠落のデモ

以下のNode.jsの例は、架空のオブジェクトとタグのみを処理し、コンテンツのダウンロード、モデルの呼び出し、アラートの送信は行いません。オブジェクト間で重複するID、プラットフォーム間で重複するID、繰り返しの観測、編集バージョン、IDの欠落、空のテキスト、未登録の対象をあえて含めており、識別と収集状態が1つの成功件数に混同されないことを確認できます。

この例の対象の状態とテキストタグは手動で設定したテスト入力であり、実際のAPIレスポンスや分類結果ではありません。ローカルでのバッチ処理のデモであり、実行をまたいだ永続化、リクエストのリトライ、カーソルの送信、通知システムは実装していません。

Bash・コピー可能なサンプル
node <<'JS'
const assert = require('node:assert/strict');
globalThis.fetch = () => { throw new Error('Offline example: no network'); };

function inspectRun(targets, rows) {
  const targetKey = r => JSON.stringify([r.platform, r.object]);
  const states = new Set(['succeeded', 'empty', 'partial', 'failed', 'unknown']);
  assert.ok(targets.length > 0);
  assert.equal(new Set(targets.map(targetKey)).size, targets.length);
  assert.ok(targets.every(t => states.has(t.state)));
  const planned = new Map(targets.map(t => [targetKey(t), t.state]));
  const identities = new Map();
  let duplicates = 0, revisions = 0, quarantined = 0;
  for (const r of rows) {
    const state = planned.get(targetKey(r));
    const canReadRows = state === 'succeeded' || state === 'partial';
    if (!canReadRows || typeof r.id !== 'string' || !r.id.trim()
        || typeof r.text !== 'string' || !r.text.trim()) {
      quarantined++;
      continue;
    }
    const key = JSON.stringify([r.platform, r.object, r.id]);
    // This fixture arrives in observation order; text alone is not an identity.
    const version = JSON.stringify([r.text]);
    if (identities.get(key) === version) duplicates++;
    else {
      if (identities.has(key)) revisions++;
      identities.set(key, version);
    }
  }
  return {
    uniqueReviewsInBatch: identities.size,
    duplicates, revisions, quarantined,
    targetCoverageGate: targets.every(t => ['succeeded', 'empty'].includes(t.state)),
  };
}

const targets = [
  { platform: 'maps-fixture', object: 'place-a', state: 'succeeded' },
  { platform: 'maps-fixture', object: 'place-b', state: 'succeeded' },
  { platform: 'notes-fixture', object: 'note-a', state: 'partial' },
  { platform: 'video-fixture', object: 'video-a', state: 'failed' },
];
const first = { platform: 'maps-fixture', object: 'place-a', id: '7', text: 'synthetic reviewA' };
const rows = [
  first,
  { ...first },
  { ...first, object: 'place-b' },
  { ...first, platform: 'notes-fixture', object: 'note-a' },
  { ...first, id: null },
  { ...first, id: '8', text: '   ' },
  { ...first, text: 'synthetic reviewedited version of A' },
  { ...first, object: 'unplanned' },
];
const report = inspectRun(targets, rows);
assert.deepEqual(report, {
  uniqueReviewsInBatch: 3, duplicates: 1, revisions: 1,
  quarantined: 3, targetCoverageGate: false,
});
assert.equal(inspectRun(targets, [...rows, rows[6]]).duplicates, 2);
assert.equal(inspectRun([{ ...targets[0], state: 'empty' }], []).targetCoverageGate, true);
assert.equal(inspectRun([{ ...targets[0], state: 'unknown' }], []).targetCoverageGate, false);
assert.equal(inspectRun([{ ...targets[0], state: 'failed' }], [first]).quarantined, 1);
assert.equal(inspectRun([targets[0]], [{ ...first, id: ' ' }]).quarantined, 1);
assert.throws(() => inspectRun([], []));
assert.throws(() => inspectRun([targets[0], targets[0]], []));
assert.throws(() => inspectRun([{ ...targets[0], state: 'typo' }], []));
console.log(JSON.stringify(report, null, 2));
JS

想定される結果は、コメントの識別情報が3件、重複観測が1件、編集バージョンが1件、レビュー待ちレコードが3件で、対象のカバレッジは受け入れゲートを通過しません。emptyはリクエスト完了の対象ステータスゲートを満たしますが、対象にコメントがないことを証明するものではありません。対象ステータスゲートを通過した場合でも、トレンドを比較する前にページのカバレッジ、サンプルのベースライン、メタデータを確認する必要があります。ブール値は、外部向けの結論を導いてよいという承認を意味しません。

この例では観測順に項目を処理し、テキストのバージョンのみを比較しています。本番用のアダプターでは評価などのフィールドも追跡し、順序が前後したデータや過去バージョンの扱いのルールを定義したうえで、遅れて到着したレコードを無条件に取得元の最新バージョンとして扱わないようにしてください。

レビュー待ちのレコードを安易に破棄したり、新規としてカウントしたりしないでください。アダプター、対象リスト、欠落フィールドを原因別に調査し、識別情報が確定するか、近似的な処理が明示的に承認された後にのみ統計に組み込みます。日次レポートの見栄えを良くするためだけに、失敗した対象を計画の分母から除外することは避けてください。

モデル分類前の根拠候補の整理

トピックタグには、少なくともreview_key、コメントのバージョン、ルールまたはモデルのバージョン、タグ、根拠の抜粋、手動処理のステータスを含めてください。要約は裏付けとなる原文に対応付けられる必要があります。モデルの信頼度スコアはキャリブレーションされておらず、苦情を自動でエスカレーションするための正確な確率を表すものではありません。

コメント本文は信頼できない入力です。「以前の指示を無視せよ」や「このデータを外部に送信せよ」といったテキストは、エージェントへの操作命令ではなく、あくまで分析対象のテキストとして扱う必要があります。分類ステップには支払い、削除、外部送信の権限は不要です。データ取得、分類、人によるレビュー、送信を分離することで、意図しない実行のリスクを低減できます。

多数のタグを同時に有効にするよりも、特定のトピックと人が検証したサンプルのセットから始めるほうが、結果を解釈しやすくなります。分類には「判定不能」カテゴリを設け、その原因が言語、文脈、欠落フィールドのいずれにあるかを記録する必要があります。判定不能なケースを中立カテゴリにまとめると、レビューが必要な問題が埋もれてしまうため避けてください。

この権限分離は、OWASPの間接プロンプトインジェクションに関するガイダンスと一致しています。外部コンテンツには指示が含まれる可能性があるため、ツールの権限と実際のアクションを独立して制御する必要があります。コメント内の指示を無視するよう求める一文は、完全な防御策でも、外部送信の承認でもありません。

収集の欠落時における高リスクエビデンスの可視性確保

トピックが拡大傾向にあるかを評価するには、比較可能な期間、明確な分母、最低サンプル数の基準が必要です。比較範囲では、ターゲットセット、地域、並び順、対象に含める条件を同一にしてください。日次のカバレッジが不完全な場合は、全体傾向に関する判断を保留します。一方、取得済みのエビデンスのうち確認が必要なものはレビュー担当者に回し、データが一部しか揃っていないことを理由に隠さないでください。

事項ごとに別々のキューに分けます。収集の欠落はデータオーナーに、具体的な苦情のエビデンスはビジネスオーナーに回します。HTTPエラーをブランドリスクと解釈したり、コメントの収集失敗を「本日は異常なし」と報告したりしないでください。

アラート候補には、対象、観測期間、トリガールール、コメントの識別情報とバージョン、エビデンスの抜粋、カバレッジの制約、担当者、処理状態を明記する必要があります。再実行期間中の重複通知を防ぐため、再実行しても変わらないイベントキーを割り当ててください。コメントが編集された場合やラベルが修正された場合は、以前の結論を黙って上書きせず、改訂履歴を記録してイベントを更新します。

身の安全、医療、差別、法的紛争、従業員の処分を扱う場合、モデルによる支援はエビデンスの整理に限定し、最終判断と外部への対応は適切な担当者が行うべきです。このブループリントが、コメントへの返信、投稿者への連絡、対応結果の公開を自動で行うことはありません。

リリース前の障害シナリオ検証

取り込み、リプレイ、候補キュー、手動処理からなるパイプライン全体を実行する前に、承認された範囲内で最小限の収集を検証してください。少なくとも次のシナリオを確認してください。

  • 同じバッチを再処理しても、コメントの識別情報が新たに追加されることはなく、重複通知も発生しません。
  • 異なる対象やプラットフォーム間で重複するIDが、誤ってマージされません。
  • 編集されたコメントは、新しいコメントエントリを作成せずに変更の証跡を保持します。
  • 一部のターゲットで障害が発生した場合、取得済みのコンテンツは保持され、未完了のターゲットの進捗は進められません。
  • 空の結果、空のテキスト、ページネーションの証跡の欠落は、まとめて「新規0件」とせず、それぞれ区別して表示されます。
  • データの保存に失敗した場合、カーソルは影響を受けたバッチを越えて進まず、復旧後に安全にリプレイできます。
  • ソースデータの修正、削除リクエスト、保持期間の満了が発生した場合に、関連するタグ、要約、アラートのエビデンスを個別に処理できます。

オフラインのデモでは、同一性とステータスに関するルールを確認できますが、永続化、収集、通知、モデル分類が受け入れ基準を満たすことまでは証明できません。本番で使用する前に、データの保持、削除、アクセス、エクスポートに関する権限を定めてください。デフォルトのログには、コメント全文、認証情報、不要な個人情報を含めないでください。

次のステップ:人による対応に適したエビデンスチェーンの提供

承認済みの少数のターゲット、1つのビジネス課題、観測期間を選び、担当者を指定します。各実行で、どの範囲で成功し、どこに欠落があるかが明確に定義されていることを示し、個々のエビデンスが二重にカウントされていないことを検証したうえで、どの項目を手動処理キューに入れるかを決めます。

顧客の声や検証可能な指標がなくても、設計をブループリントとしてレビューできますが、ブランドを支援した実績として提示することはできません。記事は、エンドツーエンドのサンプルを備えた実践的なチュートリアルとして扱うか、同意、ベースライン、結果、制約をすべて揃えた顧客事例として扱ってください。両者では、必要なエビデンスが異なります。

参考資料 LF-07