KAIROS.WORKSPACE
技术宇宙 / ONLINE
返回文章列表
前端工程intermediate7 分钟

构建期搜索索引:一个 20KB 的 JSON 为什么够用了

中小内容站不需要搜索引擎——讲清构建期生成扁平 JSON 索引 + 浏览器端过滤的设计:结构、字段取舍、更新链路与失效边界。

#搜索#架构#构建#性能
排版
字号
行宽
目录 · 5 节

几百条以内的内容站,搜索不需要 Elasticsearch,不需要 Pagefind,甚至不需要后端接口:一份构建期生成的 JSON + 浏览器里的字符串匹配就是完整答案。本文讲这个极简方案的设计与它的失效边界。

索引结构:为“匹配”而非“检索”设计

{
  "generatedAt": "2026-09-02T12:00:00Z",
  "items": [
    {
      "type": "posts",
      "title": "构建期搜索索引",
      "href": "/posts/static-search-index-design",
      "excerpt": "讲清构建期生成扁平 JSON 索引…",
      "tags": ["搜索", "架构"],
      "date": "2026-09-02"
    }
  ]
}

字段取舍的原则:

  • 只放“会被匹配”和“会被展示”的字段——标题、摘要、标签是匹配面;正文全文不进索引(体积爆炸且浏览器过滤无收益),需要正文检索再考虑分片或换方案;
  • type 支撑分组展示(文章/项目/踩坑…);
  • href 是唯一出口,不存内部 ID;
  • generatedAt 用于调试“索引是不是旧的”。

一个 20 条目、全中文字段的索引约 10–20KB gzip 后 3KB 以内——比一张缩略图小,这就是“够用”的量化依据。

生成链路:挂在 prebuild,构建即更新

{
  "prebuild": "npm run content:check && npm run search:index && npm run graph:data",
  "build": "astro build"
}

关键决策链:

  1. 数据源是内容集合本身(frontmatter + 摘要),不是渲染后的 HTML——生成脚本直接读 markdown,零抓取;
  2. 草稿过滤在生成器里做draft !== true),而不是“全量生成、前端隐藏”——搜索索引是公开产物,任何非公开内容都不该出现在字节里;
  3. content:check 排在 search:index 之前:坏 frontmatter 先失败,不产出半截索引;
  4. 索引写入 public/,随构建发布,天然带内容级更新(下次部署即最新)。

浏览器端:两级消费

命令面板(全局):fetch 一次、缓存到模块变量、includes + 逐字符模糊打分,输入防抖 120ms 内完成几百条的过滤毫无感知。

搜索页(页面):更进一步,把索引在服务端渲染时直接内联进 HTML<script type="application/json">),省掉首屏一次网络往返——静态索引 + 静态页面,本可以是一个请求。

内联与外置的选择标准:该页专属的数据(本页图谱、本页索引)内联;跨页复用的(命令面板索引)外置 + HTTP 缓存。

失效边界:什么时候这个方案会崩

诚实地列出适用上限:

信号 阈值经验 应对
条目数 > 2,000–5,000 索引体积 > 500KB,分片(按类型/首字母)或上专用索引
需要正文全文 任何规模 构建期分词(Pagefind 类)或服务端检索
需要相关性排序/拼写纠错 视体验要求 换专用方案,别手写打分
多语言词形变化 简单 includes 天然不支持,需分词器

中小站常年碰不到这些边界,而达到之前的每一分复杂度都是纯负债——这就是“20KB JSON 够用”的真正含义:它不是将就,是这个规模下的正确架构。

一个必做的护栏

索引是“第二份事实”,会和渲染产物漂移(改了 frontmatter 没重跑生成)。防线两条:prebuild 强制重跑(构建即最新);release 门禁里校验“索引条目的每个 href 都存在于站点可达路由”——本项目正是这条断言抓出过索引残留。

Conversation

评论与互动

正在加载评论…

提交后需审核,不会立即公开。

Keep exploring

继续探索