用 Astro + Supabase 从零构建全网盘聚合搜索引擎:PGroonga 中文检索实战

创作背景

先和大家坦白:本项目是纯零基础、全程依托 AI 辅助完成的完整全栈实战项目。我本身没有任何代码经验,对 Astro 框架、Supabase 数据库、PGroonga 全文检索、服务端部署等技术栈完全零基础,也没有专业的产品设计和开发团队。

整个项目所有架构设计、数据库适配、链接检测、性能优化方案,都是在 AI 辅助下逐步打磨落地。零基础也能独立完成完整可上线的开源项目,这也是我想分享这篇实战文章的核心原因——普通人靠 AI 赋能,也能落地真实可用的全栈产品,快速积累实战开发经验。

目前项目已完整上线,欢迎大家体验、学习、支持!

[在线体验 →] (https://www.panws.net)

本文会完整分享技术实现,包括:

  • 为什么最终选了 Astro 而不是 Next.js
  • Supabase + PGroonga 实现中文全文搜索(比 LIKE 快两个数量级)
  • 多数据库适配层设计(Supabase / Neon / Turso 一键切换)
  • 静态站如何实现动态搜索
  • 链接存活检测的工程化方案和踩坑
整体架构

先看一下整体数据流,后面的实现都围绕这个架构展开:

  1. 数据采集层:从各渠道收集网盘资源链接,写入 PostgreSQL
  2. 存储检索层:Supabase PostgreSQL + PGroonga 扩展,支持中文全文索引
  3. 适配层:抽象数据库接口,支持 Supabase / Neon / Turso 三种后端
  4. 应用层:Astro 框架,静态页面 SSG + 搜索页 SSR 混合模式
  5. 检测层:异步批量检测链接存活状态,缓存结果
  6. 部署层:Vercel / Netlify 双平台适配,静态资源强缓存
技术选型

框架:Astro

最初考虑过 Next.js,最终选择 Astro,不是因为 Next.js 不好,而是这个项目的场景更匹配 Astro 的特性:

  1. SSG 优先:首页、分类页、关于页全部静态生成,只有搜索结果页用 SSR。首屏加载极快,Lighthouse 性能分 95+
  2. 零 JS 框架开销:默认输出纯 HTML,不需要为了一个搜索框加载整个 React runtime
  3. 多框架岛屿支持:需要交互的地方可以混入 React / Vue / Svelte 组件,不强制全站统一
  4. 部署灵活:Vercel / Netlify 一键切换,adapter 抽象得很干净

实际踩坑:Astro 的 SSR 模式在 Vercel 上默认用 Edge Runtime,而 PostgreSQL 连接在 Edge 环境下有兼容问题,需要显式配置为 Node.js runtime。这个后面会详细说。

import { defineConfig } from 'astro/config';
import vercel from '@astrojs/vercel/serverless';

export default defineConfig({
  site: 'https://www.panws.net',
  output: 'hybrid',  // 混合模式:默认静态,指定页面 SSR
  adapter: vercel(),
});

注意这里用的是 output: 'hybrid' 而不是 'static'。混合模式下,页面默认静态生成,只有加了 export const prerender = false 的页面才走 SSR,比纯 SSR 更省资源。

数据库:Supabase (PostgreSQL)

选择 Supabase 的核心理由:

  • 免费额度对个人项目足够(500MB 数据库 + 带宽)
  • 内置 PGroonga 扩展,中文全文搜索体验远超 PostgreSQL 自带的 tsvector
  • PostgreSQL 生态,随时可以迁移到 Neon、自托管等其他方案
  • 自带 Auth、Storage、Realtime,后续扩展功能不用额外搭服务
核心实现

1. 数据库设计

资源表是整个系统的核心,字段设计兼顾检索效率和展示需求:

CREATE TABLE resources (
  id SERIAL PRIMARY KEY,
  title TEXT NOT NULL,
  url TEXT UNIQUE NOT NULL,       -- 网盘链接
  disk_type VARCHAR(20),          -- baidu/quark/aliyun 等
  password VARCHAR(20),           -- 提取码
  size VARCHAR(50),               -- 文件大小
  source VARCHAR(100),            -- 来源频道
  is_alive BOOLEAN DEFAULT true,  -- 链接存活状态
  last_checked TIMESTAMP,         -- 上次检测时间
  created_at TIMESTAMP DEFAULT NOW(),
  updated_at TIMESTAMP DEFAULT NOW()
);

-- 常用查询索引
CREATE INDEX idx_resources_disk_type ON resources(disk_type);
CREATE INDEX idx_resources_created_at ON resources(created_at DESC);
CREATE INDEX idx_resources_is_alive ON resources(is_alive);

相比原文,我增加了 is_alivelast_checked 两个字段。存活状态不应该每次查询时实时检测(太慢),而是异步检测后写回数据库,查询时直接过滤。

2. PGroonga 中文全文搜索

这是整个项目最关键的技术选型。PostgreSQL 自带的全文搜索对中文支持很差(需要装 zhparser 或用第三方分词),而 PGroonga 对中文原生支持,且查询性能优异。

-- 启用扩展(Supabase 已内置,直接启用即可)
CREATE EXTENSION IF NOT EXISTS pgroonga;

-- 创建全文搜索索引
CREATE INDEX idx_resources_search ON resources USING pgroonga(title);

-- 搜索查询(&@~ 是 PGroonga 的全文搜索操作符,支持中文分词)
SELECT * FROM resources
WHERE title &@~ '三体'
  AND is_alive = true
ORDER BY created_at DESC
LIMIT 20;

在 Astro 页面中调用:

---
import { getDB } from '../lib/db';

const { q } = Astro.url.searchParams;
let results = [];

if (q) {
  const db = getDB();
  results = await db`
    SELECT id, title, url, disk_type, password, size, created_at
    FROM resources
    WHERE title &@~ ${q}
      AND is_alive = true
    ORDER BY created_at DESC
    LIMIT 20
  `;
}
---

<html>
  <body>
    <h1>搜索结果:{q}</h1>
    {results.map(item => (
      <div class="card">
        <h3>{item.title}</h3>
        <p>{item.disk_type} · {item.size}</p>
        <a href={item.url}>打开链接</a>
      </div>
    ))}
  </body>
</html>

性能对比:在 10 万条数据下,LIKE '%三体%' 查询耗时约 800ms-1.2s(全表扫描),而 PGroonga 索引查询稳定在 20-50ms,提升约 20-40 倍。数据量越大差距越明显。

3. 多数据库适配层

为了不被 Supabase 绑定,我设计了一层数据库适配。切换数据库只需要改环境变量,业务代码零改动。

import { createClient } from '@libsql/client';   // Turso (LibSQL)
import postgres from 'postgres';                  // Supabase / Neon

const DB_SOURCE = process.env.DB_SOURCE || 'supabase';

export function getDB() {
  switch (DB_SOURCE) {
    case 'turso':
      return createClient({
        url: process.env.TURSO_URL,
        authToken: process.env.TURSO_TOKEN,
      });
    case 'neon':
      return postgres(process.env.DATABASE_URL);
    case 'supabase':
    default:
      return postgres(process.env.SUPABASE_URL);
  }
}

这里有个设计取舍:Turso 用的是 LibSQL 协议,API 和 postgres 库不完全一样。实际项目中我在适配层上面又包了一层统一的查询接口,避免业务代码直接操作底层 client。上面的代码是简化版,展示核心思路。

4. 链接存活检测

用户最讨厌的就是点开失效链接。但实时检测每个链接太慢(一个请求至少几百毫秒),所以采用异步批量检测 + 结果缓存的方案。

async function checkLink(url, diskType) {
  const timeout = 5000;
  try {
    const controller = new AbortController();
    const timer = setTimeout(() => controller.abort(), timeout);

    const response = await fetch(url, {
      method: 'HEAD',
      signal: controller.signal,
      redirect: 'follow',
    });
    clearTimeout(timer);

    return response.status === 200;
  } catch {
    return false;
  }
}

// 批量检测(控制并发,避免被封 IP)
async function batchCheck(urls, concurrency = 5) {
  const results = [];
  for (let i = 0; i < urls.length; i += concurrency) {
    const batch = urls.slice(i, i + concurrency);
    const batchResults = await Promise.allSettled(
      batch.map(url => checkLink(url))
    );
    results.push(...batchResults.map((r, j) => ({
      url: batch[j],
      alive: r.status === 'fulfilled' && r.value,
    })));
  }
  return results;
}

5. 响应式设计

移动端流量占比超过 60%,响应式是刚需。用 CSS Grid 的 auto-fill 实现自适应卡片布局:

.results {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
  gap: 16px;
}

@media (max-width: 640px) {
  .results {
    grid-template-columns: 1fr;
  }
}
踩坑与避坑

这部分是原文没有的,但也是最有价值的。分享几个实际开发中遇到的坑:

坑 1:Vercel Edge Runtime 不支持 PostgreSQL 连接

Astro + Vercel adapter 默认用 Edge Runtime 跑 SSR 页面,但 postgres 库依赖 Node.js 的 net 模块,在 Edge 环境下直接报错。

解决方案:在 astro.config.mjs 中配置 adapter 为 serverless(Node.js runtime),或者在页面级别指定:

// src/pages/search.astro 顶部
export const prerender = false;
// Vercel 配置文件中指定该路由使用 Node.js runtime

坑 2:HEAD 请求被网盘服务器拦截

部分网盘(尤其是百度网盘)对 HEAD 请求返回 403,但 GET 请求是正常的。导致存活检测误判。

解决方案:检测失败时降级为 GET 请求,但只读取 response headers 就 abort,不下载完整内容:

async function checkLink(url) {
  // 先试 HEAD
  try {
    const res = await fetch(url, { method: 'HEAD' });
    if (res.status !== 403) return res.status === 200;
  } catch {}
  // HEAD 被拦截,降级 GET(只读 header)
  try {
    const controller = new AbortController();
    const res = await fetch(url, { signal: controller.signal });
    controller.abort(); // 拿到 header 就取消,不下载 body
    return res.status === 200;
  } catch {
    return false;
  }
}

坑 3:PGroonga 在 Supabase 免费版的限制

Supabase 免费版数据库会在 7 天无活动后暂停,而 PGroonga 索引在数据库恢复后需要重建。另外免费版的内存只有 500MB,数据量超过 50 万条时索引构建会比较慢。

建议:个人项目 10 万条以内完全够用;数据量更大时考虑升级到 Pro 版,或者迁移到自托管 PostgreSQL。

坑 4:批量检测容易触发限流

并发太高会被网盘服务器封 IP,甚至触发验证码。实际测试中,并发数控制在 3-5 比较安全,同时每个请求之间加 200-500ms 的随机延迟。

部署优化

Vercel 部署

{
  "headers": [
    {
      "source": "/_astro/(.*)",
      "headers": [
        { "key": "Cache-Control", "value": "public, max-age=31536000, immutable" }
      ]
    }
  ]
}

Netlify 部署

[build]
  command = "npm run build"
  publish = "dist"

[[headers]]
  for = "/_astro/*"
  [headers.values]
    Cache-Control = "public, max-age=31536000, immutable"

Astro 构建产物的静态资源文件名自带 hash,所以可以放心设置一年的强缓存。

性能数据
指标数值说明
首屏加载(静态页)< 1sSSG 页面,CDN 直出
搜索响应(SSR)< 500msPGroonga 索引查询 20-50ms,其余为网络和渲染
Lighthouse 性能分95+移动端测试
页面 JS 体积< 20KB零框架 runtime,只有少量交互脚本
总结

做完这个项目,有几个比较深的体会:

  1. Astro 非常适合内容型 + 轻交互的网站。SSG + 少量 SSR 的混合模式,既保证了性能,又保留了动态能力,比纯 SPA 或纯 SSR 都更合适
  2. PGroonga 是中文全文搜索的性价比之选。不需要额外部署 Elasticsearch,一个 PostgreSQL 扩展就够了,个人项目完全够用
  3. 多数据库适配不难,但要尽早做。在项目初期抽象一层,后面迁移成本几乎为零;等业务代码写满了再重构就痛苦了
  4. 链接存活检测是体验刚需,但工程化要做好。异步检测 + 结果缓存 + 并发控制,三者缺一不可
如果你也在找网盘资源,欢迎体验:

体验地址:https://www.panws.net

GitHub 仓库:https://github.com/scenlinx/panws.net


欢迎在评论区交流:你们用 Astro 做混合渲染项目时踩过哪些坑?PGroonga 和其他中文搜索方案(Meilisearch、Elasticsearch、zhparser)你们怎么选?

0
0
0
0
评论
未登录
暂无评论