This commit is contained in:
toom1996
2026-10-18 17:42:10 +08:00
parent 1b06bd5bda
commit 1de1ec46a6
58 changed files with 5281 additions and 19191 deletions

View File

@ -0,0 +1,68 @@
-- 2026-10-12-01 下线 pHash 去重 + 启用 image_embeddings 存 FashionSigLIP 向量
--
-- 对应规格:docs/superpowers/specs/2026-10-12-fashionclip-siglip-vision-service-design.md §5.2 / §8
--
-- 不可逆:DROP COLUMN / DROP TABLE 无回滚手段,执行前务必确认 db/backups/ 有备份。
--
-- 应用后请重新导出结构:
-- ./bin/dbtool dump -clean -out db/backups/db_dump.sql
-- ---------------------------------------------------------------------------
-- ① 下线自动去重:DROP 三列 + 两个 HNSW 索引
--
-- 注意:两个图片视图为 `SELECT i.*`,因此**必须先删视图**,删列后再按 `i.*` 重建——
-- 否则 DROP COLUMN 会报 "cannot drop column ... because other objects depend on it"。
-- 重建后 `i.*` 展开的列集自动不含已删的三列,无需显式枚举。
-- (与 db/migrations/2026-09-24-01-runway-parent-image-id.sql 处理 look_index 的做法一致。)
-- ---------------------------------------------------------------------------
DROP VIEW IF EXISTS public_brand_runway_images;
DROP VIEW IF EXISTS public_street_snap_images;
DROP INDEX IF EXISTS brand_runway_images_phash_hnsw;
DROP INDEX IF EXISTS street_snap_images_phash_hnsw;
ALTER TABLE brand_runway_images
DROP COLUMN IF EXISTS phash,
DROP COLUMN IF EXISTS is_duplicate,
DROP COLUMN IF EXISTS dup_of;
ALTER TABLE street_snap_images
DROP COLUMN IF EXISTS phash,
DROP COLUMN IF EXISTS is_duplicate,
DROP COLUMN IF EXISTS dup_of;
CREATE VIEW public_brand_runway_images AS
SELECT i.* FROM brand_runway_images i
JOIN brand_runways r ON r.id = i.runway_id
WHERE i.is_deleted = 0 AND r.status = 'published' AND r.is_deleted = 0;
CREATE VIEW public_street_snap_images AS
SELECT i.* FROM street_snap_images i
JOIN street_snaps s ON s.id = i.snap_id
WHERE i.is_deleted = 0 AND s.status = 'published' AND s.is_deleted = 0;
-- ---------------------------------------------------------------------------
-- ② 删死表 image_duplicates(2026-09-21 建表后全仓零引用)
-- ---------------------------------------------------------------------------
DROP TABLE IF EXISTS image_duplicates;
-- ---------------------------------------------------------------------------
-- ③ image_embeddings 改造为可用状态
-- 该表 2026-09-21 建表后一直零引用、零数据,此处原地改造:
-- - kind: smallint -> varchar(8),对齐 dto.IngestKind("runway" / "street")
-- - embedding: vector(512) -> vector(768),M0 实测模型输出 768 维
-- - 补 (kind, image_id) 唯一索引:原表只有自增主键,
-- 不加唯一约束会导致同一张图重复写入多行
-- ---------------------------------------------------------------------------
ALTER TABLE image_embeddings
ALTER COLUMN kind TYPE varchar(8) USING kind::varchar(8),
ALTER COLUMN embedding TYPE vector(768);
-- 旧向量维度与新维度不同,重建该列上的 HNSW 索引(pgvector 索引绑定列的 typmod)
DROP INDEX IF EXISTS image_embeddings_vec_idx;
CREATE UNIQUE INDEX IF NOT EXISTS uq_image_embeddings_kind_image
ON image_embeddings (kind, image_id);
CREATE INDEX IF NOT EXISTS image_embeddings_vec_idx
ON image_embeddings USING hnsw (embedding vector_cosine_ops);

