2024/05/08
はじめに
個人ブログである本サイトへの検索エンジンの実装および、その発展の変遷についての話の備忘録。
post数もだいぶ増えていて、簡易なものでも検索エンジンを導入したかった。
完璧なものである必要はなく、キーワードを前提とした(ほぼ)全文検索を実現したかった。
前提
- ブログはNext.js (SSG)で構築されている
- SSGで生成した静的ファイルをGitHub Pagesでホスティングしているため、ビルド時に自由な処理を実行できるものとする
- 個人ブログなので、継続性のためにランニングコストを抑えたく、バックエンドサーバは用いない
課題解決のためのアプローチ
まずはSSGであることを活かして、 next build
のタイミングでインデックスを生成し、そのインデックスをフロント側で用いて、ローカルで検索するというアプローチを目指すことにした。
検索手法
全文検索エンジンをイメージしていたので、転置インデックスか n-gramのどちらか(もしくは両方)を用いるのが頭に浮かんだ。
それらの取捨選択についてだが、ここでの実装ではフロントがインデックスを取得することになるので、完全な全文検索よりもインデックスの軽さを重視し、n-gramは用いずに、形態素解析を用いた転置インデックスを採用することにした。
この決定により、検索できるワードは限定され、細かいニュアンスや接頭辞検索などはできず、単語検索エンジンを目指すことになった。
実装について
初期実装
形態素解析は kuromoji
を用い、その結果から stop-wordsの除去と名詞の抽出を行い、それを Record<string, {slug:string; count:number}>
のような形式でJSONに保存し、これをインデックスとして用いることにした。
これをフロントエンド側で import
し、これを利用して単語検索およびその結果表示を行うコンポーネントでそれを利用した。
動作例は以下の通り。
rust
という単語を検索した時は、そのcount
で降順ソートし表示react typescript
で検索したときはreact
とtypescript
の両方が含まれる記事を、それらのcount
の和で降順ソートし表示
この時点で静的解析エンジンの全容は完成している。
インデクシングにおけるキャッシュの導入
next build
する毎にすべてのファイルのインデックスを生成しており、ビルド時間がいたずらに増えていった。
そこで、形態素解析の結果をキャッシュし、変更がないファイルについては再解析を行わないようにすることで、ビルド時間の短縮を実現した。
インデックス分割による初期ロードの高速化
ここまで読むと想像がつくと思うが、かなり大きいJSONを初期ロード時に必ず読み込むことになってしまっている。
そしてこれは、postが増えていくにつれて増えていくことが考えられる。
初期表示時間短縮のための手法として、
- ①
{slug:string; count:number}
を{id:number, count:number}
にし、別途{id:number, slug:string}
を用意する - ② 単語ごとにインデックスを分割する
という手法が考えられた。両方とも必要な対策ではあるものの、①はより最適化を目指す場合に有効ではあるがこれだけでは記事数の増加に対処できず、②は初期ロードを抜本的に抑えつつ記事の増加による影響も少ないと考察した。
そこで、将来①をやる可能性はあるものの、まずは②を実現した。
各単語のインデックスを( rust
であれば rust.json
として)準備し、それを public/
の配下に配置し、SSGの結果とともに静的ホスティングにアップロードした。
フロントでは初期では空のインデックスを持ち、単語を入力する毎に、静的ホスティングからインデックスをFetchし、それをフロントでマージし、実装済みの検索につなげた。一度Fetchしたインデックスはフロントでキャッシュし、再度Fetchすることはない。
余分なFetchを削減
インデックスを分割したことにより、初期ロード時の負荷は減ったが、それに伴い、入力文字すべてをFetchしてしまうと、入力毎に存在しないインデックスもリクエストするようになった。
これでは、無駄なFetchが発生して嬉しくなかった。
そこで、初期ロードを遅くしてしまうが、妥協案として、「インデックスが存在する単語リスト」を初期にロードし、それを元にFetchの有無を判断することで、Fetchの回数を必要最低限に抑えた。
おわりに
通常の全文検索エンジンとは経路が違うものの、それに近い実装を経験することができた上、フロントエンドにおけるレンダリング時間の改善を意識した開発ができたので、非常に楽しかった。
より完璧な検索エンジンを用いる場合は、バックエンドサーバを用意したほうが良さそうではあるが、フロントでできる中ではきちんとした検索エンジンが完成したので満足した。