资讯动态

同一职位发在 3 个平台 = 1 条记录:freehire 归一化与稳定键去重设计详解

发布时间:2026/10/22 7:33:27 来源:尧图企业网站定制
同一职位发在 3 个平台 1 条记录freehire 归一化与稳定键去重设计详解【免费下载链接】freehirefreehire — the open-source search engine for job seekers项目地址: https://gitcode.com/GitHub_Trending/fr/freehirefreehire 是一个开源的职位搜索引擎它把来自数百个招聘平台的职位归一化为统一结构并用稳定键去重机制让同一个职位无论被发到多少个平台、多少个城市最终都收敛为搜索结果中的1 条记录。本文将拆解它背后的三层去重设计唯一键、内容指纹与模糊匹配以及为什么标记而不是删除是关键抉择。为什么同一职位发 3 次是个问题如果你用过任何招聘网站都会遇到这样的场景同一份Senior Backend Engineer公司官网发一份、ATS 平台发一份、聚合站再转手发三份。对搜索引擎来说这意味着搜索被灌水用户搜一次同一份工作出现 5 次统计失真热门岗位和冷门岗位无法区分索引膨胀重复文档占用宝贵的检索资源。freehire 面对的是真实数据规模——数千万条职位记录。它的解法不是简单的 URL 比对而是一套分层的归一化 去重管线整体流程为抓取fetch→ 归一化normalize→ 去重dedup→ 写入upsert第一层归一化——把 N 种格式变成 1 种结构不同 ATS 平台Workday、Greenhouse、Lever、Ashby……返回的字段名、格式、单位五花八门。freehire 为每个平台实现了独立的适配器adapter位于internal/ingest/sources/所有适配器都实现同一个Source接口把平台原始数据转换进同一套职位 schema。归一化完成后写入路径只认一种语言。这一步的意义在于去重逻辑不需要关心职位来自哪个平台只处理统一结构即可。相关约定见 internal/ingest/pipeline/AGENTS.md。第二层稳定键去重——UNIQUE(source, external_id)归一化解决长得一样但还要解决是同一个。最可靠的身份标识是来源 外部 IDsource这个职位来自哪个平台external_id该平台给这条职位分配的唯一编号。数据库上为jobs表建了UNIQUE (source, external_id)唯一约束所有写入都走ON CONFLICT的 upsert。这带来一个极其重要的性质重复爬取是免费的。同一个职位爬 100 次数据库里永远只有一行重复请求只是刷新last_seen_at时间戳。这个唯一键就是所谓的稳定键stable key——它不随时间、URL 变化或页面刷新而改变是整个去重体系的基石。第三层role_fingerprint——识别换了马甲的同一岗位稳定键只能识别同一来源的同一职位但解决不了更隐蔽的情况同一岗位发到不同平台external_id 完全不同同一公司在30 个城市各发一份同样的职位职位撤下后用新 ID重新发布repost。为此freehire 引入了role_fingerprint角色指纹定义见 migrations/0003_role_fingerprint.sql指纹包含指纹刻意排除公司标识posted_at发布日期归一化后的职位名称职位 URL归一化后的职位描述public_slug排除易变字段是精髓只要公司职位名描述实质相同无论 ID、日期、链接怎么变都会落到同一个指纹上。所有共享(company_slug, role_fingerprint)的职位构成一个角色集群集群中min(id)的那条行被选为规范记录canonical其余行打上duplicate_of标记指向它。这一设计见 migrations/0012_job_duplicate_of.sql。注意这里没有删除任何行。原因很实际repost 计数、批量发布信号等数据恰恰依赖行数本身而列表页和搜索索引会排除带标记的行详情接口仍可访问被标记的职位。折叠发生在读取时而非物理删除。标记分治三个去重通道各管一列早期三个去重通道角色指纹、聚合站压制、模糊匹配共用duplicate_of一列会互相擦掉对方的标记导致每天数百万行无谓的重复写入且两个通道之间存在长达一小时的重复窗口。重构方案见 migrations/0114_jobs_duplicate_marker_owner_columns.sql 与 migrations/0115_jobs_derive_duplicate_of.sql每个通道独占一列duplicate_of_aggregator、duplicate_of_role、duplicate_of_fuzzy一个通道只能清除自己设的标记duplicate_of改为触发器派生NEW.duplicate_of : COALESCE( NEW.duplicate_of_aggregator, NEW.duplicate_of_role, NEW.duplicate_of_fuzzy );优先级顺序为聚合站 → 角色 → 模糊它精确复现了线上哪个通道赢下争议行的现状避免重构偷偷改变行为。其中模糊去重通道按描述文本的词重叠度阈值 0.9聚类实现见 cmd/reindex/fuzzy.go。写入时的同步去重不让重复先上车批量重算每几小时跑一次但爬虫是持续写入的——两条时间线之间同一岗位的多份副本会短暂地都以规范记录的身份出现在搜索里。internal/job/jobdedup/包在写入事务内同步回答这个岗位是否已有更老的规范记录规则见 internal/job/jobdedup/jobdedup.go。两条设计约束值得留意只认更老的规范记录批量重算选min(id)同步通道若把新行指向上更新的兄弟行下次重算就会把它反转等于自我打架失败即放行查询出错时该行照常以无标记写入——去重是优化绝不能成为职位丢失的条件。被同步标记的行不入搜索索引、不入 LLM 富化队列从源头省掉了下游成本。用户视角1 条记录如何呈现最终效果就是你在 freehire 界面上看到的左侧按地区、工作方式、专业领域、职级筛选右侧是去重后的职位卡片——同一公司同一岗位不会出现多张卡片还有一个容易被忽略的细节当用户从某个聚合站复制一条职位链接过来查询时系统会通过normalize_job_url归一化 URL剥离协议、www.、追踪参数、锚点、尾部斜杠再匹配见 migrations/0042_jobs_normalized_url_idx.sql。即使用户站在那条职位是重复副本的页面上系统也会回答它对应的那条规范记录——我知道这个职位而不是因为去重偏好它的孪生体就答没有。小结freehire 的3 平台 1 条记录并非一技之长而是一套分层设计层级机制解决的问题归一化适配器 统一 schemaN 种平台格式 → 1 种结构稳定键UNIQUE(source, external_id)重复爬取幂等角色指纹role_fingerprintmin(id)规范记录跨平台、跨城市、repost 折叠标记分治三列 触发器派生多通道互不踩踏同步去重写事务内CanonicalForRole缩短重复可见窗口加上标记不删除的读取时折叠哲学这套设计在保持数据完整性的同时把数千万行目录收敛为一份干净的搜索结果。对想自建搜索系统、内容聚合或任何多源数据去重项目的读者来说这五个决策点都极具参考价值。【免费下载链接】freehirefreehire — the open-source search engine for job seekers项目地址: https://gitcode.com/GitHub_Trending/fr/freehire创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