serpbase + GraphQL wrapper 实战:让 SERP 数据走 GraphQL schema

背景

前端不用解析 serpbase 的 JSON 嵌套结构,直接走 GraphQL 查字段更省事。5 步搭一个 GraphQL wrapper,接前端 / AI agent。

1. 准备工作

bash 复制代码
pip install fastapi strawberry-graphql uvicorn requests

2. 定义 GraphQL schema

python 复制代码
import strawberry
import requests
import os
from typing import List, Optional

SERPBASE_KEY = os.environ["SERPBASE_KEY"]

@strawberry.type
class SerpSource:
    rank: int
    title: str
    link: str
    snippet: Optional[str] = None
    date: Optional[str] = None

@strawberry.type
class PaaItem:
    question: str
    answer: Optional[str] = None
    source: Optional["SerpSource"] = None

@strawberry.type
class SerpResult:
    query: str
    gl: str
    hl: str
    organic: List[SerpSource]
    people_also_ask: List[PaaItem]
    total_results: int
    elapsed_ms: int
    request_id: str

@strawberry.type
class Query:
    @strawberry.field
    def serp_search(
        self,
        query: str,
        gl: str = "us",
        hl: str = "en",
        num: int = 5,
    ) -> SerpResult:
        """搜 Google SERP"""
        r = requests.post(
            "https://api.serpbase.dev/google/search",
            headers={"X-API-Key": SERPBASE_KEY},
            json={"q": query, "gl": gl, "hl": hl, "num": num},
            timeout=10,
        )
        r.raise_for_status()
        data = r.json()

        return SerpResult(
            query=data["query"],
            gl=data.get("gl", "us"),
            hl=data.get("hl", "en"),
            organic=[
                SerpSource(
                    rank=item["rank"],
                    title=item["title"],
                    link=item["link"],
                    snippet=item.get("snippet"),
                )
                for item in data.get("organic", [])
            ],
            people_also_ask=[
                PaaItem(question=q.get("question", ""))
                for q in data.get("people_also_ask", [])
            ],
            total_results=len(data.get("organic", [])),
            elapsed_ms=data.get("elapsed_ms", 0),
            request_id=data["request_id"],
        )

3. 启动 GraphQL 服务

python 复制代码
from fastapi import FastAPI
from strawberry.fastapi import GraphQLRouter

schema = strawberry.Schema(Query)
graphql_app = GraphQLRouter(schema)
app = FastAPI()
app.include_router(graphql_app, prefix="/graphql")

4. 测试查询

graphql 复制代码
# 简单查询
query {
  serpSearch(query: "best serp api", gl: "us", num: 5) {
    query
    totalResults
    elapsedMs
    organic {
      rank
      title
      link
    }
  }
}

返回:

json 复制代码
{
  "data": {
    "serpSearch": {
      "query": "best serp api",
      "totalResults": 5,
      "elapsedMs": 1240,
      "organic": [
        {"rank": 1, "title": "...", "link": "..."},
        {"rank": 2, "title": "...", "link": "..."}
      ]
    }
  }
}

5. 5 个工程细节

细节 1:错误处理

python 复制代码
@strawberry.type
class SerpError:
    code: int
    message: str

@strawberry.type
class Query:
    @strawberry.field
    def serp_search(self, query: str) -> SerpResult | SerpError:
        try:
            r = requests.post(...)
            r.raise_for_status()
            return parse(r.json())
        except Exception as e:
            return SerpError(code=500, message=str(e))

细节 2:缓存

python 复制代码
from functools import lru_cache

@strawberry.type
class Query:
    @strawberry.field
    def serp_search(self, query: str, gl: str = "us") -> SerpResult:
        cache_key = f"serp:{query}:{gl}"
        if cache_key in CACHE:
            return CACHE[cache_key]
        result = call_serpbase(query, gl)
        CACHE[cache_key] = result
        return result

细节 3:subscription(实时)

python 复制代码
import asyncio

