Google Kills Custom Search API on Jan 1, 2027
AI 深度解读
背景
2026年1月,Google 在一篇低调的开发者博客以及一批控制台邮件通知中宣布:Custom Search Engine JSON API(CSE JSON API)将于2027年1月1日彻底关闭,届时返回 HTTP 410 Gone。截至2026年8月,该 API 仍在技术上可用,但留给开发者的迁移窗口仅剩约9个月。CSE API 自2006年推出以来,一直是独立开发者、研究团队、学术机构以及小型 SaaS 公司获取“Google 搜索结果 JSON 格式”的官方途径,关闭意味着这一长达20年的服务正式终结。
Google 官方推荐的替代方案是 Vertex AI Search,但这是一个完全不同的产品——它属于企业级“基于自有语料库构建语义搜索”的套件,基础层定价约每1000次查询2美元,且随着扩展功能快速攀升。Vertex AI Search 不返回 CSE 所提供的公开网页搜索结果,Google 的官方立场是:对于“大规模网页搜索”,开发者应“探索第三方提供商”,这实际上是“我们退出这一业务”的企业翻译。
核心内容
CSE 的重要性与用户群体
Google CSE JSON API 多年来是网络唯一官方认可的“搜索互联网”API。v1 于2006年推出,v2 在2011年左右,而大多数当前集成使用的 JSON v1 变体始于2015年。它服务了三类截然不同的用户:
- “站点搜索”运营者:将 CSE 限定到一组特定网站,用作自家营销站的搜索栏。便宜、准确,约占 CSE 使用量的40%。
- “为代理/机器人/助手做的网页搜索”:需要公开网页查询的机器人、研究助手或 RAG 流水线,CSE 返回每个查询的 Google 前10条自然结果,是摘要或下游抓取的干净输入,约占35%。
- “研究与学术工作”:语料构建者、媒体研究员、研究信息检索的政治学家、验证引用的律所。CSE 提供了一条稳定、可商业授权的路径来获知“Google 认为今天这个查询的最佳结果”,约占25%。
CSE 的 JSON 响应结构
参考返回格式如下(已省略部分字段,但原文完整呈现):
{
"kind": "customsearch#search",
"url": { ... },
"queries": { "request": [{ "title": "Google Custom Search - climate change", "totalResults": "124000000", "searchTerms": "climate change", "count": 10, "startIndex": 1, ... }], "nextPage": [...] },
"context": { "title": "My CSE" },
"searchInformation": { "searchTime": 0.42, "totalResults": "124000000", "formattedTotalResults": "124,000,000" },
"items": [
{
"kind": "customsearch#result",
"title": "Climate change - Wikipedia",
"htmlTitle": "Climate change - Wikipedia",
"link": "https://en.wikipedia.org/wiki/Climate_change",
"displayLink": "en.wikipedia.org",
"snippet": "Climate change includes both human-induced...",
"htmlSnippet": "Climate change includes both...",
"formattedUrl": "...",
"htmlFormattedUrl": "...",
"pagemap": { ... }
}
]
}
第三方替代方案
- SerpApi:每月75–275美元
- ScaleSerp:约每月50美元
- Bright Data SERP API:企业级
这些服务各自有专属的 JSON schema,从 CSE 迁移意味着重写解析层。每个服务对应不同的 rewrite。
直接替代方案:google-cse-replacement actor
一个名为 google-cse-replacement 的 actor(托管在 Apify 平台)采用了与 Dark Sky 替代天气数据相似的思路:它在 SERP 抓取后端(Apify 的 GOOGLE_SERP 代理)之上输出精确的 CSE JSON schema。如果你的代码解析 searchInformation.totalResults、items[].title、items[].link、items[].snippet,只需更改两行代码即可继续使用。
该 actor 的架构如下:
[your app]
|
| (与 CSE 相同的 HTTP 调用形状)
v
[google-cse-replacement actor]
+-> 输入验证器(支持 q, cx, key忽略, num, start, lr, cr, safe, siteSearch, searchType)
+-> Apify GOOGLE_SERP 代理(轮换 IP 和会话,处理 Google 限速信号,返回原始 SERP HTML)
+-> SERP 解析器(提取自然结果、元数据 totalResults/searchTime、尽可能提取 pagemap)
+-> CSE schema 序列化器(以精确的 CSE v1 JSON 形状返回响应)
v
[CSE 形状的 JSON 响应]
定价对比(以每月10万次搜索的中型应用为例)
| 方案 | 月度费用 | |------|----------| | Google CSE 付费层($5/千次,单引擎上限1万次/天) | ~$500 | | google-cse-replacement($0.005/次,PPE 模式) | ~$500 | | SerpApi($75-$275/月,限制次数) | 按计划 | | Vertex AI Search($2/千次起始) | ~$200+ 扩展费 |
在10万次/月场景下,该 actor 与 CSE 费用持平。低用量时 PPE 模式更灵活:每月1千次查询仅需 $5,而非第三方服务 $75 的最低门槛。
CSE 配额现状
- 免费层:每天100次查询,太平洋时间零点重置。
- 付费层:每千次 $5,每个 CSE engine ID 每天最多1万次查询。
- 多个 engine ID:一个 Google 账户可配置多个 CSE 引擎,每个引擎享有1万次/天上限,有经验的团队通过5个引擎可理论推到5万次/天。
- 无官方并发限制,但持续超过50 QPS 会触发隐式限流。
CSE 关闭后,利用多引擎突破每日上限的技巧将彻底失效。而该 actor 没有 per-engine 上限,按查询付费,通过代理池管理并发。有效吞吐量约持续100-200 QPS,突发500+ QPS,受 Apify 层级并发限制影响。
迁移代码示例
原有CSE代码(Python):
import requests
resp = requests.get("https://www.googleapis.com/customsearch/v1", params={
"key": GOOGLE_API_KEY,
"cx": CSE_ENGINE_ID,
"q": "climate change",
"num": 10,
})
data = resp.json()
for item in data["items"]:
print(item["title"], item["link"])
迁移后代码:
from apify_client import ApifyClient
client = ApifyClient("APIFY_TOKEN")
run = client.actor("nexgendata/google-cse-replacement").call(run_input={
"q": "climate change",
"num": 10,
})
data = client.dataset(run["defaultDatasetId"]).iterate_items().__next__()
for item in data["items"]:
print(item["title"], item["link"])
下游解析的字段(items[].title、items[].link、items[].snippet、items[].displayLink、searchInformation.totalResults)均以相同方式填充。
RAG 流水线迁移示例
一种常见模式:使用 CSE 查找相关 URL 供 LLM 摘要的 RAG 流水线:
from apify_client import ApifyClient
client = ApifyClient("APIFY_TOKEN")
def google_search_for_rag(query, n=5):
run = client.actor("nexgendata/google-cse-replacement").call(run_input={
"q": query,
"num": n,
"lr": "lang_en",
})
data = client.dataset(run["defaultDatasetId
