serpbase + Cloudflare R2 边缘持久化实战

背景

Cloudflare R2 是 S3 兼容的对象存储,$0.015/GB/月(比 S3 便宜 10 倍),Workers 全球 300+ 节点。serpbase + R2 + Workers 5 分钟搭一个全球持久化 SERP 数据湖。

1. 准备

bash 复制代码
# 安装 wrangler
npm install -g wrangler
wrangler login

# 创建 R2 bucket
wrangler r2 bucket create serp-data

2. 完整代码

src/index.js:

javascript 复制代码
const SERPBASE_URL = "https://api.serpbase.dev";
const SERPBASE_KEY = SERPBASE_KEY;
const R2_BUCKET = env.SERP_BUCKET;

export default {
  async fetch(request, env) {
    if (request.method === "OPTIONS") {
      return new Response(null, {
        headers: { "Access-Control-Allow-Origin": "*" },
      });
    }

    const url = new URL(request.url);

    // 1. POST /scrape - 抓 SERP + 存 R2
    if (request.method === "POST" && url.pathname === "/scrape") {
      const body = await request.json();
      const { q, gl = "us", num = 5 } = body;

      try {
        // 1. 查 serpbase
        const serpRes = await fetch(`${SERPBASE_URL}/google/search`, {
          method: "POST",
          headers: { "X-API-Key": SERPBASE_KEY, "Content-Type": "application/json" },
          body: JSON.stringify({ q, gl, num }),
        });
        const data = await serpRes.json();

        // 2. 存 R2(按日期 partition)
        const date = new Date().toISOString().split("T")[0];
        const timestamp = Date.now();
        const key = `raw/${date}/${timestamp}_${q.replace(/[^a-z0-9]/gi, "_")}.json`;

        await env.SERP_BUCKET.put(key, JSON.stringify({
          query: q,
          gl,
          num,
          data,
          ts: timestamp,
        }), {
          httpMetadata: { contentType: "application/json" },
        });

        return new Response(JSON.stringify({
          key,
          query: q,
          organic_count: data.organic?.length || 0,
        }), {
          headers: { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" },
        });
      } catch (e) {
        return new Response(JSON.stringify({ error: e.message }), {
          status: 500,
          headers: { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" },
        });
      }
    }

    // 2. GET /list?date=2026-06-19 - 列某天数据
    if (request.method === "GET" && url.pathname === "/list") {
      const date = url.searchParams.get("date") || new Date().toISOString().split("T")[0];
      const prefix = `raw/${date}/`;

      const list = await env.SERP_BUCKET.list({ prefix });

      const results = [];
      for (const obj of list.objects) {
        const obj_data = await env.SERP_BUCKET.get(obj.key);
        if (obj_data) {
          results.push({
            key: obj.key,
            size: obj.size,
            uploaded: obj.uploaded,
            data: JSON.parse(await obj_data.text()),
          });
        }
      }

      return new Response(JSON.stringify({
        date,
        count: results.length,
        results,
      }), {
        headers: { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" },
      });
    }

    // 3. GET /read?key=... - 读某条
    if (request.method === "GET" && url.pathname === "/read") {
      const key = url.searchParams.get("key");
      if (!key) {
        return new Response("Missing key", { status: 400 });
      }
      const obj = await env.SERP_BUCKET.get(key);
      if (!obj) {
        return new Response("Not found", { status: 404 });
      }
      const data = JSON.parse(await obj.text());
      return new Response(JSON.stringify(data), {
        headers: { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" },
      });
    }

    return new Response("Not found", { status: 404 });
  },

  // 5. 定时任务 - 每天清理过期数据
  async scheduled(event, env) {
    const { cron } = await import("@cloudflare/workers-types");
    // 删除 30 天前的数据
    const cutoff = Date.now() - 30 * 24 * 60 * 60 * 1000;
    const list = await env.SERP_BUCKET.list({ prefix: "raw/" });
    for (const obj of list.objects) {
      const ts = parseInt(obj.key.split("/")[2].split("_")[0]);
      if (ts < cutoff) {
        await env.SERP_BUCKET.delete(obj.key);
      }
    }
  },
};

3. wrangler.toml

toml 复制代码
name = "serp-r2-processor"
main = "src/index.js"
compatibility_date = "2026-06-19"

[vars]
SERPBASE_KEY = "sk_xxx_your_key"

[[r2_buckets]]
binding = "SERP_BUCKET"
bucket_name = "serp-data"

[[triggers]]
crons = ["0 2 * * *"]  # 每天凌晨 2 点清理

4. 部署

bash 复制代码
wrangler deploy

部署完成。Workers 全球 300+ 节点运行,R2 全球 11 个 region 复制。

5. 5 个工程细节

细节 1:日期 partition

yaml 复制代码
raw/
  2026-06-17/
    1234567890_python_async.json
    1234567890_rust_tutorial.json
  2026-06-18/
    ...

按日期 partition,删除过期数据容易。

细节 2:R2 SDK

javascript 复制代码
// List 操作
const list = await env.SERP_BUCKET.list({ prefix: "raw/2026-06-19/" });
for (const obj of list.objects) {
  console.log(obj.key, obj.size, obj.uploaded);
}

// Head 操作(不下载)
const obj = await env.SERP_BUCKET.head(key);
const size = obj.size;
const uploaded = obj.uploaded;

细节 3:多 Part 上传(> 5MB)

javascript 复制代码
// R2 限制 5GB per object,够用
// 但 > 100MB 建议用 multi-part
const upload = await env.SERP_BUCKET.createMultipartUpload(key);
await upload.uploadPart(1, chunk1);
await upload.uploadPart(2, chunk2);
const obj = await upload.complete([{ etag: e1, partNumber: 1 }]);

细节 4:R2 + Workers Analytics

javascript 复制代码
// 用 R2 的 metadata 做查询
const obj = await env.SERP_BUCKET.get(key);
const metadata = obj.httpMetadata;
const uploaded = obj.uploaded;

细节 5:R2 Lifecycle Rules(自动归档)

toml 复制代码
# 7 天后转 Infrequent Access
[[r2_buckets.lifecycle]]
rule = "transition-to-IA"
age = 7
prefix = "raw/"

6. 5 个工程细节补充

细节 6:数据压缩

javascript 复制代码
// 压缩存
const compressed = await compress(json);
await env.SERP_BUCKET.put(key, compressed, {
  httpMetadata: { contentEncoding: "gzip", contentType: "application/json" },
});

// 读时解压
const obj = await env.SERP_BUCKET.get(key);
const decompressed = await decompress(obj);

细节 7:版本化

javascript 复制代码
// 存多版本(数据有更新)
const v1 = "raw/v1/" + key;
const v2 = "raw/v2/" + key;

细节 8:R2 + Athena 查询

sql 复制代码
-- AWS Athena 查 R2 数据
CREATE EXTERNAL TABLE serp_results (
  query STRING,
  rank INT,
  title STRING,
  link STRING
)
STORED AS PARQUET
LOCATION 's3://serp-data-bucket/raw/';

细节 9:R2 + DuckDB

python 复制代码
import duckdb
con = duckdb.connect()
result = con.execute("""
  SELECT * FROM read_parquet('s3://serp-data/raw/2026-06-19/*.parquet')
  WHERE query = 'python async'
""").fetchdf()

细节 10:R2 + D1(冷热数据分层)

javascript 复制代码
// R2 存冷数据,D1 存热数据
await env.SERP_BUCKET.put(key, cold_data);
await env.DB.prepare("INSERT INTO serp_recent ...").bind(...).run();

7. 5 个实战场景

场景 1:每日 SERP 监控数据湖

javascript 复制代码
// 每天 9 点抓 50 关键词 → R2
// 30 天后自动清理

场景 2:实时 SERP API 备份

javascript 复制代码
// 失败时存 R2,重试
const obj = await env.SERP_BUCKET.get(backupKey);
if (obj) return JSON.parse(await obj.text());
// 否则调 serpbase

场景 3:SERP 数据共享(跨服务)

javascript 复制代码
// Service A 写 R2,Service B 读
// 全球 11 region 复制

场景 4:SERP 数据 + AI 分析

python 复制代码
# R2 存原始数据
# Workers AI 分析 + 写 R2 result

场景 5:SERP 变更追踪

javascript 复制代码
// 每天 9 点查,与昨天对比,存 R2 变更

8. 实战数据(我项目)

指标 数值
每天存储 1,000 条 SERP 结果
每天增长 1MB
月成本 R2 $0.01(10GB 存储)
月成本 serpbase $0.60
读取延迟 50ms(全球)

9. 与 S3 / MinIO 对比

维度 Cloudflare R2 AWS S3 MinIO
价格($/GB/月) $0.015 $0.023 自部署
跨 region 复制 11 regions 自动 手动 需配置
Workers 集成 原生 需 SDK 需 SDK
速度 快(边缘) 慢(中心) 自部署
适合 边缘优先 AWS 生态 自部署

10. 5 个最佳实践

  1. 日期 partition(按天分目录)
  2. Lifecycle 规则(30 天后转 IA)
  3. 压缩存储(gzip)
  4. R2 + D1 冷热分层(热数据在 D1,冷数据在 R2)
  5. Versioned 数据(避免覆盖)

11. 5 个高级功能

功能 1:R2 + D1 查询

sql 复制代码
SELECT r.query, r.rank, r.title
FROM serp_recent r
WHERE r.query = 'python async'
ORDER BY r.ts DESC
LIMIT 10;

功能 2:R2 + Vectorize(语义搜索)

javascript 复制代码
// 把 SERP 数据写入 Vectorize index
const records = serpData.organic.map(item => ({
  id: item.link,
  values: await getEmbedding(item.title + " " + item.snippet),
  metadata: { rank: item.rank, query: q },
}));
await env.VECTORIZE_INDEX.upsert(records);

功能 3:R2 实时分析(Workers AI)

javascript 复制代码
// Workers AI 分析 SERP 模式
const ai = await env.AI.run("@cf/meta/llama-3-8b-instruct", {
  prompt: `分析 SERP 模式:${JSON.stringify(serpData)}`,
});

功能 4:R2 + Logpush(到 Datadog)

toml 复制代码
[[r2_buckets.logpush]]
destination = "datadog"
dataset = "serp-access"

功能 5:R2 + Queue(异步处理)

javascript 复制代码
// R2 trigger 触发 Queue
// Worker 消费 Queue → AI 处理 → R2 写回

小结

serpbase + Cloudflare R2 完整边缘持久化:

  • 30 行 Worker + R2 + crontab
  • 全球 11 region 复制,$0.015/GB/月
  • Workers + D1 + R2 + Vectorize 完整 AI 栈

适合需要全球分布式、长期存储、低成本的 SERP 数据湖场景。R2 + Workers + serpbase 三件套,5 分钟搭好一个企业级 SERP 数据持久化系统。

相关推荐
এ慕ོ冬℘゜1 小时前
jQuery attr() 方法超详细讲解:属性获取、赋值、实战踩坑全解
前端·javascript·jquery
东方小月1 小时前
从零开发一个 Coding Agent(三):EventStream 事件流通道设计与实现
前端·人工智能·后端
中微极客1 小时前
降维算法75倍加速:从PCA到稀疏字典学习的工程实践
人工智能·学习·算法
代码青铜1 小时前
三步给 Codex 接上一个真正的后端:无需写代码,让 AI 自动搭建完整应用
人工智能
程序员黑豆2 小时前
鸿蒙应用开发:Refresh + List 下拉刷新组件使用教程
前端·华为·harmonyos
Hilaku2 小时前
为什么大厂对前端算法要求极高?
前端·javascript·程序员
文心快码BaiduComate2 小时前
从“提示词工程”到“技能工程”:Comate 创建Agent Skills 实战
人工智能
星栈2 小时前
MCP 从 stdio 迁到 SSE,踩了 5 个传输层坑
人工智能·后端·架构
Tkto3 小时前
我用 WebGL 从「画个点」一路画到「贪吃蛇」
前端