技術情報

WordPressの遅い文字検索を改善する、Elasticsearchで全文検索

2016.10.27


デフォルトの文字検索で検索してくれるのはタイトルと本文、抜粋だけ

WordPressには初めから文字検索ができる機能がついています。
よくウェブサイト内で見かけるコレ、

sofpblog20161027_02

ですが、この検索窓から入力された「検索文字列」は、そのサイトのURLに ?s=検索文字列とオプションを付けて投げ込まれます。WordPress側では検索機能を呼び出して、結果を表示してくれます。

http://example.com/?s=検索文字

この機能はタイトルと本文にのみ有効で、カテゴリ名やタグには適用されません。もちろん、ユーザーが勝手にセットしたカスタムフィールドも無視されます。

試しにWordPressの最初の投稿、「Hello world!」に「テスト分類01」というカテゴリを付加して、この文字で検索してみます。

sofpblog20161027_04_591x162

残念ながら検出できませんでした。

Query Monitor」というデータベースに渡すクエリをモニタリングするプラグインを入れて、wordpressが発行している検索部分のSQL文を確認すると下記のようになります。

SELECT SQL_CALC_FOUND_ROWS wp_posts.ID
FROM wp_posts 
WHERE 1=1 
AND (((wp_posts.post_title LIKE '%テスト分類01%')
OR (wp_posts.post_excerpt LIKE '%テスト分類01%')
OR (wp_posts.post_content LIKE '%テスト分類01%'))) 
AND wp_posts.post_type IN ('post', 'page', 'attachment')
AND (wp_posts.post_status = 'publish'
OR wp_posts.post_author = 1
AND wp_posts.post_status = 'private') 
ORDER BY wp_posts.post_title LIKE '%テスト分類01%' DESC, wp_posts.post_date DESC
LIMIT 0, 10

ちょっとわかりにくいのですが、post_title(タイトル)、post_content(本文)、post_excerpt(抜粋)をLIKE(中間一致)で検索しているのがわかります。

プラグインSearch Everythingは遅い

これらの検索対象外の項目も検索で表示したい場合に多くの方が定番の「Search Everything」と いうプラグインを入れているかと思います。このプラグインは、カテゴリ名やタグはもちろんのこと、カスタムフィールドを含めて検索してくれます。もちろん、設定で検索する項目は決められます。

それではSearch Everythingsを入れて検索してみます。

sofpblog20161027_05

これで検索できるようになりました。

検索している部分のSQL文は以下になります。結構長いですね。

SELECT DISTINCT SQL_CALC_FOUND_ROWS wp_posts.*
FROM wp_posts 
LEFT JOIN wp_term_relationships AS trel
ON (wp_posts.ID = trel.object_id)
LEFT JOIN wp_term_taxonomy AS ttax
ON ( ( ttax.taxonomy = 'category' ) 
AND trel.term_taxonomy_id = ttax.term_taxonomy_id)
LEFT JOIN wp_terms AS tter
ON (ttax.term_id = tter.term_id) 
LEFT JOIN wp_comments AS cmt
ON ( cmt.comment_post_ID = wp_posts.ID ) 
LEFT JOIN wp_postmeta AS m
ON (wp_posts.ID = m.post_id) 
WHERE 1=1
AND ( ( (((((wp_posts.post_title LIKE '%テスト分類01%')
OR (wp_posts.post_content LIKE '%テスト分類01%')))
OR ((tter.slug LIKE '%%e3%83%86%e3%82%b9%e3%83%88%e5%88%86%e9%a1%9e01%')) 
OR ((ttax.description LIKE '%テスト分類01%')) 
OR ((m.meta_value LIKE '%テスト分類01%')) 
OR (((cmt.comment_content LIKE '%テスト分類01%'))
AND cmt.comment_approved = '1') )) 
AND wp_posts.post_type IN ('post', 'page', 'attachment')
AND (wp_posts.post_status = 'publish'
OR wp_posts.post_author = 1
AND wp_posts.post_status = 'private'))
AND post_type != 'revision')
AND post_status != 'future' 
ORDER BY wp_posts.post_title LIKE '%テスト分類01%' DESC, wp_posts.post_date DESC
LIMIT 0, 10

