update backend
This commit is contained in:
@ -4,7 +4,7 @@
|
||||
// 依赖关系清晰可见,也让 repository 可以在测试中替换为独立的数据库实例。
|
||||
//
|
||||
// 数据库已迁移至 PostgreSQL(见 scripts/pgvector 的本地 Docker 环境)。
|
||||
// 表结构由 GORM AutoMigrate 在启动时幂等创建,不再依赖手写的 MySQL 迁移脚本。
|
||||
// 表结构由 GORM AutoMigrate 在启动时幂等创建,不再依赖手写的旧迁移脚本。
|
||||
package database
|
||||
|
||||
import (
|
||||
@ -52,7 +52,7 @@ func New(cfg config.DatabaseConfig) (*gorm.DB, error) {
|
||||
|
||||
// AutoMigrate 幂等创建 / 更新全部表结构。
|
||||
//
|
||||
// 表结构由 GORM 模型定义托管(替代原 scripts/sql、db/migrations 下的手写 MySQL 脚本)。
|
||||
// 表结构由 GORM 模型定义托管(替代原 scripts/sql、db/migrations 下的手写旧迁移脚本)。
|
||||
// 仅在缺少列/索引时增量变更,已存在的表不会被重建;对存量大表(如 brand_runway_images)
|
||||
// 的加列操作可能短暂加锁,属一次性开销。
|
||||
func AutoMigrate(db *gorm.DB) error {
|
||||
@ -60,6 +60,92 @@ func AutoMigrate(db *gorm.DB) error {
|
||||
if err := db.Exec("CREATE EXTENSION IF NOT EXISTS vector").Error; err != nil {
|
||||
return fmt.Errorf("create extension vector: %w", err)
|
||||
}
|
||||
// 兼容旧库:phash 早期以 bigint 存储 64-bit dHash 指纹(uint64),现模型改为 vector(64)。
|
||||
// pgvector 不支持 bigint→vector 直接转换,需逐位拆成 0/1 向量(位 i → 第 i 维,与 ToVectorBits 一致,
|
||||
// 保证旧数据与新写入的向量在同一语义空间可比)。已为 vector/text 类型的列跳过,幂等可重复执行。
|
||||
const migratePhashSQL = `
|
||||
DO $$
|
||||
DECLARE
|
||||
t text;
|
||||
dt text;
|
||||
BEGIN
|
||||
FOR t IN
|
||||
SELECT tablename FROM pg_tables
|
||||
WHERE schemaname = 'public'
|
||||
AND tablename IN ('brand_runway_images','brand_runway_draft_images','street_snap_images','street_snap_draft_images')
|
||||
LOOP
|
||||
SELECT data_type INTO dt
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = 'public' AND table_name = t AND column_name = 'phash';
|
||||
|
||||
IF dt = 'bigint' THEN
|
||||
-- bigint 即 64-bit dHash 指纹;pgvector 不支持 bigint->vector 直接转换,且 ALTER ... USING 的
|
||||
-- transform 表达式不允许子查询(0A000)。改用纯表达式:bigint::bit(64) 取 64 位模式(MSB 在左),
|
||||
-- reverse 成 LSB 在左(与 phash.ToVectorBits 维度顺序一致:第 i 维 = bit i),插逗号后 text::vector(64)。
|
||||
-- NULL 自然透传。
|
||||
EXECUTE format($e$
|
||||
ALTER TABLE %I ALTER COLUMN phash TYPE vector(64)
|
||||
USING (
|
||||
('[' ||
|
||||
rtrim(regexp_replace(reverse(phash::bit(64)::text), '(.)', '\1,', 'g'), ',') ||
|
||||
']')::vector(64)
|
||||
)
|
||||
$e$, t);
|
||||
RAISE NOTICE 'migrated phash (bigint -> vector) on %', t;
|
||||
ELSIF dt IN ('character varying','text') THEN
|
||||
EXECUTE format($e$
|
||||
ALTER TABLE %I ALTER COLUMN phash TYPE vector(64) USING phash::vector(64)
|
||||
$e$, t);
|
||||
RAISE NOTICE 'migrated phash (text -> vector) on %', t;
|
||||
END IF;
|
||||
END LOOP;
|
||||
END $$;`
|
||||
if err := db.Exec(migratePhashSQL).Error; err != nil {
|
||||
return fmt.Errorf("migrate phash column: %w", err)
|
||||
}
|
||||
// 回填历史 NULL:迁移 012 给 4 张 image 表的 is_duplicate/dup_of 声明了 not null,
|
||||
// 但存量行可能是 NULL;GORM 直接 SET NOT NULL 会失败(23502)。先按默认值回填再交给 GORM(幂等)。
|
||||
// 注意:各表 schema 演化路径不同,可能缺其中某列——故按列存在性动态拼 SQL,只回填真实存在的列;
|
||||
// 缺失的列交由随后的 db.AutoMigrate 新建(带默认值,不会触发 NULL 问题)。
|
||||
const backfillDedupSQL = `
|
||||
DO $$
|
||||
DECLARE
|
||||
t text;
|
||||
setc text := '';
|
||||
wherec text := '';
|
||||
has_dup boolean;
|
||||
has_dupof boolean;
|
||||
BEGIN
|
||||
FOR t IN
|
||||
SELECT unnest(ARRAY[
|
||||
'brand_runway_images','brand_runway_draft_images',
|
||||
'street_snap_images','street_snap_draft_images'])
|
||||
LOOP
|
||||
SELECT
|
||||
EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema='public' AND table_name=t AND column_name='is_duplicate'),
|
||||
EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema='public' AND table_name=t AND column_name='dup_of')
|
||||
INTO has_dup, has_dupof;
|
||||
|
||||
setc := '';
|
||||
wherec := '';
|
||||
IF has_dup THEN
|
||||
setc := setc || 'is_duplicate = COALESCE(is_duplicate, 0)';
|
||||
wherec := wherec || 'is_duplicate IS NULL';
|
||||
END IF;
|
||||
IF has_dupof THEN
|
||||
IF setc <> '' THEN setc := setc || ', '; wherec := wherec || ' OR '; END IF;
|
||||
setc := setc || 'dup_of = COALESCE(dup_of, '''')';
|
||||
wherec := wherec || 'dup_of IS NULL';
|
||||
END IF;
|
||||
|
||||
IF setc <> '' THEN
|
||||
EXECUTE format('UPDATE %I SET %s WHERE %s', t, setc, wherec);
|
||||
END IF;
|
||||
END LOOP;
|
||||
END $$;`
|
||||
if err := db.Exec(backfillDedupSQL).Error; err != nil {
|
||||
return fmt.Errorf("backfill dedup columns: %w", err)
|
||||
}
|
||||
return db.AutoMigrate(
|
||||
&model.Brand{},
|
||||
&model.User{},
|
||||
@ -79,37 +165,47 @@ func AutoMigrate(db *gorm.DB) error {
|
||||
)
|
||||
}
|
||||
|
||||
// ensureDedupSchema 在 GORM AutoMigrate 之外,补充去重所需的索引与语义 embedding 表。
|
||||
// 全部幂等(IF NOT EXISTS),可随服务启动重复执行。
|
||||
// EnsureDedupSchema 在 GORM AutoMigrate 之外,补充去重所需的索引与语义 embedding 表。
|
||||
// 全部幂等(IF NOT EXISTS / DROP IF EXISTS),可随服务启动重复执行。
|
||||
//
|
||||
// - 各 image 表的 content_sha1 部分唯一索引(排除迁移前遗留的空串行,避免唯一约束冲突);
|
||||
// - phash 的 HNSW 索引(bit_hamming_ops,汉明距离近邻检索,替代原先的全表扫描);
|
||||
// - phash 的 HNSW 索引(vector_l2_ops,L2²==汉明距离,替代原先的全表扫描);
|
||||
// - 幂等清理 content_sha1 精确去重废弃后遗留的旧索引(uq_*_sha1 及其 HNSW);
|
||||
// - image_embeddings 表 + cosine HNSW 索引:语义 embedding(CLIP/DINOv2)落库位,
|
||||
// 待后续接入推理填充,当前不写入。
|
||||
func ensureDedupSchema(db *gorm.DB) error {
|
||||
tables := []struct {
|
||||
name string
|
||||
idx string
|
||||
}{
|
||||
{"brand_runway_images", "uq_br_imgs_sha1"},
|
||||
{"brand_runway_draft_images", "uq_br_draft_imgs_sha1"},
|
||||
{"street_snap_images", "uq_ss_imgs_sha1"},
|
||||
{"street_snap_draft_images", "uq_ss_draft_imgs_sha1"},
|
||||
// 待后续推理接入填充,当前不写入。
|
||||
func EnsureDedupSchema(db *gorm.DB) error {
|
||||
// 统一去重方案:废弃早期独立 image_dhash 表(与 phash 直接挂各 image 表冲突)。
|
||||
// 该表从未被 Go 写入,启动即幂等清理遗留表,避免与现行方案并存造成混淆。
|
||||
if err := db.Exec("DROP TABLE IF EXISTS image_dhash CASCADE").Error; err != nil {
|
||||
return fmt.Errorf("drop legacy image_dhash: %w", err)
|
||||
}
|
||||
for _, t := range tables {
|
||||
// 精确去重:content_sha1 唯一(排除空串遗留行)。
|
||||
if err := db.Exec(fmt.Sprintf(
|
||||
"CREATE UNIQUE INDEX IF NOT EXISTS %s ON %s (content_sha1) WHERE content_sha1 <> ''",
|
||||
t.idx, t.name,
|
||||
)).Error; err != nil {
|
||||
return fmt.Errorf("create unique index %s: %w", t.idx, err)
|
||||
// content_sha1 精确去重已废弃:幂等清理其遗留的唯一索引 uq_*_sha1 与旧 HNSW 索引 uq_*_sha1_phash_hnsw。
|
||||
// 新 HNSW 索引统一改名为 <table>_phash_hnsw。
|
||||
legacyIdx := []string{
|
||||
"uq_br_imgs_sha1", "uq_br_draft_imgs_sha1",
|
||||
"uq_ss_imgs_sha1", "uq_ss_draft_imgs_sha1",
|
||||
}
|
||||
for _, idx := range legacyIdx {
|
||||
for _, suffix := range []string{"", "_phash_hnsw"} {
|
||||
if err := db.Exec(fmt.Sprintf("DROP INDEX IF EXISTS %s%s", idx, suffix)).Error; err != nil {
|
||||
return fmt.Errorf("drop legacy index %s%s: %w", idx, suffix, err)
|
||||
}
|
||||
}
|
||||
// 近重复检索:dHash 向量(vector(64) 二进制)汉明 HNSW 索引。
|
||||
}
|
||||
tables := []string{
|
||||
"brand_runway_images",
|
||||
"brand_runway_draft_images",
|
||||
"street_snap_images",
|
||||
"street_snap_draft_images",
|
||||
}
|
||||
for _, name := range tables {
|
||||
// 近重复检索:dHash 向量(vector(64) 的 0/1)汉明 HNSW 索引。
|
||||
// 用 vector_l2_ops:两 {0,1}^64 向量的 L2² 恰等于汉明距离,故 L2 距离即汉明语义;
|
||||
// 且 vector_l2_ops 在所有 pgvector 版本均可建 HNSW(bit_hamming_ops 在旧版不可用于 HNSW)。
|
||||
if err := db.Exec(fmt.Sprintf(
|
||||
"CREATE INDEX IF NOT EXISTS %s_phash_hnsw ON %s USING hnsw (phash bit_hamming_ops)",
|
||||
t.idx, t.name,
|
||||
"CREATE INDEX IF NOT EXISTS %s_phash_hnsw ON %s USING hnsw (phash vector_l2_ops)",
|
||||
name, name,
|
||||
)).Error; err != nil {
|
||||
return fmt.Errorf("create hnsw index %s: %w", t.idx, err)
|
||||
return fmt.Errorf("create hnsw index %s: %w", name, err)
|
||||
}
|
||||
}
|
||||
// 语义 embedding 表(CLIP/DINOv2 等 512 维向量):待后续推理接入填充;当前不写入。
|
||||
|
||||
Reference in New Issue
Block a user