POSTGRES_MIGRATION.md 12.9 KB

PostgreSQL 迁移说明

本文档说明为什么要从 .pkl 索引迁移到 PostgreSQL、PostgreSQL 在这个项目里承担什么角色,以及从本地初始化到导入曲库的具体步骤。

1. 为什么迁移

当前 outputs/indexes/library_lyrics.pkl 是一个 Python DuplicateChecker 对象快照。它适合实验和离线评估,但不适合线上服务:

  • 增量新增、删除、回滚不方便。
  • 多实例部署时很难同步同一个 .pkl
  • 没有数据库事务、审计、版本管理。
  • 索引结构绑定 Python pickle,不利于运维和长期维护。

PostgreSQL 的目标不是替代全部判重逻辑,而是替代“数据存储 + 候选召回”部分。最终判定仍保留在 Python 服务层。

2. PostgreSQL 在本项目里的角色

推荐分工:

PostgreSQL:
  保存原始歌词、规范化歌词、行级特征
  用 exact_hash 做精确召回
  用 pg_trgm 做文本近似召回
  用 lyric_lines 做行级重合召回

Python:
  normalize_lyrics
  计算 Jaccard / line coverage
  处理翻译行、副歌碰撞、片段保护
  最终输出 duplicate / review / new

不要让 PostgreSQL 的 similarity() 直接决定 duplicate。数据库只负责找候选,最终判定仍应复用当前算法规则。

3. pkl 索引和 PostgreSQL 的对应关系

当前 .pkl 里主要有:

_records              每首歌词的完整索引记录
_exact_hash_to_ids    exact hash -> record ids
_line_to_ids          normalized line -> record ids
_token_to_ids         n-gram token -> record ids
_lsh                  MinHash LSH 桶

迁移到 PostgreSQL 后:

lyrics.exact_hash             对应 _exact_hash_to_ids
lyrics.primary_text + pg_trgm 对应近似文本召回
lyric_lines.line_hash         对应行级倒排召回
lyrics / lyric_lines          对应 _records 的可持久化部分

第一阶段不迁移 MinHash LSH。先用 exact_hash + pg_trgm + line_hash 验证召回效果。

4. 本地 PostgreSQL 基础

PostgreSQL 是关系型数据库,常用概念:

database   数据库,例如 lyric_dedup
schema     命名空间,默认 public
table      表,例如 lyrics
index      索引,用来加速查询
extension  扩展,例如 pg_trgm
DSN        连接字符串

本机数据库连接字符串常见写法:

postgresql:///lyric_dedup

含义是:使用当前系统用户名,连接本机 PostgreSQL 的 lyric_dedup 数据库。

如果有用户名密码:

postgresql://postgres:postgres@localhost:5432/lyric_dedup

5. 当前已新增脚本

项目里已经加入:

scripts/postgres_schema.sql
scripts/init_postgres.py
scripts/import_library_postgres.py
scripts/evaluate_postgres.py

用途:

postgres_schema.sql          建表、建索引、启用 pg_trgm
init_postgres.py             自动执行 schema SQL
import_library_postgres.py   扫描 data/library,规范化后导入 PostgreSQL
evaluate_postgres.py         使用 PostgreSQL 召回候选并评测 CSV

6. 安装依赖

当前 Python 环境需要安装 PostgreSQL 驱动:

python -m pip install 'psycopg[binary]'

如果你使用 conda 环境,确认命令运行在当前项目所用的 (base) 或目标环境里。

验证:

python - <<'PY'
import psycopg
print(psycopg.__version__)
PY

7. 创建数据库

你已经执行过:

createdb lyric_dedup

如果需要确认数据库存在:

psql -l | grep lyric_dedup

进入数据库:

psql postgresql:///lyric_dedup

退出 psql

\q

8. 初始化表结构

执行:

python scripts/init_postgres.py \
  --dsn postgresql:///lyric_dedup

它会执行:

create extension if not exists pg_trgm;
create table if not exists lyrics (...);
create table if not exists lyric_lines (...);
create index if not exists ...;

成功输出类似:

initialized schema from scripts/postgres_schema.sql

可以检查表:

psql postgresql:///lyric_dedup -c '\dt'

检查扩展:

psql postgresql:///lyric_dedup -c 'select * from pg_extension;'

9. 表结构说明

lyrics

保存每首歌词的主记录:

