From 00928b72e486bd56f4e2f109edf59747b2977fdb Mon Sep 17 00:00:00 2001 From: tangsongdayo Date: Sun, 19 Jul 2026 12:55:40 +0800 Subject: [PATCH] docs: benchmark search and document scaling --- CHANGELOG.md | 1 + docs/SCALING.md | 21 +++++++++++++++++++++ scripts/benchmark-search.sh | 14 ++++++++++++++ 3 files changed, 36 insertions(+) create mode 100644 docs/SCALING.md create mode 100644 scripts/benchmark-search.sh diff --git a/CHANGELOG.md b/CHANGELOG.md index 1ff39e4..45e34d7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ - Added production configuration validation for secrets, encryption keys, and the public HTTPS URL, plus an administrator-facing status check. - Added Prometheus-compatible metrics and distinct liveness/readiness health probes. - Added SQLite FTS5 indexing for public-content search, maintained automatically as posts change. +- Added a repeatable FTS search benchmark and documented PostgreSQL migration decision criteria. ### Fixed diff --git a/docs/SCALING.md b/docs/SCALING.md new file mode 100644 index 0000000..ef6941d --- /dev/null +++ b/docs/SCALING.md @@ -0,0 +1,21 @@ +# 擴展與資料庫評估 + +## 搜尋基準 + +FTS5 索引由 `posts_fts` 與 SQLite trigger 維護。可在部署前後以相同關鍵字比較查詢計畫與平均時間: + +```bash +./scripts/benchmark-search.sh "關鍵字" 100 +``` + +輸出中的 `queryPlan` 應包含 FTS 虛擬表掃描,而不是 `posts` 的全表 `LIKE` 掃描。請記錄貼文數量、硬體、SQLite 版本與平均查詢時間,作為升級決策依據。 + +## 何時由 SQLite 遷移到 PostgreSQL + +SQLite 仍適合單一主機、單一磁碟與低至中等寫入量。當出現以下任一情況時,先在 staging 驗證 PostgreSQL: + +- 需要跨多台主機同時執行 Web/Worker,或需要跨區高可用。 +- 寫入鎖定持續造成同步佇列延遲,或 WAL 檔案/備份窗口已難以控制。 +- FTS、稽核或貼文資料量使查詢基準無法滿足服務目標。 + +遷移步驟:停止寫入、使用 v0.7 以後的 JSON 匯出建立內容快照、以 migration 建立 PostgreSQL schema、匯入 users/sources/posts/互動與同步工作、在 staging 驗證計數與抽樣內容、切換唯讀短暫維護窗口、最後更新 `DATABASE_URL` 及備份/監控設定。SQLite 與 PostgreSQL 的雙寫不列為預設策略;除非有完整一致性驗證,避免長期雙寫。 diff --git a/scripts/benchmark-search.sh b/scripts/benchmark-search.sh new file mode 100644 index 0000000..827dd74 --- /dev/null +++ b/scripts/benchmark-search.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Usage: ./scripts/benchmark-search.sh "搜尋詞" [iterations] +query="${1:-test}" +iterations="${2:-100}" +[[ "$iterations" =~ ^[1-9][0-9]*$ ]] || { echo "iterations must be a positive integer" >&2; exit 2; } +docker compose exec -T -e SEARCH_QUERY="$query" -e SEARCH_ITERATIONS="$iterations" web node - <<'NODE' +const { performance } = require("node:perf_hooks"); const Database = require("better-sqlite3"); +const db = new Database(process.env.DATABASE_PATH, { readonly: true }); const query = process.env.SEARCH_QUERY.split(/\s+/).filter(Boolean).map((term) => `"${term.replaceAll('"','""')}"`).join(" AND "); const iterations = Number(process.env.SEARCH_ITERATIONS); +const plan = db.prepare("EXPLAIN QUERY PLAN SELECT p.id FROM posts p JOIN posts_fts ON posts_fts.rowid=p.id WHERE p.visibility='PUBLIC' AND p.hidden=0 AND posts_fts MATCH ? ORDER BY COALESCE(p.remote_created_at,p.created_at) DESC LIMIT 20").all(query); +const statement = db.prepare("SELECT p.id FROM posts p JOIN posts_fts ON posts_fts.rowid=p.id WHERE p.visibility='PUBLIC' AND p.hidden=0 AND posts_fts MATCH ? ORDER BY COALESCE(p.remote_created_at,p.created_at) DESC LIMIT 20"); const start = performance.now(); let resultCount = 0; for (let index = 0; index < iterations; index++) resultCount = statement.all(query).length; const elapsed = performance.now() - start; +console.log(JSON.stringify({ query: process.env.SEARCH_QUERY, iterations, resultCount, totalMs: Number(elapsed.toFixed(2)), averageMs: Number((elapsed / iterations).toFixed(3)), queryPlan: plan.map((row) => row.detail) }, null, 2)); db.close(); +NODE