@strawberry.type
class Subscription:
    @strawberry.subscription
    async def serp_changes(self, query: str) -> SerpResult:
        while True:
            await asyncio.sleep(300)  # 5 分钟查一次
            yield call_serpbase(query)

细节 4:权限控制

python 复制代码
def require_api_key(info):
    api_key = info.context["request"].headers.get("X-API-Key")
    if api_key != os.environ["INTERNAL_API_KEY"]:
        raise Exception("Unauthorized")

schema = strawberry.Schema(query=Query, mutation=Mutation, mutation=require_api_key)

细节 5:批量查询

python 复制代码
@strawberry.type
class Query:
    @strawberry.field
    def batch_serp_search(
        self,
        queries: List[str],
        gl: str = "us",
    ) -> List[SerpResult]:
        return [self.serp_search(q, gl) for q in queries]

6. 与 REST 对比

维度 GraphQL REST
前端请求 一次,字段自选 多次,固定结构
后端实现 复杂 简单
文档 自动 手写
类型安全 ✓(schema 强)
性能 单次请求多个字段 多次请求

7. 实战数据(我项目 30 天)

指标 数值
启动时间 5 分钟
Schema 字段数 15
查询平均延迟 1.5s
SerpBase 月成本 $0.6
端到端测试 100% 通过

8. 配合 AI agent

AI agent 用 GraphQL 比 REST 更高效:

  • LLM 一次 query 拿到所有字段
  • 不用先 GET 一遍看有哪些字段
  • 节省 token
python 复制代码
import anthropic

def agent_with_graphql(query):
    client = anthropic.Anthropic()

    # LLM 调用 GraphQL query(自选字段)
    response = client.messages.create(
        model="claude-sonnet-4-5",
        tools=[{
            "name": "search",
            "input_schema": {
                "type": "object",
                "properties": {
                    "query": {"type": "string"},
                },
            },
        }],
        messages=[{"role": "user", "content": query}],
    )

    # 执行 GraphQL query(LLM 生成的)
    gql_query = response.content[0].input["query"]
    data = requests.post("http://localhost:8000/graphql", json={
        "query": f"{{ serpSearch(query: \"{gql_query}\") {{ organic {{ rank title }} }}",
    }).json()

    return data

小结

serpbase + GraphQL wrapper 5 步搭好:

  1. 定义 schema(strawberry)
  2. resolver 调用 serpbase
  3. 启动 FastAPI + GraphQLRouter
  4. 客户端用 GraphQL query
  5. AI agent 也能用

30 行代码 + 5 分钟,前端不用解析嵌套 JSON,直接按字段拿。serpbase auto-refund 100% 触发,GraphQL wrapper 失败返 0 数据 + 错误信息,前端不崩。

相关推荐
小白勇闯网安圈1 小时前
Django 响应对象、文件上传与类视图
数据库·django·sqlite
努力的小雨2 小时前
同一份 SQL 跑多套环境:Ksql 变量怎么用才安全
数据库
科技绘图3 小时前
快鲸GEO vs 传统AI搜索优化:全链路自动化与高效内容生产在转化闭环上的对比
数据库·人工智能·自动化
ltl3 小时前
数据库作为 LLM 记忆体:语义缓存、RAG 与一致性
数据库
ltl3 小时前
WAL 与崩溃恢复:ARIES 协议怎么跑通
数据库
ltl3 小时前
TEE 数据库:EnclaveDB、Oblivious 原语与机密 SQL
数据库
麻瓜code4 小时前
【Mysql】重新学一遍 SQL 执行顺序
数据库·sql
小田学Python5 小时前
重新定义 Agent:为什么大模型不能直接干活,需要一层“壳”
大模型·api·ai agent
这个DBA有点耶5 小时前
数据库一体机架构演进:从硬件堆叠到软硬深度耦合
服务器·网络·数据库·硬件架构·运维开发·database·数据库架构
安_6 小时前
RAG的向量数据库:为LLM提供语义搜索能力
数据库