静的サイトジェネレータ Hugo を使って生成したコンテンツに 全文検索を取り付けました。Hugo でのコンテンツと一緒にインデックスファイルも同時に書き出し、クライアント側の JavaScript で日本語のキーワードを検索をします。

はじめに

この件は、ゼロベースから考えました。

ちょうど WordPress で運用していたデータを静的ジェネレータ (SSG) の Hugo に移行し、CI を使って GitHub Pages 上に展開したところです。

最低限の CSS だけのテンプレートを使って静的なページを作っているので、JavaScript を使うにしても出来る限り絞り込みたいというのが、前提条件でした。

元原稿は AsciiDoc の集まりなので、そこからインデックスファイルを作り、このファイルを JavaScript で検索するページを作ればよいだろうとザックリと考えました。サイトのコンテンツの規模は、1.2MB、70 ファイルです。

JavaScript で全文検索

JavaScript の日本語の全文検索について調べていたところ、 JavaScriptにBlogの全文検索をやらせてみるテスト を見つけました。事前にインデックスファイルを JavaScript のオプジェクトとして保存し、クライアント側で検索する作りになっています。 高速に検索し、使い勝手が気に入りました。これを自分の Hugo のコンテンツと一緒に使えないかと考えました。なお、再利用と改変について作者ご本人から直接承諾を得ました。

このサイトでも、派生系のスクリプトでも、インデックスを生成するためのスクリプトを別途走らせています。この部分もSSG にやらせたいと考えました。ページを生成するのも、インデックスファイルを生成するのも大差ないのですから。

インデックスファイルの生成

Hugo に全文検索用のインデックスファイルを書き出させるには、ちょっとしたコツが必要です。Hugo のコミュニティサイトから学びました。それは、インデックスファイル index.js を書き出すための特別な Type のテンプレートを作成し、ダミーの投稿と使って書き出すというものです。素晴らしいアイデアです。

これは lunr.js についての投稿ですが、これによって当初の企画が実現できました。

これから実際にインデックスファイルを Hugo を使って書き出す手順を説明します。

インデックスファイルのテンプレート

このインデックスを書き出す Type を "js" と決め、書き出し用テンプレートを layouts/js/single.html に配置しました。

single.html
var data = [{{ range $index, $page := where .Site.Pages "Section" "post"}}
{{ if ne $index 0 }},{{ end }}{
url: "{{ $page.Permalink }}",
title: "{{ $page.Title }}",
content: "{{ .PlainWords }}"
}{{ end }}]

Section が Post の投稿に限定して、題名、URL、本文を取り出しています。なお、本文の書き出しには HTML を外すために変数 .PlainWords を使いました。

.PlainWords の様な Hugo の文書化していないテキストに関する機能と JSON 形式でインデックスファイルを作成する場合のヒントについて 遺補 にまとめました。

インデックスファイルを生成する空の投稿

テンプレートで設定した内容を実際に生成するための空の投稿ファイル作ります。Front Matter で Type を "js" とし、url を index.js と指定しました。

indexjs.adoc
---
date: "2016-03-21T14:35:52+09:00"
type: "js"
url: "index.js"
---

このサイトでは AsciiDoc で書いているので拡張子を adoc にしましたが md にしても機能します。

その結果が http://rs.luminousspice.com/index.js になります。容量は 700 KB です。 インデックスファイルが実利用に耐えうるサイズになるか、当初心配しました。 過去 4 年でこのサイズですから、このサイトではこれから 4−5 年は問題なく使えると判断しました。

検索ページの作成

次に、作成したインデックスファイル index.js を検索するユーザーインタフェイスを作ります。

検索ページテンプレートの作成

検索ページ用の Type "search" のテンプレート layouts/search/single.html を作り、その中にオリジナルの最速インターフェース研究会のスクリプトを取り込みました。

収録コンテンツの検索内容との兼ね合いで、オリジナルで使用している JavaScriptによるローマ字仮名変換ライブラリ roma.js は使いませんでした。

検索ページの作成

検索ページを配置するための投稿を作成します。Front Matter は次のように設定しました。Type を "Search" に指定しています。

search.adoc (検索ページの Front Matter)
---
date: "2016-03-05T21:10:52+01:00"
type:  "search"
url: "search"
title: "全文検索"
---

完成品はこれだ

全文検索 が完成品です。

矢印キーとリターンキーでページ送りすることができます。 極めて高速に検索でき、自分が抱いていた検索のユーザー体験を破壊してくれました。 複数キーワードを使った複雑な検索などは検索エンジンを使ってもらうことにしましょう。

