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

データベース設計

このページでできること

  • クヌギスキーマが使う SQLite データベース(data/schema.db)の全テーブル構造を確認できます。
  • クロール 1 回ごとに何がどのテーブルに書き込まれるかを把握できます。
  • バックアップ・移行時に保全すべきファイルがわかります。

方針

バックアップ対象 data/schema.db 本体に加え、WAL モードでは data/schema.db-waldata/schema.db-shm も同時に必要です。バックアップ時はこの 3 ファイルをセットでコピーするか、SQLite の .backup コマンドを使ってください。

テーブル一覧

テーブル名役割主なキー
usersログインユーザー(メールアドレス+パスワードハッシュ+ロール)id / UNIQUE email
productsクロール対象を束ねる「プロダクト」(サイト / サービス単位)id
user_productsユーザー × プロダクトの紐付け(将来拡張用・現状は未使用でも作成される)複合主キー (user_id, product_id)
product_urlsプロダクト配下の事前登録 URL(クロール対象)id / UNIQUE (product_id, url)
crawl_runsクロール 1 回分の実行ログ(開始 / 終了 / 件数 / トリガー)id
crawl_resultsクロール結果 1 URL 分の詳細(HTML メタ情報 + 解析 JSON)id / INDEX (product_id, created_at)
cron_scheduleプロダクトごとの cron 実行日(毎月 1〜31 から複数選択)product_id(PK)
cron_logcron 実行のサマリーメッセージログid
password_reset_tokensパスワード再発行用のワンタイムトークン(ハッシュ保存)id / UNIQUE token_hash
product_website_expectationsプロダクト単位の WebSite 期待値・パンくずセレクターproduct_id(PK)
product_schema_detection_settings提案・照合から除外する構造化マークアップ種別product_id(PK)
app_settingsシステム全体の key-value(cron 上限など)key(PK)

クロール対象を絞り込む主要ヘルパー(Repository)

各画面(手動でクロール実行・cron・ダッシュボード)はテーブルを直接 SQL で触らず、src/Repository.php のヘルパーメソッドを通じて取得します。新仕様で重要なものは下記です。

