今回はFessが提供する検索APIを利用して、クライアントサイドで検索と結果表示を行う方法をご紹介します。Fessの検索APIを利用することで、既存のウェブサイトにHTMLの変更だけで組み込むことも可能になります。

JSON API

Fessは通常のHTMLによる検索表現以外にAPIとしてJSONによる検索結果の応答が可能です。JSON APIを利用することで、既存のシステムから検索結果だけを問い合わせすることも簡単に実現できます。

検索結果を開発言語に依存しない形式で扱えるので、FessをJava以外のシステムに統合することも容易にできます。

Fessの提供しているAPIがどのような応答を返してくるかについてはFessのサイトを参照してください。

Fessは検索エンジンとしてElasticsearchを利用していますが、ElasticsearchとFessのAPIは異なります。

FessのAPIを利用するメリットは、検索ログの管理や閲覧権限の制御など、Fess固有の機能を利用できることが挙げられます。

ドキュメントクロールの仕組みをゼロから独自に開発したい場合はElasticsearchを利用するのが良いと思いますが、簡単に検索機能を追加したい場合はFessを利用する方が多くの開発コストを削減できます。

JSON APIを利用した検索サイトの構築

CORS

JSONでアクセスする際にはSame-Originポリシーに注意する必要があります。HTMLを出力するサーバとFessサーバが異なるドメインに存在する場合はCORS(Cross-Origin Resource Sharing)を利用する必要があります。

今回はHTMLが置いてあるサーバとFessサーバが異なるドメインにある想定で説明します。

Fessの設定

Fess 13.2.0を利用します。

FeesのダウンロードとインストールはFessのインストールのページを参照してください。

FessはCORSに対応しており、設定値はapp/WEB-INF/classes/fess_config.propertiesで設定可能です。デフォルトでは以下が設定されています。

api.cors.allow.origin=*
api.cors.allow.methods=GET, POST, OPTIONS, DELETE, PUT
api.cors.max.age=3600
api.cors.allow.headers=Origin, Content-Type, Accept, Authorization, X-Requested-With
api.cors.allow.credentials=true

今回はこのまま利用しますが、設定を変更した場合はFessを再起動をしてください。

作成するファイル

今回はHTML上でJavaScriptを利用して検索処理を実装します。わかりやすく実装するため、jQueryを利用しています。

作成するファイルは以下になります。

  • 検索フォームと検索結果を表示するHTMLファイル「index.html」
  • Fessサーバと通信するJSファイル「fess.js」

今回の構築例では以下の機能を実装しています。

  • 検索ボタンで検索リクエストの送信
  • 検索結果の一覧の表示
  • 検索結果のページング処理

HTMLファイルの作成

まず、検索フォームと検索結果を表示するHTMLを作成します。今回は以下の内容のHTMLファイルを利用します。

<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<title>検索サイト</title>
</head>
<body>
<div id="header">
  <form id="searchForm">
    <input id="searchQuery" type="text" name="query" size="30"/>
    <input id="searchButton" type="submit" value="検索"/>
    <input id="searchStart" type="hidden" name="start" value="0"/>
    <input id="searchNum" type="hidden" name="num" value="20"/>
  </form>
</div>
<div id="subheader"></div>
<div id="result"></div>
<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script>
<script type="text/javascript" src="fess.js"></script>
</body>
</html>

bodyタグ以下を見ていくと、id属性がheaderのdivタグの箇所で検索入力欄と検索ボタンを配置しています。hiddenフォームでは表示開始位置(start)と表示件数(num)を保持しています。

検索リクエスト送信後にJavaScriptでstartとnumの値は更新されますが、今回のサンプルコードでは表示件数を変更する機能はないので、numの値は変更されません。

次のsubheaderのdivタグの箇所で検索にヒットした件数などの情報が表示されます。resultのdivタグでは検索結果およびページングリンクが表示されます。

最後にjQueryのJSファイルと今回作成するfess.jsを読み込みます。jQueryのJSファイルを「index.html」と同じディレクトリに保存しても構いませんが、今回はGoogleのCDN経由で取得するようにしています。

JSファイルの作成

次にFessサーバと通信して検索結果を表示するJSファイル「fess.js」を作成します。以下の内容で「fess.js」を作成し、「index.html」と同じディレクトリに配置します。

