博客星系图开发记录:聚类与可视化实践

这次把博客的文章列表从「线性列表」扩展成一个可探索的二维“星系图”。核心思路是先在本地完成向量化、聚类与降维,再把结果作为静态 JSON 输出给 Astro 前端渲染,最终实现“本地预计算 → 静态部署”的完整链路。

目标与约束

  • 平台:Astro SSG,最终部署到 Cloudflare Pages。
  • 计算:本地 GPU 进行预计算(CUDA 优先)。
  • 运行方式:生成一次 JSON,前端只做渲染。

工具结构

这次的逻辑拆成独立工具目录,方便维护和扩展:

tools/blog-clustering/
  ├─ generate_embeddings.py
  ├─ requirements.txt
  └─ README.md

模型下载缓存统一放在仓库根目录 model/(已在 .gitignore 中忽略)。

数据处理流程

流程大致分为四步:

  1. 扫描 src/content/blog 中的 Markdown / MDX
  2. 读取 Front Matter,过滤 draft 文章
  3. 使用 BAAI/bge-m3 生成向量
  4. KMeans 聚类 + t-SNE 降维,输出到 src/data/clusters.json

最终 JSON 结构如下:

[
  {
    "title": "Post Title",
    "slug": "post-slug-url",
    "date": "YYYY-MM-DD",
    "cluster": 0,
    "x": 12.34,
    "y": -5.67
  }
]

前端渲染方案

前端通过 src/components/BlogGalaxy.astro 读取 JSON,使用 ECharts 绘制散点图:

  • 隐藏坐标轴,呈现“星系”效果
  • 启用 roam 支持缩放与拖拽
  • Tooltip 展示标题与日期
  • 点击跳转:/blog/{slug}

星系图页面入口在:

/blog/galaxy

并且做了全屏展示(保留顶部导航)。

显存与性能优化

我的显卡只有 8GB 显存,所以脚本默认做了“轻量化”参数设置:

  • --batch-size 4
  • --max-length 1024
  • GPU 默认 fp16

如果显存仍然吃紧,可以进一步降到:

python tools/blog-clustering/generate_embeddings.py --batch-size 2 --max-length 512

或者干脆走 CPU:

python tools/blog-clustering/generate_embeddings.py --device cpu

使用方式

生成与渲染的完整流程:

pip install -r tools/blog-clustering/requirements.txt
npm install
npm run update-graph
npm run dev

然后访问:

http://localhost:4321/blog/galaxy

小结

星系图把文章集合从“列表”变成“空间”,让内容探索更有趣。接下来可以继续扩展,比如:

  • 在图上增加筛选(标签/时间/系列)
  • 为聚类添加自定义配色方案
  • 增加聚类说明与统计

如果你也在做内容可视化,希望这次记录能带来一些灵感。