采集的数据存哪?搜索数据存储选型与实践

采集搜索数据,数据量不大时「存哪都行」,但一进入「每天采集、要查趋势、要回溯、要审计」的状态,存储就成了架构问题。这篇记录我的存储选型过程:从 SQLite 起步,到分库/分层的完整方案,以及 schema 设计和增长管理。

搜索数据的存储特点

先认清搜索数据的数据特征:

  • 写多读少:每次采集大量写入,查询相对少
  • 时间序列:几乎都是带时间戳的快照
  • 不可变:历史快照基本不修改,只有新增
  • 体积可控:单条 JSON 几 KB,但长期累积会膨胀

这些特征决定了选型方向:能支撑「时间维度的查询」+「增量增长」

阶段一:SQLite 起步(个人/小团队)

数据量不大(几万条以内),SQLite 是最佳起点——零运维、单文件、够用:

import sqlite3

con = sqlite3.connect("serp.db")
con.execute("""CREATE TABLE IF NOT EXISTS snapshots (
    keyword TEXT,
    day TEXT,
    rank INTEGER,
    title TEXT,
    url TEXT,
    snippet TEXT,
    PRIMARY KEY (keyword, day, url)
)""")
con.execute("CREATE INDEX IF NOT EXISTS idx_day ON snapshots(day)")
  • 单文件,备份就是复制文件
  • (keyword, day, url) 主键天然去重
  • 按天加索引,时间查询够快

什么时候离开 SQLite:并发写入变多(多任务同时写会锁库)、数据量到百万级、要跨机器访问。

阶段二:PostgreSQL/MySQL(团队/生产)

升到 PostgreSQL 或 MySQL,核心是「表设计按时间序列来」:

-- 时间序列表:按天分区
CREATE TABLE serp_snapshots (
  keyword text NOT NULL,
  fetched_at timestamptz NOT NULL,
  rank int,
  title text,
  url text,
  snippet text,
  PRIMARY KEY (keyword, fetched_at, url)
) PARTITION BY RANGE (fetched_at);

-- 按天分区
CREATE TABLE serp_20260821 PARTITION OF serp_snapshots
  FOR VALUES FROM ('2026-08-21') TO ('2026-08-22');
  • 按天分区:查询按天裁剪,删除旧数据就是 drop 分区,快且省
  • 索引策略(keyword, fetched_at) 复合索引覆盖「某词的趋势」查询
  • 数据与元数据分离:业务数据一张表,request_id/credits_charged 等元数据一张表(对账、排查用)

阶段三:对象存储冷归档(历史数据)

历史数据(比如 90 天前的原始响应)不适合一直在热库里占空间,挪到对象存储:

# 伪代码:历史数据归档到对象存储
def archive_old(days=90):
    cutoff = date.today() - timedelta(days=days)
    rows = db.query("SELECT * FROM snapshots WHERE fetched_at < ?", cutoff)
    with open(f"archive_{cutoff}.jsonl", "w") as f:
        for r in rows:
            f.write(json.dumps(r) + "\n")
    upload_to_object_storage(f"archive_{cutoff}.jsonl")
    db.delete("snapshots WHERE fetched_at < ?", cutoff)
  • 热库只留近期:查询快
  • 对象存储存全部历史:便宜、可恢复(配合备份篇的思路)
  • 原始响应单独归档:带 request_id 的原始 JSON 是最终证据

Schema 设计要点

不管用哪个库,三个 schema 经验:

  1. 主键包含时间(keyword, day, url)(keyword, fetched_at, url),天然防重复 + 支持时间查询
  2. 数据与元数据分离:业务字段和 request_id/credits_charged 分开,各表各用
  3. 原始响应单独存:清洗后的数据和原始 JSON 分开,重处理有原料
-- 元数据表:对账、排查、审计
CREATE TABLE billing_audit (
  keyword text, endpoint text, request_id text,
  credits numeric, elapsed_ms int, status int, ts timestamptz
);

增长管理:数据会膨胀

采集数据的增长是「只增不减」,要主动管理:

  • 按天分区 + 定期归档:热库瘦身
  • 增量版本化(增量篇):快照只存变化,省一半以上
  • 保留策略:业务数据 90 天热存、历史归档永久(备份篇的保留策略)
  • 监控表大小:超过阈值告警,别等磁盘报警

选型决策表

场景 选型
个人/小团队、几万条 SQLite
团队生产、百万级、时间查询多 PostgreSQL 按天分区
大规模/多机器 PostgreSQL + 对象存储冷归档
只需要「近期 + 简单查」 SQLite/MySQL 足够

原则:别一上来就上重存储。SQLite 起步,数据量/并发到了瓶颈再升级,升级的抓手是「分区 + 归档」。

踩坑记录

坑 1:SQLite 并发写锁死。 多任务同时写,database is locked。方案:单写者模式(所有写入走一个队列)或提前上 PostgreSQL。

坑 2:主键漏了时间。(keyword, url) 做主键,跨天的数据互相覆盖。加上 day 才正确。

坑 3:索引乱建。 一开始给所有字段建索引,写放大严重。按「查询模式」只建 (keyword, fetched_at) 复合索引。

工程清单沉淀

  1. SQLite 起步,数据量大再升级
  2. 主键含时间,天然防重复 + 时间查询
  3. 数据与元数据分离,原始响应单独归档
  4. 按天分区 + 冷归档,控制增长
  5. 保留策略明确,热库瘦身

存储是采集系统的「容器」——选对、设计对,趋势查询、对账、回溯都有据可查;选错了,数据越多越乱。

接口字段结构(哪些是业务数据、哪些是元数据)在 SerpBase 官方文档,设计 schema 时对照着分。你的采集数据存哪?评论区聊聊各自的存储方案。

posted @ 2026-09-02 17:59  蜘蛛人  阅读(11)  评论(0)    收藏  举报