メソッド戻り値用途
listUrls($pid) 事前登録 URL の全行 手動「全 URL を再クロール」・URL管理画面の一覧表示
listUrlsNotCrawledOnDate($pid, $jstDate) 「指定 JST 日付にまだクロール結果が無い URL」を id ASC 順で cron の毎時消化+手動「本日未クロールのみクロール」
countUrls($pid) 事前登録 URL の総数 手動実行画面・ダッシュボードの件数表示
countUrlsCrawledOnDate($pid, $jstDate) 「指定 JST 日付に既にクロール済みの URL 件数」 手動実行画面の「本日クロール済み」表示
allSchedulesDueToday() 本日が実行日かつ enabled=1 のプロダクト一覧(product_id ASC cron の対象プロダクト選定
getCronMaxUrlsPerRun() / setCronMaxUrlsPerRun($n) cron 1 回あたりの上限件数(既定 500、0 で無制限) Cron スケジュール 画面のグローバル設定カード
listUrlsByIds($pid, $ids) 指定 ID の事前登録 URL のみ 手動クロール mode=selected
deleteUrls($pid, $ids) 複数 URL の一括削除件数 URLの追加・修正・削除 の一括削除
getRunWithStats($runId) Run の進捗(done / is_finished / last_result_at 等) pages/run_status.php の JSON 応答
getWebsiteExpectation() / saveWebsiteExpectation() WebSite 期待値の取得・UPSERT メタ情報の事前登録
getBreadcrumbSelectors() / saveBreadcrumbSelectors() パンくず用 CSS セレクター配列 同上・Crawler の解析オプション
getDisabledDetectionTypes() / saveDisabledDetectionTypes() 検出オフ種別リスト 構造化マークアップ検出設定
getAppSetting($key, $default) / setAppSetting($key, $value) app_settings テーブルの key/value 汎用アクセサ グローバル設定の薄いラッパを作るときの土台

users — ログインユーザー

CREATE TABLE users (
  id            INTEGER PRIMARY KEY AUTOINCREMENT,
  email         TEXT NOT NULL UNIQUE,
  password_hash TEXT NOT NULL,
  role          TEXT NOT NULL DEFAULT 'admin',   -- 'admin' or 'product_manager'
  created_at    TEXT NOT NULL DEFAULT (datetime('now'))
);

products — プロダクト

CREATE TABLE products (
  id         INTEGER PRIMARY KEY AUTOINCREMENT,
  name       TEXT NOT NULL,
  created_at TEXT NOT NULL DEFAULT (datetime('now'))
);

user_products — ユーザー × プロダクト

CREATE TABLE user_products (
  user_id    INTEGER NOT NULL,
  product_id INTEGER NOT NULL,
  PRIMARY KEY (user_id, product_id),
  FOREIGN KEY (user_id)    REFERENCES users(id)    ON DELETE CASCADE,
  FOREIGN KEY (product_id) REFERENCES products(id) ON DELETE CASCADE
);

product_urls — 事前登録 URL

CREATE TABLE product_urls (
  id         INTEGER PRIMARY KEY AUTOINCREMENT,
  product_id INTEGER NOT NULL,
  url        TEXT NOT NULL,
  created_at TEXT NOT NULL DEFAULT (datetime('now')),
  UNIQUE (product_id, url),
  FOREIGN KEY (product_id) REFERENCES products(id) ON DELETE CASCADE
);

product_website_expectations — メタ情報の事前登録

CREATE TABLE product_website_expectations (
  product_id                  INTEGER PRIMARY KEY,
  name                        TEXT,
  url                         TEXT,
  alternate_name              TEXT,
  description                 TEXT,
  in_language                 TEXT,
  publisher_name              TEXT,
  publisher_url               TEXT,
  search_action_target        TEXT,
  search_action_query_input   TEXT,
  same_as_json                TEXT NOT NULL DEFAULT '[]',
  breadcrumb_selectors_json   TEXT NOT NULL DEFAULT '[]',
  updated_at                  TEXT,
  FOREIGN KEY (product_id) REFERENCES products(id) ON DELETE CASCADE
);

product_schema_detection_settings — 検出設定

CREATE TABLE product_schema_detection_settings (
  product_id           INTEGER PRIMARY KEY,
  disabled_types_json  TEXT NOT NULL DEFAULT '[]',
  updated_at           TEXT,
  FOREIGN KEY (product_id) REFERENCES products(id) ON DELETE CASCADE
);

crawl_runs — クロール実行ログ(Run 単位)

CREATE TABLE crawl_runs (
  id            INTEGER PRIMARY KEY AUTOINCREMENT,
  product_id    INTEGER NOT NULL,
  trigger       TEXT    NOT NULL DEFAULT 'manual',  -- 'manual' or 'cron'
  started_at    TEXT    NOT NULL DEFAULT (datetime('now')),
  finished_at   TEXT,
  total_urls    INTEGER NOT NULL DEFAULT 0,
  success_count INTEGER NOT NULL DEFAULT 0,
  error_count   INTEGER NOT NULL DEFAULT 0,
  delay_seconds INTEGER NOT NULL DEFAULT 0,
  FOREIGN KEY (product_id) REFERENCES products(id) ON DELETE CASCADE
);

crawl_results — クロール結果(1 URL 単位)

CREATE TABLE crawl_results (
  id               INTEGER PRIMARY KEY AUTOINCREMENT,
  run_id           INTEGER NOT NULL,
  product_id       INTEGER NOT NULL,
  product_url_id   INTEGER,
  url              TEXT    NOT NULL,
  final_url        TEXT,
  http_status      INTEGER,
  content_type     TEXT,
  detected_charset TEXT,
  title            TEXT,
  meta_description TEXT,
  canonical_url    TEXT,
  primary_heading  TEXT,        -- h1(最初の 1 件)
  result_json      TEXT,        -- 解析結果一式(JSON テキスト)
  error_message    TEXT,
  is_error         INTEGER NOT NULL DEFAULT 0,    -- 0/1
  created_at       TEXT NOT NULL DEFAULT (datetime('now')),
  FOREIGN KEY (run_id)     REFERENCES crawl_runs(id) ON DELETE CASCADE,
  FOREIGN KEY (product_id) REFERENCES products(id)   ON DELETE CASCADE
);
CREATE INDEX idx_crawl_results_product ON crawl_results(product_id, created_at);
CREATE INDEX idx_crawl_results_iserror ON crawl_results(product_id, is_error);

result_json の中身

解析結果は 1 つの JSON オブジェクトにまとめて保存しています。画面側はこれを json_decode して必要なキーだけ取り出します。

{
  "entities": [
    { "source": "JSON-LD", "index": 1, "type": "Article",
      "properties": { "headline": "...", "author": "...", ... },
      "raw": "<script type=\"application/ld+json\">...</script>" }
  ],
  "structured_data": {
    "json_ld":    [ { "index": 1, "raw": "...", "data": { ... } } ],
    "microdata":  [ ... ],
    "rdfa":       [ ... ]
  },
  "suggestions": [
    { "type": "FAQPage", "status": "missing",
      "reason": "ページ内に FAQ ライクな見出し-本文ペアを検出しました。",
      "recommended_properties": ["mainEntity", "name", "acceptedAnswer"] }
  ],
  "consistency_checks": [
    { "status": "mismatch", "source": "JSON-LD", "type": "Article",
      "field": "headline", "schema_value": "...", "page_value": "...",
      "message": "title / h1 とヘッドラインが一致していません。" }
  ]
}

エラー時の挙動

created_at の保存タイムゾーン(重要)

cron_schedule — Cron 実行日設定

CREATE TABLE cron_schedule (
  product_id           INTEGER PRIMARY KEY,
  days_json            TEXT    NOT NULL DEFAULT '[]',   -- 例: '[1,15]'
  crawl_delay_seconds  INTEGER NOT NULL DEFAULT 5,
  enabled              INTEGER NOT NULL DEFAULT 1,
  last_triggered_date  TEXT,                            -- 'YYYY-MM-DD'
  updated_at           TEXT,
  FOREIGN KEY (product_id) REFERENCES products(id) ON DELETE CASCADE
);

cron_log — Cron 実行サマリーログ

CREATE TABLE cron_log (
  id      INTEGER PRIMARY KEY AUTOINCREMENT,
  ran_at  TEXT NOT NULL DEFAULT (datetime('now')),
  message TEXT
);

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

CREATE TABLE password_reset_tokens (
  id         INTEGER PRIMARY KEY AUTOINCREMENT,
  user_id    INTEGER NOT NULL,
  token_hash TEXT    NOT NULL UNIQUE,
  expires_at TEXT    NOT NULL,
  used_at    TEXT,
  FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
CREATE INDEX idx_prt_user    ON password_reset_tokens(user_id);
CREATE INDEX idx_prt_expires ON password_reset_tokens(expires_at);

app_settings — グローバル設定 key/value

CREATE TABLE app_settings (
  key   TEXT PRIMARY KEY,
  value TEXT
);

クロール 1 回で書き込まれるテーブル

手動でも cron でも、クロール 1 回が走ると以下のテーブルへの書き込みが連動して発生します。新仕様の cron は 1 時間に 1 回起動するため、1 日に同じテーブルへ何度も書き込みが入る前提です。

  1. crawl_runs に Run を 1 件 INSERT(triggermanualcron か)。
  2. 1 URL ごとに crawl_results へ 1 行 INSERT(成功でもエラーでも必ず残る)。同一 URL を当日中に重複クロールしないのは、cron / 手動「未クロールのみ」モードが listUrlsNotCrawledOnDate() で事前にフィルタしているためです(テーブル側に UNIQUE 制約は無し)。
  3. 全件処理が終わったら crawl_runs.finished_at + 件数を UPDATE。
  4. cron 経由の場合のみ、cron_schedule.last_triggered_date を本日に UPDATE + cron_log へ 1 行 INSERT。0 件スキップ時も last_triggered_date は更新されます。
容量の目安 crawl_results.result_json は 1 URL あたり数 KB〜数十 KB程度になります(JSON-LD が大きい大型ページでは 100KB を超えることもあります)。50 URL × 月 2 回 × 12 ヶ月 = 約 1,200 行で、DB ファイルは数十 MB に収まるのが一般的です。