ブログをGatsbyに移行してからずっとサボっていたサイト内検索機能ですが、ゴールデンウィークで時間があったので実装してみました。
なお、当記事はAlgolia自体の知識がある前提で書いてます。
Algolia自体の説明やMiddleman製サイトに検索機能を導入する話に関してはこちらの記事をご参照ください。
当記事はAlgoliaを用いた実現手段の紹介になります。他の手段(JSONファイルに記事データを出力してFuse.js等を用いて全文検索etc)との比較検討などはしていません。
当記事で使用してる技術に関して、Gatsbyのドキュメントでも紹介されてるのでそちらも併せてご参照ください。
Middleman時代はフルスクラッチで作っていたのですが、スクラッチだと記事の内容も実装の手間も無駄にかさんでしまうので、楽に導入できるよう今回はAlgolia公式のライブラリを2つ使用します。
これらを利用することで導入コストをかなり抑えられました。
gatsby-plugin-algoliaのリポジトリはこちらです。
— algolia/gatsby-plugin-algolia: A plugin to push to Algolia based on graphQl queries
記事執筆時のREADMEではサイト内のすべてのページを登録するような処理になっていますが、ブログ記事だけを登録したいのでGraphQLのクエリを変えました。
当サイトではこんな感じで設定にしてあります。
transformerオプションには「オブジェクトの配列を返す必要がある」「配列の各要素はid
という名前のフィールドをもつ必要が有ります」という2点の制約が有ります。
id
フィールドはすべての記事において一意な任意の文字列を指定できます。個人的に自動生成される値ではなくcontrollableな値が好きなのでslugをidにしました。
それ以外のフィールドや構造に制約はなく、返却したオブジェクのすべてのフィールドがAlgoliaに登録されます。
記事の本文を検索対象に含めたい場合、1objectあたりのサイズ制限を突破するために複数のチャンクにちぎって送信するなどの工夫が必要ですが、そもそも本文を検索対象に含めることはAlgoliaが非推奨だと言ってるので本文を含めないように設定しました。
- In most cases, having bigger objects is a sign that you’re not using Algolia at its full capacity:
- Having very big chunks of text is usually bad for relevance, because most objects end-up having a lot of similar words, and they will match with a lot of irrelevant queries
- It’s often better to de-duplicate big objects into several smaller ones
— Is there a size limit for my index/records? | Basics FAQ | Docs Algolia
設定を済ませてgatsby build
を実行すると、記事の一覧がAlgoliaに登録されます。こんな感じ。
うまく行かない場合、transform関数がオブジェクトの配列を返せているか確認しましょう。
Algoliaに記事を登録して検索できるようになったので、フロントエンドを作ります。
検索フォームや検索結果の表示にはReact InstantSearchを利用します。
InstantSearch.jsというAlgoliaが提供している検索用UI用のコンポーネントのReact版です。
InstantSearchを利用することで、Algoliaの検索ロジックや状態管理をスクラッチで実装する必要がなくなり、検索UIと検索結果の見た目を作ることだけに注力できます。
(VueやAngularなどの各種フレームワークやPure JavaScript、webのみならずiOSやAndroid、React Nativeでも使えるように複数バージョンが提供されており、今回はReact版を採用しています)
公式のLive Demo (codesandbox)を見てみると、複雑な検索UIにも対応できるようです。
React版を使って実装してみて、UIの自由度やカスタマイズ製も高くかなり使いやすかったので採用しました。
当サイトの右上にある検索窓が触れるデモです。
特にチューニングやパラメータの最適化などしていませんが、えげつない速度で検索結果が表示されます。スクショとしてはこんな感じです。
コードはこちら(OnSiteSearchコンポーネント)を読むとInstantSearchコンポーネントとwidgetsの使い方がわかると思います。
https://www.algolia.com/doc/api-reference/widgets/instantsearch/react/
これを読みつつお好みでウィジェットを足していく感じになると思います。
基本的にドキュメント見ながらやるだけなんですが、実装してて思った注意事項を以下に残します。
ローカルと本番環境で検索対象のインデックスを分けるためにAPI KEYなどを環境変数に逃がしたくなると思います。
GatsbyにはwebpackのDefinePluginが入っているので、ちょっと設定をしてあげれば各設定値を環境変数に逃がせます。
デフォルトのウィジェットだけで検索UIを組むと、何も入力してない状態でも検索結果が表示されていると思います。
サイト内検索では何か入力したときだけ結果を表示するためカスタマイズしました。
connectStateResults
というHoCがexportされているので、それを利用してStateに対する見た目を制御できます。詳しくはドキュメント参照。
なお、こちらのドキュメントに検索結果が空の場合、エラーハンドリングについても記載が有ります。
— Conditional display | Building Search UI | Guide | Algolia Documentation
こちら(OnSiteSearchHitListコンポーネント)のように実装しました。
InstantSearchのPanel
コンポーネントを利用してヘッダとフッタを作り、フッタにPoweredBy
コンポーネントを表示してCSSでちょっと見た目を調整しました。
CSSに関してはこちらをご参照ください。
Gatsby製サイトに検索機能を追加するときの参考になれば幸いです。
スクラッチで作るよりはるかに簡単に導入できた(デバッグ含めて3hくらい)ので、Gatsbyのようにエコシステムが充実しているものに乗っかるのは正義だなと思いました。