静的サイトの全文検索:索引はビルド時に、実行時にサーバーなし
静的サイトの検索は Algolia を繋がなくても作れます。索引をビルド時に生成し、検索は完全にブラウザ内で実行する——代償はビルドが数秒伸びるだけです。
静的サイトで必ず詰まる要件が検索です。サーバーが無いのに、誰がクエリを処理するのか。 私の答えは「誰も」です。索引をビルド時に作っておき、照合はブラウザにやらせます。使うのは Pagefind、コマンド 1 つ、バックエンドなし。
検索サービスを繋がない理由
まず切り捨てた選択肢から。
| 方法 | 代償 |
|---|---|
| ホスト型検索(Algolia / DocSearch など) | アカウント、クォータ、全ページへの第三者スクリプト、そして入力のたびに他人のサーバーを通る |
| JSON 索引を 1 つ配ってブラウザで絞る | 訪問者が本文全体をダウンロードする。10 本なら平気、100 本なら数 MB |
| 検索を提供しない | ナビゲーションだけが頼りになる |
最初の 2 つに共通する欠点はコストを読者に押し付けることです。片方は第三者リクエストとして、もう片方は下り通信として。このサイトの本文はビルドが終わった時点ですでに静的ファイルです。索引だけ別のタイミングで作る理由がありません。
Pagefind の位置
パイプラインのかなり後ろです。
npm run build
# = astro build && pagefind --site dist
Astro が HTML を出し切ったあと、Pagefind が dist/ を走査し、細かく刻んで圧縮し、分割して dist/pagefind/ に書き出します。ブラウザは後から小さなランタイムを読み、実際に必要な断片だけを取得します。
つまりこれは検索サービスではなく、ビルド成果物の一部です。HTML・CSS・画像と一緒にアップロードすれば終わりです。
ビルドログの最後はこうなります。
Indexed 3 languages
Indexed 94 pages
Indexed 2883 words
Indexed 3 filters
何を索引し、何を索引しないか
既定ではページ上の読めるテキストを全部飲み込みますが、たいていそれは望みではありません。ヘッダー・フッター・言語切り替えは全ページに繰り返し出るので、索引に入れると結果が「ホーム」「ブログ」で埋まります。
属性 2 つで足ります。
data-pagefind-body:索引の範囲をこの中に限定。ヘッダーとフッターは自動的に外れます。data-pagefind-filter="lang:zh":ページにラベルを付け、検索時に絞り込めます。
このサイトでは 2 つのレイアウトが <main> に両方を付けています。
<main id="main" data-pagefind-body data-pagefind-filter="lang:zh">
Indexed 3 filters の行が、その 3 言語分のファセットです。
検索の質は、何を索引したかと同じくらい、何を索引しなかったかで決まります。
覚えておくべき 4 つの罠
① npm run dev には存在しません。 索引はビルド成果物で、dev は Astro しか動かしません。dev で何も出ないのは期待どおりで、バグではありません。検証には build のあとの preview が必要です。私はしばらく検索が壊れたと思って調べていました。
② 順序は入れ替えられません。 Pagefind は dist/ を読むので、Astro のビルドの後に走る必要があり(&& を間違えないこと)、しかも最終的な HTML しか見ません。ソースで見つからないマーカーが、出力ではすでにインライン化されていることがあります。
③ JavaScript が描画するページはほぼ索引されません。 Pagefind は HTML ファイルを読みます。ゲームとツールの本文はクライアント側でマウントされるので、索引できるのは静的な殻だけです。検索可能にするなら重要なテキストをビルド時に HTML へ出す必要があります——アプリページを「静的殻 + 静的コンテンツ」にした理由の 1 つです。
④ 成果物を grep してマーカーを探さないこと。 Astro は小さなスタイルとスクリプトを HTML にインライン化するので、圧縮後の出力は書いたソースとは別物です。検証は実際に build して preview するしかありません。
向かない場面
- 言語をまたぐ横断検索(1 クエリで中英日の結果): Pagefind は言語ごとに索引を持ちます。統合は自前です。
- 語幹処理・同義語・誤字許容: 前方一致であり、意味論ではありません。
- ユーザー投稿をリアルタイムに索引: そもそも静的サイトの仕事ではありません。
私は 3 つとも必要ありません。このブログは数十本の手書き記事で、そう頻繁には変わりません。その規模では「ビルド時に索引を作る」が唯一まともな形です。
サーバーを通さずに答えられるクエリなら、サーバーに触れるべきではありません。

コメント
…