record_id          当前文件生成的稳定 id
source_path        原始文件路径
title / artist     从文件名解析的元数据
raw_text           原始歌词
normalized_text    清洗后的全文
primary_text       原文行拼接文本,主要用于自动判重
translation_text   翻译行拼接文本
exact_hash         规范化原文 hash
split_confidence   翻译拆分置信度
split_reason       翻译拆分原因
line_count         有效歌词行数
deleted_at         软删除字段

lyric_lines

保存行级特征:

lyric_id           对应 lyrics.id
role               primary / translation / unknown
line_no            行号
normalized_line    规范化歌词行
line_hash          行 hash

用途:快速找“哪些歌包含相同行”。

10. 小批量导入测试

先导入 1000 条,确认环境和 schema 都正常:

python scripts/import_library_postgres.py \
  --dsn postgresql:///lyric_dedup \
  --lyrics-dir data/library \
  --limit 1000

导入脚本默认会在导入结束后执行一次低风险库内去重:

exact_hash 完全一致的记录只保留一条,其余记录 soft delete,即设置 lyrics.deleted_at。

重复清理报告默认写到:

outputs/results/postgres_exact_duplicates.csv

如果只是想导入,不做 exact 去重:

python scripts/import_library_postgres.py \
  --dsn postgresql:///lyric_dedup \
  --lyrics-dir data/library \
  --limit 1000 \
  --skip-dedup-exact

查看数量:

psql postgresql:///lyric_dedup -c 'select count(*) from lyrics;'
psql postgresql:///lyric_dedup -c 'select count(*) from lyric_lines;'

查看几条数据:

psql postgresql:///lyric_dedup -c \
  'select id, record_id, title, artist, line_count from lyrics limit 5;'

11. 全量导入

确认小批量没问题后,导入全量:

python scripts/import_library_postgres.py \
  --dsn postgresql:///lyric_dedup \
  --lyrics-dir data/library

脚本会显示进度:

[pg-import] files: 70295
[pg-import] import: 500/70295
...

导入是 upsert,同一个 record_id 再导入会更新,不会重复插入。

如果想额外生成“高行覆盖率近重复候选”报告,但不自动删除:

python scripts/import_library_postgres.py \
  --dsn postgresql:///lyric_dedup \
  --lyrics-dir data/library \
  --line-duplicate-report outputs/results/postgres_line_duplicates.csv \
  --line-coverage-threshold 0.95

注意:行覆盖率近重复报告可能较慢,且只用于抽查。当前脚本不会自动 soft delete 这些近重复候选。

12. 基础 SQL 验证

exact hash 重复

找规范化 hash 重复:

psql postgresql:///lyric_dedup -c "
select exact_hash, count(*)
from lyrics
where deleted_at is null
group by exact_hash
having count(*) > 1
order by count(*) desc
limit 20;
"

如果导入时没有加 --skip-dedup-exact,这里理论上不应该再出现 active exact 重复;已经清理的重复记录可以这样查看:

psql postgresql:///lyric_dedup -c "
select count(*)
from lyrics
where deleted_at is not null;
"

pg_trgm 相似查询

测试 pg_trgm

psql postgresql:///lyric_dedup -c "
select id, title, similarity(primary_text, '我爱你在每个夜里') as sim
from lyrics
where primary_text % '我爱你在每个夜里'
order by sim desc
limit 10;
"

行级重合

找某一行出现在哪些歌:

psql postgresql:///lyric_dedup -c "
select l.id, l.title, ll.normalized_line
from lyric_lines ll
join lyrics l on l.id = ll.lyric_id
where ll.normalized_line = '我爱你在每个夜里'
limit 20;
"

13. 后续查重查询应该怎么做

未来 PostgreSQL 版查重流程:

1. Python 读取新增歌词
2. normalize_lyrics
3. SQL exact_hash 召回
4. SQL pg_trgm 召回
5. SQL lyric_lines 行级召回
6. 合并候选 id
7. 拉候选 normalized 数据
8. Python 复用当前打分规则
9. 输出 duplicate / review / new

示意 SQL:

select id
from lyrics
where exact_hash = $1
  and deleted_at is null;
select id, similarity(primary_text, $1) as sim
from lyrics
where deleted_at is null
  and primary_text % $1
order by sim desc
limit 200;
select lyric_id, count(*) as matched_lines
from lyric_lines
where role = 'primary'
  and line_hash = any($1)
group by lyric_id
order by matched_lines desc
limit 200;

14. 增量更新设计

新增一首歌:

