トップ / データベース設計

データベース設計

このページでできること

  • クヌギGEO がどのテーブルにどんなデータを保存しているかを把握できます。
  • SQLite を 2 種類のファイルに分割している意図と、バックアップ・移行で扱う対象を理解できます。
  • ご自身でレポートを実装する際に、参照先のテーブル・カラムを特定できます。

1. 設計思想:システム DB + プロダクト DB

クヌギGEO は SQLite を 2 種類のファイルに分けて使用しています。スナップショット(日次で積み上がる最大級のテーブル)が肥大化しても、他のメタ情報を巻き込まずにバックアップ・移行・削除ができるようにすることが目的です。

用途ファイル主な格納物
システム DB data/app.db ユーザー・アプリ設定・プロダクト・キーワード・GA4 認証・拡張レポート ON/OFF など、全プロダクト共通のメタ情報
プロダクト DB data/products/product_<productId>.db そのプロダクトに紐づく 調査スナップショットsurvey_snapshots。日次で増え続けるため分割)

スナップショットへのアクセス時は、システム接続に ATTACH DATABASE <product_<id>.db> AS pdb でアタッチし、pdb.survey_snapshots として参照します(api/bootstrap.phpllmo_attach_product_db())。

WAL モード 両 DB とも WAL(Write-Ahead Logging)モードで動かしています。バックアップする場合、app.db 単独ではなく app.dbapp.db-walapp.db-shm の 3 ファイルを揃えてコピーするのが安全です(プロダクト DB 側も同様)。

2. システム DB のテーブル一覧

users — アカウント

Web ログインアカウントを管理します。usernameemail はメールアドレスで同期されます。

