Mono Works

チラシのすきま

Hugoで javascriptを使った全文検索(インクリメンタルサーチ)

こちらの記事を真似て、うちのサイトにも検索ページを作ってみました。

実際に使ってみると、検索結果はインクリメンタルサーチとして機能し、文字入力するごとに完全または途中まで一致する単語で絞り込まれていきます。また、A《スペース》Bの結果は、A and Bでも A or Bでもなく、スペースも含めて一つの単語としてA《スペース》Bに完全または途中まで一致する部分をピックアップしてきます。

2018-07-09-hugo-js-full-text-search01 先日オープンしたパン屋さん

導入方法は、参考ページまんまですが、少しだけハマったところがあったので、その補足説明を加えて、導入方法をまとめました。

参考サイト:Hugo に全文検索を取り付けた | the right stuff

更新履歴

変更: 2019/08/08

  • 2019/08/08:下記サイトを参考にインデックスファイルの生成を見直し、検索方法を変更しました。「JavaScriptの文字列として含んではいけない文字をエスケープするために、Hugoのjsonify関数を使用」されていて、こちらの問題も回避できました。

参考サイト:全文検索(インクリメンタルサーチ)の機能を付ける | まくまくHugo/Goノート

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

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

全文検索用のインデックスファイル index.js を生成するためのテンプレートファイル(single.html)を作成して、layouts/js/single.html に配置。

single.htmlの中身がこちら

{{ define "escape" }}
  {{- trim (replace . "\n" " ") " " | replaceRE " +" " " | jsonify -}}
{{ end }}

var data = [
{{- range $index, $page := where .Site.Pages "Section" "post" }}
  {
    url: {{ $page.Permalink | jsonify }},
    title: {{ $page.Title | jsonify }},
    date: {{ $page.Date | jsonify }},
    body: {{ template "escape" (printf "%s %s" $page.Title $page.Plain) }}
  },
{{- end }}
];

上記内容で、Sectionが postの記事から、URLタイトル日付内容を抽出して、インデックスを生成します。

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

テンプレートで設定した内容を書き出すための空の投稿ファイル(indexjs.md)を作成して、Front matterのみを記述。

+++
date = "2010-10-01T00:00:01+09:00"
type = "js"
url = "index.js"
+++

日付は適当に、typejsurlindex.jsを指定して、固定ページを置いてある content/pages/indexjs.mdに配置しました。

インデックスファイルを生成してみる

ビルドして、public/index.jsを確認すると、うちのサイトの場合、過去5年分233個の記事が抽出されて、ファイルサイズは 1,109KB でした。

検索ページの作成

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

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

検索ページ用のテンプレートファイル(single.html)を作成して、layouts/search/single.htmlに配置。

single.htmlの中身は、参考ページのソースを真似させていただき、CSSで調整しました。

