把搜索数据采集封装成内部服务:从散落脚本到统一入口

项目一开始,搜索数据是「各业务各自调 API」:排名功能发一次请求、报表功能发一次、AI 接地又发一次。每个地方都要管 API key、重试、失败处理,谁改了参数别人都不知道。这篇记录我怎么把这些散落调用收拢成一个内部数据服务------统一入口、统一鉴权、统一限流,业务方只调一个接口。

先看问题:散落调用的四个症状

  1. API key 到处放:每个服务一份,轮换 key 时要逐个改
  2. 重试逻辑各写各的:A 服务重试 2 次,B 服务不重试,行为不一致
  3. 扣费重复:同一关键词不同模块各查一遍,没人做缓存
  4. 换供应商是灾难:每个调用点都要改

这些都是「把采集逻辑内联在各业务里」的必然结果。解法是抽一个内部服务:所有对搜索数据 API 的调用都走它。

设计目标

复制代码
┌────────────┐   ┌─────────────────────────────┐   ┌────────────┐
│  业务A      │──>│                             │──>│   SerpBase  │
│  业务B      │──>│   搜索数据内部服务(网关)      │──>│  (外部API)  │
│  业务C      │──>│  统一鉴权/限流/缓存/重试/审计    │──>│            │
└────────────┘   └─────────────────────────────┘   └────────────┘

关键决策:内部服务只暴露业务语义接口,不暴露供应商细节。业务方说「我要查这个关键词的排名」,不说「我要调 /google/search 带什么参数」。

第一步:统一入参接口

内部服务的接口设计成「业务意图优先」:

python 复制代码
# 内部服务:POST /api/v1/search
# 入参:{ query, market="us", lang="en", device="default", type="web" }
# type 支持 web / news / images / videos / maps

def handle_search(params):
    endpoint = {
        "web": "/google/search",
        "news": "/google/news",
        "images": "/google/images",
        "videos": "/google/videos",
        "maps": "/google/maps/search",
    }[params["type"]]

    # 统一走:鉴权 -> 缓存 -> 限流 -> 调用 -> 审计
    return serve(endpoint, {
        "q": params["query"],
        "gl": params.get("market", "us"),
        "hl": params.get("lang", "en"),
        "device": params.get("device", "default"),
    })

业务方不用知道端点叫什么、参数怎么拼,只表达「要什么类型的数据」。

第二步:把公共能力收进服务

之前散在各处的逻辑,全部收进来:

python 复制代码
def serve(endpoint, params):
    # 1. 缓存优先(复用之前的缓存层)
    key = cache_key(endpoint, params)
    hit = cache_get(key)
    if hit:
        return hit, "cache"

    # 2. 限流:按内部账号限流,防止一个业务打爆额度
    if not rate_allow(params.get("account", "default")):
        raise RateLimited("账号额度超限")

    # 3. 调用外部 API(失败自动退 credits,重试零成本)
    data = call_serpbase(endpoint, params)

    # 4. 缓存成功结果
    if data.get("status") == 0:
        cache_set(key, data, ttl_for(params["type"]))
    return data, "remote"
  • 缓存:复用之前做的缓存层,内部服务天然让多个业务共享缓存,重复扣费直接消失
  • 限流:按内部账号维度限流,防止某个业务打爆整体 QPS
  • 重试:统一重试策略,失败/超时自动退 credits 的设计让重试零成本

第三步:审计与计费归属

内部服务要解决「钱是谁花的」------每笔调用打上业务账号:

python 复制代码
def audit(account, endpoint, data):
    log(
        account=account,
        endpoint=endpoint,
        request_id=data.get("request_id"),
        credits=data.get("credits_charged"),
        elapsed_ms=data.get("elapsed_ms"),
        status=data.get("status"),
    )

落一张审计表,月底按账号聚合,谁用了多少 credits 一目了然------这比「各业务自己记」靠谱得多。

第四步:客户端一个薄封装

业务方接入时,给一个极薄的客户端(读内部的,不是外部 API):

python 复制代码
# 业务方用
import requests

def search_rank(account, query, market="us", lang="en"):
    resp = requests.post(
        "http://search-svc.internal/api/v1/search",
        headers={"X-Account": account, "X-Key": INTERNAL_KEY},
        json={"query": query, "market": market, "lang": lang, "type": "web"},
        timeout=30,
    )
    return resp.json()

内部 key 和服务地址统一管理,业务方永远不用接触外部 API key。

换供应商?改一处

内部服务最大的红利:供应商隔离 。如果哪天要换或增加备用源,只改 call_serpbase() 这一个函数,业务方无感知。这也为「多供应商容灾」铺好了路------切换是网关内部的事。

踩坑记录

坑 1:接口参数设计得太「供应商化」。 第一版内部接口直接透传 /google/search 的参数(qhlgl...),结果业务方还是得懂外部 API。改成「业务意图优先」后(query/market/lang/type),接入门槛才真正降下来。

坑 2:缓存命中率低。 各业务传的参数格式不统一(有的传 en,有的传 en-US),缓存键对不上。解决:在服务层做参数归一化,统一再算缓存键。

坑 3:没做内部限流。 一个报表任务把并发拉满,其他业务跟着变慢。加上按账号限流后,才互不干扰。

效果沉淀

收拢成内部服务后:

  • API key 只存在于一处,轮换零成本
  • 缓存让重复扣费消失(多个业务共享)
  • 审计表让 credits 归属清清楚楚
  • 换供应商改一个函数

服务化不是银弹,但对「多个业务都要搜索数据」的场景,它是性价比最高的收敛方式。外部 API 的端点、参数、计费规则细节在 SerpBase 官方文档,做服务封装时先把它读透。

有把外部数据源收拢成内部服务的同学,欢迎交流你的接口设计。