update
This commit is contained in:
68
db/migrations/2026-10-12-01-drop-dedup-add-embeddings.sql
Normal file
68
db/migrations/2026-10-12-01-drop-dedup-add-embeddings.sql
Normal 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);
|
||||
21
db/migrations/2026-10-13-01-image-list-indexes.sql
Normal file
21
db/migrations/2026-10-13-01-image-list-indexes.sql
Normal 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
|
||||
31
db/migrations/2026-10-13-02-image-similar-pairs.sql
Normal file
31
db/migrations/2026-10-13-02-image-similar-pairs.sql
Normal 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
|
||||
62
db/migrations/2026-10-16-01-brands-renumber-imported-ids.sql
Normal file
62
db/migrations/2026-10-16-01-brands-renumber-imported-ids.sql
Normal 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 $$;
|
||||
43
db/migrations/2026-10-16-02-brands-renumber-repair.sql
Normal file
43
db/migrations/2026-10-16-02-brands-renumber-repair.sql
Normal 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 $$;
|
||||
@ -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` 幂等,无不可逆点。 |
|
||||
|
||||
Reference in New Issue
Block a user