FISHGAP / ENGINEERING NOTES
从 205,939 行回到 369 条:
一次重复同步修复
同一批内容反复同步,检索记录却一直增加。问题不在“内容变多”,而在每次写入都缺少稳定的内容身份约束。
先分清对象:本文谈的是 Fishgap 自己的站内检索辅助表,不是 Google、Bing 的收录库。行数减少不能证明排名、AI 引用、响应速度或获客改善。
同样 369 项内容,为什么出现 20 多万行?
修复前,站内检索表有 205,939 行;按“内容类型 + 内容 ID”去重后只有 369 项,对应当时 270 篇文章与 99 个工具。反复同步时,辅助表继续追加已有内容,物理行数逐渐偏离逻辑内容数。
一次同步执行成功,只能说明那次写入没有报错,不能说明同一内容没有被重复写入。这里先比较总行数与逻辑记录数,再查看哪些内容键出现多份。
| 验收阶段 | 辅助表行数 | 说明 |
|---|---|---|
| 修复前 | 205,939 | 逻辑内容键为 369 个 |
| 完成去重与约束 | 369 | 保留每项内容对应的记录 |
| 在数据库副本第一次重跑 | 369 | 同一批公开内容 |
| 在数据库副本第二次重跑 | 369 | 重复运行未再次膨胀 |
下载本次验收摘要(JSON)。这是本站记录的摘录,不是独立第三方审计;摘要不含服务器路径、访问者记录或业务联系方式。
内容身份要同时包含类型和 ID
文章 ID 为 1,工具 ID 也可能为 1;如果只按 ID 去重,就可能把两种内容误认为同一条。因此,本次采用 (content_type, content_id) 作为检索记录的唯一身份。不同业务需要先确定自己的键,不能直接照搬。
可以先在已有只读连接中执行以下查询。它们只统计和列出重复键,不会修改数据。
SELECT COUNT(*) AS total_rows FROM search_index;
SELECT COUNT(*) AS logical_records
FROM (
SELECT content_type, content_id
FROM search_index
GROUP BY content_type, content_id
);
SELECT content_type, content_id, COUNT(*) AS copies
FROM search_index
GROUP BY content_type, content_id
HAVING COUNT(*) > 1;“发现重复”与“决定留哪份”是两件事。如果多份记录的正文、来源或更新时间有冲突,要先确定权威内容。本站的辅助索引可以从公开内容目录同步,不应把索引清理方法套用到订单或其他业务主表。
先处理存量,再约束后续写入
- 留存备份并在副本验证。先比较记录数、逻辑内容键与公开目录,确保恢复路径清楚。
- 清理已存在的重复项。本站迁移按每个内容键保留最大记录 ID 对应的行,再按公开内容同步校正。最大 ID 只是此处的取舍规则,不通用于所有数据,也不等于内容质量更高。
- 为内容键增加唯一约束。让同一类型与 ID 无法再无约束地新增多份。
- 把追加写入改为冲突更新。已有键更新标题、正文摘要和标签;新键新增。内容没有变化时保留已有向量,变化时清除过期向量。
- 连续重跑并核对结果。检查行数、内容键、记录身份和向量状态,而不只检查退出状态。
SQLite 的 UPSERT 文档说明,ON CONFLICT ... DO UPDATE 在指定唯一性冲突时更新既有记录。这里的约束和更新语句必须配合:只有更新语句、却没有对应唯一约束,不能完成这套设计。
这次也有一次回滚:首次上线检查时迁移尚未触发,行数验收未通过,随后恢复了原源码与构建。补充显式初始化并重新验证后才完成发布。把“文件已复制”当成“迁移已完成”,会漏掉这种问题。
先预测,再跑一个两条内容的小实验
假设只有两条内容:blog / 1 和 demo / 1。完整同步两次,普通追加表会有几行?有唯一键与冲突更新的表又会有几行?
展开预期结果与原因
普通追加表有 4 行;冲突更新表仍有 2 行。两种类型的 ID 都是 1,不应被合并为一行。修改其中一条内容后仍是 2 行,但对应的旧向量应失效。
下载离线 SQLite 小实验(Python),用已安装的 Python 运行:
python search-index-example.py脚本只创建 :memory: 内存数据库,不读取或修改磁盘上的数据库,也不连接网络。它使用合成内容和示意向量,不是本站生产数据库的导出。示例依赖 Python 的 sqlite3 标准库,以及支持 UPSERT 的 SQLite;SQLite 从 3.24.0 起提供此能力。
本次在 Python 3.13.13 / SQLite 3.50.4 运行,输出依次为:追加表 4 行;冲突更新表在首次同步、重复同步和修改内容后均为 2 行;记录身份保持不变;未改内容保留示意向量,修改后清空示意向量。
这次结果证明了什么,还没有证明什么?
- 证明了这批已核对内容在两次副本重跑后,没有再次制造重复索引行。
- 测试覆盖了同键更新、跨类型同 ID、未变内容保留向量、内容变化清除向量。
- 没有测量搜索召回率、响应耗时、磁盘文件缩小幅度、外部搜索排名或商业转化。
- 新增内容、并发写入和数据源变化仍需分别验收;两次重跑不是对未来所有输入的保证。
如果你的报表、目录或同步任务也会重跑,先写出业务键,再用一份能人工核对的样本验证“新增、重复、更新、失败恢复”。不要仅凭日志里的“成功”作判断。