検索機能を実装する

Kurocoでサイト内検索を実装する方法として、下記2つの方法があります。

  • filter機能を利用する
  • 商用サービス(Algolia、Syncsearch等)を利用する

本チュートリアルでは、filter機能を利用したコンテンツ検索機能の実装方法を記載します。

filter機能を利用して検索を実装する方法

APIのfilter機能を利用することで、コンテンツの条件検索やキーワード検索を実装することができます。

Image (fetched from Gyazo)

検索機能を実装するにあたって、まずはfilter機能の概要を説明します。

filterは、APIエンドポイントの取得対象データを絞り込むための機能です。エンドポイントの「filter」パラメータに次のようなクエリを指定すると、条件に合致するデータのみを取得することができます。

// topics_idが1のコンテンツを指定
topics_id = 1

完全一致/部分一致、数値や日付の比較、AND/ORなど、条件を柔軟に指定できるため、複雑な検索を実現することも可能です。

// タイトルが「WORD」を含むコンテンツ、または2021-01-01 ~ 2021-12-31の間に追加されたコンテンツを指定
subject contains "WORD" OR (inst_ymdhi >= "2021-01-01" AND inst_ymdhi <= "2021-12-31")

filterパラメータは下記の2箇所から指定することができ、それぞれ役割が異なります。

指定箇所説明
エンドポイント設定常に付与される固定の検索条件を設定します。
例えば、ここで inst_ymdhi >=:relatively "-1 year" を設定した場合、現在日時から1年以内に追加されたコンテンツを常に取得します。
GETパラメータ検索条件を動的に指定します。
エンドポイント設定でもfilterを設定済みの場合は、以下のように両方のクエリをAND条件で結合した状態で検索を行い、結果を返します。
filterクエリ(エンドポイント設定) AND filterクエリ(GETパラメータ)

今回の検索機能のように、ユーザーの入力内容に応じて取得結果を変えたい場合は、GETパラメータでfilterクエリを指定します。
固定の検索条件を追加で指定したい場合は、必要に応じてエンドポイント設定にもクエリを入力してください。

注: filterは、対象のAPIモデルが機能をサポートしている場合のみ利用できます。エンドポイントの設定画面、またはSwagger UIの画面を確認し、対象のエンドポイントに「filter」パラメータが存在するかを確認してください。

それではfilterを利用した検索の実装方法を説明していきます。今回は下記2パターンでの実装方法を説明します。

  • 条件検索
  • キーワード検索

条件検索の実装

ここからは先ほどの機能概要を踏まえて、条件検索機能を実装する方法を説明していきます。

1. コンテンツ定義とエンドポイントを作成する

下記のコンテンツ定義とエンドポイントを作成します。

コンテンツ定義

コンテンツ定義は下記の設定で作成します。

  • グループ名:Search
  • グループID:34(自動採番されます)

Image (fetched from Gyazo)

また、拡張項目を使用し下記フィールド追加しています。

フィールドid項目名設定項目オプション
ext_col_01Textテキスト-
ext_col_02Select選択形式01::option1
02::option2
03::option3
ext_col_03Checkbox複数選択(チェックボックス)01::option1
02::option2
03::option3

Image (fetched from Gyazo)

また、作成したコンテンツ定義「Search」にて、下記のようにテストデータを3件登録しました。

Image (fetched from Gyazo)

エンドポイント

エンドポイントは下記の設定で作成します。

項目
パス/rcms-api/14/content
カテゴリーコンテンツ
モデルTopics (v1)
オペレーションlist
topics_group_id34

注: topics_group_idには、ご自身のコンテンツ定義のIDを記入してください。

Image (fetched from Gyazo)

Image (fetched from Gyazo)

2. エンドポイントを設定する

[API] -> API LIST画面から先ほど作成したエンドポイントを選択し、「更新」 をクリックします。

Image (fetched from Gyazo)

次に、filter_request_allow_listの設定項目に移動し、検索対象の項目名を指定します。

Image (fetched from Gyazo)

filter_request_allow_listは、GETパラメータでの検索を許可する項目を指定するための設定です。

初期状態では、GETパラメータによるfilterクエリの指定は無効化されています。ここで個別に項目名を指定することで初めて、対象項目への検索が有効になります。

