ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

六个机制在写 closed_at:freehire 永不硬删除职位的软关闭生命周期设计

六个机制在写 closed_at:freehire 永不硬删除职位的软关闭生命周期设计

【免费下载链接】freehire

freehire — the open-source search engine for job seekers

项目地址:https://gitcode.com/GitHub_Trending/fr/freehire
点击查看免费下载

freehire 是一款开源的职位搜索引擎(the open-source search engine for job seekers),它每天索引数十万条职位,却有一条铁律:从不硬删除任何职位。一个职位是否"活着",只由jobs表里的closed_at一列决定——closed_at IS NULL即视为开放。写入这个字段的不是一个人,而是六个各管一段的自动化机制,像接力一样覆盖职位下线的所有场景。

为什么选择"软关闭"而不是删除?

对用户来说,删除是最干净的做法;但对搜索引擎来说,删除是危险动作:

  • 链接不能断:职位详情被外链、被收藏、被邮件引用过。freehire 的规则是——列表、搜索、公司页都过滤closed_at IS NULL,但详情页仍然服务已关闭的职位,历史链接永远不 404。
  • 可以免费重开:爬虫是"有罪推定",偶尔会误判。职位一旦被错误关闭,下一轮爬取把它再次看到时,upsert 会自动清空closed_at将其重开(见 TouchJob 中的closed_at = NULL逻辑)。删除的行则永远找不回来。
  • 数据可以审计:freehire 曾在早期有一个cmd/prune命令批量硬删"不属于 IT 职位板"的条目,后来目录扩展到 IT 之外就退役了,相关记录归档进独立的pruned_jobs表。项目甚至明确拒绝让closed_at兼职表达"被清理"的含义——迁移 0071_jobs_closed_reason.sql 的注释写道,复用一列表达两种含义"会腐蚀三个机制已经在写的信号"。

一句话总结:关闭是状态,删除是事故。

六个机制逐一拆解

这六条路径互不重叠:五个基于"证据"关闭,一个基于"猜测"关闭;前四个盯的是单个职位,后两个盯的是整块看板的长期病史。完整约定见 docs/agents/job-lifecycle.md。

机制一:ingest 清扫——48 小时没再见就关(unseen)

这是主力机制,跑在每次爬取之后。爬虫每次成功看到一个职位都会刷新它的last_seen_at时间戳;清扫任务(CloseUnseenJobs,位于 cmd/ingest/)随后把该来源下超过 48 小时没再出现的职位标记关闭。

它故意保守:只关本次运行真正爬到的公司的职位,宁可漏关也不误关。若单条UPDATE因坏行失败,还有逐行兜底CloseUnseenJobByID把整批继续走完。

机制二:liveness 探针——连续两次"过期"才判死(probe_expired)

有些职位(手动导入、URL 解析进来的)不在任何爬取范围内,机制一永远够不到它们。cmd/liveness工作器会直接抓取这些"孤儿职位"的 URL,用纯启发式规则(HTTP 404/410、跳转到列表页、多语言"已关闭"措辞等)判断页面状态,连续两次读到expired才关闭(liveness_strikes计数器,一次健康探测即清零)。它只凭正面证据行动,且从不主动重开——倾向于"少关"。

机制三:流式源的自我关闭(feed_removed)

像jobtech(瑞典公共就业局)这类提供增量变更流的来源,会在订阅流里直接推送"该职位已下架"事件。ingest 管道收到Removed: true后调用CloseJobBySourceExternalID,按(source, external_id)精确关闭。这类来源因此被排除在机制一之外——它们的流只报告变化的职位,靠last_seen_at反而会把仍在线的职位误关。

机制四:年龄规则——唯一的"猜测式"关闭(expired)

这是六个机制里唯一没有证据可查的:telegram来源的"职位 URL"其实就是那条帖子本身,招聘结束后帖子依然在线,任何探测都读不出"已下线"。于是 freehire 采用 45 天年龄规则:超过 45 天直接按expired关闭(CloseStaleUnsignalledJobs,跑在 cmd/liveness/ 中)。

正因为它是猜测,项目刻意把来源列表交给调用方显式传入——新数据源不可能"不知不觉"滑进年龄关闭。也正因如此,它被单独打标,让误关的 46 天仍在招人的职位可以作为一个集合被找回。

机制五:僵尸看板兜底(board_unreachable)

机制一/三都很保守,但有一个没有上限的盲区:一块看板如果彻底死了(上游退役、封禁我们、迁移域名),它的职位会永远开放着,却没人能证实它们还存在。

