WordPressでインクリメンタルサーチを自前で実装する

## はじめに
『WordPress 検索』でググるとだいたい `search.php` や `searchform.php` を使った同期的な検索の実装方法がヒットします。
非同期でインクリメンタルサーチを行う方法がパッと出てこなかったのでこの記事にまとめます。他により良い方法があればぜひ教えてください。

## 要件
- カスタムポストを検索
- 表示項目はサムネイルと名前
- インクリメンタルサーチ
- NIKE,ないき,ナイキのどれでもヒットするようにする

## バージョン
```
WordPress 4.9.8
Advanced Custom Fields 5.7.10
Custom Post Type UI 1.6.1
WP REST API 2.0-beta15
```

## 完成形
こんなイメージになります。
![incrementalsearch.gif](https://qiita-image-store.s3.amazonaws.com/0/112614/56bc40a9-4cb8-77c1-98da-fea9a45f4c1a.gif)


## 実装の流れ

- 「ブランド」というカスタムポストを作る
- カスタムポスト に検索用キーワードというカスタムフィールド を付与する
- 表示項目の「サムネイル」はカスタムポスト のアイキャッチ画像で対応し「名前」はカスタムポスト のタイトルで対応する。
- WP REST APIを使って検索結果はJSON形式で返すようにする
- そのレスポンスを元にjQueryでゴリゴリ描画する

## 具体的な手順
### 必要なプラグインをインストールする

- WP REST API
- Advanced Custom Fields
- Custom Post Type UI



![スクリーンショット 2019-01-30 9.53.23.png](https://qiita-image-store.s3.amazonaws.com/0/112614/27d4341a-bf12-e3a0-c6f6-00b1b08f2793.png)

![スクリーンショット 2019-01-30 14.23.27.png](https://qiita-image-store.s3.amazonaws.com/0/112614/accf09eb-061b-d859-ed8a-74e6a5d1d6f0.png)

![スクリーンショット 2019-01-30 14.23.54.png](https://qiita-image-store.s3.amazonaws.com/0/112614/d7ed02f6-f4af-fedd-3f8b-167ef2bdd061.png)

### 固定ページ「検索ページ」を作成する

![スクリーンショット 2019-01-30 9.52.15.png](https://qiita-image-store.s3.amazonaws.com/0/112614/903475a8-e155-63a4-3be7-820363f8f886.png)

### カスタムポスト 「ブランド」を作成する

メニューから「投稿タイプの追加と編集」を選択
![スクリーンショット 2019-01-30 14.26.26.png](https://qiita-image-store.s3.amazonaws.com/0/112614/04dd4080-be1c-175f-deaa-560a7d81c7e6.png)

下記のように入力する

![スクリーンショット 2019-01-30 20.08.43.png](https://qiita-image-store.s3.amazonaws.com/0/112614/53579afa-576c-44b3-8c45-7e0576004e49.png)

### カスタムフィールドを作る


検索用キーワードのカスタムフィールド を作る

![スクリーンショット 2019-01-30 14.51.07.png](https://qiita-image-store.s3.amazonaws.com/0/112614/310e1f4c-7e80-35c4-3dac-0c8986c49378.png)

このカスタムフィールド グループをカスタムポスト 「ブランド」に紐づける
![スクリーンショット 2019-01-30 14.51.20.png](https://qiita-image-store.s3.amazonaws.com/0/112614/79cda7ae-cfd5-6b97-2aa1-7bd2dcc3abc9.png)


### カスタムポストを作成する
以下を入力してください。
- タイトル
- アイキャッチ画像
- 検索用キーワード

検索用キーワードにはヒットさせたい文字列を続けて入力してください。
`NIKE`でも`ないき`でも`ナイキ`でもヒットさせたいので
「NIKEないきナイキ」としました。

この実装方法は一般的にはアンチパターンだとは思いますがWordPressだと他に実装方法が思いつきませんでした。

![スクリーンショット 2019-01-30 14.53.15.png](https://qiita-image-store.s3.amazonaws.com/0/112614/1fb9899a-a379-bb01-acf2-a13b32dd3069.png)

同様に他にもデータを作成してください。今回はNIKEの他にadidasとUnderArmourを作りました。

### functions.phpを編集する
以下をfunctions.phpに追加してください。

```php

// rest_{post_type}_query
add_filter( 'rest_brand_query', 'my_rest_brand_query', 10, 2 ); 

function my_rest_brand_query( $args, $request ) {
  // keywordというところはカスタムフィールド 「検索用フィールド」のslug名
  if ( isset( $request['keyword'] ) ) {
    $args['meta_query'] = array(
      array(
      "key" => "keyword",
      "value" => $request['keyword'],
      "compare" => "LIKE" // 部分一致なので"LIKE"とした。完全一致だったら"="にする
      )
    );
   return $args;
  }
}
```

### 固定ページの検索ページに対応するテンプレートを作成する
`page-search.php` を作成する
`page-{固定ページのslug名}.php` という命名規則。よくわからない人はテンプレートの優先順位を確認してください。

```php:page-search.php
<?php
get_header(); ?>

<div class="wrap">
	<div id="primary" class="content-area">
		<main id="main" class="site-main" role="main">
			

		</main><!-- #main -->
	</div><!-- #primary -->
</div><!-- .wrap -->
<?php get_footer();

```

### 検索フォームと検索結果を表示する領域を作る

```php:page-search.php
<?php
get_header(); ?>

<div class="wrap">
	<div id="primary" class="content-area">
		<main id="main" class="site-main" role="main">
			<input id="search-form" type="search">
			
			<ul id="result-lists"></ul>

		</main><!-- #main -->
	</div><!-- #primary -->
</div><!-- .wrap -->
<?php get_footer();


```

### Ajax通信でカスタムポスト を検索する処理を書く

```php:page-search.php
.
.
省略
<script>
	jQuery(function($){
		$("#search-form").on("keyup", function(event){
			// 入力値が変更されたらとりあえず結果を消す
			$("#result-lists").empty()
			// 入力値が空だったら処理終了
			if (event.target.value == "") { return }
			$.ajax({
              // localhost:8000の部分は各自置き換えてください。
              // _embedパラメータはアイキャッチ画像をレスポンスに含めるのに必要です。
              // keywordパラメータに検索フォームの入力値を設定します。
               url:"http://localhost:8000/wp-json/wp/v2/brand/?_embed&keyword=" + event.target.value,
               type:'GET'
            })
            // Ajaxリクエストが成功した時発動
            .done( (data) => {
                // 検索結果を描画する
                refreshHTML(data)
            })
            // Ajaxリクエストが失敗した時発動
            .fail( (data) => {
            	// エラー処理
            })
            // Ajaxリクエストが成功・失敗どちらでも発動
            .always( (data) => {

            });
		})
                // 検索結果描画用の関数
		function refreshHTML(data) {
			if (data.length > 0) {
				$.each(data, function(index, val) {
                    // タイトルは val.title.renderedで取得できる
                    // 画像のURLは val._embedded["wp:featuredmedia"][0].source_urlで取得できる
					$("#result-lists").append("<li><img src=" + val._embedded["wp:featuredmedia"][0].source_url + " style='width: 50px; height: 50px; vertical-align: middle;'>" + val.title.rendered + "</li>")
				})
			}
		}
	})
</script>
```

とりあえずこれでインクリメンタルサーチができるはずです。
ただこれだと`nike`で検索しようとした時に
`n`を打ったタイミングで通信が行われ
`i`を打ったタイミングで通信が行われ
`k`を打ったタイミングで通信が行われ
`e`を打ったタイミングで通信が行われ
という動きになります。

素早く`ni`と打ったタイミングでは、`n`を打ったタイミングで行われた通信を中断してやるのが親切です。
つまり直前のリクエストを中断する処理があったほうが親切ということです。
それを次に書いて終わります。

### 直前のリクエストを中断する

```php:page-search.php
.
.
省略
<script>
	jQuery(function($){
		// 追加 ajax中止用
		var jqxhr;
		$("#search-form").on("keyup", function(event){
			// 入力値が変更されたらとりあえず結果を消す
			$("#result-lists").empty()
			//　追加 直前のajax通信を中断する
			if (jqxhr) {
				jqxhr.abort();
			}
			// 入力値が空だったら処理終了
			if (event.target.value == "") { return }
			// 追加Ajax通信
			jqxhr = $.ajax({
              // localhost:8000の部分は各自置き換えてください。
              // _embedパラメータはアイキャッチ画像をレスポンスに含めるのに必要です。
              // keywordパラメータに検索フォームの入力値を設定します。
               url:"http://localhost:8000/wp-json/wp/v2/brand/?_embed&keyword=" + event.target.value,
               type:'GET'
            })
            // Ajaxリクエストが成功した時発動
            .done( (data) => {
                // 検索結果を描画する
                refreshHTML(data)
            })
            // Ajaxリクエストが失敗した時発動
            .fail( (data) => {
				// ajax通信を中断した時もfailの方にくるのでエラー処理とは分けて考える
				if (data.statusText == "abort") { return }
            	// エラー処理
            })
            // Ajaxリクエストが成功・失敗どちらでも発動
            .always( (data) => {

            });
		})
                // 検索結果描画用の関数
		function refreshHTML(data) {
			if (data.length > 0) {
				$.each(data, function(index, val) {
                    // タイトルは val.title.renderedで取得できる
                    // 画像のURLは val._embedded["wp:featuredmedia"][0].source_urlで取得できる
					$("#result-lists").append("<li><img src=" + val._embedded["wp:featuredmedia"][0].source_url + " style='width: 50px; height: 50px; vertical-align: middle;'>" + val.title.rendered + "</li>")
				})
			}
		}
	})
</script>
```


これでこのような動きが実現できると思います。
再掲

![incrementalsearch.gif](https://qiita-image-store.s3.amazonaws.com/0/112614/ee4be0bb-667a-901a-ac63-3751c636d309.gif)