検索クエリーを見てみると、LEFT JOINという部分がいくつも追加されて複雑になっています。
JOIN検索はあるテーブルの検索結果から別の検索テーブルの結果を探し出すので、必然的に遅くなるのです。
当然ながら「LIKE %文字列%」という部分一致検索も追い打ちをかけるように遅い原因になっています。

検索項目が少ない場合には気にならなかった速度も、記事数が1000単位になってくるとかなり遅くなってしまいます。

WordPressが利用しているデータベースのテーブル構成は、残念ながら「タイトル」と「本文」「抜粋」以外の文字検索は考慮していませんので、時間がかかってしまいます。これを解決するには本来であれば一旦検索する部分を考慮した設計をし直すのが王道です。

※ カスタムフィールドの遅い問題を解決するプラグインがあります。

文字検索が早くなければならない理由

「検索文字を打ち込むような熱心な閲覧者は10秒ぐらい待っていても平気だよね〰」なんて思う人がいるかもしれません。しかし何人ものユーザーが一斉に文字検索をし始めた場合、サーバーが悲鳴を上げます。そして、速度が早くなければできない事もあります。

sofpblog20161027_01

そうです。Googleをはじめ、Amazonや楽天、Yahooショッピング等、大手のECサイトは軒並み取り入れている、サジェスト(おすすめ)候補の表示機能です。

この機能は文字入力をしている途中で検索の候補が表示される必要があるので、かなりの検索速度が要求されます。一文字打ち込む度に10秒待たされる、なんてことは許されないのです。

日本語の文字検索は単純ではない

また、もうひとつWordPressの文字検索には問題があり、検索する文字は完全に一致していなければなりません。

例えばWordPressの最初の投稿文「WordPress へようこそ。これは最初の投稿です。編集もしくは削除してブログを始めてください !」は「始めて」では検出できますが、「始める」や「始めた」では検出できないですし、「はじめる」でも当然ダメです。

これでは自社サイト内の用語を統一しようと頑張って校正したのにもかかわらず、検索してくる人は多種多様な入力をして、実用性に乏しいサイトになってしまった、という几帳面なサイト管理者ほど損をする矛盾が発生してしまいす。

WordPressが利用するMySQL(またはMariaDB)はリレーショナルデータベース(RDB)と言われる、エクセルのような表形式に似た構造を扱うシステムで、データの蓄積と正確な検索結果が求められていた経緯もあり、残念ながら上記のような要望の文字検索のような場合には向きません。

文字検索に特化した全文検索システムを導入する

そこで「検索速度が遅い問題」と「文字が完全に一致しないと検索できない問題」という2つの問題をElasticsearchという全文検索エンジンを用いて一気に解決する方法を解説したいと思います。

ElasticsearchはElastic社が開発しサービスを提供している全文検索エンジンで「Apache license 2」で公開されているオープンソースなので、だれでも無料で自由に利用する事ができます。

同様の全文検索エンジンでオープンソース製品のApache Solrというものがありますが、Elasticsearchの方が後発で拡張性に優れているので今回はこちらを組み込んでみます。

javaで動くサービスサービスなのでElasticsearchを利用するにはインストールするサーバーが必要になりますが、amazonのサービスもあります。

CentOS7の例ですが、下記のようにインストールします。

$ sudo yum -y install java-1.8.0-openjdk
$ sudo rpm --import https://packages.elastic.co/GPG-KEY-elasticsearch

/etc/yum.repos.d/elasticsearch.repo を下記のように編集して保存します。

[elasticsearch-2.x]
name=Elasticsearch repository for 2.x packages
baseurl=https://packages.elastic.co/elasticsearch/2.x/centos
gpgcheck=1
gpgkey=https://packages.elastic.co/GPG-KEY-elasticsearch
enabled=1

これで、yum コマンドでインストールできます

  $ suto yum install elasticsearch

全文検索といっても日本語の場合には結構複雑になります。elasticsearchではkuromojiというプラグインを入れるのが一般的です。