cmd/close-chronic-boards(cmd/close-chronic-boards/)读取board_health表——每次爬取的成功/失败史都记录在这里——对连续 60 天(CHRONIC_BOARD_CLOSE_WINDOW_DAYS)无法触达的看板整体关闭,closed_reason = 'board_unreachable'(见 0147_jobs_closed_reason_board_unreachable.sql)。执行前还会在 SQL 语句内部二次校验看板健康度,防止"读取到关闭"之间的时间窗口里看板恰好复活。

机制六:空 Feed 兜底(feed_empty)

这是机制五的"镜像盲区":看板能访问、每次都返回 HTTP 200,但里面一条职位都没有。真实事故中曾有 107 条职位对着一个已清空的 Feed 开放了一个月。

board_health.last_yield_at记录看板"最后一次真正产出职位"的时间。同一个工作器的第二遍扫描,对可达但 30 天(EMPTY_FEED_CLOSE_WINDOW_DAYS)零产出的看板整体关闭,打上feed_empty标签(0159_jobs_closed_reason_feed_empty.sql)。

💡 除以上六条自动化路径外,还有两类人工关闭:审核员处理举报时的moderated,以及雇主在自助面板下架自己发布职位的employer_closed(0175_jobs_closed_reason_employer_closed.sql)——不同角色、不同信任级别,用不同标签区分。

closed_reason:给每次关闭盖一个"责任章"

2026 年的迁移 0071_jobs_closed_reason.sql 为jobs表增加了closed_reason列,用数据库 CHECK 约束限定其取值,并且每次关闭必须写明是哪个机制写的:

标签写入者含义
unseeningest 清扫48h 没再从看板上看到
probe_expiredliveness 探针连续两次页面探测过期
feed_removed流式来源变更流报告职位下架
expired年龄规则超过 45 天,无证据可查
board_unreachable僵尸看板兜底看板 60 天无法触达
feed_empty空 Feed 兜底看板 30 天可达但零产出
moderated/employer_closed审核员 / 雇主人工关闭

这一列的价值在注释里说得很直白:"一条规则如果证明过于宽泛,现在可以单独被审计、单独被撤销"。存量行统一保留''(未知)——事后猜测给历史数据贴标签等于"发明历史"。重开时标签会被清空(有集成测试 job_close_reason_integration_test.go 守着这条契约),避免一个"复活"的职位还挂着上次的死亡证明。

值得留意的是:迁移 0071_jobs_closed_reason.sql 特意用了no-transaction+NOT VALID拆分约束的做法——生产库jobs表约 660 万行,普通加约束会长时间持有排他锁,排队在后面的每个读者都会一起卡死。

关闭之后:搜索摘除与免费重开

职位关闭后要立刻从搜索结果里消失,否则会出现"搜到已下线职位"的尴尬。freehire 的答案是search_delete_outbox删除队列(0113_search_delete_outbox.sql):

  • 原子性:每条关闭 SQL 都用 CTE 把UPDATE ... RETURNING id的结果直接灌进删除队列——关了就入队,回滚了就什么都不入队。
  • 无外键:删除队列刻意不建指向jobs的外键,否则"职位关闭 → 入队 → 行被清理 → 队列入被级联删掉"会让脏文档永久滞留在索引里。
  • 生产环境实测:每天约 19,827 条职位关闭,若不等全量重建,这些职位会在索引里多活 3 小时。

而"重开"完全是免费的:职位重新出现在爬取结果中时,upsert 的ON CONFLICT分支清空closed_at,顺便重置 liveness 失败计数,下一轮搜索重建时它自然回到结果里——无需任何人工介入。

小结

freehire 的软关闭设计可以浓缩成三句话:

  1. 一列定生死:closed_at IS NULL即开放,六个机制(清扫、探针、流事件、年龄规则、双兜底)分段协作,任何一条路径误判都能被下一次的"再出现"自动纠正;
  2. 死亡必须有名字:closed_reason让"为什么关"永远可审计、可单独回滚,连"猜测式"关闭都被明确打标;
  3. 永不硬删:关闭的职位保留public_slug、富化数据与用户收藏引用,链接永不断裂,重开只是清一个字段。

这套"用状态代替删除、用标签代替遗忘"的生命周期设计,值得任何管理海量时效性数据的开源项目参考。

【免费下载链接】freehire

freehire — the open-source search engine for job seekers

项目地址:https://gitcode.com/GitHub_Trending/fr/freehire
点击查看免费下载

相关推荐

上一篇:从语言模型卷到 K 线模型:金融大模型赛道现在有多拥挤
下一篇:如何为Android项目配置GitHub Actions CI流水线:Parchment的Lint、构建与测试自动化实战

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

返回列表