View File

@ -0,0 +1,21 @@
-- 为「按所属图集列出图片」补索引。
--
-- 背景:`brand_runway_images` / `street_snap_images` 原本只有主键 (id),
-- 而列表与编辑页的取图查询是:
-- SELECT * FROM <图片表> WHERE <fk> = ? AND is_deleted = 0 ORDER BY sort_order
-- EXPLAIN 显示为 Seq Scan + Sort(含 Index = false):图片表已有上万行,
-- 每打开一次图集页都要全表扫一遍再排序。
--
-- 索引列顺序 (外键, is_deleted, sort_order) 同时满足等值过滤与排序,
-- 使该查询可以走 Index Scan 并直接按序返回,省掉 Sort。
--
-- 幂等:IF NOT EXISTS。可重复执行。
-- DROP 无回滚需求:索引可随时重建。
CREATE INDEX IF NOT EXISTS idx_br_images_runway
ON public.brand_runway_images (runway_id, is_deleted, sort_order);
CREATE INDEX IF NOT EXISTS idx_ss_images_snap
ON public.street_snap_images (snap_id, is_deleted, sort_order);
-- 应用后请重新导出结构:./bin/dbtool dump -clean -out db/backups/db_dump.sql

View File

@ -0,0 +1,31 @@
-- 「疑似同款」结果表:把跨图集近邻对在**入库时**算好落库,后台页面只读结果。
--
-- 背景(2026-10-13 实测):原实现在**打开图集页时**对每张图实时做一次向量近邻检索 ——
-- 一个 116 张图的图集要发 116 次查询、每次约 20KB SQL(768 维向量文本在 SELECT 与
-- ORDER BY 各出现一次)。本机 PostgreSQL 跑在 WSL2 里、Windows 经 wslrelay 访问,
-- 该链路大 payload 吞吐实测约 0.47MB/s:服务端执行单条只要 0.45ms,客户端往返却要 45ms,
-- 合计 116×45ms ≈ 5 秒。改为「入库时(或 dbtool similarbackfill 回填时)算一次存表」后,
-- 页面退化为一次几百字节的小查询,瓶颈消失。
--
-- 设计要点:
-- 1. **双向存两行**(A→B 与 B→A):这样打开任一张图所在的图集都能直接看到对方,读侧无需 UNION。
-- 2. **只存 id 与相似度**,不存对方图片的描述:描述靠读侧 join 图片表/图集表取得,
-- 图片被改(改名、换图集)时不会留下陈旧文案。
-- 3. **不存「重复」语义**:与规格 §9 一致 —— 只提示、不隐藏、不计入重复率、不参与任何去重。
-- 4. 图片被软删后这里的行可能残留,读侧 join 时按 is_deleted 过滤即可,无需级联清理。
--
-- 幂等:IF NOT EXISTS。无不可逆点(纯新增表,可 DROP 重建)。
CREATE TABLE IF NOT EXISTS public.image_similar_pairs (
kind varchar(8) NOT NULL, -- 图片类型:runway | street(一对近邻同属一种)
self_id integer NOT NULL, -- 本图
other_id integer NOT NULL, -- 近邻图
sim real NOT NULL, -- 余弦相似度 0~1
created_at integer NOT NULL DEFAULT 0,
PRIMARY KEY (kind, self_id, other_id)
);
-- 主键 (kind, self_id, other_id) 已完整覆盖读法「按 kind+self_id 取某张图的所有近邻」,
-- 无需额外索引。
-- 应用后请重新导出结构:./bin/dbtool dump -clean -out db/backups/db_dump.sql

View File

