Asciidoctorを使うと、技術文書や手順書などのドキュメントを簡単にHTMLで公開できますが、ドキュメントが多くなると自分の調べたいキーワードが、どのドキュメントに載っているのか探すのが困難になってきます。
そこで今回は、Dockerで全文検索サーバのFessを立てて、
ドキュメントからJavaScript経由でFessのJSON APIを呼び出すことで、
全文検索を簡単に導入する方法をご紹介します。
導入するとドキュメントの右上に検索窓が出てきて全文検索できるようになります。
Fessとは
Fessは**「5 分で簡単に構築可能な全文検索サーバー」**です。
Javaベースで構築されており、Apacheライセンスで提供されるオープンソース製品で、無料で使用できます。
自分でクローラを設定して、自分だけの検索エンジンがつくれるようなイメージです。
デフォルトで検索画面を提供していますが、JSON APIも提供しているので、様々なシステムと連携可能です。
全体像
ドキュメント用Webサーバに全文検索用のJavaScriptファイルとCSSファイルを追加し、Dockerで全文検索用のFessサーバを立てます。
Fessサーバはドキュメント用Webサーバをクロールし、その情報を保存しておきます。
ドキュメントで全文検索を実施すると、JavaScript経由でFessサーバのJSON AIPを呼び出して、全文検索結果をドキュメントに表示します。
導入手順
この導入手順は、下記のような環境(ローカルPCのDocker上にドキュメント用WebサーバとFessサーバが立っている環境)を作った時のものです。
Dockerを使わなくても、ローカルPC以外でも、導入可能です。導入手順は適宜読み替えてください。
Fessのインストール
Docker Hubのcodelibs/fessを使用します。今回ポートは10084で公開します。
Dockerを使わない場合はFess インストールガイドを参考にしてください。
$ docker run -d -p 10084:8080 --name fess codelibs/fess:latest
Fessの設定
クローラの設定
http://[PCのローカルIPアドレス]:10084/login
にアクセスするとログイン画面が表示されます。
デフォルトのID/PASS admin/admin
でログインしましょう。
ログインするとダッシュボードが表示されます。左ペインのクローラ
> Web
を選択しましょう。
Webクローラにはまだ何も登録されていないので、左上の+ 新規作成
ボタンをクリックしましょう。
設定項目は色々ありますが、とりあえず下記項目だけ設定しましょう。
- 名前
- 任意の名前を設定してください。
- URL
- ドキュメント用Webサーバにおいてドキュメントが格納されているルートフォルダのURLを指定してください。末尾に
/
を付けてください。
- ドキュメント用Webサーバにおいてドキュメントが格納されているルートフォルダのURLを指定してください。末尾に
- クロール対象とするURL
- 正規表現で値を設定します。上記
URL
で設定したルートフォルダ配下の全資産を対象とするために、URLで設定した値
+.*
を指定してください。
- 正規表現で値を設定します。上記
- 検索対象とするURL
- 正規表現で値を設定します。HTMLのみを検索対象にしたい(JS、CSSなどは除外したい)ので、
URLで設定した値
+.+\.html$
を指定してください。
- 正規表現で値を設定します。HTMLのみを検索対象にしたい(JS、CSSなどは除外したい)ので、
値を設定したら、画面を下にスクロールして+ 作成
ボタンをクリックします。
すると下記のようにWebクロールのデータが1件登録されます。
クローラの実行
左ペインで システム
> スケジューラ
を選択してジョブスケジューラを開きます。
ジョブスケジューラで Default Crawler
を選択します。
しばらくしてF5
キーを押してブラウザを更新してください。
クロールが終了すると、スケジューラの状態が実行中
から有効
になります。
クローラ実行結果の確認
左ペインのシステム情報
>クロール情報
を選択すると、先ほど実行したクロールの結果が表示されています。その行を選択します。
ここにドキュメント用Webサーバのドキュメントが全て表示されればOKです。
ドキュメント用Webサーバに全文検索用資産を配置
全文検索用の資産はfull-text-search.js
とfull-text-search.css
の2つです。
full-text-search.js
の変数 FESS_JSON_ENDPOINT(FESSサーバのJSON APIのエンドポイント) は適時置き換えてください。
これらの資産をドキュメント用Webサーバのドキュメントのルートフォルダ直下に配置してください。
$(function() {
'use strict';
// FESSサーバのJSON APIのエンドポイント(FessサーバのIPアドレス + /json)
var FESS_JSON_ENDPOINT = 'http://192.168.1.5:10084/json';
// 1ページあたりの検索結果表示件数
var COUNT_PAR_PAGE = 10;
// 目次の
$('#toc')
// 一番上に検索条件入力エリアを挿入
.prepend(
'<div id="search-area">' +
'<form id="search-form">' +
'<div class="search-input-area">' +
'<i class="fa fa-search left-icon"></i>' +
'<input id="search-query" placeholder="全文検索" />' +
'<i class="fa fa-close right-icon"></i>' +
'</div>' +
'<input id="search-start" type="hidden" value="0"/>' +
'<input id="search-num" type="hidden" value="' + COUNT_PAR_PAGE + '"/>' +
'<form>' +
'</div>')
// イベント登録
.ready(function() {
var $searchArea = $(this);
// 入力項目の検索条件でEnterを押したら、検索処理を実行する
$searchArea.find('#search-form').submit({navi:0}, doSearch);
// 虫眼鏡アイコン押下したら、検索処理を実行する
$searchArea.find(".left-icon").click({navi:0}, doSearch);
// 検索条件入力したら、
$searchArea.find("#search-query").keyup(function(){
var $this = $(this);
var $rightIcon = $this.parent().find(".right-icon");
if($this.val().length > 0) {
// 検索条件に値がある場合は×アイコンの色を濃くする
$rightIcon.css('color','#555');
} else {
// 検索条件に値がない場合は×アイコンの色を薄くする
$rightIcon.css('color','#ccc');
}
});
// ×アイコン押下したら、
$searchArea.find(".right-icon").click(function(){
// ×アイコンの色を薄くして
$(this).css('color','#ccc')
// 検索条件をクリアする
.parent().find("input").val('');
});
});
// ドキュメントタイトルの
$('#header>h1')
// 直下に検索結果エリアを挿入
.before(
'<div id="search-result-area">' +
'<div id="search-result-subheader"></div>' +
'<div id="search-result-content"></div>' +
'</div>')
// イベント登録
.ready(function() {
$(this)
.find('#search-result-area')
// 検索結果エリアのバツアイコンをクリックしたら、
.on("click", '#remove-search-result', function(e) {
var $searchResultArea = $(e.delegateTarget)
// 検索結果エリアを非表示モードにする
$searchResultArea.removeClass('show');
// 検索結果エリアの中身を削除する
$searchResultArea.find('#search-result-subheader').empty();
$searchResultArea.find('#search-result-content').empty();
})
// 前ページリンクをクリックしたら、1ページ前を検索する
.on("click", "#prevPageLink", {navi:-1}, doSearch)
// 次ページリンクをクリックしたら、1ページ後を検索する
.on("click", "#nextPageLink", {navi:1}, doSearch);
});
/**
* 検索処理
*
* @param {eventObject} event
* @return {boolean} submit処理を中断させるために必ずfalseを返却する
*/
function doSearch(event){
// 検索フィールドの値をトリムして取得
var searchQuery = $.trim($('#search-query').val());
// 空の場合は検索処理を実行しない
if(searchQuery.length == 0) {
return false;
}
// 表示開始位置、表示件数の取得
var start = parseInt($('#search-start').val()),
num = parseInt($('#search-num').val());
// 表示開始位置のチェック
if(start < 0) {
start = 0;
}
// 表示件数のチェック
if(num < 1 || num > 100) {
num = 20;
}
// 表示ページ情報の取得
switch(event.data.navi) {
case -1:
// 前のページの場合
start -= num;
break;
case 1:
// 次のページの場合
start += num;
break;
default:
case 0:
start = 0;
break;
}
// URLを構築
var url = FESS_JSON_ENDPOINT + '?callback=?' + // 別ドメインを想定してJSONP形式でリクエストを送信する
'&q=' + encodeURIComponent(searchQuery) +
'&start=' + start +
'&num=' + num;
// 検索リクエスト送信
// 別ドメインを想定してJSONP形式でリクエストを送信する
$.ajax({
url: url,
dataType: 'jsonp',
success: renderSearchResult
});
// ページ情報の更新
$('#searchNum').val(num);
// ページ表示を上部に移動
$(document).scrollTop(0);
// サブミットを抑止するためにfalseを返す
return false;
};
/**
* 検索成功時に検索結果を描画する
*
* @param {Anything} data レスポンスデータ
*/
function renderSearchResult(data) {
// 検索結果処理
var dataResponse = data.response;
// ステータスチェック
if(dataResponse.status != 0) {
alert("検索中に問題が発生しました。");
return;
}
// 検索結果領域を表示する
$('#search-result-area').addClass('show');
var $searchResultSubheader = $('#search-result-subheader'),
$searchResultContent = $('#search-result-content'),
record_count = dataResponse.record_count;
// 検索結果がない場合
if(record_count == 0) {
// サブヘッダーに出力
$searchResultSubheader[0].innerHTML = '<div id="remove-search-result" style="float:right;"><i class="fa fa-times"></i></div>';
// 結果領域に出力
$searchResultContent[0].innerHTML = '<b>' + dataResponse.q + '</b>に一致する情報は見つかりませんでした。';
return;
}
// 検索にヒットした場合
var page_number = dataResponse.page_number,
page_size = dataResponse.page_size,
page_count = dataResponse.page_count,
startRange = (page_number - 1) * page_size + 1,
endRange = page_number * page_size,
i = 0,
max,
offset = startRange - 1;
$('#search-start').val(offset);
// サブヘッダーに出力
$searchResultSubheader[0].innerHTML = '<b>' + dataResponse.q + '</b> の検索結果 ' +
record_count + " 件中 " + startRange + ' - ' +
endRange + ' 件目 (' + dataResponse.exec_time + ' 秒)' +
'<div id="remove-search-result" style="float:right;"><i class="fa fa-times"></i></div>'
// 検索結果領域のクリア
$searchResultContent.empty();
// 検索結果の出力
var $resultBody = $("<ol/>");
var results = dataResponse.result;
for(i = 0, max = results.length; i < max; i++) {
var element =
'<li>' +
'<h4 class="title">' +
'<a href="' +results[i].url_link + '">' + results[i].title + '</a>' +
'</h4>' +
'<div class="body">' +
results[i].content_description +
'<br/>' +
'<cite>' + results[i].site + '</cite>' +
'</div>' +
'</li>';
$(element).appendTo($resultBody);
}
$resultBody.appendTo($searchResultContent);
// ページ番号情報の出力
var pageArea = [];
pageArea.push('<div id="pageInfo">', page_number, 'ページ目<br/>');
if(page_number > 1) {
// 前のページへのリンク
pageArea.push('<a id="prevPageLink" href="#"><<前ページへ</a> ');
}
if(page_number < page_count) {
// 次のページへのリンク
pageArea.push('<a id="nextPageLink" href="#">次ページへ>></a>');
}
pageArea.push('</div>');
$(pageArea.join("")).appendTo($searchResultContent);
}
});
@charset "UTF-8";
#search-area {
margin-bottom: 1em;
}
.search-input-area {
position:relative;
}
/* 入力項目 */
#search-query {
padding: 0.7em 2em;
width: 100%;
color: black;
font-family: arial,sans-serif;
font-size: 1em;
border: 1px solid #ccc;
border-radius: 2em;
outline: 0;
}
.search-input-area input:focus {
border: 1px solid #4d90fe;
}
/* アイコンは入力項目の左と右に配置する */
.search-input-area .left-icon,
.search-input-area .right-icon {
/* 縦方向の中央寄せ */
position:absolute;
top: 50%;
margin-top: -0.5em;
font-sise: 1em;
/* 要素にマウスを合わせたら、マウスポインタのマークを変える */
cursor:pointer;
}
.search-input-area .left-icon {
left: 0.7em;
color:#444;
}
.search-input-area .right-icon {
right: 0.7em;
/* 最初は、グレーアウトしておく */
color: #ccc;
}
/* アイコンにマウスを合わせたら、サイズを大きくする */
.search-input-area .left-icon:hover,
.search-input-area .right-icon:hover {
font-size: 1.4em;
}
.search-input-area .left-icon:hover {
left: 0.5em;
}
.search-input-area .right-icon:hover {
right: 0.5em;
}
/* 検索結果表示時に適用するスタイル */
#search-result-area.show {
background: #f8f8f7;
border: 0px solid;
border-radius: 0.5em;
margin-top: 1em;
margin-bottom: 1em;
padding: 1em;
}
ドキュメントに全文検索用資産の読み込み処理を追加
前手順でドキュメント用Webサーバに配置したfull-text-search.js
とfull-text-search.css
を、
各ドキュメントから読み込むようにします。
full-text-search.js
はjQueryに依存しているので、
既存のドキュメントで読み込んでいない場合はjQueryも読み込みも追加してください。
ドキュメント用Webサーバがこのようなフォルダ構成だとしたら、
ドキュメント用Webサーバのドキュメントルート
├── full-text-search.css
├── full-text-search.js
└── asciidoctor-sample
└── asciidoctor-sample.html
asciidoctor-sample.adoc
には下記を追加します。
++++
<link rel="stylesheet" href="../full-text-search.css"></link>
<script
src="https://code.jquery.com/jquery-3.2.1.min.js"
integrity="sha256-hwg4gsxgFZhOsEEamdOYGBf13FyQuiTwlAQgxVSNgt4="
crossorigin="anonymous"></script>
<script src="../full-text-search.js"></script>
++++
全文検索ができるかの確認
以上の手順を実施すると、Asdciidoctorで全文検索ができるようになります。
ドキュメントをブラウザで見ると、目次上部に検索窓が追加されています。
検索条件を入力し、虫眼鏡アイコンをクリックすると、検索結果が表示されます。
まとめ
FessのREST APIを使用してAsciidoctorで全文検索できるようにする方法を紹介しました。
FessのREST APIを使えば、Asciidoctorに限らず、様々なシステムに全文検索機能を導入できそうです。