株式会社ライブキャストロゴ 株式会社ライブキャスト

WordPressに全文検索エンジンAlgoliaの導入を検討(インデックス構築編)の続きです。

InstantSearch.jsをフロント側に導入して検索機能を作成したいと思います。

前回のおさらい

だいぶ時間が空いてしまいましたので前回のおさらいを簡単に。一言で言うと「WordPressの記事をAlgoliaのindexに保存する」機能を構築しました。

  1. pluginを作る
  2. Algoliaフレームワークのインストール
  3. プラグインの中にコマンドで実行する処理を実装
  4. 記事容量が大きいときに分割して格納するための仕組みを実装
  5. インデックス構築

今回の検索機能構築の手順は、

  1. Algoliaのindexの設定を変更
  2. InstantSearch.jsの導入
  3. 検索ボックスUIの構築
  4. 検索結果一覧の表示
  5. 検索条件を検索結果画面のurlパラメータにする実装

こんな感じになります。それでは早速始めていきましょう。

Algoliaのindexの設定を変更

Algoliaの管理画面にログインします。該当するindexのページに移動しましょう。

前回、記事容量が大きいときに分割して格納するための仕組みを実装したので、indexには同じ記事が分割されて登録されている、つまり、同じタイトルの記事が複数件になっている場合があるので、まとめてやらないといけません。

「Configuration」のタブに移動して、メニューの一番下の方にある「Deduplication and Grouping」を選択してやりましょう。以下のような画面になると思います。

Deduplication and Grouping menu

①distinctをtrueに変更
②Attribute for Distinctを「distinct_key」に変更
します。「distinct_key」は前回、indexに作成したフィールドの1つです。

続いて、①「Searchable attributes」を選択して検索対象となるフィールドを確定します。

Searchable Attributes

ここでは、②「Add a Searchable Attribute」ボタンをクリックしてtitleとcontentを追加しました。

ホームページの検索結果画面に表示させるのもtitleとcontentにしようと決めたのですが、contentは記事の本文全文なので結界一覧の文章が長くなってしまいます。抜粋を表示させたいのでそれを設定するために、これまたメニューの下の方にある「Snippeting」を選択します。

Snippeting menu

①「Add an Attribute」ボタンをクリックしフィールドを登録
contentを選択し抜粋になるように、桁数を調整します。ここはデザインによって変わってくるかと思います。

indexの設定は以上になります。

InstantSearch.jsの導入

それではjsファイルとcssファイルを読み込みの手順からやっていきましょう。この部分だけではないのですが参考になるページを探すのに結構苦労しました。
Building Search UI | WordPress | Algolia Documentation
このページのリンクから、

  • algoliasearch-lite.umd.js
  • instantsearch.production.min.js
  • algolia-min.css

ダウンロードしてWordPressのテーマフォルダにコピーしておきます。WordPressのfunctions.phpにこれらのファイルを読み込むアクションフックを追記します。Algoliaのサイトにもありますが、.jsも.cssもまとめるとこのような感じになるかと思います。

function algolia_load_assets() {
    $clientPath = '/js/vendor/algoliasearch-lite.umd.js';
    $instantSearchPath = '/js/vendor/instantsearch.production.min.js';

    $clientVersion = date("ymd-Gis", filemtime( get_template_directory() . $clientPath));
    $instantSearchVersion = date("ymd-Gis", filemtime( get_template_directory() . $instantSearchPath));

    wp_enqueue_script('algolia-client', get_template_directory_uri() . $clientPath, array(), $clientVersion, true);
    wp_enqueue_script('algolia-instant-search', get_template_directory_uri() . $instantSearchPath, array('algolia-client'), $instantSearchVersion, true);

    $algoliaPath = '/js/algolia-search.js';
    $algoliaVersion = date("ymd-Gis", filemtime(get_template_directory() . $algoliaPath));
    wp_enqueue_script('algolia-search', get_template_directory_uri() . $algoliaPath, array('algolia-config'), $algoliaVersion, true);
    wp_enqueue_style('algolia-theme', get_template_directory_uri() . '/algolia-min.css');
}
add_action('wp_enqueue_scripts', 'algolia_load_assets');

algolia-min.cssの読み込みは好みでよろしいかと思います。algolia-search.jsを実装していきましょう。まずは初期設定してやりましょう。

const searchClient = algoliasearch("Application_ID", "Search-Only_API_Key");

const search = instantsearch({
  indexName: "YourIndexName",
  searchClient,
});

Algoliaの管理画面からApplication IDとSearch-Only API Keyを準備しておきましょう。instantsearchのオブジェクトを作成します。

検索ボックスUIの構築

続いて、検索ボックスウィジェットの追加の手順です。テンプレートの方には、idがsearchboxという名前の空のdiv要素を用意しておきます。

<div id="searchbox"></div>

ここに検索ボックスを差し込みます。

search.addWidgets([
  instantsearch.widgets.searchBox({
    container: "#searchbox",
  }),
]);

とするとdivの部分が、

検索ボックス

のような表示になります。

検索結果一覧の表示

同じように検索結果一覧のウィジェットを追加していきます。マークアップはオリジナルのものにしたいので、connectHitsというものを使います。
hits | InstantSearch.js | API parameters | API Reference | Algolia Documentation

