トップ / データベース設計
データベース設計
このページでできること
- クヌギスキーマが使う SQLite データベース(
data/schema.db)の全テーブル構造を確認できます。 - クロール 1 回ごとに何がどのテーブルに書き込まれるかを把握できます。
- バックアップ・移行時に保全すべきファイルがわかります。
方針
- クヌギスキーマは SQLite 単一 DB(
data/schema.db)で動きます。複数 DB の分割はしていません。 - マイグレーションは
src/Database.phpのmigrate()内で毎回CREATE TABLE IF NOT EXISTSを実行するシンプルな方式です。初回起動時に自動で全テーブルが作られます。 - 手動クロール中でも他タブが結果を閲覧できるよう、WAL モード(
PRAGMA journal_mode=WAL)・synchronous=NORMAL・busy_timeout=30000・キャッシュ拡張などを起動時に設定しています。外部キー(PRAGMA foreign_keys=ON)も有効です。 - クロール結果の生データは
crawl_results.result_jsonに JSON テキストとして一括保存しています。スキーマ変更に強く、画面側で柔軟にパースできます。
バックアップ対象
data/schema.db 本体に加え、WAL モードでは data/schema.db-wal・data/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_log | cron 実行のサマリーメッセージログ | 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'))
);
emailは小文字化+前後空白除去して保存。検証はFILTER_VALIDATE_EMAIL。password_hashは PHP のpassword_hash($pw, PASSWORD_DEFAULT)(現状は bcrypt)で保存。roleはadmin(システム管理者)とproduct_manager(プロダクト管理者)の 2 種。詳細は アカウント管理。- 旧バージョンに存在した
display_name・allowed_pagesカラムは、起動時に検出されるとALTER TABLE DROP COLUMNで自動削除されます(SQLite 3.35+ の場合のみ。古いバージョンでは残ったままになりますが、アプリ側は参照しません)。
products — プロダクト
CREATE TABLE products (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
- 「プロダクト」はクロール対象を束ねる単位です。自社のコーポレートサイト、商品 LP 群、ブランドサイトなどを個別の単位として登録する想定。
- 旧バージョンの
descriptionカラムは起動時マイグレーションで削除されます(SQLite 3.35+)。 - プロダクトを削除すると
ON DELETE CASCADEにより、配下の URL・クロール履歴・期待値・検出設定・cron スケジュールなどがすべて削除されます。
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
);
- 将来的に「プロダクト管理者をプロダクト単位で割り当てる」拡張を見据えたテーブルです。現バージョンでは未使用ですが、起動時に作成されます。
- 現状のアクセス制御はページ単位(
Auth::PRODUCT_MANAGER_PAGESの固定 5 ページ)で行っています。詳細は アカウント管理。
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
);
- 1 プロダクト内で同一 URL の重複登録を禁止します(
UNIQUE (product_id, url))。複数行を一括投入したとき、既存と重複した URL はINSERT OR IGNOREでスキップされます。 - 旧
noteカラムはマイグレーションで削除されます。一覧表示の HTTP / 最終クロールはcrawl_resultsからの集計です。
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
);
- プロダクト 1 件につき 1 行。WebSite 期待値とパンくずセレクターを同居させています(メタ情報の事前登録)。
- 空文字のフィールドは NULL として保存し、照合時は「未設定=スキップ」を表現します。
same_as_json/breadcrumb_selectors_jsonは JSON 配列文字列です。
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
);
- チェックを外した構造化マークアップ種別(代表 @type 名)のリストを JSON で保存します(構造化マークアップ検出設定)。
- 行が無いプロダクトは「すべて検出 ON」と同義です。
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
);
- 1 回のクロール実行 = 1 行。手動実行画面 / cron 自動実行のどちらでも作成されます。
triggerでどちらから起動されたかが分かります。 started_atは INSERT 時のdatetime('now')、finished_atは全 URL 処理完了時にUPDATEされます。- ダッシュボードの「直近のクロール実行」テーブルは、このテーブルの新しい 5 件を表示しています。
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 とヘッドラインが一致していません。" }
]
}
entities: 検出された全エンティティ。sourceはJSON-LD/Microdata/RDFaのいずれか。同じソース内で複数ある場合はindexで識別します。structured_data: 形式別の生データ(生 HTML をrawに保持)。結果詳細 画面で「クリックで展開」表示するのに使います。suggestions: 「ページの中身的に追加した方が良さそうなスキーマ」の提案。statusはimplemented/missing/warningなど。consistency_checks: 「マークアップで宣言した値」と「ページの実コンテンツ」が一致しているかの判定行リスト。詳細は マークアップ × ページ内容の一致確認。
エラー時の挙動
- HTTP 4xx / 5xx の場合も例外にはせず、
is_error=1として 1 行保存します。error_messageに「HTTP 404 Not Found」などの文字列が入ります。 - cURL レベルで失敗した(接続不可・タイムアウトなど)場合も同様に
is_error=1で保存され、http_statusは0またはNULLになります。 - エラー行は エラーURL管理 画面で一覧表示・削除できます。
created_at の保存タイムゾーン(重要)
crawl_results.created_atは SQLite のdatetime('now')で保存されるため、UTC(協定世界時)です。- 「当日まだクロールしていない URL を抽出する」処理(
Repository::listUrlsNotCrawledOnDate())では、date(created_at, '+9 hours')で JST(日本時間)日付に補正してから比較しています。これにより、サーバの OS タイムゾーン設定に依存せず、日本のカレンダー基準で日次サイクルが回ります。 - 同様に
Repository::countUrlsCrawledOnDate()も+9 hours補正を行います。手動でクロール実行画面の「本日クロール済み件数」表示も内部的にこの関数を使っています。
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
);
- 1 プロダクト = 1 行です(
product_idが主キー)。スケジュール未設定のプロダクトは行が存在せず、cron からはスキップされます。 days_jsonには 1〜31 の整数配列を JSON で保存します。今日の日付(date('j'))がこの配列に含まれているかで実行可否を判定。last_triggered_dateは「最後に cron がクロールを動かした日」の記録として更新されます。新仕様(1 時間に 1 回 cron を回す)では当日内の二重起動を許可するため、この列で重複ブロックはしません。重複防止は「URL 単位でcrawl_resultsを見て」行います(Repository::listUrlsNotCrawledOnDate())。
cron_log — Cron 実行サマリーログ
CREATE TABLE cron_log (
id INTEGER PRIMARY KEY AUTOINCREMENT,
ran_at TEXT NOT NULL DEFAULT (datetime('now')),
message TEXT
);
- cron 1 起動につき複数行が追加されます(プロダクトごとに 1 行+全体サマリー)。
- 詳細なクロール状況はあくまで
crawl_runs/crawl_results側に残ります。cron_logは「いつ起動されて何プロダクト処理したか」の最小限の足跡です。
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);
- 「パスワードをお忘れの方はこちら」から発行されるワンタイムリンクのトークンを、ハッシュ化して保存します(生のトークンは DB に残しません)。
expires_atを過ぎたトークンは無効。使用済みのものはused_atがセットされ、再利用不可になります。- 詳細フローは ログイン/パスワード再発行。
app_settings — グローバル設定 key/value
CREATE TABLE app_settings (
key TEXT PRIMARY KEY,
value TEXT
);
- システム全体に効くグローバル設定の汎用キーバリューです。読み書きは
Repository::getAppSetting()/setAppSetting()。 - 現バージョンで利用しているキーは下記のとおりです。
key 意味 既定値 cron_max_urls_per_runcron 1 回あたりに処理する URL の最大数。Cron スケジュール 画面から編集できます。0 で無制限。 500 - 新しいグローバル設定を増やしたい場合は、ここに行を足す代わりに
getAppSetting()/setAppSetting()で薄いラッパ関数を作る運用が想定されています(例:getCronMaxUrlsPerRun())。
クロール 1 回で書き込まれるテーブル
手動でも cron でも、クロール 1 回が走ると以下のテーブルへの書き込みが連動して発生します。新仕様の cron は 1 時間に 1 回起動するため、1 日に同じテーブルへ何度も書き込みが入る前提です。
crawl_runsに Run を 1 件 INSERT(triggerがmanualかcronか)。- 1 URL ごとに
crawl_resultsへ 1 行 INSERT(成功でもエラーでも必ず残る)。同一 URL を当日中に重複クロールしないのは、cron / 手動「未クロールのみ」モードがlistUrlsNotCrawledOnDate()で事前にフィルタしているためです(テーブル側に UNIQUE 制約は無し)。 - 全件処理が終わったら
crawl_runs.finished_at+ 件数を UPDATE。 - 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 に収まるのが一般的です。