mottox2 blog

GatsbyJSブログに検索を実装してみた

devブログ開発gatsby

あけましておめでとうございます。今回はGatsbyでつくってる当ブログに検索を実装しました。
なぜ自分で実装したのか、Gatsbyの構成でどういう実装を選択したのかを書きました。

つくったもの

プルリクエスト

結構 :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を利用する人がそれなりに簡単に実現できる解決案を考えた結果、次の方針が良さそうでした。

  1. 静的ファイルのビルド時に検索用のJSONを生成
  2. ブラウザでReactがマウントされたらそのJSONを呼び出す
  3. クライアントで検索

静的ファイルのビルド時に検索用のJSONを生成

Gatsbyではgatsby-node.jsでGatsbyのライフサイクルに処理を差し込むことが出来ます。
動的なページを生成する際に使われるcreatePagesで、ページ追加処理の他にJSONを組み立ててファイルに保存する処理を書きました。

gatsby-node.js
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))
  //...
}

https://github.com/mottox2/website/blob/5507a406e761b4bd6ad67da786bac647e0b46a9e/gatsby/createPages.js#L64-L74

ブラウザでReactがマウントされたらJSONを呼び出す

これは簡単で、Reactのマウントタイミングで上記のJSONを呼び出します。
検索用のJSONが重いのでは?と思う方もいるかもしれませんが、現状50記事ちょっとで2KB程度なのでパフォーマンス的には問題ありません。下手な画像を設置する方がよほどパフォーマンスへの影響があります。

Search.jsx
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
  }
}

https://github.com/mottox2/website/blob/5507a406e761b4bd6ad67da786bac647e0b46a9e/src/components/Search.tsx#L54-L55

クライアントで検索

テキストフィールドが変化するたびに取得したデータの配列をfilterを通して表示します。
実装前は重くなりそうだと思っていたのですが、データ量が少なかったのでほとんど問題なく動きます。重かったらイベントを間引いたり、Workerスレッドに検索処理を移動するつもりでした。

TitleとTagを元に検索しており、精度に関しては劣りますがざっくりとした結果は得られます。本当は本文の特徴語などをビルド時に出力しておくといいのかもしれません。

感想

以上の対応で問題なく動いたので勢いでリリースしました。
所要時間はミニマムリリース地点で6時間程度でした。検索処理よりもUIの方が時間がかかりました。

モバイルもやっつけで対応しましたが、レスポンシブ対応のスタイルとStateによる制御が合わさってスパゲッティコードに近いコードを書いてしまいました。
普通のアプリケーションであればwindowのサイズをJSで見て処理を分岐させたかもしれませんが、静的ファイルをジェネレートする関係上、JSを読み終わるまで適切な表示に切り替わりません。それを避けるためかなり無理矢理感のある実装になりました。
実装にやさしくないUIな気もするので、気が向いたら改修していきます。

静的サイトジェネレーターを使ったサイトを運用していて検索の実装で困っていた人に対して一つの方針が示せたのであれば嬉しいです。お金があればAlgoriaを使ってください。

B!
dev

CLIで利用するNode.jsの書き捨てプログラムを作成するコツ

プログラマーであれば単純作業をプログラムに任せると思います。今回は備忘録を兼ねてNode.jsで書き捨て前提のプログラムを書く際に、よく使っているテクニックをまとめてみました。 やっていることは同じ

nodejs
tool

おしゃれなコード画像を生成するCLIツール『Silicon』

自分はLT資料を作る際に、『Carbon』というおしゃれなコード画像を生成するWebサービスを利用しています。今回はCarbonをインスパイアした『Silicon』というRust製のCLIツールを発見

toolrust
event

「技術書同人誌博覧会」にサークル参加してきた

2019/07/27に大田区産業プラザPioで開催された技術書同人誌博覧会(通称「技書博」)にサークル参加してきました。 「ブログを書くまでがイベント」ということですので、新刊の話、次回の新刊、当日

技術同人誌