☰
每天 1.4 万个新职位如何被搜索引擎收录:freehire 用 Google Indexing API + IndexNow 的账本式配额设计
2026/10/22 2:22:33 网站建设 项目流程

【免费下载链接】freehire

freehire — the open-source search engine for job seekers

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

freehire 是一个开源的职位搜索引擎(open-source search engine for job seekers)。它的站点每天新增约 1.4 万个技术类职位页面,而 Google 的爬虫一天只主动抓取 65 个——如果只靠"等爬虫来",绝大多数新职位在上架很久之前就过期了。freehire 的解法是一套围绕Google Indexing API + IndexNow的"账本式配额设计":不是把 URL 塞进队列排队,而是记下"已经告诉过谁",每次运行重新挑选最值得报的新页面。本文带你拆解这套设计的来龙去脉。

为什么"等爬虫"在 69 万页规模下失效

设计这套系统的起点,是一组生产环境的实测数据(2026-09-14):

指标数值
Googlebot 当天抓取的职位页65 个
sitemap 声明的职位页~690,000
每天新增技术类职位~14,000
从 sitemap 抽样做 URL Inspection 的结果URL is unknown to Google

URL is unknown to Google不是"被拒绝"或"质量低",而是从未被抓过。而职位页面是"易腐"的——雇主一撤下原链接,页面就该关闭,几个月后才被首次抓取的页面,在可达之前就已经过期了。

于是 freehire 把策略反转过来:不等人来爬,主动告诉搜索引擎"这里有个新页面"。相关背景还记录在 docs/seo-baseline.md 里。

核心设计:账本(Ledger),不是队列(Outbox)

这是整个方案最有意思的一点。schema 里其他队列(search_outbox、recent_feed_outbox等)都满足"排空速度 > 填充速度",唯独这个不能:

  • Google Indexing API 的默认配额是每天 200 次 publish,而每天新增上万职位;
  • 如果用常规 outbox(ingest 写入、worker 消费),队列会无限增长,最老的一行永远轮不到;
  • 所以 migrations/0162_job_search_pings.sql 里记录的从来不是"待办",而是**"已发送"的事实**。
job_search_pings (job_id, engine, kind, pinged_at)

每次cmd/search-ping运行时,通过反连接(anti-join)找出"该引擎还没被告知过的最新合格职位",挑完即发。选择逻辑可以随时改变,不存在需要消化的积压。

"最新优先"是唯一的选择策略

在配额远小于目录的情况下,按created_at DESC取最新,一举两得:

  1. 职位页只在招聘中才值得公告,公告越早价值越高;
  2. 它顺带替代了一个过滤器——sitemap 会排除"likely-evergreen"(长期常开)职位,这个类别只存在于搜索索引里无法在这里 join,但它是靠"长期不关闭"赚来的,所以最新行天然不可能是它。

技术职位优先,其他类别兜底

目录并不只有 IT。合格门槛是"搜索索引愿意收录":开放、canonical、公开、有正文、类别可解析。在此之上分两层(tier):

层级内容配额待遇
TierTech(is_tech为真)技术类职位Google 的 200/天先给它
TierBeyondTech其余有类别的职位(医疗、零售、物流…)只能用剩下的

注意这是优先级,不是资格规则——医疗或物流职位同样是站点主张的页面、同样带JobPosting标记。而 IndexNow 没有配额,每一层每次都能领到一整批。选择查询写在 internal/platform/db/queries/job_search_pings.sql,两层是"一次查询跑两遍"而不是ORDER BY is_tech DESC——后者无法走created_at索引,会为挑 200 条而排序全部开放职位。

两个事件:创建与关闭,都是 URL_UPDATED

一个职位页会被公告两次:出现时(created)、关闭后(closed),但两次都发URL_UPDATED,见 migrations/0163_job_search_pings_kind.sql 的设计说明:

  • 关闭后的页面仍是 HTTP 200,保留JobPosting标记,只是validThrough移到了过去——这是 Google 文档认可的"下架职位"方式之一,所以需要的是重新抓取;
  • 对仍在线的页面发URL_DELETED属于误用 API,惩罚就是配额本身。

kind列因此把同一职位拆成两条独立事实,否则反连接会因为"创建"那行而永久跳过关闭公告。另外,新职位先花预算:新职位带来访客,关闭公告只是整理一个不属于自己的索引。在 200/天 对 1.4 万/天的量级下,第一遍就把配额吃完是正确行为;配额一旦提升,关闭公告无需改代码自动开始流动。

两个引擎的分工:Google Indexing API vs IndexNow

Google Indexing APIIndexNow
覆盖引擎只有 GoogleBing、Yandex、Seznam、Naver
预算200 次/天(可申请上调)无公开配额
凭证服务账号,且必须是 Search Consoleowner站点公开提供的一个 key 文件
批量一次调用只报 1 个 URL(批量省的是 HTTP,不是配额)一次最多 10,000 个
部分失败正常现象不可能——整批同答