今回は下記を追加します。

  • subject
  • inst_ymdhi
  • ext_col_01
  • ext_col_02
  • ext_col_03

Image (fetched from Gyazo)

参考:
ここで :ALL を指定した場合、全ての項目に対する検索が許可されます。便利な機能ですが、これは時にAPIのセキュリティを弱める原因にもなり得ます。(例えば、特定の項目をレスポンスデータに返さないよう設定している場合、その項目も含めて検索対象となります)
そのため、基本的には対象の項目を個別に設定することを推奨します。
:ALLを設定する場合は、対象のエンドポイントが返すデータの内容を確認し、本当に問題がないかどうかを確認してください。

設定が完了したら、[Update] ボタンをクリックし保存してください。

Image (fetched from Gyazo)

3. エンドポイントの動作を確認する

API LIST画面より「Swagger UI」をクリックし、Swagger UI画面に移動します。

Image from Gyazo

設定したエンドポイントをクリックし、動作確認を行います。

Image (fetched from Gyazo)

[Try it out] をクリックします。

Image (fetched from Gyazo)

「filter」パラメータにクエリを入力します。今回は「subject contains "Test"」と記入します。

参考:
記述可能なクエリの形式については、下記ドキュメントを参照してください。
リファレンス:検索機能の使い方

Image (fetched from Gyazo)

入力が完了したら [Execute] ボタンをクリックします。

Image (fetched from Gyazo)

結果が表示されるので、期待通りに検索が行えているかを確認してください。

Image (fetched from Gyazo)

以上でエンドポイントの動作が確認できました。

4. 検索機能を実装する

では、先ほど設定したエンドポイントを利用して、実際に条件検索画面を実装していきましょう。

今回はNuxt.jsを使い、以下のようにシンプルな検索フォーム・検索結果テーブルを表示するコンポーネントを作成します。

Image (fetched from Gyazo)

まずは下記のコンポーネントを用意します。

<template>
  <div>
    <div class="search-form">
      <p>
        <label for="subject">Title</label>
        <input v-model="searchInput.subject" type="text">
      </p>
      <p>
        <label for="inst_ymdhi">Created at</label>
        <input v-model="searchInput.inst_ymdhi.from" type="date"> 
        ~
        <input v-model="searchInput.inst_ymdhi.to" type="date"> 
      </p>
      <p>
        <label for="ext_col_01">Text</label>
        <input v-model="searchInput.ext_col_01" type="text">
      </p>
      <p>
        <label for="ext_col_02">Select</label>
        <select v-model="searchInput.ext_col_02">
          <option value="">Not selected</option>
          <option value="01">option1</option>
          <option value="02">option2</option>
          <option value="03">option3</option>
        </select>
      </p>
      <p>
        <label for="ext_col_03">Checkbox</label>
        <input v-model="searchInput.ext_col_03" type="checkbox" value="01">option1
        <input v-model="searchInput.ext_col_03" type="checkbox" value="02">option2
        <input v-model="searchInput.ext_col_03" type="checkbox" value="03">option3
      </p>  
      <button type="button">Search</button>
    </div>
    <div v-if="Object.keys(searchResult).length > 0" class="search-result">
      <template v-if="(searchResult.errors || []).length === 0">
        <table>
          <tr>
            <th>ID</th>
            <th>Title</th>
            <th>Created at</th>
            <th>Text</th>
            <th>Select</th>
            <th>Checkbox</th>
          </tr>
          <tr v-for="content in searchResult.list" :key="content.topics_id">
            <td>{{ content.topics_id }}</td>
            <td>{{ content.subject }}</td>
            <td>{{ content.inst_ymdhi }}</td>
            <td>{{ content.ext_col_01 }}</td>
            <td>{{ content.ext_col_02 }}</td>
            <td>{{ content.ext_col_03 }}</td>
          </tr>
        </table>
      </template>
      <template v-else>
        {{ searchResult.errors }}
      </template>
    </div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      searchInput: {
        subject: '',
        inst_ymdhi: {
          from: '',
          to: '',
        },
        ext_col_01: '',
        ext_col_02: '',
        ext_col_03: []
      },
      searchResult: {},
    }
  },
  mounted() {},
  methods: {}
}
</script>

