work.log

元エンジニアの備忘録的ブログ

ElasticsearchでWordPressの検索機能を改善する(導入と組み込み編)

投稿:

オープンソースの検索エンジンであるElasticsearchを使い、WordPressの検索機能を改善できないか検討したのでメモに残します。

WordPressのデフォルトの検索って必要最低限の機能と言った感じで、思い通りに検索結果をカスタマイズしようと思うと結構難しいです。

記事に重み付けをして優先度を上げたり、暫く更新されていないページは逆に優先度を落としたりしたいという時は、Elasticsearchだと簡単に出来るので検索部分はこちらへ任せようという考えです。

WordPressは高機能化しどんどん進化しているものの、この検索機能部分は時が止まっているように思えたので改善出来ないか頑張ってみます。

とりあえず今回はElasticsearchをCentOSへインストールして、WordPressと連携させるまでを書きます。

スポンサーリンク

Elasticsearchのインストール

今回はCentOS7へElasticsearchをインストールしています。

Elasticsearchはyumでインストールが可能なのでリポジトリを先に追加します。

# vi /etc/yum.repos.d/elasticsearch.repo

書き込む内容はこちらです。

[elasticsearch-7.x]
name=Elasticsearch repository for 7.x packages
baseurl=https://artifacts.elastic.co/packages/7.x/yum
gpgcheck=1
gpgkey=https://artifacts.elastic.co/GPG-KEY-elasticsearch
enabled=1
autorefresh=1
type=rpm-md

GPG-KEYをインポートした後はyumを叩いてバージョン7.0.1のElasticsearchをインストールします。

使いたいプラグインがあるのですがElasticsearchは全てパッケージのバージョンを揃えないと動作しないので、そのプラグインにバージョンを合わせる形になります。

また、Elasticsearchのクエリテストとかにあると便利なのでKibanaも一緒にインストールしておきます。

# rpm --import https://artifacts.elastic.co/GPG-KEY-elasticsearch
# yum install elasticsearch-7.0.1 kibana-7.0.1

Elasticsearchの設定

Elasticsearchが利用するメモリ量を設定します。最初はデフォルトの1GBで問題無いと思いますが、アクセスが多い場合はここを調整すると良いと思います。

# vi /etc/elasticsearch/jvm.options

----- 設定箇所 -----
# Xms represents the initial size of total heap space
# Xmx represents the maximum size of total heap space

-Xms1g
-Xmx1g

次はElasticsearchのデータをダンプ、リストアする為に必要な設定を追加しておきます。

ここに指定したディレクトリにデータを配置してダンプやリストアを行いますが、設定反映には再起動が必要なので最初にやっておきます。

# vi /etc/elasticsearch/elasticsearch.yml

----- 設定箇所 -----
path.repo: ["/home/elasticsearch"]

その他、設定したい項目がある場合は適時やっておきます。

プラグインのインストール

日本語データを扱うのでそれに必要なプラグインをインストールします。

新語に対応するためにelasticsearch-analysis-kuromoji-ipadic-neologdというサードパーティ製のプラグインを導入しますが、公式は頻繁に更新するのでElasticsearchのバージョンはここに合わせています。

# /usr/share/elasticsearch/bin/elasticsearch-plugin install analysis-kuromoji
# /usr/share/elasticsearch/bin/elasticsearch-plugin install org.codelibs:elasticsearch-analysis-kuromoji-ipadic-neologd:7.0.0
# /usr/share/elasticsearch/bin/elasticsearch-plugin install analysis-icu

最新ではないElasticsearchを使っているため一部警告が表示されますが無視します。

インストールされたプラグインを確認するにはこのようにします。

# /usr/share/elasticsearch/bin/elasticsearch-plugin list
analysis-icu
analysis-kuromoji
analysis-kuromoji-ipadic-neologd

プラグインをアンイストールする場合はこのようにします。

# /usr/share/elasticsearch/bin/elasticsearch-plugin remove analysis-kuromoji

Elasticsearchの起動

ここまで終えたら自動起動の設定をし、ElasticsearchとKibanaを起動させます。

# systemctl daemon-reload
# systemctl enable elasticsearch
# systemctl enable kibana
# systemctl start elasticsearch
# systemctl start kibana

9200番ポートにcurlで接続してこのようなヘルスチェックが返ってくればOKです。

# curl -XGET localhost:9200
{
  "name" : "example.com",
  "cluster_name" : "elasticsearch",
  "cluster_uuid" : "WnlBqsIrSdWIwaVpiaPtlQ",
  "version" : {
    "number" : "7.0.1",
    "build_flavor" : "default",
    "build_type" : "rpm",
    "build_hash" : "e4efcb5",
    "build_date" : "2019-04-29T12:56:03.145736Z",
    "build_snapshot" : false,
    "lucene_version" : "8.0.0",
    "minimum_wire_compatibility_version" : "6.7.0",
    "minimum_index_compatibility_version" : "6.0.0-beta1"
  },
  "tagline" : "You Know, for Search"
}

Kibanaは5601番ポートで起動していると思うので、ポートを開けるかプロキシしてアクセスします。