kuromojiは日本語の文章を解析して、検索しやすいように単語毎に分離、変換してくれるオープンソースの「形態素解析エンジン」です。

例えば「すもももももももものうち」という文は「すもも」と「もも」に分離して、検索できるようにしてくれます

kuromojiは下記ようにインストールします。

セットアップができましたら、確認します。コマンドラインからcurlでアクセスする事で確認できます。

$ curl -XGET 'http://localhost:9200/_analyze?pretty=true&analyzer=kuromoji' 
-d '今日はいい天気ですね'
{
  "tokens" : [ {
    "token" : "今日",
    "start_offset" : 0,
    "end_offset" : 2,
    "type" : "word",
    "position" : 0
  }, {
    "token" : "いい",
    "start_offset" : 3,
    "end_offset" : 5,
    "type" : "word",
    "position" : 2
  }, {
    "token" : "天気",
    "start_offset" : 5,
    "end_offset" : 7,
    "type" : "word",
    "position" : 3
  } ]
}

できればブラウザで状態を確認できると便利なので、Headプラグインを入れ、/etc/elasticsearch/elasticsearch.yml外部からのアクセスを有効にする設定をします。

network.host: 0.0.0.0

ただ、このままではIPアタックで外部のどこからでもポート:9200でelasticsearchにアクセスできてしまうので、危険な状態です。

下記はnginxですが、proxy設定をしてelasticsearch用のホスト名(例:es.example.com)でアクセスできるようにします。
passwd.txtというBasic認証用のパスワードファイルを読み込んでいます。

server {
  listen 80;
  server_name es.example.com;
  charset utf-8;

  location / {
    auth_basic "Restricted";
    auth_basic_user_file /home/www/passwd.txt;

    proxy_pass http://127.0.0.1:9200/;
  }
}

http://es.example.com/_plugin/head/にアクセスすると下記のように表示されます。

sofpblog20161027_03_694x250

これで外部からBsic認証+ホスト名でアクセスできるようになりましたので少し安心です。
nginxやapache等でアクセスを制御できれば、IP制限も簡単にできますね。

さて、WordPressでElasticsearchを利用するにはプラグインが存在します。
プラグイン画面から「elasticsearch]で検索すると、いくつか表示されます。

日本人のhiroikeさんが作成した[WP Simple Elasticsearch」というプラグインがありますので、有り難く利用させてもらいます。

設定画面で、下記のようにします。同じサーバー内なのでホストはlocalhost、ポートは9200になります。

sofpblog20161027_06

として登録してみます。

elasticsearch側で特に設定しなくてもうまくデータが同期できたようです。

さっそく検索してみましょう。

「始める」では一致しないようです。

手動のクエリでは一致するので、検索方法が違うようです。

調べてみると、query_strongという手法でelasticsearchにクエリを投げていました。
公式のマニュアルFull text queriesを見るとこの方法は曖昧検索はないようです。

曖昧検索ができるように 下記のようにmulti_matchのクエリに変更してみます。

wp-elasticsearch.phの冒頭

  use Elastica\Query\MultiMatch;

とnew QueryString() している部分を変更します。

// $qs = new QueryString();
$qs = new MultiMatch();
$qs->setQuery( $search_query );
$set_fields = ['post_title', 'post_content'];
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' );
   $set_fields = array_merge($set_fields, $custom_fields);
}
$qs->setFields($set_fields);

これで「始める」で検索すると・・

sofpblog20161027_07

検出できました!とりあえずkuromojiを利用した曖昧検索はできたようです。

次回の記事ではサジェスト機能をやりたいですが、調べる事が多そうです。

まとめ

サイトを運営していると、扱う記事や商品は増える一方なのに、スマートフォンの普及により、表示できるエリアは狭くなっていきます。

限られたスペースで、大量のデータの中から目的のデータを見つけてもらうために、表示するメニュー構成やカテゴリー、タグを工夫している方は多いと思います。しかしいくら工夫しても、うまくカテゴリ分けができなかったり、項目名が変わったりと苦労が多い部分かと思います。

今後は検索窓が大きな位置を占めてくるのではないでしょうか。