两个适配器分别实现在 internal/job/searchping/google.go 和 internal/job/searchping/indexnow.go,统一实现Engine接口:Name/DailyBudget/Accepts/Announce。Announce返回的是引擎接受的 URL 子集——账本只记引擎"拿走"的,从不记"递出"的,否则部分失败的批次会被整批入账,没发出去的部分就再也不会被选中。

公司页:只能走 IndexNow 的资产

通过 Bing Webmaster API 实测(2026-09-15),Bing 索引里的 25.5 万个页面中,曝光最高的正是/companies/<slug>,背后的查询形态全是"公司名 + careers";Bingbot 两天里抓了 6,275 个公司页 vs 3,679 个职位页。公司页是 freehire 自己装配的"某雇主全部在招岗位",雇主自己往往根本不发布——这是真正值钱的页面。

但 Google Indexing API 只接受JobPosting和BroadcastEvent两类页面,把公司页发过去就是违反条款,惩罚是配额。所以拒绝逻辑写在引擎自己身上(Engine.Accepts):Google 拒收KindCompany,IndexNow 全收。公司页另有独立账本 migrations/0164_company_search_pings.sql,且没有 kind 列——公司页只有一个事件(存在),职位全部关闭它也仍是 200 页面。

藏在细节里的四个陷阱

1. 配额日在太平洋时间午夜重置。预算日既不是 UTC 也不是主机时区。代码里直接time.LoadLocation("America/Los_Angeles")并导入time/tzdata内嵌时区库——因为固定-8会在夏令时期间让边界晚一小时,那小时的发送"看起来没花钱,实际 Google 在记账",运行就会超支。剩余配额不是问 API 问来的(API 只会在用完时回429,不会自报余量),而是从账本读回来的:PingsSince数出该引擎今天已发的条数。

2. 发送是"至少一次"(at-least-once),方向是刻意选的。HTTP 调用无法加入记录它的数据库事务,两种顺序必有一个吃亏:先发后记,失败代价是一次重复公告(有界、可见、无害);先记后发,失败则这个职位永久且静默地失去公告机会(已入账的行永远不会再被选中)。loud and bounded 的失败才是该保留的那个。

3.siteFullUser不是siteOwner。用"Full"权限把服务账号加进 Search Console 可以读所有报告,但 Indexing API 会回403 ... Failed to verify the URL ownership——报错里根本不会提示你"去改角色"。

4. IndexNow 的 key 天生是公开的。它放在站点静态目录(web/static/<key>.txt)里供引擎回验,是所有权证明而非机密。key 同时存在于站点文件和 worker 环境变量INDEXNOW_KEY两处,一旦漂移,IndexNow 会对每次提交回403,看起来像集成坏掉了。所以启动时VerifyKey会抓取文件比对一次,不一致就拒绝运行。

一小时一次的运行与无感回滚

入口 cmd/search-ping/main.go 每次运行给每个"引擎 × 事件"发一批后退出,建议按小时调度,SEARCH_PING_BATCH(默认 200)限制单批规模:

./search-ping # 正式公告 ./search-ping -dry-run # 只打印将要发送的 URL,不发送

事件竞争引擎"一天"的固定顺序就是策略本身:新建·技术→新建·其他→公司页→关闭。有配额的引擎按顺序消耗剩余量,无配额引擎(IndexNow)每遍各领一整批;429 一到就停止该引擎的剩余批次(继续发只会烧掉明天的配额——除了 429 以外的失败都耗配额)。

回滚同样优雅:清掉GOOGLE_INDEXING_KEY_FILE和INDEXNOW_KEY两个环境变量即可。没有凭证的引擎视为"未配置",静默跳过;两个都没有时,运行是一次连数据库都不打开的空操作,定时器可以原样保留。

小结

这套设计给大规模内容站做了 SEO 收录的团队留下了几条通用经验:

  1. 配额是设计约束,不是管道参数——先确认"一天能花多少",再决定数据结构;
  2. 账本优于 outbox:当消费速度永远追不上生产速度时,记录"已做"比排队"待做"更可靠;
  3. 把平台规则写进引擎适配器(Accepts、DailyBudget),让策略顺序和具体引擎解耦;
  4. 接受 at-least-once,选那个失败"响亮且有界"的写入顺序。

核心实现都在 internal/job/searchping/ 目录下,配合 docs/seo-baseline.md 中收录的实测数据,可以完整复现这套决策的过程。

【免费下载链接】freehire

freehire — the open-source search engine for job seekers

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

相关推荐

上一篇:17种Java代码坏味道大清单:CoDiver代码嗅探器帮你根除技术债务
下一篇:官宣 Opus 4.8 级性能!这只"自己出题自己练"的开源AI,还能塞进 iPhone 17

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询