Help us understand the problem. What is going on with this article?

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

More than 3 years have passed since last update.

全世界億千万人のAMIMOTOファンの皆様こんにちは!
AMIMOTO Advent Calendar 2015 13日目の記事です。

スクリーンショット 2015-12-14 1.40.15.png

WordPressの検索でまだ消耗してるの?

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

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

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

ということで、最近出来たAWSのAmazon Elasticsearch ServiceとAMIMOTOを接続してデフォルトの検索をElasticsearchで実施するようにしてみました。
WP Elasticsearch
こんな感じでプラグインにしてGithubにも上げてます。

構成

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

Amazon Elasticsearch Serviceのセットアップ

Amazon Elasticsearch ServiceとFluentdで遊ぶ
Serverworksさんのブログです。この辺り参照してください。
恐ろしく簡単にセットアップできてしまいますので割愛します。

IAM設定


{
  "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によるアクセス制御のようにしてあげたほうがいいです。principalを指定して、自身のアカウントのEC2インスタンスのみからアクセスできるとかがいいですね。

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

WordPressの記事コンテンツをElasticsearchに登録する

スクリーンショット 2015-12-14 1.33.17.png
こんな感じの管理画面を作りました。Elasticsearch側に検索対象となるコンテンツをmappingさせる必要があります。今回はカスタムフィールドの値も検索対象にできるようにしてます。Coustom Fieldsに改行区切りでカスタムフィールドのキーを入力すると検索対象となります。

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

    /**
     * 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フックでクエリを改変します。

    /**
     * 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;
        }
    }

実行結果

スクリーンショット 2015-12-14 1.49.47 1.png
like_singerというカスタムフィールドを作ります。

スクリーンショット 2015-12-14 1.46.30.png
そして管理画面からPost Data sync Elasticsearchのボタンを押します。
するとManagement Console上にてちゃんとlike_singerがmappingされているのがわかります。

スクリーンショット 2015-12-14 1.47.41.png
フロント画面から検索を実施してみるとこんな感じでカスタムフィールドのデータも検索できました。

まとめ

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

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

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away