const renderHits = (renderOptions, isFirstRender) => {
  const { hits, widgetParams } = renderOptions;

  widgetParams.container.innerHTML = `
    ${hits
      .map(
        item =>
        `<section class="entry">
            <h2><a href="${item.url}">
              <strong>
              ${instantsearch.highlight({ attribute: 'title', hit: item })}
              </strong>
            </a></h2>
            <div class="post">
              <p>${instantsearch.snippet({ attribute: 'content', hit: item })}</p>
            </div>
          </section>`
      )
      .join('')}
  `;
};
const customHits = instantsearch.connectors.connectHits(renderHits);

テンプレートには、idがhitsという名前の空のdiv要素を用意しておきます。

<div id="hits"></div>

search.addWidgetsの部分に、

  customHits({
    container: document.querySelector('#hits'),
  }),

を追記します。search.addWidgetsのところのsearchbox部分とまとめるとこのようになります。

search.addWidgets([
  instantsearch.widgets.searchBox({
    container: "#searchbox",
  }),
  customHits({
    container: document.querySelector('#hits'),
  }),
]);

search.start();

最後に、instantsearchをstartしてやりましょう。

検索条件を検索結果画面のurlパラメータにする実装

ここのソースコードの部分のroutingのところを参考にしました。

Routing URLs | Building Search UI | Guide | Algolia Documentation

ただ、ここはカテゴリも含んだ検索条件になっています。今回はそれを含める必要はないので、カテゴリに関連する部分を取り除いて、前半でやったinstantsearchのオブジェクトを作成するところに追記してやりましょう。このようになります。

const search = instantsearch({
  indexName: index_name,
  searchClient,
  routing: {
    router: instantsearch.routers.history({
      createURL({ qsModule, routeState, location }) {
        const urlParts = location.href.match(/^(.*?)\/search/);
        const baseUrl = `${urlParts ? urlParts[1] : ''}/`;
        const queryParameters = {};

        if (routeState.query) {
          queryParameters.query = encodeURIComponent(routeState.query);
        }
        if (routeState.page !== 1) {
          queryParameters.page = routeState.page;
        }

        const queryString = qsModule.stringify(queryParameters, {
          addQueryPrefix: true,
          arrayFormat: 'repeat'
        });

        return `${baseUrl}search/${queryString}`;
      },

      parseURL({ qsModule, location }) {
        const pathnameMatches = location.pathname.match(/search\/(.*?)\/?$/);
        const { query = '', page } = qsModule.parse(
          location.search.slice(1)
        );

        return {
          query: decodeURIComponent(query),
          page,
        };
      }
    }),

    stateMapping: {
      stateToRoute(uiState) {
        const indexUiState = uiState[index_name] || {};

        return {
          query: indexUiState.query,
          page: indexUiState.page,
        };
      },

      routeToState(routeState) {
        return {
          livecast: {
            query: routeState.query,
            page: routeState.page,
          }
        };
      }
    }
  }
});

ここまでのJavaScriptの内容をまとめるとこのようになります。

const searchClient = algoliasearch(application_id, search_api_key);

const search = instantsearch({
  indexName: index_name,
  searchClient,
  routing: {
    router: instantsearch.routers.history({
      createURL({ qsModule, routeState, location }) {
        const urlParts = location.href.match(/^(.*?)\/search/);
        const baseUrl = `${urlParts ? urlParts[1] : ''}/`;
        const queryParameters = {};

        if (routeState.query) {
          queryParameters.query = encodeURIComponent(routeState.query);
        }
        if (routeState.page !== 1) {
          queryParameters.page = routeState.page;
        }

        const queryString = qsModule.stringify(queryParameters, {
          addQueryPrefix: true,
          arrayFormat: 'repeat'
        });

        return `${baseUrl}search/${queryString}`;
      },

      parseURL({ qsModule, location }) {
        const pathnameMatches = location.pathname.match(/search\/(.*?)\/?$/);
        const { query = '', page } = qsModule.parse(
          location.search.slice(1)
        );

        return {
          query: decodeURIComponent(query),
          page,
        };
      }
    }),

    stateMapping: {
      stateToRoute(uiState) {
        const indexUiState = uiState[index_name] || {};

        return {
          query: indexUiState.query,
          page: indexUiState.page,
        };
      },

      routeToState(routeState) {
        return {
          livecast: {
            query: routeState.query,
            page: routeState.page,
          }
        };
      }
    }
  }
});

const renderHits = (renderOptions, isFirstRender) => {
  const { hits, widgetParams } = renderOptions;

  widgetParams.container.innerHTML = `
    ${hits
      .map(
        item =>
        `<section class="entry">
            <h2><a href="${item.url}">
              <strong>
              ${instantsearch.highlight({ attribute: 'title', hit: item })}
              </strong>
            </a></h2>
            <div class="post">
              <p>${instantsearch.snippet({ attribute: 'content', hit: item })}</p>
            </div>
          </section>`
      )
      .join('')}
  `;
};

const customHits = instantsearch.connectors.connectHits(renderHits);

search.addWidgets([
  instantsearch.widgets.searchBox({
    container: "#searchbox",
  }),
  customHits({
    container: document.querySelector('#hits'),
  }),
]);

search.start();

この結果できあがったのがこちらになります!
サイト内検索 | 株式会社ライブキャスト