という要望は普通にあると思ってまして、ツイッタとかでたまにhexoで検索すると、どうやって実装しようとしているのか迷っている人とかがいるので、自分の知ってる範囲でいくつか方法を紹介します。また、下記の方法の一部はHexo以外の静的サイトジェネレータでも可能です。

Googleカスタム検索


一番手っ取り早いです。ただし、控えめに言って見た目がダサいので、見た目にこだわる人には向かないです。

Algoliaを使う


Algoliaという全文検索サービスがあります。これを利用することで静的サイトジェネレータで生成したサイトでも検索機能を付与することが可能です。以前書いたのですがVue.jsのサイトも今のところHexoで構築させており、検索はAlgoliaが使用されています

Hexoでは2つのプラグイン(いずれも非公式)が提供されており、これらを使えばそこまで労力をかけずに実装が可能ではないかと思います。(やったことないですが、たぶんそんなに難しくなさそうです)

ただ、今見た感じだと、無料だと意外と上限がきつそうですね…。また、言うまでもないですが_config.ymlにキーの情報を含めてGitHubとかにアップロードしないように注意が必要です。

追記 Algoliaの無料プランの制限とかについては下記の記事が詳しいです

Algoliaを利用してサイト内検索機能を実装する

ローカル検索用プラグインを使う


hexo-generator-searchという記事情報をJSONもしくはXMLで生成するプラグインがあります。これを利用すると、外部のサービスを利用することなくJavaScriptをちょろっと書くだけでサイトに検索機能を実装することが可能です。

流れとしては下記のようになります。

  • hexo generate時に記事データのJSONかXMLを生成
  • サイトにデプロイ
  • JavaScriptでデータ取得してパースして検索する

ただし、後述しますがいくつか問題があります。とりあえず先にコードを説明します。

まずJavaScriptからです。下記のようなJavaScriptを作成します。XMLで生成した場合です。まあ、コードはちょっと古臭い感じがしますが、私が書いたというよりプラグインの説明で記述されているのをちょっと改造しただけなので勘弁してください。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
'use strict';

var searchFunc = function(path, search_id, content_id) {
$.ajax({
url: path,
dataType: "xml",
success: function( xmlResponse ) {
// get the contents from search data
var datas = $( "entry", xmlResponse ).map(function() {
return {
title: $( "title", this ).text(),
content: $("content",this).text(),
url: $( "url" , this).text()
};
}).get();
var $input = document.getElementById(search_id);
if (!$input) return;
var $resultContent = document.getElementById(content_id);
$input.addEventListener('input', function(){
var str='<ul class=\"search-result-list\">';
var keywords = this.value.trim().toLowerCase().split(/[\s\-]+/);
$resultContent.innerHTML = "";
if (this.value.trim().length <= 0) {
return;
}
// perform local searching
var numOfPostFound = 0; // keeping track of # of result
datas.forEach(function(data) {
var isMatch = true;
var content_index = [];
var data_title = data.title.trim().toLowerCase();
var data_content = data.content.trim().replace(/<[^>]+>/g,"").toLowerCase();
var data_url = data.url;
var index_title = -1;
var index_content = -1;
var first_occur = -1;
// only match artiles with not empty titles and contents
if(data_title != '' && data_content != '') {
keywords.forEach(function(keyword, i) {
index_title = data_title.indexOf(keyword);
index_content = data_content.indexOf(keyword);
if( index_title < 0 && index_content < 0 ){
isMatch = false;
} else {
if (index_content < 0) {
index_content = 0;
}
if (i == 0) {
first_occur = index_content;
}
}
});
}
// show search results
if (isMatch) {
numOfPostFound += 1; // keeping track of # of results
str += "<li><a href='"+ data_url +"' class='search-result-title'>"+ data_title +"</a><hr>";
var content = data.content.trim().replace(/<[^>]+>/g,"");
if (first_occur >= 0) {
// cut out 100 characters
var start = first_occur - 20;
var end = first_occur + 80;
if(start < 0){
start = 0;
}
if(start == 0){
end = 100;
}
if(end > content.length){
end = content.length;
}
var match_content = content.substr(start, end);
// highlight all keywords
keywords.forEach(function(keyword){
var regS = new RegExp(keyword, "gi");
match_content = match_content.replace(regS, "<em class=\"search-keyword\">"+keyword+"</em>");
});
str += "<p class=\"search-result\">" + match_content +"...</p>"
}
str += "</li><br>";
}
});
str += "</ul>";
// attaching a summary of searching result
if (numOfPostFound > 0) {
if (numOfPostFound > 1) {
summary = numOfPostFound + " 件見つかりました。";
} else {
summary = numOfPostFound + " 件見つかりました。";
}
} else {
summary = "見つかりませんでした。";
}
var summary = "<p class=\"text-xlarge text-color-base archieve-result search-result-summary\">" + summary + "</ul><br><br>";
$resultContent.innerHTML = summary + str;
});
}
});
}

で、こいつをsearch.jsとかそういう名前で保存します。次に検索用のページを作成します。search.jsを読み込んで、下記のような感じのhtmlを書きます。CSSは適当に調整してください。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<script src="/assets/js/search.js"></script>
<div>
<h1 class="post-title">検索</h1>
<input type="text" id="local-search-input" name="q" results="0" placeholder="キーワードを入力してください" class="form-control input--large" autofocus="autofocus"/>
<hr>
<div id="local-search-result"></div>
<script>
var search_path = "search.xml";
if (search_path.length == 0) {
search_path = "search.xml";
}
var path = "/" + search_path;
searchFunc(path, 'local-search-input', 'local-search-result');
</script>
</div>

こんな感じで外部サービスを使用せずに検索機能を実装することが可能です。可能なんですが…前述のとおり問題があります。それは…

  • ファイルサイズが大きい(全記事情報を含んだデータを取得するので)のでユーザの通信量が増える
    • ただし、大きいといっても当サイトの場合は400記事で2MBくらいでした
    • 通信はページにアクセスした際にのみ発生します(従って検索都度発生するわけではない)
  • スクレイピングをするまでもなく全コンテンツが取得できる

です。上記のあたりが気になる人はやめた方がいいと思います。ちなみにこのサイトも少し前まで使用していたのですが、後者を考慮して使うのをやめました。

全文検索サーバを構築する


少し前に記事で書いたのですが、FESSなどを利用して全文検索サーバを自前で構築します。ちなみに、私はやりかかっていたのですが設定が悪かったのか上手くインデックスされませんでした。調査する時間が取れないため止まっている状況です。

自前でなにかしらを構築・作成する


前述のhexo-generator-searchを使用するパターンは何が問題なのかというと、直接全データを取得してしまうのが問題なのです。ということはデータのみhexo-generator-searchで生成して、検索は自前で作成するという手があります。

JSON形式で記事生成をして、データのみ検索用の別サーバないしWebサーバがアクセスできないところにデプロイしてから自前で作成したAPIサーバでそいつを読み込んで検索文字に応じて結果を返すとかです。Node.jsでAPIサーバを作るとかであれば、そう難しくもなく構築可能だと思います。ただ、キャッシュやデータの再読み込みとかも実装するとやや手間はかかりそうですが…。ちなみに私はScalaでやろうとして別のことに興味が移ってしまったのでコレも頓挫しています。

他にはS3にデータを置いてLambdaを使って…とかであればもっと簡単にできる気がします。

まとめ


一長一短ですが、Googleカスタム検索以外にも実現する方法はあります。