使用AMIMOTO + Amazon Elasticsearch Service增强WordPress的搜索功能

大家好,我是亿万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的配置

在Serverworks先生的博客中,提到了如何使用Amazon Elasticsearch Service和Fluentd。由于设置非常简单,所以在这里不再详细叙述,请参考相关文章。

我是IAM设置。


{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "",
      "Effect": "Allow",
      "Principal": {
        "AWS": "*"
      },
      "Action": "es:*",
      "Resource": "<作成したEalsticsearchのARN>",
      "Condition": {
        "IpAddress": {
          "aws:SourceIp": "<IPアドレス>"
        }
      }
    }
  ]
}

这次我们在IP上进行了限制。
但是,这样不太好。如果有眼力的人可能会明白,限制IP会导致无法实现自动扩展。而且将IP嵌入到IAM中也在管理上不可行。

最好在Amazon Elasticsearch Service中通过IAM角色实现访问控制。可以指定主体,仅允许自己账户的EC2实例访问。

希望的话,为了发送请求,我们需要实现带有签名的URL机制。
由于这个机制相当复杂,所以这次我们选择了通过IP限制来实现。
希望AWS能够提供一些方便的解决方案。

将WordPress的文章内容注册到Elasticsearch中。

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

实际上将映射过程编写成代码的部分如下所示。我们使用了一个名为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
スクリーンショット 2015-12-14 1.46.30.png
スクリーンショット 2015-12-14 1.47.41.png

总结

使用AMIMOTO可以轻松与AWS内的服务进行连接,这是很好的。就用途而言,我认为有相当数量的网站可能会在WordPress搜索增强方面使用它。而且,考虑到多站点等,跨站点的横向搜索也会更加方便。

祝你拥有愉快的亚马逊 Elasticsearch 服务之旅!

广告
将在 10 秒后关闭
bannerAds