$(function(){
    // Fess の URL
    var baseUrl = "http://SERVERNAME:8080/json/?q=";
    // 検索ボタンのjQueryオブジェクト
    var $searchButton = $('#searchButton');

    // 検索処理関数
    var doSearch = function(event){
      // 表示開始位置、表示件数の取得
      var start = parseInt($('#searchStart').val()),
          num = parseInt($('#searchNum').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;
      }
      // 検索フィールドの値をトリムして格納
      var searchQuery = $.trim($('#searchQuery').val());
      // 検索フォームが空文字チェック
      if(searchQuery.length != 0) {
        var urlBuf = [];
        // 検索ボタンを無効にする
        $searchButton.attr('disabled', true);
        // URL の構築
        urlBuf.push(baseUrl, encodeURIComponent(searchQuery),
          '&start=', start, '&num=', num);
        // 検索リクエスト送信
        $.ajax({
          url: urlBuf.join(""),
          dataType: 'json',
        }).done(function(data) {
          // 検索結果処理
          var dataResponse = data.response;
          // ステータスチェック
          if(dataResponse.status != 0) {
            alert("検索中に問題が発生しました。管理者にご相談ください。");
            return;
          }

          var $subheader = $('#subheader'),
              $result = $('#result'),
              record_count = dataResponse.record_count,
              offset = 0,
              buf = [];
          if(record_count == 0) { // 検索結果がない場合
            // サブヘッダー領域に出力
            $subheader[0].innerHTML = "";
            // 結果領域に出力
            buf.push("<b>", dataResponse.q, "</b>に一致する情報は見つかりませんでした。");
            $result[0].innerHTML = buf.join("");
          } else { // 検索にヒットした場合
            var page_number = dataResponse.page_number,
                startRange = dataResponse.start_record_number,
                endRange = dataResponse.end_record_number,
                i = 0,
                max;
            offset = startRange - 1;
            // サブヘッダーに出力
            buf.push("<b>", dataResponse.q, "</b> の検索結果 ",
              record_count, " 件中 ", startRange, " - ",
              endRange, " 件目 (", dataResponse.exec_time,
                " 秒)");
            $subheader[0].innerHTML = buf.join("");

            // 検索結果領域のクリア
            $result.empty();

            // 検索結果の出力
            var $resultBody = $("<ol/>");
            var results = dataResponse.result;
            for(i = 0, max = results.length; i < max; i++) {
              buf = [];
              buf.push('<li><h3 class="title">', '<a href="',
                results[i].url_link, '">', results[i].title,
                '</a></h3><div class="body">', results[i].content_description,
                '<br/><cite>', results[i].site, '</cite></div></li>');
              $(buf.join("")).appendTo($resultBody);
            }
            $resultBody.appendTo($result);

            // ページ番号情報の出力
            buf = [];
            buf.push('<div id="pageInfo">', page_number, 'ページ目<br/>');
            if(dataResponse.prev_page) {
              // 前のページへのリンク
              buf.push('<a id="prevPageLink" href="#"><<前ページへ</a> ');
            }
            if(dataResponse.next_page) {
              // 次のページへのリンク
              buf.push('<a id="nextPageLink" href="#">次ページへ>></a>');
            }
            buf.push('</div>');
            $(buf.join("")).appendTo($result);
          }
          // ページ情報の更新
          $('#searchStart').val(offset);
          $('#searchNum').val(num);
          // ページ表示を上部に移動
          $(document).scrollTop(0);
        }).always(function() {
          // 検索ボタンを有効にする
          $searchButton.attr('disabled', false);
        });
      }
      // サブミットしないので false を返す
      return false;
    };

    // 検索入力欄でEnterキーが押されたときの処理
    $('#searchForm').submit({navi:0}, doSearch);
    // 前ページリンクが押されたときの処理
    $('#result').on("click", "#prevPageLink", {navi:-1}, doSearch)
    // 次ページリンクが押されたときの処理
      .on("click", "#nextPageLink", {navi:1}, doSearch);
  });

baseUrlでFessサーバのURLを指定します。SERVERNAMEをFessサーバ名に書き換えてください。JSON形式で取得するので、サーバのURLの後にjson/?q=を指定します。

「fess.js」では検索処理関数doSearchを定義しています。検索フォームがサブミットされたとき、ページリンクがクリックされたときにdoSearchを呼び出します。

検索処理関数doSearch

ここからは検索処理関数doSearchについて説明します。doSearchでは検索リクエストの送信と、検索結果の表示をおこないます。

リクエストが正常に返ってくると、doneの関数が実行されます。 doneの引数には Fess サーバーから返却された検索結果のオブジェクトが渡されます。

検索結果はdata.response.resultに配列として格納されています。 results[i].〜でアクセスすることで検索結果ドキュメントのフィールド値を取得することができます。

実行

「index.html」にブラウザでアクセスすると、検索フォームが表示されます。

検索フォーム

適当な検索語を入力して、検索ボタンを押下すると検索結果が表示されます。 デフォルトの表示件数は20件ですが、ヒットした検索件数が多い場合には検索結果一覧の下に次のページへのリンクが表示されます。

検索結果

*  *  *

FessのJSON APIを利用してjQueryベースのクライアント検索サイトを構築してみました。 JSON APIを利用することでブラウザベースのアプリケーションに限らず、別のアプリケーションから呼び出してFessを利用するシステムも構築できます。

著者紹介

菅谷 信介 (Shinsuke Sugaya)

Apache PredictionIOにて、コミッター兼PMCとして活動。また、自身でもCodeLibs Projectを立ち上げ、オープンソースの全文検索サーバFessなどの開発に従事。