<div class="entry-content">
  <p><h1 id="ブログ インクリメンタルサーチ">ブログ インクリメンタルサーチ</h1>
    <div class="ulist">
      <ul>
        <li>複数キーワードで検索したい場合は、右のGoogle検索をお使い下さい。</li>
        <li>キーワードにメタ文字を含む場合、エスケープが必要です。</li>
      </ul>
    </div>

    <script src="/index.js"></script>

    <style>
      #searchbox > input {
        color: #666;
        font-size: 1.2em;
        font-weight: bolder;
        border: solid;
        border-color: #ccc;
        border-width: 1px;
        padding: 5px;
      }
      input::-webkit-input-placeholder {
        color: #999;
      }
      #result {
        margin: 1em;
      }
      .item_title {
        text-decoration: none;
        color: #009FE8;
        font-weight: bolder;
      }
      .item_excerpt {
        background: #fff;
        margin: 0.5em 2em 1em;
        padding: 0.5em;
        border: dashed 1px #ddd;
        font-size: smaller;
      }
      .item_excerpt b {
        background: #A3DDE3;
      }
    </style>

    <div id="searchbox">
      <input onkeyup="search(this.value)" size="15" autocomplete="off" autofocus placeholder="検索ワードを入力" />
      <span id="inputWord"></span> <span id="resultCount"></span>
      <div id="result"></div>
    </div>

    <script>
      function search(query) {
        var result = searchData(query);
        var html = createHtml(result);
        showResult(html);
        showResultCount(result.length, data.length);
      }

      function searchData(query) {
        var result = [];

        query = query.trim();
        if (query.length < 1) {
          return result;
        }
        var re = new RegExp(query, 'i');
        for (var i = 0; i < data.length; ++i) {
          var pos = data[i].body.search(re);
          if (pos != -1) {
            result.push([i, pos, pos + query.length]);
          }
        }
        return result;
      }

      function createHtml(result) {
        var htmls = [];
        for (var i = 0; i < result.length; ++i) {
          var dataIndex = result[i][0];
          var startPos = result[i][1];
          var endPos = result[i][2];
          var url = data[dataIndex].url;
          var title = data[dataIndex].title;
          var body = data[dataIndex].body;
          htmls.push(createEntry(url, title, body, startPos, endPos));
        }
        return htmls.join('');
      }

      function createEntry(url, title, body, startPos, endPos) {
        return '<div class="item">' +
          '<a class="item_title" href="' + url + '">' + title + '</a>' +
          '<div class="item_excerpt">' + excerpt(body, startPos, endPos) + '</div>' +
          '</div>';
      }

      function excerpt(body, startPos, endPos) {
        return [
          body.substring(startPos - 30, startPos),
          '<b>', body.substring(startPos, endPos), '</b>',
          body.substring(endPos, endPos + 200)
        ].join('');
      }

      function showResult(html) {
        var el = document.getElementById('result');
        el.innerHTML = html;
      }

      function showResultCount(count, total) {
        var el = document.getElementById('resultCount');
        el.innerHTML = '<b>' + count + '</b> 件見つかりました(' + total + '件中)';
      }
      </script>

    <noscript><p class="notice">注意: この検索機能は JavaScript を使用しています。</p></noscript>
  </p>
</div>

検索ページ表示用のファイル作成

テンプレートで設定した検索ページを表示するためのファイル(search.md)を作成して、Front matterのみを記述。

+++
date = "2010-10-01T00:00:01+09:00"
type = "search"
url = "search"
title = "ブログ全文検索"
+++

日付は適当に、typesearchurlsearchを指定して、インデックスファイルと同じく固定ページを置いてある content/pages/search.mdに配置しました。

問題発生(2019/08/08 解決)

下記サイトを参考にインデックスファイルの生成を見直したので、この問題は解決しました。

参考サイト:全文検索(インクリメンタルサーチ)の機能を付ける | まくまくHugo/Goノート

ビルドして、検索ページが表示されたので、大丈夫かと思ったのですが、実際に検索してみると、何も検索結果が表示されませんでした。

あれ?と思い、いろいろと見直した結果、作成したインデックスファイル(index.js)が壊れていたことが分かりました。

2018-07-09-hugo-js-full-text-search02

具体的には、記事内の 《バックスラッシュ》uの部分に 16 進の数字が必要です。というエラーが発生していました。

参考:16 進数の数字が必要です。 - MSDN - Microsoft

直接インデックスファイル(index.js)を修正する場合、 《バックスラッシュ》u《バックスラッシュ》《バックスラッシュ》uとすれば良いのですが、ビルドの度に手動でインデックスファイル(index.js)を修正するのは面倒だったので、元記事を下記のように変更して対応しました。(対処療法なのは見逃して…)

変更前 google《バックスラッシュ》usb_driverフォルダに~

変更後 google《スラッシュ》usb_driverフォルダに~

2つの検索方法

最初に書いたように、今回導入した検索は、インクリメンタルサーチとして動作し、入力する単語は1つのキーワードとして検索をおこないます。これに対して、これまで利用してきたGoogle検索は、複数のキーワードをスペースで区切って検索してくれます。

どちらも良いところがあるので、これまで利用してきた使ってきたGoogle検索はサイドメニューに Googleブログ検索と名前だけ変えてそのまま残し、今回導入した検索は、もうひとつの検索として ページ上部メニューの右端に追加しました。用途に応じて使い分け下さいませませ。

おしまい

コメント

コメントなどありましたら、GitHubのディスカッションへお願いします。(書き込みには、GitHubのアカウントが必要です)
執筆者
"ぽぽろんち" @pporoch
pporoch120
Mono Worksの中の人。好きなことをつらつらと書き留めてます。
ギターを始めてから 練習動画をYouTubeにアップしてます。ご笑納ください。
"DQX@ぬここ(UD487-754)、コツメ(NO078-818)"
採用案内