Elasticsearchのマッピングを設計する

次はElasticsearchにインデックスを作ります。マッピングの設定はDBでいうところのテーブル設計のような感じです。

マッピング情報はJSON形式で記述し、wordpress.jsonというファイル名で保存しておきます。

今回はWordPressのデータをインデックスに投入していきますのでこのようなマッピングにしました。

{
	"index_patterns" : ["wordpress-*"],
	"settings": {
		"number_of_shards": 3,
		"number_of_replicas": 0,
		"index": {
			"analysis": {
				"tokenizer": {
					"ja_text_tokenizer": {
						"type": "kuromoji_ipadic_neologd_tokenizer",
						"mode": "search"
					},
					"ngram_tokenizer": {
						"type": "nGram",
						"min_gram": 2,
						"max_gram": 3,
						"token_chars": [
							"letter",
							"digit"
						]
					}
				},
				"analyzer": {
					"ja_text_analyzer": {
						"type": "custom",
						"tokenizer": "ja_text_tokenizer",
						"char_filter": [
							"html_strip",
							"icu_normalizer",
							"kuromoji_ipadic_neologd_iteration_mark"
						],
						"filter": [
							"kuromoji_ipadic_neologd_part_of_speech",
							"icu_normalizer"
						]
					},
					"ngram_analyzer": {
						"type": "custom",
						"tokenizer": "ngram_tokenizer",
						"char_filter": [
							"html_strip",
							"icu_normalizer"
						]
					}
				}
			}
		}
	},
	"mappings": {
		"properties": {
			"id": {
				"type": "keyword"
			},
			"link": {
				"type": "keyword"
			},
			"title": {
				"type": "text",
				"analyzer": "ja_text_analyzer"
			},
			"content": {
				"type": "text",
				"analyzer": "ja_text_analyzer"
			},
			"date": {
				"type": "date",
				"format": "date_time_no_millis"
			},
			"modified": {
				"type": "date",
				"format": "date_time_no_millis"
			},
			"registration_date": {
				"type": "date",
				"format": "date_time_no_millis"
			}
		}
	}
}

肝心なのはtokenizer、analyzer、mappingsの箇所で、tokenizeとanalyzerはデータの扱い方法を。mappingsはデータベースでいうところのカラムみたいな物で、カラム毎にデータをどう扱うかを設定します。

例えばtitleは ja_text_analyzer が設定されているので、ここへの検索キーワードはneologd対応のkuromojiで分かち書きして検索され、もしngram_analyzerを使った場合は単語を指定文字数で切って検索します。

日本語の文章であれば分かち書きを使うkuromojiで検索した方が良い結果になりやすく、ひらながやカタカナだけならngramの方が良かったりします。

WordPressだと検索キーワードが完全一致する部分が無いと拾ってこないですが、このマッピングの設定ではキーワードの一部分だけでもヒットするようになります。

tokenizeとanalyzerはもっとチューニングする必要がありそうですがまずはこんな感じマッピングします。

Elasticsearchにインデックス作る

マッピングの設計が完了したらインデックスを作ります。

マッピングをテンプレート登録 → インデックス作成 → インデックスのエイリアスを作成という順でやっていきます。

マッピングをテンプレート登録

$ curl -XPUT -H "Content-Type: application/json" localhost:9200/_template/wordpress_template_1?pretty -d @wordpress.json
{
  "acknowledged" : true
}

先程のマッピングデータで「”index_patterns” : [“wordpress-*”]」という箇所がありますが、このテンプレートを登録しておけば “wordpress-” のプレフィックスが付いたインデックス名を作成する時に、自動でこのテンプレートが使われるようになります。

インデックスを作成

$ curl -XPUT -H "Content-Type: application/json" localhost:9200/wordpress-20191129?pretty
{
  "acknowledged" : true,
  "shards_acknowledged" : true,
  "index" : "wordpress-20191129"
}

プレフィックスに日付を付けた形式でインデックスを作成しておきます。

エイリアスの作成

$ curl -XPUT -H "Content-Type: application/json" localhost:9200/wordpress-20191129/_alias/wordpress?pretty
{
  "acknowledged" : true
}

wordpress-20191129には wordpress という名前でエイリアスを張っておきます。

そうする事でアプリケーションからはwordpressというインデックス名でアクセスできるようになり、インデックスを変更した場合でもエイリアスの張替えだけで簡単にできるようになります。

Elasticsearchのインデックスにデータを投入する

WordPress用のインデックスが出来たのでここへデータを投入します。

WordPressからコンテンツデータを引っこ抜いてAPIでElasticsearchに流し込んでいくだけなのでここは割愛。

ElasticsearchとWordPressを連携する

ようやくメインの部分。両者を連携させWordPressの検索をElasticsearchに処理させます。

検索するとそれっぽいWordPressプラグインもあるのですが、メンテが止まっていて動かなかったり有効化した後にエラーが起きて大変な目に合ったので自作しました。

APIを叩くだけなのでそう難しい処理では無いです。

<?php

