update
This commit is contained in:
105
.workbuddy/memory/2026-09-17.md
Normal file
105
.workbuddy/memory/2026-09-17.md
Normal file
@ -0,0 +1,105 @@
|
||||
# 2026-09-17 工作日志
|
||||
|
||||
## 修复详情页空壳 + 评估 components/views 目录 ✅(15:12–15:30)
|
||||
用户问 `components/views/` 是否还有必要(pages 不就是 view 吗),并指出 detail 组件是空的,疑似之前拆文件拆错。
|
||||
|
||||
### 根因(已查清)
|
||||
- 详情实现原本在 `src/components/views/ItemPage.astro`(一个组件用 `type` 参数通吃 runway/street,含收藏/历史脚本)。
|
||||
- commit `70a3304`(拆分)把 4 个 `[id].astro` 的 import 从 `ItemPage`(带 `type`)改成 `RunwayLooksDetail`/`StreetSnapsDetail`,但**这两个新文件建成空的(0 字节)**,且把 `ItemPage.astro` **删除了**。
|
||||
- 后果:走秀/街拍详情页(`prerender=false` 的动态路由)此后一直渲染空壳,无报错不易察觉。
|
||||
- 验证手段:`git log --all -- '*ItemPage*'` 定位 → `git show ad028c9:...ItemPage.astro` 找回 158 行完整实现;`70a3304` 该路径已不存在(在另一分支)。
|
||||
|
||||
### 修复(恢复原始可用结构)
|
||||
- 从 `ad028c9` 恢复 `src/components/views/ItemPage.astro`(内容逐字还原,含 Props 接口、SSR 取数、TDK、收藏/历史 `<script>`)。
|
||||
- 4 个 `[id].astro`(en/cn × runway-looks/street-snaps)改回 `import ItemPage` 并显式传 `type="runway"|"street"`。
|
||||
- 删除两个空文件 `RunwayLooksDetail.astro`、`StreetSnapsDetail.astro`。
|
||||
- 验证:`npm run build` Complete;`npx astro check` 0/0/0;残留搜索 `RunwayLooksDetail|StreetSnapsDetail` = 0 命中;`ItemPage` 仅出现在 4 个页面 + 自身。
|
||||
|
||||
### 关于 components/views 是否有必要(结论)
|
||||
**有必要保留,不要并入 pages。**
|
||||
- 本项目 `prefixDefaultLocale: true` → `src/pages/{en,cn}/...` 是**两套** locale 页面树,每个页面文件都是薄壳(仅 `export const prerender` + `import` 共享视图 + 渲染)。
|
||||
- `components/views/*` 是 en/cn 两棵页面树**共用的页面体实现**(语言靠 `Astro.currentLocale` 运行时区分)。若把视图内容并进 pages,就得在 en/ 和 cn/ 各抄一份;保留 views/ 才是单一事实源。
|
||||
- `views/` 命名与 pages 略重复是约定问题,但目录本身承担"跨 locale 共享页面体"职责,非冗余。可改名为 `components/pages/` 或直接摊平到 `components/`,但非必要,未做(避免无谓改动)。
|
||||
- 现状 views/ 实际成员:Account / Home / Login / RunwayLooks / StreetSnaps / ItemPage(后两者非页面级路由,而是被 [id].astro 复用的详情体)。
|
||||
|
||||
## 待办/提醒
|
||||
- 详情页为 `prerender=false` 动态 SSR,依赖后端 `:8090` 接口;本次仅验证编译/类型通过,真实渲染需起后端联调(用户本地点一下 /runway-looks/<id> 最稳)。
|
||||
|
||||
## ItemPage 按 type 拆成两个 Detail ✅(16:10–16:18)
|
||||
用户要求把 `ItemPage.astro`(用 `type` 参数通吃 runway/street)拆成两个独立 Detail。
|
||||
- 关键事实:runway/street 的差异只在「取数函数(getSsrArticle/getSsrStreetSnap) / 卡片组件(RunwayLookCard/StreetSnapCard) / TDK 文案 / meta.type 值」;而收藏/历史 `<script>` 类型无关(全靠 data-* 透传)。
|
||||
- 新建 `RunwayLooksDetail.astro`、`StreetSnapsDetail.astro`(各只收 `id` prop,type 在组件内固化),各管自己的 SSR 取数 + TDK + 卡片 + fav 按钮标记。
|
||||
- 把那段 ~50 行收藏/历史脚本抽成 `src/lib/detailFav.ts` 的 `initDetailFav()`(读 `#fav-fab` 的 data-*);两个 Detail 各用 `<script> import { initDetailFav }` 调用,避免重复。该脚本是打包模块(有 import,无 define:vars),合规。
|
||||
- 4 个 `[id].astro`(en/cn × runway-looks/street-snaps)改指各自 Detail(`type` 不再透传);删 `ItemPage.astro`。旧 `/item/[id]` 仅做 301 重定向,不依赖 ItemPage,不受影响。
|
||||
- 注意:这 4 个页面文件在 15:14→16:10 间被改过(注释/顺序变了),首次批量 replace 因 old_str 不匹配失败;重新读取真实内容后才改成功。教训:改他人可能动过的文件前先重新读取。
|
||||
- 验证:`ItemPage` 引用搜索 0 命中;`npm run build` Complete;`npx astro check` 0/0/0。
|
||||
|
||||
## 后端图片去重现状调研(用户问爬虫判重怎么设计)✅(17:10–17:20)
|
||||
用户想给爬虫加"爬到图先判是否库里已存过"。调研 `backend_v2` 发现**这套已实现**,无需从零设计:
|
||||
- `internal/pkg/phash/phash.go`:纯标准库 **dhash**(64-bit 指纹,非向量),`DefaultThreshold=10`(汉明距离≤10 判近似重复),解码失败返回 0。包名虽叫 phash 实为 dhash(注释已说明)。
|
||||
- 入库管线 `ingest_service.go`:`downloadOne` 每张图下载后 `phash.Of(data)`;`processRunway/processStreet` 调 `tagDuplicates→matchDuplicates→repo.ListImagePHashes` 比对并标 `IsDuplicate=1 + DupOf`。
|
||||
- 模型 `runway_image/street_snap/*_draft` 均有 `Phash uint64 / IsDuplicate uint8 / DupOf string` 三列。
|
||||
- **关键行为**:当前是「只标记、不拦截止」(留痕交人工裁决),且比对库**仅已晋升正式表**(runway_images+street_snap_images),跳过 is_deleted 与 phash=0,**不含草稿表/本批内部**。
|
||||
- 存储用 sha1 内容寻址 key → 同字节幂等不堆孤儿文件,但 DB 行仍会多一条。
|
||||
- 效率注:每次入库任务全量拉 phash 表,千级 OK;到十万级需改分批/缓存。
|
||||
- **待用户拍板**:他想要的是「标出来给人看」(现在就能用)还是「命中就拦截不存」(需改 ingest_service 两处 + 引用计数)。阈值 10 可后续按真实样本调。
|
||||
|
||||
## 移除后端 phash 感知哈希去重 ✅(17:20–17:55)
|
||||
用户明确:图片量会很大,不想要 phash 了(当前 `ListImagePHashes` 每任务全表扫描,O(N) 不具扩展性)。
|
||||
- 彻底移除感知哈希层:删 `internal/pkg/phash` 包;`ingest_service.go` 去掉 `downloadOne` 的 `phash.Of` 与 4 返回值(uint64)、`fetchImages` 的 `[]uint64` 返回与 `phashes`、两个 `fetchLookImages`/`fetchImages` 调用处解包、processRunway/processStreet 的 `phashList`/`phs` 与 `tagDuplicates` 调用,并删除 `tagDuplicates`/`matchDuplicates` 两个函数;`ingest_repository.go` 删 `ListImagePHashes` 方法、`ImagePHash` 结构体及其接口声明;4 个 model 文件删 `Phash/IsDuplicate/DupOf` 三列;`review_repository.go` 晋升时不再拷贝这三列。
|
||||
- 实现手法:用一段 Python 脚本(正则+精确串替换,`\t` 转义规避 tab 对齐)批量改 `ingest_service.go`,`go build ./...` 通过后删 phash 包与临时脚本。
|
||||
- **刻意保留**:`downloadOne` 的 sha1 内容寻址存储 key(与 phash 无关,能天然防重复文件/孤儿文件),未动。
|
||||
- 验证:`go build ./...` 两次均 EXIT:0;残留扫描 phash/Phash/IsDuplicate/DupOf/tagDuplicates/matchDuplicates/ListImagePHashes/phashList/phs[ = 0。
|
||||
- 提醒:库表 `phash/is_duplicate/dup_of` 列 GORM 不会自动 DROP(仅 ADD),已升为孤儿列(可后续加迁移清掉);若将来要多来源"近似重复"仍要,应改 LSH/ANN 索引而非全表扫描。前端从不消费这三个字段。
|
||||
|
||||
## 去重替代方案方向(用户移除 phash 后问"好的方案有哪些",18:43)💡 待用户拍板
|
||||
核心认知:删的是"全表扫描的 phash 实现"≠去重需求没了。给出分层建议:
|
||||
- **Tier1 精确去重(推荐先做)**:复用现有 sha1 存储 key,新增 `content_sha1` 列 + UNIQUE 索引,入库前 O(1) 查重/upsert;挡同字节/同URL重爬;代价近零、可无限扩展。
|
||||
- **Tier2 近似去重**:复用 dhash(纯标准库),但加 `phash_bucket`(高12~16bit)索引,仅桶内算汉明距离,避免全表扫;解决用户"量大跑不动"死穴。
|
||||
- **Tier3 embedding+ANN 向量库(overkill)**:CLIP/CNN + Qdrant/Milvus/pgvector,抓语义级重复;对 runway/street snap 爬取偏重,暂不建议。
|
||||
- 待用户确认"重复"指:字节级(1)/近重复尺寸水印(2)/语义级(3);再出具体改法(含孤儿列复用 phash 名但重建结构)。
|
||||
|
||||
## 用户提议 pgvector+bit(64)+HNSW 方案(19:33)💡 已评估为 overkill
|
||||
用户建议:装 pgvector,dHash 存 `bit(64)` 建 HNSW 索引做近似去重。核实 pgvector **确实支持** `bit(N)`+Hamming(`bit_hamming_ops`,算子`<~>`,`hamming_distance<=10` 后过滤)。
|
||||
- **评估**:对 64 位 dHash 是杀鸡用牛刀——① HNSW 为高维(float/binary 数百~数千维)设计,64 维优势发挥不出且为近似索引(召回不如精确桶内比对);② 后端是 MySQL,为 64 位去重引 PG+pgvector 性价比低。
|
||||
- **更优替代**:MySQL 上 `BIGINT phash` + 抽高16位 `phash_bucket` 建 B-tree 索引,桶内算汉明 → O(1) 精确、零新基建(即 Tier2 桶化版,正解原"全表扫跑不动"痛点)。
|
||||
- **pgvector 正确用武之地**:语义档 CLIP `vector(1536)` + cosine HNSW(高维优势+二进制量化省存储),那才对得起基建。
|
||||
- 仍待用户确认重复类型(字节级/同图变形/不同角度同造型)再定方案与是否引 PG。
|
||||
|
||||
## 重要更正:backend_v2 实际已是 PostgreSQL+pgvector(19:36)⚠️
|
||||
核查 go.mod(gorm.io/driver/postgres + jackc/pgx,**无 mysql 驱动**)、internal/database/postgres.go(注释\"数据库已迁移至 PostgreSQL\" + gorm.Open(postgres.Open))、config.go DSN() 返回 postgres://、scripts/pgvector(PG16+pgvector Docker,initdb 自动建 vector 扩展,setup_pg_docker.sh 含 embedding vector(512)+hnsw 范例)。
|
||||
- **结论**:用户说\"就用pg把,早打算迁移mysql→pg\"实为**既成事实**——后端早就是 PG 了,无需迁移。MEMORY.md 的 \"GORM(MySQL)\" 已更正为 PostgreSQL+pgvector。
|
||||
- 因此去重可直接在 pgvector 上落地:content_sha1 唯一索引(精确) + phash bit(64)+bit_hamming_ops HNSW(近重复) + (可选)embedding vector(512)+cosine HNSW(语义)。
|
||||
- 注意:本会话早些时候删 phash 包/列是真实发生的;现需重建 phash.go + 给 image 模型重回 phash/content_sha1 列,并把去重逻辑接成\"索引查\"而非之前的\"全表扫\"。
|
||||
- 结论:上轮我给的「SQLite+LSH 分桶」方案在其已有 MySQL+全表扫 面前偏重,量到十万级再上 LSH 即可。
|
||||
|
||||
## 三档去重写入 backend_v2(用户拍板"都加把")✅(19:40–20:00)
|
||||
用户确认"都加把"——精确 + 近重复 + 语义三档全部落地。已逐个核对磁盘代码,确实落地:
|
||||
|
||||
### 1) 重建 `internal/pkg/phash/phash.go`(dHash,纯标准库)
|
||||
- `Of(data) uint64`:解码失败(webp 等)返回 0;`ToVectorBits(h) string` 转 `vector(64)` 二进制串 `[0,1,...]`(h=0 返回空串 → NULL);`Hamming(a,b) int`;`Size=64`、`DefaultThreshold=10`。
|
||||
- 注释明确:指纹只写库,检索交给 pgvector HNSW,不再全表扫。
|
||||
|
||||
### 2) 4 张 image 模型加列
|
||||
`BrandRunwayImage`/`BrandRunwayDraftImage`/`StreetSnapImage`/`StreetSnapDraftImage` 均加:
|
||||
- `ContentSha1 string`(`size:64` + 部分唯一索引 `uk_*_content_sha1,unique,where:content_sha1 <> ''`,排除遗留空串行);
|
||||
- `Phash sql.NullString`(`type:vector(64)`,text 扫描,规避 pgx 原生类型坑);
|
||||
- `IsDuplicate uint8` + `DupOf string`(标记用,不拦截)。
|
||||
|
||||
### 3) `internal/database/postgres.go` 加 `ensureDedupSchema(db)`
|
||||
- AutoMigrate 前先 `CREATE EXTENSION IF NOT EXISTS vector`;
|
||||
- 幂等:`content_sha1` 部分唯一索引 + `phash vector(64)` 列 + `CREATE INDEX ... USING hnsw(phash bit_hamming_ops)`;
|
||||
- 新建 `image_embeddings` 表(`image_id int`/`kind smallint`/`embedding vector(512)`/`created_at int`)+ `embedding vector_cosine_ops` HNSW(语义档位,当前不写,待 CLIP/DINOv2 接入)。
|
||||
- 注意:`AutoMigrate` 只 ADD 不 DROP → 之前移除阶段遗留的孤儿列已无关(新结构用新列名)。
|
||||
|
||||
### 4) 入库管线 `ingest_service.go` + `ingest_repository.go`
|
||||
- `downloadOne` 复算 `phash.Of` 并回填 `ContentSha1`/`Phash`;
|
||||
- 新增 `dedupImage`:① 精确 → `repo.ImageExistsBySha1`(跨 4 表 `content_sha1=? AND is_deleted=0`)命中则**整行跳过不插**;② 近重复 → `repo.FindNearDuplicateImage`,用 `phash <~> ?::vector <= threshold(10)` 取最近一条,命中设 `IsDuplicate=1`+`DupOf`(仅标记不拦);③ 本批内 `seen` 去重;
|
||||
- draft 晋升(`SaveRunwayFromDraft`/`SaveStreetSnapFromDraft`)复制去重字段。
|
||||
|
||||
### 验证
|
||||
`go build ./...` EXIT 0;`go vet ./internal/service/` 干净;单测 `TestFetchImagesCleansUpOnFailure` 绿(`ok fashionapi/internal/service`)。
|
||||
|
||||
### 待办(未做)
|
||||
- **运行期烟测**:起 pgvector Docker 实连,验证 GORM 把 `vector(64)` 列读进 `sql.NullString`(pgx text 格式扫描)无报错。尚未跑。
|
||||
- 汉明阈值 10 可按真实样本微调。
|
||||
@ -5,7 +5,7 @@
|
||||
|
||||
## 架构总览
|
||||
- 前端 `d:/project/frontend_v2`:Astro(`adapter: node` standalone,SSR 取数)+ Tailwind v4 + Alpine.js。构建 `npm run build`;类型检查 `npm run check`(= `astro check`)。
|
||||
- 后端 `d:/project/backend_v2`:Go + Gin + GORM(MySQL)。公开引擎 :8090(`/api/v1/public/*`、`/api/v1/auth/*`);SSG 引擎 :8091(仅 127.0.0.1,nginx 不反代)。
|
||||
- 后端 `d:/project/backend_v2`:Go + Gin + GORM(**PostgreSQL** + **pgvector**)。本地库见 `scripts/pgvector`(Docker 一键,PG16+,容器名 pgvector,库 fashion,账号 fashion/fashion_dev_2026;`initdb` 自动 `CREATE EXTENSION vector`)。公开引擎 :8090(`/api/v1/public/*`、`/api/v1/auth/*`);SSG 引擎 :8091(仅 127.0.0.1,nginx 不反代)。
|
||||
- 接口 i18n:后端按 `?locale=cn|en` 选列;**语言以 URL 前缀为准**;`prefixDefaultLocale: true`(/en、/cn 都带前缀;根 `/` 由服务端 302 到 /en)。
|
||||
- **base 地址全在 `src/lib/config/`**:`config.baseApi`(浏览器) / `config.baseApiSsr`(SSR 绝对) / `config.baseApiSsg`(构建期内部端口)。密钥类(SSG_TOKEN)直读 env。
|
||||
- 后端 Windows 编译:`go build -o server-bin.exe`(非 `server-bin`)。前端 dev 不热加载 `.env`,改 base 后 `astro dev stop`+`--background`+浏览器硬刷新。
|
||||
@ -61,3 +61,15 @@
|
||||
- Layout `<head>` 顶部 `is:inline`+`define:vars` 脚本:进首页(path==='/' 或 '/'+defaultLocale)且记住非默认语言时 `location.replace('/'+stored)`。
|
||||
- 语言切换器 `x-data="langSwitch()"`;`<a>` `@click="open=false; pick('<code>')"`;footer 注册 `langSwitch`(`pick(code){ storeLocale(code) }`)。
|
||||
- **铁律:只在显式点击切换器时写 localStorage,绝不按页面加载 URL 写**——否则与首页自动跳转冲突(点了英文又被 URL 的 en 覆盖,首页永远跳不回去)。
|
||||
|
||||
## 目录约定(用户 2026-09-17 确认)
|
||||
- **`src/components/views/` 必须保留,勿并入 `src/pages/`,也勿把内容提取摊平。** 原因:本项目 `prefixDefaultLocale: true` → `src/pages/{en,cn}/` 是两套 locale 页面树,每个页面文件只是薄壳(路由 + `export const prerender` + 引入共享视图 + 渲染);`views/*` 是 en/cn 两棵页面树**共用的页面体实现**(语言靠 `Astro.currentLocale` 运行时区分)。并进 pages 会迫使 en/cn 各抄一份,制造重复。
|
||||
- `views/` 命名与 pages 略撞语义,但目录本身职责明确(跨 locale 共享页面体单一事实源),仅属 cosmetic,非必要不改名。
|
||||
- 现状 `views/` 成员:Account / Home / Login / RunwayLooks / StreetSnaps / RunwayLooksDetail / StreetSnapsDetail(`RunwayLooksDetail`/`StreetSnapsDetail` 非独立路由,是被 `pages/{en,cn}/{runway-looks,street-snaps}/[id].astro` 复用的详情体;2026-09-17 由 ItemPage 按 `type` 拆成两个,共享脚本抽到 `lib/detailFav.ts` 的 `initDetailFav()`)。
|
||||
|
||||
## 图片去重(backend_v2,2026-09-17 落地)
|
||||
- **三档全上**:Tier1 精确(sha1 内容寻址 + `content_sha1` 部分唯一索引,命中整行跳过不插);Tier2 近重复(`phash` 存 `vector(64)` 二进制串,`bit_hamming_ops` HNSW,`<~> <= 10` 命中标 `IsDuplicate`/`DupOf`,只标不拦);Tier3 语义(`image_embeddings` 表 `embedding vector(512)` + `vector_cosine_ops` HNSW,待 CLIP/DINOv2 接入,当前不写)。
|
||||
- 关键文件:`internal/pkg/phash/phash.go`(dHash 纯标准库,`ToVectorBits`→`vector(64)` 串,webp 解码失败→0→NULL)、`internal/database/postgres.go`(`ensureDedupSchema` 幂等 DDL + `CREATE EXTENSION IF NOT EXISTS vector`)、`internal/service/ingest_service.go`(`dedupImage`)、`internal/repository/ingest_repository.go`(`ImageExistsBySha1`/`FindNearDuplicateImage`)。
|
||||
- `phash` 走 `sql.NullString` + `type:vector(64)` text 扫描,规避 pgx 原生类型坑;空串→NULL 不参与近邻检索。
|
||||
- **索引查取代全表扫**(用户"图片量会很大"痛点):原 `ListImagePHashes` 全表扫 O(N) 已移除。
|
||||
- 待办:pgvector Docker 实连烟测 GORM 读 `vector(64)` 进 `sql.NullString`;汉明阈值 10 可按样本微调。
|
||||
|
||||
Reference in New Issue
Block a user