静态站搜索的三种做法,以及它们各自真实的上限
静态站没有后端,所以搜索必须换个地方放。只有三个位置:
放进浏览器、放进构建产物、放到另一台服务器上。
选哪种,取决于你愿意接受哪一种上限。
一、放进浏览器:把内容全量打到前端#
最简单:构建时把全部文章导成一个 JSON,页面加载后在前端过滤。
const results = all.filter(p => p.title.includes(q) || p.body.includes(q));
上限在「全量」两个字上。 索引就是全部正文,文章一多,用户为了搜一个词要下载整站内容。
50 篇中等长度的文章大约 1–2 MB。1000 篇就是几十 MB——搜索框还没用上, 流量已经烧完了。
它适合几十篇以内的站点。超过这个量级,你会开始想「能不能只加载摘要」, 而那正是第二种方案。
二、放进构建产物:只下载倒排索引#
构建时切词、建倒排索引,把索引作为静态资源发布。用户只下载索引,不下载正文; 命中之后再按需取片段。
- Fuse.js 是纯前端模糊匹配,仍然需要你把待搜内容喂给它——所以它解决的是 「怎么匹配」,没解决「内容放哪」
- Pagefind 是构建后跑的索引器,直接读你的 HTML 产物。它把索引切成小块, 搜索时只加载相关分片——这才是真正的「索引比正文小」
Pages/Netlify 这类托管上,Pagefind 是当前最省事的选择:一条命令, 零后端,还能按语言分别处理。
上限在分词上。
Pagefind 对中文不做词干提取——中文本来也没有词形变化,所以这不影响正确性。 真正的影响是召回:它按字/词切分建索引,你搜「排版」能命中包含「排版」的段落, 但搜「网页排版」不一定能命中只写了「排版」的地方。
英文靠词干回退(running ↔ run)缓解这类问题,中文没有对应机制。 这是静态站搜索的固有限制,不是某个库的缺陷。
三、外挂服务:Algolia、Meilisearch 这类#
把索引放到一台真的服务器上,查询走网络。
功能上限最高:分词器可换、同义词、拼写纠错、按字段加权、结果高亮、 搜索分析——都能做。
代价也很清楚:
- 多了一个要运维的东西。 静态站的全部好处就是「没有服务器」, 为了搜索把它加回来,这笔账要自己算
- 多了一个会挂的东西。 搜索服务挂了,站还在,但搜索框废了
- 多了一个花钱的地方。 免费额度通常按「记录数 × 查询次数」计
它适合「搜索是核心功能」的站点——文档站、电商、知识库。 对个人博客,通常是过度配置。
怎么选#
| 文章数 | 建议 | 理由 |
|---|---|---|
| < 50 | 第一种就够 | 全量 JSON 也就几百 KB,省一套索引流程 |
| 50 – 几千 | 第二种 | 索引与正文分离,零运维 |
| 搜索本身是产品 | 第三种 | 只有它能在分词这一层做文章 |
判断的关键不是功能表,是「索引存在哪」:在用户浏览器里、在构建产物里、 还是在别人的服务器上。三种位置对应三种上限,没有例外。
顺带一句:搜索需要 JavaScript,这是要认的#
前两种方案都跑在浏览器里,所以没有 JS 就没有搜索。
这不是可以糊弄过去的——正确的做法是给一个 <noscript> 出口,
让关掉 JS 的读者至少能走到归档页,而不是对着一个永远转圈的输入框。
相关条目:内容协商。
参考#
- Pagefind 官方文档(索引分片与多语言处理)
- Fuse.js 官方文档(模糊匹配算法与阈值)
- 本文提到的中文召回限制来自本站在 2026-09 的实测:搜「排版」7 条命中, 搜「行高」6 条命中,而搜「网页排版」只有 4 条——少掉的那几条正是 正文里只写了「排版」、没写完整四个字的地方
复现方式:
npm run build之后到/search/搜这三个词。npm run dev里搜不到任何东西——索引是构建期(Pagefind)生成的, 开发服务器没有它。这个数字也会随内容增删而变化,上面记的是当时的快照。
这一页也有 markdown 版本:/letterpress/static-site-search.md
直接打开上面的 .md 链接即可读取;在支持内容协商的部署平台上, 也可以对页面 URL 发送 Accept: text/markdown。