<style scoped>
.search-form {
  border: 1px solid;
  padding: 10px;
}
.search-form label {
  display: block;
  float: left;
  width: 100px;
}
.search-result {
  width: 100%;
  margin-top: 20px;
}
.search-result table, th, td {
  border: solid 1px;
  border-collapse: collapse;
}
.search-result th, td {
  padding: 5px;
}
.search-result table {
  width: 100%;
}
</style>

.search-formは検索フォーム、.search-resultは検索結果を表示するための要素です。

dataオブジェクトのsearchInputにはユーザーの入力内容を、searchResultには検索結果のレスポンスを格納します。

data() {
  return {
    // ユーザーの入力内容 (.search-form)
    searchInput: {
      subject: '',
      inst_ymdhi: {
        from: '',
        to: '',
      },
      ext_col_01: '',
      ext_col_02: '',
      ext_col_03: []
    },
    // 検索結果のレスポンス (.search-result)
    searchResult: {},
  }
},

ここまでの状態でサーバーを立ち上げ、ブラウザでアクセスしてみましょう。画面上には以下の入力フォームが表示されます。

Image (fetched from Gyazo)

しかしながら、まだエンドポイントの呼び出し処理を実装していないため「Search」ボタンをクリックしても何も起こらない状態です。ここからはmethodsを定義し、実際にエンドポイントを呼び出して検索を行う処理を実装していきます。

まずは、searchメソッドを作成します。ここには、「1. エンドポイントを設定する」で設定したエンドポイントを呼び出す処理を記述します。

filterクエリの生成処理については、buildFilterQueryメソッドに移譲するようにしています。(このメソッドの具体的な処理については、後ほど記述します。)

methods: {
  // エンドポイントへのリクエストを行い、取得結果をsearchResultに格納
  search() {
    // 自分の環境で設定したエンドポイントのURLに置き換えてください
    this.$axios.get("https://example.g.kuroco.app/rcms-api/14/content", {
      params: {
        filter: this.buildFilterQuery()
      }
    }).then(response => {
      this.searchResult = response.data || {};
    }).catch(({ response }) => {
      this.searchResult = !(response instanceof Object) || !Array.isArray(response.data.errors)
        ? { errors: ['Unexpected error'] }
        : response.data;
    });
  },
  // filterクエリの生成
  buildFilterQuery() {
    return '';
  }
}

作成が完了したら、searchメソッドを[Search] ボタンのクリックイベントとして定義します。 [Search] ボタンは .search-form 要素の最下部に存在します。

<!--
    <button type="button">Search</button> <= @click="search" を追記
-->
<button type="button" @click="search">Search</button>

続いては、buildFilterQueryの具体的な処理を記述していきます。data属性 searchInput に格納されている値を、filterクエリに変換します。

今回は、以下のような条件でクエリを生成します。

  • 日付: 範囲指定
  • テキスト: 部分一致
  • 選択(select): 完全一致
  • 複数選択(checkbox): 部分一致

値が未入力の場合は、対象の項目は検索条件として指定しないものとします。

生成すべきクエリはinputの形式、対象の項目、機能要件などによって異なります。必要に応じて適した処理を実装してください。

methods: {
  // ...
  buildFilterQuery() {
    const filterQuery = Object.entries(this.searchInput).reduce((queries, [col, value]) => {
      switch (col) {
        // 日付: 範囲指定
        case 'inst_ymdhi':
          if (value.from !== '') {
            queries.push(`${col} >= "${value.from}"`);
          }
          if (value.to !== '') {
            queries.push(`${col} <= "${value.to}"`);
          }
          break;
        // テキスト: 部分一致
        case 'subject':
        case 'ext_col_01':
          if (value !== '') {
            queries.push(`${col} contains "${value}"`);
          }
          break;
        // 選択(select): 完全一致
        case 'ext_col_02':
          if (value !== '') {
            queries.push(`${col} = "${value}"`);
          }
          break;
        // 複数選択(checkbox): 部分一致
        case 'ext_col_03':
          if (value.length > 0) {
            queries.push('(' + value.map(v => `${col} contains "${v}"`).join(' OR ') + ')');
          }
          break;
        default:
          break;
      }
      return queries;
    }, []).join(' AND ');

    return filterQuery;
  }
}

