AMIMOTO + Amazon Elasticsearch ServiceでWordPressの検索機能をパワーアップする

全世界億千万人のAMIMOTOファンの皆様こんにちは！
[AMIMOTO Advent Calendar 2015](http://www.adventar.org/calendars/1107) 13日目の記事です。

<img width="817" alt="スクリーンショット 2015-12-14 1.40.15.png" src="https://qiita-image-store.s3.amazonaws.com/0/65478/995e18fd-d1ff-9e22-d211-d60107b96ce5.png">

## WordPressの検索でまだ消耗してるの？
WordPressの検索機能って、単純にタイトルと本文に対してlike検索を行うだけなのでそこまで精度は良くありません。

また、プラグインを使ってカスタムフィールドを検索対象にしたい場合もありますが、カスタムフィールドを構成するMySQLのテーブルがText型でカラムを持っており、いくつもカスタムフィールドをLike検索の対象にすると一気に検索が重たくなってしまいます。

この検索の精度は作るサイトの種類によっては結構困るんではないでしょうか。
通常のブログではそこまで気にならないかもですが、イベントのサイトを作るときなんかは検索もこだわりたいケースも多いと思います。

ということで、最近出来たAWSのAmazon Elasticsearch ServiceとAMIMOTOを接続してデフォルトの検索をElasticsearchで実施するようにしてみました。
[WP Elasticsearch](https://github.com/horike37/wp-elasticsearch)
こんな感じでプラグインにしてGithubにも上げてます。

## 構成
構成はシンプルです。AMIMOTOのEC2インスタンスからAmazon Elasticsearch Serviceにリクエストを送るようにするだけです。ElasticsearchはEndpointに対してGETやPOSTを送ってデータやりとりします。なので今回はAPI GatewayやLambdaといった仲介業者的な存在は出現しません

## Amazon Elasticsearch Serviceのセットアップ
[Amazon Elasticsearch ServiceとFluentdで遊ぶ](http://blog.serverworks.co.jp/tech/2015/10/30/play-with-amazon-es/)
Serverworksさんのブログです。この辺り参照してください。
恐ろしく簡単にセットアップできてしまいますので割愛します。

## IAM設定
```json

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "",
      "Effect": "Allow",
      "Principal": {
        "AWS": "*"
      },
      "Action": "es:*",
      "Resource": "<作成したEalsticsearchのARN>",
      "Condition": {
        "IpAddress": {
          "aws:SourceIp": "<IPアドレス>"
        }
      }
    }
  ]
}
```
今回はIPで制限かけています。
しかし、これイケてないです。勘のいい人ならわかるかもですが、IPで制限してしまったらAutoScaling対応できないです。あとIPをIAMに埋め込むのも管理上イケてません。

[Amazon Elasticsearch ServiceのIAM Roleによるアクセス制御](http://dev.classmethod.jp/cloud/aws/amazon-es-iam-based-access-control/)のようにしてあげたほうがいいです。principalを指定して、自身のアカウントのEC2インスタンスのみからアクセスできるとかがいいですね。

ただそうするとリクエスト送るのに署名付きURLの仕組みを実装しないといけません。
それも結構大変なので今回はIP制限で許してくださいということでIP制限で実装しました。
この辺は楽にできるようにAWSさんがなんとかしてくれることに期待。

## WordPressの記事コンテンツをElasticsearchに登録する
<img width="662" alt="スクリーンショット 2015-12-14 1.33.17.png" src="https://qiita-image-store.s3.amazonaws.com/0/65478/245ab671-76f4-1e5b-1f4e-f44e930bd3ab.png">
こんな感じの管理画面を作りました。Elasticsearch側に検索対象となるコンテンツをmappingさせる必要があります。今回はカスタムフィールドの値も検索対象にできるようにしてます。Coustom Fieldsに改行区切りでカスタムフィールドのキーを入力すると検索対象となります。

実際にmappingさせる部分のコードは以下のような感じです。ElasticaっていうElasticsearch用のライブラリを使用しています。

```php
	/**
	 * admin_init action. mapping to Elasticsearch
	 *
	 * @return true or WP_Error object
	 * @since 0.1
	 */
	private function _data_sync() {
		try {
			$options = get_option( 'wpels_settings' );
			$client = $this->_create_client( $options );
			if ( ! $client ) {
				throw new Exception( 'Couldn\'t make Elasticsearch Client. Parameter is not enough.' );
			}
			$options = get_option( 'wpels_settings' );
			$index = $client->getIndex( $options['index'] );
			$index->create( array(), true );
			$type = $index->getType( $options['type'] );
			$mapping = array(
							'post_title' => array(
												'type' => 'string',
												'analyzer' => 'kuromoji',
											),
							'post_content' => array(
												'type' => 'string',
												'analyzer' => 'kuromoji',
											),
						);
			if ( ! empty( $options['custom_fields'] ) ) {
				$custom_fields = explode( "\n", $options['custom_fields'] );
				$custom_fields = array_map( 'trim', $custom_fields );
				$custom_fields = array_filter( $custom_fields, 'strlen' );
				foreach ( $custom_fields as $field ) {
					$mapping[ $field ] = array(
										'type' => 'string',
										'analyzer' => 'kuromoji',
										);
				}
			}
			$type->setMapping( $mapping );
			$my_posts = get_posts( array( 'posts_per_page' => -1 ) );
			$docs = array();
			foreach ( $my_posts as $p ) {
				$d = array(
					'post_title' => (string) $p->post_title,
					'post_content' => (string) strip_tags( $p->post_content ),
				);
				if ( ! empty( $options['custom_fields'] ) ) {
					foreach ( $custom_fields as $field ) {
						$d[ $field ] = (string) strip_tags( get_post_meta( $p->ID, $field, true ) );
					}
				}
				$docs[] = $type->createDocument( (int) $p->ID, $d );
			}
			$bulk = new Bulk( $client );
			$bulk->setType( $type );
			$bulk->addDocuments( $docs );
			$bulk->send();
			return true;
		} catch (Exception $e) {
			$err = new WP_Error( 'Elasticsearch Mapping Error', $e->getMessage() );
			return $err;
		}
	}
```

## Elasticsearchでの検索

検索のコードは以下のような感じです。
投稿IDでElasticsearch側のIDと紐付けしています。単純に返ってきた値を元にしてpre_get_postsフックでクエリを改変します。

```php
	/**
	 * search query to Elasticsearch.
	 *
	 * @param $search_query
	 * @return true or WP_Error object
	 * @since 0.1
	 */
	public function search( $search_query ) {
		try {
			$options = get_option( 'wpels_settings' );
			$client = $this->_create_client( $options );
			if ( ! $client ) {
				throw new Exception( 'Couldn\'t make Elasticsearch Client. Parameter is not enough.' );
			}
			$type = $client->getIndex( $options['index'] )->getType( $options['type'] );
			$qs = new QueryString();
			$qs->setQuery( $search_query );
			$query_es = Query::create( $qs );
			$resultSet = $type->search( $query_es );
			$post_ids = array();
			foreach ( $resultSet as $r ) {
				$post_ids[] = $r->getID();
			}
			return $post_ids;
		} catch (Exception $e) {
			$err = new WP_Error( 'Elasticsearch Search Error', $e->getMessage() );
			return $err;
		}
	}
```

## 実行結果
<img width="815" alt="スクリーンショット 2015-12-14 1.49.47 1.png" src="https://qiita-image-store.s3.amazonaws.com/0/65478/5e000fcf-0a95-3137-5fa6-92ecac3b0e88.png">
like_singerというカスタムフィールドを作ります。

<img width="389" alt="スクリーンショット 2015-12-14 1.46.30.png" src="https://qiita-image-store.s3.amazonaws.com/0/65478/0883f8a1-a9cf-7fb9-1c3b-ba3094cf61ca.png">
そして管理画面からPost Data sync Elasticsearchのボタンを押します。
するとManagement Console上にてちゃんとlike_singerがmappingされているのがわかります。

<img width="1192" alt="スクリーンショット 2015-12-14 1.47.41.png" src="https://qiita-image-store.s3.amazonaws.com/0/65478/4fe6ec4d-724b-d3cc-d3bd-425c3a5e27d1.png">
フロント画面から検索を実施してみるとこんな感じでカスタムフィールドのデータも検索できました。

## まとめ
AMIMOTO使うとAWS内のサービスと簡単に連動できるのが良いです。
用途としてはWordPressの検索強化ってところでつかたいサイトもそれなりにあるんじゃないかなあと思っています。さらにマルチサイトとかも考えるとサイトをまたいだ横断検索もやりやすくなりそうですね。

それでは良いAmazon Elasticsearch Serviceライフを！