/* Elasticsearch検索用の関数 */
function elasticsearch( $query ) {

	// ホスト名とインデックス名の設定
	$es_scheme = 'http';
	$es_server = 'localhost';
	$es_port = 9200;
	$es_index = 'wordpress';

	$end_point = '%1$s://%2$s:%3$d/%4$s/';
	$end_point = sprintf( $end_point, $es_scheme, $es_server, $es_port, $es_index );

	// Elasticsearch用の検索クエリ、テストなのでタイトルから検索
	$es_query = array(
		'query' => array( 'match' => array( 'title' => $query ) ),
		'_source' => array( 'id' ),
		'size' => 10000
	);
	$es_query = json_encode( $es_query );

	//var_dump( $es_query );

	$curl_opt = array(
		CURLOPT_URL => $end_point . '_search',
		CURLOPT_HTTPHEADER => array( 'Content-Type: application/json' ),
		CURLOPT_CUSTOMREQUEST => 'GET',
		CURLOPT_POSTFIELDS => $es_query,
		CURLOPT_CONNECTTIMEOUT_MS => 5000,
		CURLOPT_SSL_VERIFYPEER => false,
		CURLOPT_SSL_VERIFYHOST => false,
		CURLOPT_RETURNTRANSFER => true
	);

	$curl = curl_init();
	curl_setopt_array( $curl, $curl_opt );
	$res = curl_exec( $curl );

	$post_ids = array();

	//var_dump($res);

	if ( CURLE_OK === curl_errno( $curl ) ) {

		$json = json_decode( $res, true );
		//print_r( $json );

		// リクエストに成功した場合はJSONから検索結果を取得
		foreach ( $json['hits']['hits'] as $doc ) {
			array_push( $post_ids, $doc['_source']['id'] );
		}

	}

	curl_close( $curl );

	return $post_ids;

}

/* デフォルトの検索をフックする処理 */
function search_filter( $search, $query ) {

	if ( ! is_admin() && $query->is_main_query() && $query->is_search ) {

		global $wpdb;

		// 入力された検索キーワードを取得
		$search_query = get_search_query();

		// Elasticsearchにクエリを投げて検索結果を取得
		$post_ids = elasticsearch( $search_query );

		// Elasticsearchで検索できた場合はその投稿IDを、無ければWordPressへ検索させる
		if ( $post_ids !== [] ) {
			$search = 'AND ' . $wpdb->posts . '.ID IN (';
			$search .= implode( ',', $post_ids );
			$search .= ')';
		}
	}

	return $search;

}
add_filter( 'posts_search', 'search_filter', 10, 2 );

ホスト名だけ環境に合わせれば、これを利用テーマのfunctions.phpに丸コピーで動きます。

Elasticsearchで検索できなかった場合はWordPressのデフォルトの検索を使うようにプログラムしていますので、Elasticsearchに障害が起きても大丈夫なはず。

テストなので簡単な検索クエリしか書いていませんが、あとは普通にWordPressの検索フォームから検索をしてみて結果が返ってくれば成功です。

とは言っても、どちらで検索しているか見た目上はわからないので適当にデバックしながらやるといいと思います。

それにしても posts_search のフックは便利ですね。

わずかこれだけのコードでWordPressとElasticsearchを連携させる事がきました。

Elasticsearchの動作デモ

折角連携させたのでElasticsearchとWordPressの検索結果を比較してみたいと思います。

kernelトラブル」というキーワードでこのブログを検索してみます。

まずはWordPressのデフォルトの検索から。

「kernelトラブル」をWordPressのデフォルト検索

この記事がヒットしますが、それ以外に「kernelトラブル」というキーワードに一致する文章は書いていないので何もヒットしないと思います。

次はElasticsearchです。

検索クエリは少しチューニングしてタイトルと本文から検索します。

「kernelトラブル」をElasticsearchで検索

数件ヒットしたかと思います。

このインデックスのアナライザでキーワードを解析してみると、「kernel」と「トラブル」に分かち書きされて検索されたようです。

$ curl -XGET -H 'Content-type: application/json' localhost:9200/wordpress/_analyze?pretty  -d '
{
  "analyzer" : "ja_text_analyzer",
  "text" : "kernelトラブル"
}'

{
  "tokens" : [
    {
      "token" : "kernel",
      "start_offset" : 0,
      "end_offset" : 6,
      "type" : "word",
      "position" : 0
    },
    {
      "token" : "トラブル",
      "start_offset" : 6,
      "end_offset" : 10,
      "type" : "word",
      "position" : 1
    }
  ]
}

Elasticsearchに投げた検索クエリには match を指定しているので、分かち書きされたどちらかのキーワードが登場する投稿がヒットします。

微妙なコンテンツまでヒットしているようですが今回はテストなのでこんな所で。

Elasticsearchは検索結果をスコア計算して返してくれるので、タイトルに重み付けしてみたりコンテンツ部分は完全一致にしてみたりと色々出来そうです。

今回はElasticsearchの導入とWordPressへの組み込みが目的だったので、検索クエリのチューニングは次回に回します。

スポンサーリンク

コメント

コメントを残す

よく読まれている記事

  • 本日
  • 週間
  • 月間