あまりに便利なので、サイト移行の使ったデータ変換作業の検証に実際に使いました。 検索機能を取り付けるまでに必要な作業はここまでです。

このサイトでの変更点

オリジナルからの変更点は次の通りです。

  • 検索結果のページ送りに矢印キーの上下を機能するようにした。

  • レイアウトデザインをテーマに合わせて調整した。

  • サイトの公開形態上、公開日表示を外した。

  • コンテンツの内容から、ローマ字かな変換機能を外した。

検索結果の調整

初期設定ではインデックスは公開日順に書き出します。つまり検索結果に複数候補がある時は、日付順に表示します。 このサイトのコンテンツは日記やブログではなく、全てのコンテンツを継続的に更新しているので経時的な順序に意味はありません。 そこで、統計情報のアクセス数や検索キーワード元に重み付けしました。アクセス数の九割を占めている収録コンテンツの半数に .Weight を使って三段階に重み付けしました。

検索結果は重み付け>日付の順に表示するように変えました。

参考: Hugo ドキュメント Ordering Content

検索対象の絞り込み

現在は、全文検索の対象は本文のみになっています。当初は検索対象に、タグや概要も含めていました。実際に完成して使ってみると、全文検索というのは検索キーワードが実際のコンテンツの文脈と一緒に表示されることに意味がある (つまり KWIC なんですが) ことを再確認しました。 そこで、タグや概要については全文検索以外の手段で利用できることから除外しました。

Hugo の検索機能の動向についてのまとめ

Hugo の検索機能について、一般的に使われている方法について簡単にまとめておきましょう。 今回の事例では極力 JavaScript の追加は制限する方針でしたが、特に制約の状況では、もっと簡単に設置できる方法が見つかります。

広く使われている方法

SSG で広く使われている全文検索機能は lunr.js が有名です。専用のプラグインがある SSG もありますが、Hugo の場合は外部のプログラムによって JSON 形式インデックスファイル生成し、検索するのが一般的なようです。

日本語化は、 lunr-languagesを使えばできるらしく、 TinySegmenter が同梱されていました。

hugo-lunr を使うと Hugo 用のインデックスファイルを生成してくれます。

Hugo での lunr.js 利用事例

lunr.js の事例は Hugo のサポートサイトで見つかります。検索ページの配置の仕方や、インデックスファイルを作り方は、自分の事例でも参考になりました。

今後有望な DocSearch というサービス

この全文検索の作業が一段落して Hugo のリポジトリをアップデートしたところ、Hugo のドキュメントの検索が Algolia の DocSearch というサービスを変更になっていることに気がつきました。

自分のサイトの URL を登録すると、クローラーがインデックスを作り、スニペッドで UI を提供してくれるようで、日本語もサポート済みとのこと。

具体的な配置方法は、 8890885 を見ると分かると思います。

既に WordPressJekyll のプラグインを提供しているようです。

これまで、自分でやって来た作業を全て肩代わりしてくれるサービスだ。なんてありがたいと思って試しに登録してみたら、このサイトは "documentation site" ではなく、クローラーも完全には正しく処理できないとお断りされました。 代わりに、10,000 Records、100,000 Operations まで無料で使える Hacker プランを勧められました。

遺補

この作業を行っているうちに見つけた関連項目をまとめます。

Hugo の文書化されていないテキスト機能

Hugo には、いまだ文書化されていないテキストに関する変数や組み込み関数があります。参考までに紹介します。 それぞれの使い方や機能については、GitHub などでコミット内容を確認ください。

変数
関数 (0.16-DEV から)

JSON 形式でインデックスファイルを作成するには

この記事のアプローチと同じ方法で JSON 形式でインデックファイルを作ることも可能です。実際に XMLHttpRequest で読んで同じように検索できることころまで確認しました。

この場合のポイントは、インデックスファイルを書き出す際に、検索対象の文字列を JSON 形式を満足するようにエスケープすることです。Hugo の新しい組み込み関数 jsonify も用意されているのですが、私の事例では全てのコンテンツに対して満足いく結果を出せませんでした。

まとめ

  • Hugo からインデックスファイルを書き出すには独自の Type を作る。

  • インデックスファイルのサイズがシステム採用の判断に影響を与える。

  • インデックスファイル内の項目順序が検索結果の表示に影響するので調整が必要。

  • Hugo の文書化されていない機能はユーザーコミュニティの類似の事例から見つけやすい。

  • Hugo の機能拡張をするなら、最新動向を一度調べた方がよい。