GatsbyJSブログに検索を実装してみた
あけましておめでとうございます。今回はGatsbyでつくってる当ブログに検索を実装しました。 なぜ自分で実装したのか、Gatsbyの構成でどういう実装を選択したのかを書きました。
つくったもの
Gatsby.jsのブログに検索を実装してみました。あけおめ実装です。https://t.co/hvz25scB7r pic.twitter.com/qSLuKZTx32
— もっと@GatsbyJSおじさん (@mottox2) January 1, 2019
プルリクエスト
- 検索機能 by mottox2 · Pull Request #27 · mottox2/website
- ミニマムリリース分
- 検索の改善 by mottox2 · Pull Request #28 · mottox2/website
- リリース後に気になった、クリック領域やアニメーションの修正。
結構 :thinking: なコードになってますが、我こそはと思う方はPRをください。待ってます。
自分で実装する理由
そもそもGatsbyやJAMstackといった文脈では、状態を持つサーバーを持たないため様々なバックエンドサービスを利用することが想定されており、検索においてはAlgoliaが利用されることが多いです。
Algoliaは検索機能を提供するSaaSです。 検索機能というとElastic Searchを利用することが多いかもしれませんが、運用がしんどいなどの問題がありました。保守する人がいるなら別ですが、手軽に導入するようなものではありませんでした。 簡単に言うと、AlgoliaはアップロードのAPIを利用しデータをインポートすると検索用のAPIで検索が利用できるというサービスです。 非常に簡単に利用できることからStripeのDoc、React公式, Gatsby公式などで利用されています。
結論からいうと今回はAlgoliaの採用を見送りました。 Algoliaの価格ページを見てもらうとわかるのですが、一番安いプランで$35/monthです。個人のブログに導入する価格ではないと思います。(オープンソースのために無料で開放してくれているみたいですが、個人ブログはプランに該当するかわからないので諦めました)
そもそもGatsbyには高速・安全などのメリットがありますが、これは企業が感じるメリットです。 今、個人でGatsby(JAMstack)なブログを利用しているエンジニアからすると、静的ファイルだけで済むので固定費が安いという点でメリットを感じています。
企業であれば迷わずAlgoliaに課金すればいいと思います。しかし個人はどうすればいいのか? その答えが「自前実装」です。
(これは嘘です、サイトの世界感を守りたいというこだわりがなければグーグルのカスタム検索が手軽でいいと思います。)
実装方針
Gatsby製のサイトでは、ビルド時に静的ファイルを出力してそのファイルを配信して動いています。 検索となるとサーバーサイドが欲しくなりますが、検索のためだけにサーバーを用意するのは本末転倒ですし、管理も煩雑になります。 また、Netlifyで運用しているため、Netlify Functions(AWS Lambda)の利用も考えましたが、誰もがNetlifyやAWSを使ってGatsbyをホスティングしているとは限りません。
Gatsbyを利用する人がそれなりに簡単に実現できる解決案を考えた結果、次の方針が良さそうでした。
- 静的ファイルのビルド時に検索用のJSONを生成
- ブラウザでReactがマウントされたらそのJSONを呼び出す
- クライアントで検索
静的ファイルのビルド時に検索用のJSONを生成
Gatsbyではgatsby-node.js
でGatsbyのライフサイクルに処理を差し込むことが出来ます。
動的なページを生成する際に使われるcreatePagesで、ページ追加処理の他にJSONを組み立ててファイルに保存する処理を書きました。
exports.createPages = () => {
// graphqlでデータを取得する
const searchJSON = allEsaPost.edges.map(postEdge => {
const postNode = postEdge.node
const { field, number, tags} = postNode
const { title } = postNode.fields
return {
title, tags, number,
path: `/posts/${number}`
}
})
fs.writeFileSync('./static/search.json', JSON.stringify(searchJSON, null , 2))
//...
}
ブラウザでReactがマウントされたらJSONを呼び出す
これは簡単で、Reactのマウントタイミングで上記のJSONを呼び出します。 検索用のJSONが重いのでは?と思う方もいるかもしれませんが、現状50記事ちょっとで2KB程度なのでパフォーマンス的には問題ありません。下手な画像を設置する方がよほどパフォーマンスへの影響があります。
import React from 'react'
import axios from 'axios'
export default class Search extends React.Component {
async componentDidMount() {
const res = await axios.get('/search.json')
this.data = res.data
}
}
クライアントで検索
テキストフィールドが変化するたびに取得したデータの配列をfilterを通して表示します。 実装前は重くなりそうだと思っていたのですが、データ量が少なかったのでほとんど問題なく動きます。重かったらイベントを間引いたり、Workerスレッドに検索処理を移動するつもりでした。
TitleとTagを元に検索しており、精度に関しては劣りますがざっくりとした結果は得られます。本当は本文の特徴語などをビルド時に出力しておくといいのかもしれません。
感想
以上の対応で問題なく動いたので勢いでリリースしました。 所要時間はミニマムリリース地点で6時間程度でした。検索処理よりもUIの方が時間がかかりました。
モバイルもやっつけで対応しましたが、レスポンシブ対応のスタイルとStateによる制御が合わさってスパゲッティコードに近いコードを書いてしまいました。 普通のアプリケーションであればwindowのサイズをJSで見て処理を分岐させたかもしれませんが、静的ファイルをジェネレートする関係上、JSを読み終わるまで適切な表示に切り替わりません。それを避けるためかなり無理矢理感のある実装になりました。 実装にやさしくないUIな気もするので、気が向いたら改修していきます。
静的サイトジェネレーターを使ったサイトを運用していて検索の実装で困っていた人に対して一つの方針が示せたのであれば嬉しいです。お金があればAlgoriaを使ってください。