1. normalize
2. 用 PostgreSQL 召回候选
3. Python 判定
4. duplicate: 拒绝或关联已有记录
5. review: 进入人工复核
6. new: 写入 lyrics 和 lyric_lines

删除一首歌:

update lyrics
set deleted_at = now(), updated_at = now()
where id = ...;

不建议物理删除,除非确认不需要审计。

更新一首歌:

1. 更新 lyrics.raw_text / normalized_text / primary_text / exact_hash
2. 删除旧 lyric_lines
3. 插入新 lyric_lines
4. 整个过程放在一个事务里

15. PostgreSQL 版评测

评测仍然需要先生成测试集。测试集是“输入样本 + 期望标签”,PostgreSQL 版评测只负责用 PostgreSQL 数据库召回候选并计算指标。

如果还没有测试集,先生成:

python -m lyric_dedup.cli generate-eval-set \
  --library-dir data/library \
  --lyrics-dir data/generated_eval/incoming \
  --csv data/generated_eval/eval_5000.csv \
  --size 5000 \
  --positive-ratio 0.3

然后跑 PostgreSQL 版评测:

python scripts/evaluate_postgres.py \
  --dsn postgresql:///lyric_dedup \
  --csv data/generated_eval/eval_hard_5000.csv \
  --base-dir data/generated_eval \
  --out outputs/results/postgres_eval_hard_5000.csv

它会:

1. 对 eval 样本 normalize
2. 用 PostgreSQL exact_hash 召回
3. 用 pg_trgm primary_text 召回
4. 用 lyric_lines.line_hash 召回
5. 合并候选
6. 用 Python DuplicateChecker 对候选重新打分
7. 输出 duplicate / review / new 和指标

如果想把 review 也算作“抓到可疑样本”:

python scripts/evaluate_postgres.py \
  --dsn postgresql:///lyric_dedup \
  --csv data/generated_eval/eval_50000.csv \
  --base-dir data/generated_eval \
  --positive-decisions duplicate,review \
  --out outputs/results/postgres_eval_50000_review_positive.csv

可调参数:

--recall-limit       每类 SQL 召回最多取多少候选,默认 100
--enable-trgm        打开 pg_trgm 整段文本召回;默认关闭,避免评测过慢
--trgm-threshold     pg_trgm 的 % 匹配阈值,默认 0.3,仅 --enable-trgm 时使用
--max-candidates     最终输出多少候选,默认 5
--statement-timeout-ms 单条 SQL 超时时间,默认 5000

注意:当前 PostgreSQL 版是原型评测脚本。默认只用 exact_hash + lyric_lines.line_hash 召回,速度更可控。pg_trgm 可以作为补充召回,但整段歌词 trigram 查询在 5 万评测集上可能很慢,建议单独开小样本验证后再用于全量。

16. 迁移验证标准

迁移不是导入完就结束。需要单独验证 PostgreSQL 版查重链路:

1. exact duplicate 是否能查到
2. punctuation / timestamp / platform noise 正例是否能召回
3. fragment / shared chorus 负例是否不会被直接判 duplicate
4. PostgreSQL 召回候选数量是否合理
5. PostgreSQL 版 evaluate 指标是否达到业务要求

第一阶段目标:

PostgreSQL 负责召回,Python 仍负责判定。

17. 常见问题

提示 Missing dependency: psycopg

运行:

python -m pip install 'psycopg[binary]'

连接失败

检查 PostgreSQL 是否启动:

pg_isready

检查数据库是否存在:

psql -l | grep lyric_dedup

pg_trgm 创建失败

确认连接用户有创建 extension 权限。本机默认用户一般可以。

手动测试:

psql postgresql:///lyric_dedup -c 'create extension if not exists pg_trgm;'

想清空重新导入

谨慎执行:

psql postgresql:///lyric_dedup -c 'truncate lyric_lines, lyrics restart identity cascade;'

然后重新运行导入脚本。

18. 当前建议执行顺序

你现在已经完成:

createdb lyric_dedup

接下来执行:

python -m pip install 'psycopg[binary]'
python scripts/init_postgres.py \
  --dsn postgresql:///lyric_dedup
python scripts/import_library_postgres.py \
  --dsn postgresql:///lyric_dedup \
  --lyrics-dir data/library \
  --limit 1000

确认数量:

psql postgresql:///lyric_dedup -c 'select count(*) from lyrics;'

确认后全量导入:

python scripts/import_library_postgres.py \
  --dsn postgresql:///lyric_dedup \
  --lyrics-dir data/library