最後に、mountedに以下のコードを追加し、初期遷移時には条件指定のないコンテンツ一覧を表示するようにします。

mounted() {
  this.search();
},

以上で、条件検索コンポーネントの実装は完了です。 localhostにアクセスしてフォームを操作し、期待通りに結果を取得できているかを確認してください。

Image (fetched from Gyazo)

以上で条件検索の実装を終わります。

キーワード検索の実装

続いては、キーワード検索を実装する方法を説明します。

キーワード検索を実装するにあたって、条件検索の例を踏まえると、どのような実装方法が考えられるでしょうか。まずは以下のように、対象のカラムに対しての部分一致検索クエリを OR で連結する方法を思いつくかもしれません。

subject contains "KEYWORD" OR ext_col_01 contains "KEYWORD"

確かにこの方法を利用すれば、キーワード検索の機能自体は実現することが可能です。ですが、検索対象のカラムが増えた場合に少し問題があります。 以下のように、非常に冗長で読みづらいクエリを渡す必要があるためです。

subject contains "KEYWORD" OR ext_col_01 contains "KEYWORD" OR ext_col_04 contains "KEYWORD" OR ext_col_05 contains "KEYWORD" OR ext_col_06 contains "KEYWORD" OR ...

そのためfilter機能では次のように、よりシンプルなクエリでキーワード検索を実装できる仕組みを用意しています。

keyword contains "KEYWORD"

今回はこの keyword 機能を利用して、キーワード検索を実装していきます。

1. コンテンツ定義とエンドポイントを作成する

今回は、下記のコンテンツ定義とエンドポイントを利用します。

コンテンツ定義

コンテンツ定義は条件検索の実装で定義した「Search」を利用します。

エンドポイント

エンドポイントは下記の設定で作成します。

項目
パス/rcms-api/14/content_keyword
カテゴリーコンテンツ
モデルTopics (v1)
オペレーションlist
topics_group_id34

注: topics_group_idには、ご自身のコンテンツ定義のIDを記入してください。

Image (fetched from Gyazo)

Image (fetched from Gyazo)

2. エンドポイントを設定する

API LIST画面から、検索機能を実装したいエンドポイントを選択し、[更新] をクリックします。

Image (fetched from Gyazo)

次に、filter_request_allow_listの設定項目に移動し、検索対象の項目名を指定します。

Image (fetched from Gyazo)

キーワード検索の許可リストを設定する場合には、以下のフォーマットを利用することができます。

形式説明
keyword全項目に対するキーワード検索を許可します。
例) keyword
keyword:[項目名1,項目名2,...]対象の項目名をカンマ区切りで入力し、指定した項目名に対するキーワード検索を許可します。
例) keyword:[subject]
例) keyword:[ext_col_01,ext_col_02]

:ALL を指定した場合は他の項目と同様に、keywordも対象の項目として含まれます。

今回は以下のように、keyword:[subject,ext_col_01]を指定し、タイトルとテキスト項目をキーワード検索の対象とします。

Image (fetched from Gyazo)

設定が完了したら、[Update] ボタンをクリックし保存してください。

Image (fetched from Gyazo)

3. エンドポイントの動作を確認する

API LISt画面より「Swagger UI」をクリックし、Swagger UI画面に移動します。

Image (fetched from Gyazo)

設定したエンドポイントをクリックし、動作確認を行います。

Image (fetched from Gyazo)

[Try it out] をクリックします。

Image (fetched from Gyazo)

「filter」パラメータにクエリを入力します。部分一致で検索させるため、 contains を指定します。 今回は「keyword contains "1"」と記入します。

Image (fetched from Gyazo)

入力が完了したら [Execute] ボタンをクリックします。

Image (fetched from Gyazo)

結果が表示されるので、期待通りに検索が行えているかを確認してください。

Image (fetched from Gyazo)

以上でエンドポイントの動作が確認できました。

4. 検索機能を実装する

