WordPressからHugoに移行するにあたって、全文検索をどうしようかけっこう悩んだ。

調べてみたら、Algoliaが良さそうだった。
https://blog.leko.jp/post/implement-site-search-with-algolia/
実際にアカウントを作ってチュートリアルやってみて、APIを見てみたら使いやすそうだったので、これを使って全文検索を実装してみた。

クライアントサイドで全文検索するLunrというライブラリにも興味あったけど、検索するたびに毎回インデックスを全部クライアント側に落としてくるような実装しか思いつかず、今回は使うのをやめておいた。

Algolia

いろんなトコロで使われているらしい全文検索サービス。
小規模なら無料で使える。
https://www.algolia.com/

チュートリアルが最高にわかりやすいから、基本的な使い方を知るには、実際にアカウント作ってチュートリアルを一通りやってみるのが一番手っ取り早いと思う。10分もかからず終わる。

APIキー

AlgoliaのAPIを実行するにはAPIキーが必要。
APIキーごとに、実行できるAPIを制限できる。

今回は、データ追加と検索をするので、それぞれの用途に応じて2つAPIキーを作る。
というのも、検索APIを実行するためのAPIキーは、ブラウザのJavaScriptのコードに直接書き込むから、人に知られる。
なので、このAPIキーにデータ追加の権限も持たせてしまうと、自分のインデックスに勝手にデータをアップロードされてしまう。
APIキーを2つに分けておけば、検索API用のAPIキーを知られたところで検索しかできないので、特に問題ない。
(とはいえ検索API用のAPIキーも、ちょっと頑張れば人目に触れないようにはできる。逆に言えば、AlgoliaのAPIキーの権限を縛っておけば、APIキーを晒しても良いから頑張らずに済む。)

APIキーは、Algoliaのダッシュボードで作れる。
権限はACLで設定できる。

この辺りのことは下記のドキュメントに書いてある。
https://www.algolia.com/doc/guides/security/api-keys/

データ追加

インデキシングの処理はAlgolia側でやってくれるから、必要なのは、検索対象のデータ(今回の場合はブロクの記事)をAlgoliaにアップロードする処理。

addObjectsというAPIを、objectIDを指定しつつ叩けば、objectIDが存在しない場合には新規作成して、objectIDが存在する場合には上書きしてくれる。
だから、記事ごとに一意な文字列(Hugo使うなら、posts配下のファイルのファイル名でいいと思う)を添えてaddObjectを叩いておけば、重複とかは考えなくていい。

データ追加はPythonでやることにする。
hugoコマンドでビルドする直前に、毎回データ追加用のコードを動かして、データ追加の処理を実行する。
https://www.algolia.com/doc/api-reference/api-methods/add-objects/?language=python

algoliasearchのインストール

下記コマンドでインストール。

$ sudo pip-3.6 install --upgrade algoliasearch

最初、pipしてもpython 2の方のpipが動いてしまってどうしたもんかなーと思って、下記コマンドでpython 3の方のpipを探した。

$ which pip
/usr/bin/pip

$ ls /usr/bin/ | grep pip
lesspipe.sh
pip
pip-2.7
pip-3.6

処理の概要

処理の概要は下記の通り。

  1. あからじめ、「ファイルの最終更新日一覧」を記録したテキストファイルを用意しておく。
  2. hugoのpostsディレクトリ配下のファイルを順に見ていく。
  3. 「ファイルの最終更新日」が変化しているファイルの内容を、addObject APIでインデクスに追加する。

10KB制限

ちょっとやっかいだったのが、データの10KB制限( https://www.algolia.com/doc/guides/indexing/formatting-your-data/#size-limit )。
Algoliaの無料のプランでは、1つのオブジェクトが10KBまでという制限がある。

いくつかこの制限にかかるファイルがあって、エラーが出た。
サイズが大きい場合は適宜データを分割してアップロードして回避した。

hugoのメタデータの処理

hugoの記事のタイトルとか更新日時とかもデータとしてアップロードしたかったので、記事からメタデータを切り出して処理した。
メタデータはyamlで書いてたので、メタデータ部分を文字列的に切り出した後PyYAMLで読むことで、カンタンに扱えた。
https://pyyaml.org/wiki/PyYAMLDocumentation

検索

Algoliaの検索についてドキュメントとかを見ていると、1文字打つごとに検索結果が絞り込まれていく、インクリメンタルサーチの実装の説明が多い。
この形で実装すると、検索APIを叩く数がすごく増えてしまう(と思う)し、ブログの記事検索で使う分にはオーバースペックに思えるので、今回は使わない。

JS Helperというのを使ってみる。
https://community.algolia.com/algoliasearch-helper-js/

下記のページ見れば、ほぼすべて使い方わかる。
https://community.algolia.com/algoliasearch-helper-js/gettingstarted.html

サンプルコードだと、keyupで.search()を叩いている。
ここを修正して、例えばボタン押したタイミングで.search()を叩くようにすれば、いわゆる普通の検索になる。
このブログでは、ヘッダの検索ボックスで検索ボタンを押した時にクエリ文字列付きで新規ページを開いて、新規ページでクエリ文字列を取得して検索して結果をリスト表示する、という実装にしている。

一度の検索でのヒット件数がデフォルト20なので、これを10,000くらいに増やしておく。
あと、デフォルトではtypoした場合にも良い具合にヒットさせてくれるけど、この機能は不要に思ったので、オフにしておいた。

検索の核心部分のコードは下記のような感じ。

helper
.setQueryParameter('hitsPerPage', 10000)
.setQueryParameter('typoTolerance', 'false')
.setQuery(queryStringObject['q'])
.search();

それと、無料でAlgoliaを使う場合はロゴを出す必要があるので、検索結果ページでロゴを表示している。