PIYO - Tech & Life -

Algoliaを使ってブログに全文検索をつける(検索index作成編)

Hugoに移行して以来かなり満足度が高いブログ活動ですが、記事機能がないのが惜しいなと思っています。検索の用途としては自分が昔書いたことを発掘するのがまず第一です。案外助けられることが多いんです。

もちろん、Google検索でsite:オプションを使うなどしても実現できるのはわかっています。ただ、やはり自分でやってみたいという気持ちがあったので今回チャレンジしました。すでにファーストバージョンは完成していて、画面右上から検索用のページへ移動できます。リアルタイムな絞り込み検索を試せます。

Hugoは静的サイトなので、検索のような動的な処理はHugo単体ではできません。

調べているとJSを使う方法や外部のサービスを使う方法など幾つかみつかりました。

ドキュメントにも挙がっていたAlgoliaというサービスが気になったのでさらに調べてみると、forestry.ioの記事で手順が紹介されていました。これを追体験してみることにしました。

検索インデックス用のJSON

HugoのCustom Output Formatsの仕組みを用いて、サイトのビルド時にAlgoliaに登録するためのJSONを生成します。RSSを出力するときと同じような方法で比較的簡単に作ることができました。

Custom Output Formats | Hugo

config.toml

AlgoliaはJSON形式で検索インデックスを登録できます。なんとなく触った感じ指定のフォーマットがないので、記事タイトル、本文、タグ当たりを含めたらいいと思います。

forestry.ioの設定をベースに、config.tomlに次のような設定を追加しました。

[outputFormats.Algolia]
baseName = "algolia"
isPlainText = true
mediaType = "application/json"
notAlternative = true

[params.algolia]
vars = ["title", "content", "date", "publishdate", "description", "permalink", "thumbnail"]
params = ["tags"]

元記事と異なるのは、paramsからcategoryexpiryDateを削除しdescriptionを追加したことと、varssummarycontentに変えたことです。

最後にoutputsにalgoriaを追加して完成です。

[outputs]
home = [ "HTML", "RSS", "algolia" ]

JSON用のテンプレート

ファイルを生成するときに使うテンプレートが必要です。layouts/_default/list.algolia.json としてファイルを作り、中身はこのようにしました。

{{/* Generates a valid Algolia search index */}}
{{- $hits := slice -}}
{{- $section := $.Site.GetPage "section" .Section }}
{{- $validVars := $.Param "algolia.vars" | default slice -}}
{{- $validParams := $.Param "algolia.params" | default slice -}}
{{- range $i, $hit := where .Data.Pages "Section" "posts" -}}
  {{- $dot := . -}}
  {{- if or (and ($hit.IsDescendant $section) (and (not $hit.Draft) (not $hit.Params.private))) $section.IsHome -}}
    {{/* Set the hit's objectID */}}
    {{- .Scratch.SetInMap $hit.File.Path "objectID" $hit.UniqueID -}}
    {{/* Store built-in page variables in iterable object */}}
    {{- .Scratch.SetInMap "temp" "date" $hit.Date.UTC.Unix -}}
    {{- .Scratch.SetInMap "temp" "publishdate" $hit.PublishDate -}}
    {{- .Scratch.SetInMap "temp" "dateString" (substr $hit.PublishDate 0 10) -}}
    {{- .Scratch.SetInMap "temp" "content" ($hit.Plain | truncate 2000) -}}
    {{- .Scratch.SetInMap "temp" "title" $hit.Title -}}
    {{- .Scratch.SetInMap "temp" "permalink" $hit.Permalink -}}
    {{- .Scratch.SetInMap "temp" "description" $hit.Description -}}
    {{- .Scratch.SetInMap "temp" "thumbnail" $hit.Params.thumbnail -}}

    {{/* Include valid page vars */}}
    {{- range $key, $param := (.Scratch.Get "temp") -}}
      {{- if in $validVars $key -}}
        {{- $dot.Scratch.SetInMap $hit.File.Path $key $param -}}
      {{- end -}}
    {{- end -}}
    {{/* Include valid page params */}}
    {{- range $key, $param := $hit.Params -}}
      {{- if in $validParams $key -}}
        {{- $dot.Scratch.SetInMap $hit.File.Path $key $param -}}
      {{- end -}}
    {{- end -}}
    {{- $.Scratch.SetInMap "hits" $hit.File.Path (.Scratch.Get $hit.File.Path) -}}
  {{- end -}}
{{- end -}}
{{- jsonify ($.Scratch.GetSortedMapValues "hits") -}}