では、先ほど設定したエンドポイントを利用して、実際にキーワード検索画面を実装していきましょう。

条件検索と同様に、今回もNuxt.jsを使って、以下のようなコンポーネントを作成します。

Image (fetched from Gyazo)

まずは下記のコンポーネントを用意します。

<template>
  <div>
    <div class="search-form">
      <p>
        <label for="keyword">Keyword</label>
        <input v-model="searchInput.keyword" type="text">
      </p>
      <button type="button" @click="search">Search</button>
    </div>
    <div v-if="Object.keys(searchResult).length > 0" class="search-result">
      <template v-if="(searchResult.errors || []).length === 0">
        <table>
          <tr>
            <th>ID</th>
            <th>Title</th>
            <th>Created at</th>
            <th>Text</th>
            <th>Select</th>
            <th>Checkbox</th>
          </tr>
          <tr v-for="content in searchResult.list" :key="content.topics_id">
            <td>{{ content.topics_id }}</td>
            <td>{{ content.subject }}</td>
            <td>{{ content.inst_ymdhi }}</td>
            <td>{{ content.ext_col_01 }}</td>
            <td>{{ content.ext_col_02 }}</td>
            <td>{{ content.ext_col_03 }}</td>
          </tr>
        </table>
      </template>
      <template v-else>
        {{ searchResult.errors }}
      </template>
    </div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      searchInput: {
        keyword: '',
      },
      searchResult: {},
    }
  },
  mounted() {
    this.search();
  },
  methods: {
    search() {
      // 自分の環境で設定したエンドポイントのURLに置き換えてください
      this.$axios.get("https://example.g.kuroco.app/rcms-api/14/content_keyword", {
        params: {
          filter: this.buildFilterQuery()
        }
      }).then(response => {
        this.searchResult = response.data || {};
      }).catch(({ response }) => {
        this.searchResult = !(response instanceof Object) || !Array.isArray(response.data.errors)
          ? { errors: ['Unexpected error'] }
          : response.data;
      });
    },
    buildFilterQuery() {
      return '';
    }
  }
}
</script>

<style scoped>
.search-form {
  border: 1px solid;
  padding: 10px;
}
.search-form label {
  display: block;
  float: left;
  width: 100px;
}
.search-result {
  width: 100%;
  margin-top: 20px;
}
.search-result table, th, td {
  border: solid 1px;
  border-collapse: collapse;
}
.search-result th, td {
  padding: 5px;
}
.search-result table {
  width: 100%;
}
</style>

下記を除いて、コンポーネントの属性定義は条件検索のものと同様です。

  • テンプレートの.search-form要素
  • dataオブジェクトのsearchInputプロパティ
  • buildFilterQueryメソッド

.search-form要素には、キーワードの入力フォームのみを定義しています。
入力された値は、searchInput.keywordに格納するようにします。

<div class="search-form">
    <p>
    <label for="keyword">Keyword</label>
    <input v-model="searchInput.keyword" type="text">
    </p>
    <button type="button" @click="search">Search</button>
</div>
data() {
  return {
    searchInput: {
      keyword: '',
    },
    searchResult: {},
  }
},

最後にキーワード検索機能を実装すれば完成です。 buildFilterQueryメソッドを編集し、キーワード検索クエリの生成処理を記述します。

methods: {
  // ...
  buildFilterQuery() {
    const filterQuery = Object.entries(this.searchInput).reduce((queries, [col, value]) => {
      switch (col) {
        case 'keyword':
          if (value !== '') {
            queries.push(`${col} contains "${value}"`);
          }
          break;
        default:
          break;
      }
      return queries;
    }, []).join(' AND ');
    return filterQuery;
  }
}

以上で、キーワード検索コンポーネントの実装は完了です。

localhostにアクセスしてフォームを操作し、期待通りに結果を取得できているかを確認してください。

Image (fetched from Gyazo)

以上でキーワード検索の実装を終わります。

参考:商用サービスを利用して検索を実装する

サイト内検索のように網羅的な全文検索を実装したい場合は、商用検索サービスを利用することで実現できます。

本チュートリアルでは具体的な説明方法は割愛しますが、より検索に力を入れたい場合は下記のサービスをご利用することもお勧めします。