説明
idINTEGER PK主キー
usernameTEXT UNIQUEログイン ID(メールアドレス)
passwordTEXTパスワードハッシュ(PHP password_hash
emailTEXTパスワード再発行メール送信先(username と同期)
initial_setup_completeINTEGER初回セットアップ済みフラグ
roleTEXTadmin / product_manager / viewer

app_settings — システム共通設定(id=1 の 1 行のみ)

説明
dataforseo_login / dataforseo_passwordDataForSEO API 認証情報(平文)
usd_jpy表示用ドル円レート(既定 159)
llm_chatgpt_model / llm_claude_model / llm_gemini_model / llm_perplexity_model各 LLM の既定モデル
llm_web_searchLLM Responses での Web 検索 ON/OFF(既定 1)
llm_models_json / llm_models_updated_atDataForSEO の最新サポートモデル一覧スナップショット
ai_overview_retryAI Overview が空のとき再試行するか(既定 1)
dataforseo_serp_plan / dataforseo_llm_planSERP / LLM の実行プラン(live / priority / standard
survey_schedule_json旧グローバルスケジュール(互換のみ。新は products.survey_schedule_json
google_oauth_client_id / google_oauth_client_secretGA4 連携用 Google OAuth 情報
brand_name / brand_logo_pathサイトブランディング(ヘッダー表示名・ロゴ)

products — プロダクト

説明
id / name / sort_order / created_at基本情報
survey_schedule_jsonこのプロダクト固有の曜日スケジュール(軸ごとの曜日 ON/OFF)

product_keywords — キーワード

説明
id / product_id主キー / プロダクト FK
keyword検索キーワード文字列
target_url検索結果順位のヒット判定に使う URL もしくはホスト
own_brandsJSON 配列(自社ブランド名一覧)
rival_brandsJSON 配列。新形式 [{"name":"...","url":"..."}, ...](最大 5 件)
fetch_rank / fetch_ai_overview / fetch_ai_modeSERP 系の取得 ON/OFF
fetch_llm_chatgpt / fetch_llm_claude / fetch_llm_gemini / fetch_llm_perplexityLLM Responses の取得 ON/OFF
location_code / device地域コード(2392 = Japan)/desktop | mobile
sort_order / created_at並び順・作成日時

user_products — ユーザのアクセス可能プロダクト

システム管理者(admin)以外(プロダクト管理者 product_manager/閲覧者 viewer)に対して、見せるプロダクトを user_id × product_id で限定するためのテーブル。システム管理者(admin)はこのテーブルに関係なく全プロダクトに到達できます。

product_ga4 — プロダクトの GA4 連携情報

説明
product_idプロダクト FK(PK)
property_idGA4 プロパティ ID(数字のみ)
google_email / google_name連携した Google アカウント情報(表示用)
access_token / refresh_token / token_expires_at / scopesOAuth 認可トークン(平文で保存されます。Basic 認証下で運用してください)
cv_mode / cv_event_namesCV 集計方針(key_events | events)と対象イベント名 JSON
track_flags_json / sources_jsonプロダクト単位の「計測対象」「流入種別と判定条件」オーバーライド
connected_at連携日時

ga4_oauth_states — OAuth フロー中の state 保持(短命)

「Google で認証」を押したときに発行する state を一時保存します。コールバック後は不要になります。

product_extended_reports — プロダクトごとの拡張レポート ON/OFF

products/extended_reports/*.php を自動検出する拡張レポート機能で、プロダクトごとに表示 ON/OFF をオーバーライドします。行が無いプロダクトは各レポートの @report-default-enabled に従います。

説明
product_id / report_key複合 PK
enabled0 / 1
updated_at更新日時

password_reset_tokens — パスワード再発行トークン

「パスワードを忘れた」フローで発行する短命トークン。token_hash はハッシュで保存され、平文は DB には保存されません。

login_attempts — ログイン試行レート制限

ログイン成功・失敗を記録し、ブルートフォース対策に使います。

3. プロダクト DB(product_<id>.db)のテーブル

survey_snapshots — 調査スナップショット

各キーワード × 軸の取得結果(JSON)を 1 行ずつ保存します。日次で増え続けるテーブルなので、プロダクトごとにファイルを分けてあります。

説明
idINTEGER PK主キー
product_keyword_idINTEGERシステム DB の product_keywords.id への論理 FK(attach 越しのため SQLite では強制されません)
captured_atTEXT同一バッチで揃えた取得日時(ISO 8601)
result_jsonTEXT取得結果一式の JSON。軸ごとのフラグ・派生指標・参照 URL・LLM 回答本文などを保持します(詳細は 下記「result_json の構造」

UNIQUE(product_keyword_id, captured_at) のため、同一バッチでの重複保存は発生しません(再取得時は INSERT OR REPLACE 相当の動作)。

result_json の構造(JSON フィールド一覧)

調査ランナー(cron/lib/survey_runner.php)と手動実行ハンドラ(api/catalog_handlers.phpllmo_catalog_save_snapshot)は、軸ごとの結果フラグ・派生指標を 1 つのオブジェクトに詰めて JSON 化し、この列に保存します。

共通/メタ

キー説明
statusstringsuccess / error。Organic または AI Mode が失敗したとき error
keywordstring調査対象のキーワード文字列
messagestringstatus=error のとき、軸別の失敗理由を | 区切りで連結
cost_usd / cost_jpynumberこのキーワードの合計コスト(Organic + AI Mode + LLM の合算)。JPY は app_settings.usd_jpy 換算

順位取得(fetch_rank

キー説明
organic_countintDataForSEO が返した organic 結果の件数
organic_rankings[]array取得できた組織検索結果を 全件保存(matrix・拡張レポート用途)
organic_rankings[].rank_absoluteint / nullSERP 全体での絶対順位
organic_rankings[].rank_groupint / nullDataForSEO の rank_group
organic_rankings[].positionstring / nullSERP の表示位置(left / right 等)
organic_rankings[].url / .domainstring結果 URL とドメイン
organic_rankings[].titlestring結果タイトル
organic_rankings[].descriptionstringスニペット(DB 肥大防止のため 400 文字でトリム
organic_rankings[].breadcrumbstring表示パンくず
organic_rankings[].is_featured_snippetboolフィーチャードスニペット枠か
target_rankint / nullキーワードの target_url にマッチした自社順位(圏外なら null
target_urlstring / null実際にマッチした URL(product_keywords.target_url ではなく検出結果)
rival_ranks[]array競合 URL ベースの順位検出結果。rival_brands の各エントリに対して 1 要素
rival_ranks[].name / .urlstring競合ブランド名と URL
rival_ranks[].rankint / nullマッチした順位(圏外 / URL 未設定なら null
rival_ranks[].matched_urlstring実際にマッチした URL

Google AI Overview(fetch_ai_overview

キー説明
has_ai_overviewboolAI Overview が返ってきたか
ai_overview_asyncboolDataForSEO が非同期 AIO(asynchronous_ai_overview)を返したか
ai_overview_retriedboolAIO が空だったため再試行したか(app_settings.ai_overview_retry が ON のとき)
brand_hitbool / null自社ブランドが AIO 本文に出現したか(own_brands 未設定時は null
brand_matched[]string[]実際に検出した自社ブランド名一覧
brand_metrics_ai_overviewobjectプロミネンス指標(下記スキーマ
target_refs_ai_overview[]string[]AIO の参照 URL のうち target_url にマッチしたもの
rival_refs_ai_overview[]array競合別の参照 URL マッチ:name / url / matched_urls[]

Google AI Mode(fetch_ai_mode

独立エンドポイント(serp/google/ai_mode)で取得します。

キー説明
has_ai_mode_ai_overviewboolAI Mode の AIO が返ってきたか
ai_mode_asyncbool非同期 AIO 扱いか
ai_mode_cost_usd / ai_mode_cost_jpynumberAI Mode 単独のコスト(合算は cost_usd
brand_hit_ai_modebool / null自社ブランドが AI Mode 本文に出現したか
brand_matched_ai_mode[]string[]検出した自社ブランド名一覧
brand_metrics_ai_modeobjectプロミネンス指標(下記スキーマ
target_refs_ai_mode[]string[]AI Mode の参照 URL のうち target_url にマッチしたもの
rival_refs_ai_mode[]array競合別の参照 URL マッチ:name / url / matched_urls[]

LLM Responses(fetch_llm_chatgpt / _claude / _gemini / _perplexity

下記の <axis>llm_chatgpt / llm_claude / llm_gemini / llm_perplexity のいずれか。各軸ごとに同じ形式で並びます。本文は llm_engines[].data として保存されます。

キー説明
has_<axis>bool有効な回答(sections あり)が得られたか
llm_<axis>_errorstring失敗時のエラーメッセージ(成功時はキー自体存在しない)
<axis>_cost_usd / <axis>_cost_jpynumber軸単独のコスト
<axis>_modelstring実際に呼び出されたモデル名
brand_hit_<axis>bool / null自社ブランドが回答本文に出現したか
brand_matched_<axis>[]string[]検出した自社ブランド名一覧
brand_metrics_<axis>objectプロミネンス指標(下記スキーマ
llm_engines[]array軸 1 件につき 1 要素。本文・引用 URL を含む
llm_engines[].axisstringllm_chatgpt
llm_engines[].vendorstringopenai / anthropic / google / perplexity
llm_engines[].modelstring利用モデル名
llm_engines[].data.mainobjectkeyword / datetime / model_name / input_tokens / output_tokens / web_search / money_spent
llm_engines[].data.sections[]array回答本文。type / text / annotations[]ref_index / title / url
llm_engines[].data.annotations[]array全引用 URL のフラット一覧(ref_index / title / url

brand_metrics_* 共通スキーマ(プロミネンス指標)

brand_metrics_ai_overview / brand_metrics_ai_mode / brand_metrics_<axis> はすべて同じ形です。本文が空のときも items は 0 値で埋めて返します。

キー説明
total_charsint解析対象テキストの全文字数
items[]array自社・競合のターゲット 1 件につき 1 要素
items[].namestringターゲット名(自社 or 競合)
items[].kindstringown / rival
items[].rival_indexint / null競合の場合 rival_brands 内インデックス(0..4)。自社は null
items[].countint本文中の出現回数
items[].first_offset / .last_offsetint / null最初/最後の出現文字位置
items[].position_rationumber / null0..1。本文先頭からの相対位置
items[].mentioned_charsintマッチした文字数の合計(出現分の延べ文字量)
items[].position_bandstring / null位置帯ラベル(先頭・中盤・末尾 等)
items[].mention_orderint / null言及順位(自社・競合間で何番目に登場したか)
items[].prominence_scorenumber登場回数 × 文字量 × 配置を合成した複合スコア
取得 OFF のときの保存内容 fetch_rank / fetch_ai_overview / fetch_ai_mode / fetch_llm_* が OFF のキーワードでは、対応する派生キー自体が result_json に登場しない(または初期値の false / null)になります。レポート実装時は isset()has_* フラグを必ずチェックしてください。

4. バックアップと復元

  1. 定期バックアップ手動での調査実行画面で「DB バックアップ」を実行すると、backup/app_YYYY-MM-DD_HHmmss.db として生成されます。cron で日次に cp -p も推奨。
  2. 取得すべきもの
    • data/app.db(および app.db-wal / app.db-shm
    • data/products/product_*.db(および -wal / -shm
    • data/auth_secret.bin(パスワードリセットトークン署名鍵 ※消すと既存リセットリンクが無効化)
    • カスタムロゴをアップロードしている場合 img/custom/
  3. 復元: サーバを停止 → 上記ファイルをまるごと差し戻し → サーバを起動。
容量見積りの目安 1 キーワード × 1 日 × 7 軸 で result_json は概ね 5〜30 KB 程度(LLM の回答テキストを含むため変動)。100 KW × 1 年 で 200〜700 MB を見込んでください。蓄積が進んだら 調査データ削除 で古い期間を切り詰められます。

5. マイグレーション方針

スキーマ変更は api/bootstrap.php の冒頭で CREATE TABLE IF NOT EXISTS および ALTER TABLE ... ADD COLUMNllmoAddColumnIfMissing ヘルパ)で適用しています。既存のカラムは原則削除しません。「旧 → 新」へ移行する場合は新カラムを並列で追加し、起動時のマイグレーションで値をコピーする戦略を採っています(例:products.survey_schedule_json は旧 app_settings.survey_schedule_json から一度だけシードされます)。