ずらっと書いてもしょうがないのですが、いくつか元記事とは異なる箇所があります。

本文を全部含めてしまうとAlgoliaの1データあたりのデータ上限を超えてしまうので、contentは.Plainを2000文字でtruncateするようにしました。また、サムネイルもを含めるようにしました。

また、tagのページなどが含まれてしまうので、whereをつかって"Section""posts"のものに絞り込んでループを回しています。

これでビルドするとjsonが作られます。ローカルではhttp://localhost:1313/algolia.jsonにアクセスすると確認ができます。

Algoliaの準備

続いてAlgoliaに登録しましょう。

Site Search & Discovery powered by AI | Algolia
Create AI-powered search & discovery across websites & apps.

Algoliaは

Algolia is the most reliable platform for building search into your business.

ということで、検索機能を提供してくれるサービスです。ここに先ほどHugoで生成したJSONを元に検索対象のデータを登録していきましょう。

なにはともあれユーザー登録を済ませます。無料で使えるライセンスがあるので僕はそれを選択しました。

無料版の制約は

  • 登録可能なデータの上限
  • データ操作回数の上限
  • algoliaのロゴを表示

といったもので、ブログ用途には十分です。

続いて、NEW APPLICATIONからアプリケーションを登録します。名前を適当にいれればOK。

するとインデックスの登録や検索に使うためのAPI Keyが発行されます。

後ほどこれらを使用します。

Algoliaへインデックスを登録

Hugoを手元でビルドしてアップロードしている人の場合はAlgoliaへの登録も手元から行えばいいのですが、Netlifyのようにビルドお任せサービスを使っている場合にはそうはいきません。幸い、生成されたalgolia.jsonはどこからもアクセスできますので、ビルド後のWebhookでAlgoliaへアップロードする仕組みをトリガーしてあげなければいけません。

やることはごくシンプルで、書き出されたJSONを読み取ってAlgoliaのAPIでデータを登録していくだけなので、自分で書くことは比較的容易です。各種言語に対応したクライアントライブラリも存在します。

ですが、それをどこで実行するのか、それ用のサーバーを別途用意するのか。それとも自分用に動かしているGoのサーバーに機能を追加するのか。今回は元記事を参考に、サーバーレスでタスクを動かせるWebtaskを利用して実施することにしました。

元記事に詳細な手順があるので、ここでは手順を簡単に紹介しておきます。

  1. WebTaskでサインアップする
  2. $ git clone https://github.com/forestryio-templates/serverless-atomic-algolia する
  3. $ npm install serverless -g && npm install で必要なものをインストール
  4. $ serverless config credentials --provider webtasks でログインする
  5. config/secrets.yml.stubファイルをconfig/secrets.ymlとしてコピーする
  6. config/secrets.ymlをAlgoliaのダッシュボードの自分のアプリの設定に書き換える
  7. config/index.jsの一部を書き換え。Algolia内のIndex名や、サイトのJSONのURLを記載。
  8. $ serverless deploy

最後のデプロイでこんな感じの↓URLが発行されます。

https://wt-60952c750afexxxxxxxxxxxxxxxx-0.sandbox.auth0-extend.com/yyyyyyyyyyy

ここにアクセスすると実際にコードが動くわけですが、僕の場合は2つほど依存関係のエラーが起こりました。追加でライブラリを追加し再度デプロイすることでエラーが解消してAlgolia側にデータが登録されることを確認できました。

  • $ npm install --save source-map-support
  • $ npm install --save babel-runtime

Algoliaに登録されたデータの様子。340recordsと表示されています。管理ページ内で検索を試みることもできます。

あとはさきほどのWebTaskが定期的に実行されればOKですね。僕の場合はNetlifyのビルド後のWebhookに登録して自動で走るようにしています。

これで検索インデックスの準備はOKです。次回はブログ上に検索ボックスを配置して検索するところを紹介したいと思います。