@ -0,0 +1,62 @@
-- 2026-10-16-01-brands-renumber-imported-ids.sql
--
-- 背景:2026-10-16 从 app_brands(MySQL dump)导入的 23218 个品牌带的是**源系统的显式 id**
-- (3097..26472)。而 brands.id 本身是 IDENTITY 列(GENERATED ... AS IDENTITY,序列
-- brands_id_seq),导入后序列仍停在 30 —— 新增品牌会从 31 开始编号,一旦涨到 3097
-- 就会与这批导入行**撞主键**。本迁移把那批行重排进真正的自增空间,并把序列对齐过去。
--
-- 安全性(迁移前已核对):全库没有任何行引用这些 id ——
-- * brand_runways.brand_id 最大只有 7(115 场走秀全部落在 19 个种子品牌内);
-- * favorites / histories 存的是内容(走秀/街拍)快照,不存品牌 id。
-- 19 个种子品牌(id ≤ 19)完全不动。
--
-- 三步:① 整批挪到负区间(避开主键中途相撞)② 按原 id 升序落回 20 起的连续区间
-- ③ 把 IDENTITY 序列对齐到新的最大值(下一次 nextval 即 max+1)。
--
-- ⚠️ 第 ② 步的 JOIN 必须用「当前的负数 id 本身」去匹配,**不能**拿子查询里的 -id(正数)
-- 去比 b.id(负数)—— 那样一行都匹配不上,会留下满表负数 id 的半途状态(本文件初版就踩了)。
--
-- 幂等:仅当「种子(≤19) 与导入批次(≥3097) 之间存在空洞(20..3096 全空)」时才重排;
-- 重排后 20..3096 已被占满,手动重跑会直接跳过。dbtool migrate 另按 schema_migrations 记账。
DO $$
DECLARE
v_before_max integer;
v_moved integer;
v_after_max integer;
BEGIN
IF EXISTS (SELECT 1 FROM brands WHERE id BETWEEN 20 AND 3096) THEN
RAISE NOTICE '[brands-renumber] 20..3096 非空,说明已重排过,跳过';
RETURN;
END IF;
-- 待处理的行:原始状态的 id >= 3097,或上一次中断留下的负数行。
IF NOT EXISTS (SELECT 1 FROM brands WHERE id < 0 OR id >= 3097) THEN
RAISE NOTICE '[brands-renumber] 没有待重排的行,跳过';
RETURN;
END IF;
SELECT max(id) INTO v_before_max FROM brands;
-- 1) 原始状态:整批挪到负区间(若已是负数则天然不匹配,空跑)。
UPDATE brands SET id = -id WHERE id >= 3097;
GET DIAGNOSTICS v_moved = ROW_COUNT;
-- 2) 按原 id 升序落回「紧接 19 个种子之后」的连续区间(排序固定,结果可复现)。
UPDATE brands b
SET id = m.new_id
FROM (
SELECT id AS neg_id,
(row_number() OVER (ORDER BY -id))::integer + 19 AS new_id
FROM brands
WHERE id < 0
) m
WHERE b.id = m.neg_id;
-- 3) 把 IDENTITY 序列对齐到新的最大值。
SELECT max(id) INTO v_after_max FROM brands;
PERFORM setval(pg_get_serial_sequence('brands', 'id'), v_after_max);
RAISE NOTICE '[brands-renumber] 完成:负区间挪动 % 行,id 最大值 % -> %,序列下次给出 %',
v_moved, v_before_max, v_after_max, v_after_max + 1;
END $$;

View File

