検索機能を実装する
Kurocoでサイト内検索を実装する方法として、下記2つの方法があります。
- filter機能を利用する
- 商用サービス(Algolia、Syncsearch等)を利用する
本チュートリアルでは、filter機能を利用したコンテンツ検索機能の実装方法を記載します。
filter機能を利用して検索を実装する方法
APIのfilter機能を利用することで、コンテンツの条件検索やキーワード検索を実装できます。
検索機能を実装するにあたって、まずは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箇所から指定でき、それぞれ役割が異なります。
指定箇所 | 説明 |
---|---|
エンドポイント設定 | Kurocoの管理画面(API画面)で設定するものです。 常に付与される固定の検索条件を設定します。 例えば、ここで inst_ymdhi >=:relatively "-1 year" を設定した場合、現在日時から1年以内に追加されたコンテンツを常に取得します。 |
GETパラメータ | フロントエンドからAPIに動的に指定するものです。 エンドポイント設定でもfilterを設定済みの場合は、以下のように両方のクエリをAND条件で結合した状態で検索を行い、結果を返します。 filterクエリ(エンドポイント設定) AND filterクエリ(GETパラメータ) |
今回の検索機能のように、ユーザーの入力内容に応じて取得結果を変えたい場合は、GETパラメータでfilterクエリを指定します。
固定の検索条件を追加で指定したい場合は、必要に応じてエンドポイント設定にもクエリを入力してください。
filterは、対象のAPIモデルが機能をサポートしている場合のみ利用できます。エンドポイントの設定画面、またはSwagger UIの画面を確認し、対象のエンドポイントに「filter」パラメータが存在するかを確認してください。
それではfilterを利用した検索の実装方法を説明していきます。今回は下記2パターンでの実装方法を説明します。
- 条件検索
- キーワード検索
事前準備
APIを追加する
API
下記のAPIを作成します。
項目 | 値 |
---|---|
タイトル | 検索機能のAPI |
版 | 1.0 |
説明 | 検索機能のAPI |
並び順 | 0 |
追加するをクリックすると、追加したAPIに遷移しますので、続いて、セキュリティの設定をします。
[セキュリティ]をクリックします。
[Cookie]を選択して[保存する]をクリックします。
CORS
次にCORSの設定をします。[CORSを設定する] をクリックします。
CORS_ALLOW_ORIGINSの [Add Origin] をクリックし、下記を追加します。
http://localhost:3000
- フロントエンドドメイン
CORS_ALLOW_METHODSの [Add Method] をクリックし、下記を追加します。
- GET
- POST
- OPTIONS
設定できたら[保存する]をクリックし、CORSの設定が完了です。
条件検索の実装
ここからは先ほどの機能概要を踏まえて、条件検索機能を実装する方法を説明していきます。
1. コンテンツ定義とエンドポイントを作成する
下記のコンテンツ定義とエンドポイントを作成します。
コンテンツ定義
コンテンツ定義は下記の設定で作成します。
- グループ名:Search
- グループID:9(自動採番されます)
また、拡張項目を使用し下記フィールド追加しています。
ID | 項目名 | 設定項目 | オプション |
---|---|---|---|
1 | Text | テキスト | - |
2 | Select | 単一選択 | 01::option1 02::option2 03::option3 |
3 | Checkbox | 複数選択可 | 01::option1 02::option2 03::option3 |
また、作成したコンテンツ定義「Search」にて、下記のようにテストデータを3件登録しました。
エンドポイント
エンドポイントは下記の設定で作成します。
項目 | 値 |
---|---|
パス | content |
カテゴリー | コンテンツ |
モデル | Topics (v1) |
オペレーション | list |
topics_group_id | 9 |
topics_group_idには、ご自身のコンテンツ定義のIDを記入してください。
2. エンドポイントを設定する
[API] -> API LIST画面から先ほど作成したエンドポイントを選択し、「更新」 をクリックします。
次に、filter_request_allow_list
の設定項目に移動し、検索対象の項目名を指定します。
filter_request_allow_list
は、GETパラメータでの検索を許可する項目を指定するための設定です。
初期状態では、GETパラメータによるfilterクエリの指定は無効化されています。ここで個別に項目名を指定することで初めて、対象項目への検索が有効になります。
今回は下記を追加します。
- subject
- inst_ymdhi
- ext_1
- ext_2
- ext_3
Kurocoをお申込みいただいたタイミングによっては、各拡張項目がext_1
ではなく、ext_col_01
となる場合があります。
うまくいかない場合は Swagger UIでレスポンスをご確認ください。
ここで :ALL
を指定した場合、全ての項目に対する検索が許可されます。便利な機能ですが、これは時にAPIのセキュリティを弱める原因にもなり得ます。(例えば、特定の項目をレスポンスデータに返さないよう設定している場合、その項目も含めて検索対象となります)
そのため、基本的には対象の項目を個別に設定することを推奨します。
:ALL
を設定する場合は、対象のエンドポイントが返すデータの内容を確認し、本当に問題がないかどうかを確認してください。
設定が完了したら、[更新] ボタンをクリックし保存してください。
3. エンドポイントの動作を確認する
API LIST画面より「Swagger UI」をクリックし、Swagger UI画面に移動します。
設定したエンドポイントをクリックし、動作確認を行います。
[Try it out] をクリックします。
「filter」パラメータに検索条件となるクエリを入力します。
タイトル検索
今回は「Test」という名前の記事を作っている前提で、「subject contains "Test"
」と記入し、この記事が取得できることを確認します。
記述可能なクエリの形式については、下記ドキュメントを参照してください。
リファレンス:検索機能の使い方
入力が完了したら [Execute] ボタンをクリックします。
結果が表示されるので、期待通りに検索が行えているかを確認してください。
単一選択などの選択形式検索
「ext_2 = "01"
」と記入し、「option1」が選択されたコンテンツを取得できることを確認します。
入力が完了したら [Execute] ボタンをクリックします。
結果が表示されるので、期待通りに検索が行えているかを確認してください。
単一選択などの選択形式のkeyは文字列で保存されます。
フィルタをかける際は明示的に " "
で囲ってください。
例:単一選択などの選択形式を以下のように登録していた場合でも、ext_2 = 1
ではなく、ext_2 = "1"
でフィルタをかける。
1::option1
2::option2
3::option3
以上でエンドポイントの動作が確認できました。
4. 検索機能を実装する
では、先ほど設定したエンドポイントを利用して、実際に条件検索画面を実装していきましょう。
今回はNuxt.jsを使い、以下のようにシンプルな検索フォーム・検索結果テーブルを表示するコンポーネントを作成します。
まずは下記のコンポーネントを、pages/search/index.vue
として用意します。
<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_1">Text</label>
<input v-model="searchInput.ext_1" type="text">
</p>
<p>
<label for="ext_2">Select</label>
<select v-model="searchInput.ext_2">
<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_3">Checkbox</label>
<input v-model="searchInput.ext_3" type="checkbox" value="01">option1
<input v-model="searchInput.ext_3" type="checkbox" value="02">option2
<input v-model="searchInput.ext_3" 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_1 }}</td>
<td>{{ content.ext_2 }}</td>
<td>{{ content.ext_3 }}</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_1: '',
ext_2: '',
ext_3: []
},
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_1: '',
ext_2: '',
ext_3: []
},
// 検索結果のレスポンス (.search-result)
searchResult: {},
}
},
npm run dev
コマンドを実行してローカル環境を立ち上げ、http://localhost:3000/search
にアクセスすると、下記の画面が表示されます。
画面上には以下の入力フォームが表示されます。
しかしながら、まだエンドポイントの呼び出し処理を実装していないため「Search」ボタンをクリックしても何も起こらない状態です。ここからはmethods
を定義し、実際にエンドポイントを呼び出して検索処理を実装していきます。
まずは、search
メソッドを作成します。ここには、「1. エンドポイントを設定する」で設定したエンドポイントを呼び出す処理を記述します。
filterクエリの生成処理については、buildFilterQuery
メソッドへ移譲するようにしています。(このメソッドの具体的な処理については、後ほど記述します。)
methods: {
// エンドポイントへのリクエストを行い、取得結果をsearchResultに格納
async search() {
let searchResult;
try {
// 自分の環境で設定したエンドポイントのURLに置き換えてください
const response = await this.$axios.get("/rcms-api/14/content", {
params: {
filter: this.buildFilterQuery()
}
})
searchResult = response?.data || {};
} catch(errorResponse) {
searchResult = { errors: errorResponse?.data?.errors || ['Unexpected error'] };
}
this.searchResult = searchResult;
},
// filterクエリの生成
buildFilterQuery() {
return '';
}
}
/rcms-api/14/content
、の箇所は、Kuroco管理画面に記載のパスをご記入ください。
作成が完了したら、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_1':
if (value !== '') {
queries.push(`${col} contains "${value}"`);
}
break;
// 単一選択(select): 完全一致
case 'ext_2':
if (value !== '') {
queries.push(`${col} = "${value}"`);
}
break;
// 複数選択可: 部分一致
case 'ext_3':
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にアクセスしてフォームを操作し、期待通りに結果を取得できているかを確認してください。
以上で条件検索の実装を終わります。
キーワード検索の実装
続いては、キーワード検索を実装する方法を説明します。
キーワード検索を実装するにあたって、条件検索の例を踏まえると、どのような実装方法が考えられるでしょうか。まずは以下のように、対象のカラムに対しての部分一致検索クエリを OR
で連結する方法も考えられます。
subject contains "KEYWORD" OR ext_1 contains "KEYWORD"
確かにこの方法を利用すれば、キーワード検索の機能自体は実現が可能です。ですが、検索対象のカラムが増えた場合に少し問題があります。 以下のように、非常に冗長で読みづらいクエリを渡す必要があるためです。
subject contains "KEYWORD" OR ext_1 contains "KEYWORD" OR ext_4 contains "KEYWORD" OR ext_5 contains "KEYWORD" OR ext_6 contains "KEYWORD" OR ...
そのためfilter機能では次のように、よりシンプルなクエリでキーワード検索を実装できる仕組みを用意しています。
search_keyword contains "KEYWORD"
今回はこの search_keyword
機能を利用して、キーワード検索を実装していきます。
Kurocoを申し込んだタイミングによってsearch_keyword
ではなくkeyword
で動作するサイトがあります。
うまく動かない場合はkeyword
でもお試しください。
1. コンテンツ定義とエンドポイントを作成する
今回は、下記のコンテンツ定義とエンドポイントを利用します。
コンテンツ定義
コンテンツ定義は条件検索の実装で定義した「Search」を利用します。
エンドポイント
エンドポイントは下記の設定で作成します。
項目 | 値 |
---|---|
パス | content_keyword |
カテゴリー | コンテンツ |
モデル | Topics (v1) |
オペレーション | list |
topics_group_id | 9 |
topics_group_idには、ご自身のコンテンツ定義のIDを記入してください。
2. エンドポイントを設定する
API LIST画面から、検索機能を実装したいエンドポイントを選択し、[更新] をクリックします。
次に、filter_request_allow_list
の設定項目に移動し、検索対象の項目名を指定します。
キーワード検索の許可リストを設定する場合には、以下のフォーマットを利用できます。
形式 | 説明 |
---|---|
search_keyword | 全項目に対するキーワード検索を許可します。 例) search_keyword |
search_keyword:[項目名1,項目名2,...] | 対象の項目名をカンマ区切りで入力し、指定した項目名に対するキーワード検索を許可します。 例) search_keyword:[subject] 例) search_keyword:[ext_1,ext_2] |
:ALL
を指定した場合は他の項目と同様に、search_keyword
も対象の項目として含まれます。
今回は以下のように、search_keyword:[subject,ext_1]
を指定し、タイトルとテキスト項目をキーワード検索の対象とします。
設定が完了したら、[更新] ボタンをクリックし保存してください。
3. エンドポイントの動作を確認する
API LIST画面より「Swagger UI」をクリックし、Swagger UI画面に移動します。
設定したエンドポイントをクリックし、動作確認を行います。
[Try it out] をクリックします。
「filter」パラメータにクエリを入力します。部分一致で検索させるため、 contains
を指定します。
今回は「search_keyword contains "1"
」と記入します。
入力が完了したら [Execute] ボタンをクリックします。
結果が表示されるので、期待通りに検索が行えているかを確認してください。
以上でエンドポイントの動作が確認できました。
4. 検索機能を実装する
では、先ほど設定したエンドポイントを利用して、実際にキーワード検索画面を実装していきましょう。
条件検索と同様に、今回もNuxt.jsを使って、以下のようなコンポーネントを作成します。
まずは下記のコンポーネントを、pages/search_keyword/index.vue
として用意します。
<template>
<div>
<div class="search-form">
<p>
<label for="keyword">Keyword</label>
<input v-model="searchInput.search_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_1 }}</td>
<td>{{ content.ext_2 }}</td>
<td>{{ content.ext_3 }}</td>
</tr>
</table>
</template>
<template v-else>
{{ searchResult.errors }}
</template>
</div>
</div>
</template>
<script>
export default {
data() {
return {
searchInput: {
search_keyword: '',
},
searchResult: {},
}
},
mounted() {
this.search();
},
methods: {
async search() {
let searchResult;
try {
// 自分の環境で設定したエンドポイントのURLに置き換えてください
const response = await this.$axios.get("/rcms-api/5/content_keyword", {
params: {
filter: this.buildFilterQuery()
}
})
searchResult = response?.data || {};
} catch(errorResponse) {
searchResult = { errors: errorResponse?.data?.errors || ['Unexpected error'] };
}
this.searchResult = searchResult;
},
// filterクエリの生成
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>
/rcms-api/5/content_keyword
、の箇所は、Kuroco管理画面に記載のパスをご記入ください。
npm run dev
コマンドを実行してローカル環境を立ち上げ、http://localhost:3000/search_keyword
にアクセスすると、下記の画面が表示されます。
画面上には以下の入力フォームが表示されます。
下記を除いて、コンポーネントの属性定義は条件検索のものと同様です。
- テンプレートの
.search-form
要素 data
オブジェクトのsearchInput
プロパティbuildFilterQuery
メソッド
.search-form
要素には、キーワードの入力フォームのみを定義しています。
入力された値は、searchInput.search_keyword
に格納します。
<div class="search-form">
<p>
<label for="keyword">Keyword</label>
<input v-model="searchInput.search_keyword" type="text">
</p>
<button type="button" @click="search">Search</button>
</div>
data() {
return {
searchInput: {
search_keyword: '',
},
searchResult: {},
}
},
最後にキーワード検索機能を実装すれば完成です。
buildFilterQuery
メソッドを編集し、キーワード検索クエリの生成処理を記述します。
methods: {
// ...
buildFilterQuery() {
const filterQuery = Object.entries(this.searchInput).reduce((queries, [col, value]) => {
switch (col) {
case 'search_keyword':
if (value !== '') {
queries.push(`${col} contains "${value}"`);
}
break;
default:
break;
}
return queries;
}, []).join(' AND ');
return filterQuery;
}
}
以上で、キーワード検索コンポーネントの実装は完了です。
localhostにアクセスしてフォームを操作し、期待通りに結果を取得できているかを確認してください。
以上でキーワード検索の実装を終わります。
参考:複数のコンテンツ定義をまたぐ検索を実装する
複数のコンテンツをまたいで検索する場合は、それぞれのコンテンツ定義で以下が同一になっている必要があります。
- 項目設定の種類
- コンテンツ項目のSlug
- コンテンツ項目のID
(項目を空で追加すると加算されていくので、同一になるよう調整してください。)
参考:商用サービスを利用して検索を実装する
本チュートリアルでは具体的な説明方法は割愛しますが、数万レコード以上や高速な大量のデータの検索が必要な場合には外部サービスを利用されるという選択肢もございます。特に利用制限等はございませんので、下記も参考にしてみてください。
サポート
お探しのページは見つかりましたか?解決しない場合は、問い合わせフォームからお問い合わせいただくか、Slackコミュニティにご参加ください。