@ -0,0 +1,43 @@
-- 2026-10-16-02-brands-renumber-repair.sql
--
-- 修复 2026-10-16-01 初版留下的半途状态:初版第 ② 步的 JOIN 条件写错(拿子查询里的 -id
-- 去比 b.id),一行都没匹配上,于是只完成了「整批挪到负区间」,紧接着第 ③ 步按当时的最大正值
-- (19)设了序列。结果:23218 个品牌的 id 变成负数、序列指向 20。
--
-- 本迁移把负数行按原 id 升序落回 20.. 的连续区间,并重新对齐序列。
-- 若 01 已正确完成(不存在负数行),本迁移直接跳过 —— 因此对全新建库也是无副作用的。
--
-- 安全性同上:没有任何行引用这批 id(brand_runways.brand_id 最大 7)。19 个种子不动。
DO $$
DECLARE
v_moved integer;
v_after integer;
BEGIN
IF NOT EXISTS (SELECT 1 FROM brands WHERE id < 0) THEN
RAISE NOTICE '[brands-renumber-repair] 不存在负数行,无需修复,跳过';
RETURN;
END IF;
-- 目标区间若已被占用,继续重排会撞主键:宁可中止也不要写出坏数据。
IF EXISTS (SELECT 1 FROM brands WHERE id BETWEEN 20 AND 3096) THEN
RAISE EXCEPTION '[brands-renumber-repair] 20..3096 已被占用,重排会撞主键,已中止';
END IF;
UPDATE brands b
SET id = m.new_id
FROM (
SELECT id AS neg_id,
(row_number() OVER (ORDER BY -id))::integer + 19 AS new_id
FROM brands
WHERE id < 0
) m
WHERE b.id = m.neg_id;
GET DIAGNOSTICS v_moved = ROW_COUNT;
SELECT max(id) INTO v_after FROM brands;
PERFORM setval(pg_get_serial_sequence('brands', 'id'), v_after);
RAISE NOTICE '[brands-renumber-repair] 完成:% 行落回连续区间,id 最大值 %,序列下次给出 %',
v_moved, v_after, v_after + 1;
END $$;

View File

@ -53,3 +53,22 @@ git show c4bafc5:scripts/migrate_single_table/main.go
- 若库里已有同名但口径更窄的旧 `uq_ss_entity (city, year)`,03 会静默跳过、留下错误结构;
这时必须跑 04 纠正(04 的 `DROP` 正是为此)。
- 反过来,若只跑 03 而库里已存在正确的同名索引,跳过是无害的。
---
## 三、2026-10-12 系列(下线 pHash 去重 + 语义向量)
| 顺序 | 文件 | 前提 / 幂等 / 不可逆点 |
| --- | --- | --- |
| 1 | `2026-10-12-01-drop-dedup-add-embeddings.sql` | 下线 pHash 去重(DROP `phash`/`is_duplicate`/`dup_of` + 两个 HNSW 索引 + 死表 `image_duplicates`);`image_embeddings` 的 `kind` 改 `varchar(8)`、`embedding` 改 `vector(768)`,补唯一索引并重建 HNSW。**不可逆**:DROP COLUMN / DROP TABLE 无回滚手段,执行前务必确认 `db/backups/` 有可用备份。`image_embeddings` 零数据,列类型变更无需 `USING`。 |
> 排序提示:本步必须排在「二、2026-09-22」系列**之后**执行——它 DROP 的列由那一系列的代码写入,先删列会让中间态代码报错。
---
## 四、2026-10-13 系列(性能)
| 顺序 | 文件 | 前提 / 幂等 / 不可逆点 |
| --- | --- | --- |
| 1 | `2026-10-13-01-image-list-indexes.sql` | 给 `brand_runway_images (runway_id, is_deleted, sort_order)` 与 `street_snap_images (snap_id, is_deleted, sort_order)` 补索引。原先两张图片表**只有主键 (id)**,「按图集列图片」的查询是 Seq Scan + Sort,图片表上万行时图集页会变慢。`IF NOT EXISTS` 幂等,可重复执行,无不可逆点。 |
| 2 | `2026-10-13-02-image-similar-pairs.sql` | 新建 `image_similar_pairs`(「疑似同款」预计算结果表)。原实现是在**打开图集页时**对每张图实时做向量近邻检索,116 张图的图集要发 116 次查询、每次约 20KB SQL;本机 PostgreSQL 跑在 WSL2 里、Windows 经 wslrelay 访问,该链路大 payload 吞吐约 0.47MB/s,合计要 5 秒。改为入库时(或 `dbtool similarbackfill`)算一次存表后,页面退化为一次小查询。纯新增表,`IF NOT EXISTS` 幂等,无不可逆点。 |