さて、以前WordPressでFULLTEXTインデックスを使った高速全文検索にトライという記事を書きましたが、その続きです。といっても、mroongaさえインストールできてしまえばどうということはないので、前述の記事の補完的な内容です。
mroongaとはなんぞや
MySQLの日本語全文検索実装としてはtritonnというものがありました。これはSennaという全文検索エンジンのMySQL実装だったわけですが、MySQL5.0までしか対応していませんでした。
その後、Sennnaの後継としてgroongaというソフトが誕生し、それをMySQLに実装したのがmroongaというわけです。特徴はこんな感じ。
- MySQL5.1以上で動く
- プラグインとして利用できるので、導入に当たってMySQLの再構築がいらない
- ストレージモード(MyISAMとかInnoDBとかのアレ)で動くので、既存のテーブルを変更できる
- トークナイザー(わかち書きするヤツ)を指定できる(Mecabで辞書型とか、Ngramとか)
mroongaのインストール
mroongaのインストールですが、これはググればすぐわかりますので、参考リンクを挙げておきます。
NgramだとMecabいらないですが、groongaを入れると勝手に入ってくるのがいまのトレンドのようです。
Macの場合はMacportsよりhomebrewの方が簡単っぽいですね。僕はロートルMacportsなのでmakeコマンド打つはめになりました。コツとしては、邪念のない無垢な心でコマンドを入力することでしょうか。
WordPress側の実装
さて、mroongaがインストールできたら、あとはWordPressに対応させるだけです。
テーブル設計
以前書いた記事とあんまり変わらないのですがこんな感じです。
- 検索対象となるテーブルwp_postsのストレージエンジンをmroongaにする
- 検索対象となるカラム(post_title、post_excerpt、post_content_filtered)にFulltextインデックスを貼る
この操作はphpMyAdminなりSequelProなりでポチポチやれば終わります。mroongaのマニュアルによれば、インデックスを貼る際にコメントでトークナイザーを指定するらしいです。
ALTER TABLE wp_posts ADD FULLTEXT(post_content_filtered, post_title, post_excerpt) COMMENT 'parser "TokenMecab"';
検索対象となる本文・タイトル・抜粋をインデックスに追加してみました。EXPLAINしてみたところ、たぶんこれであってるとは思うんですが、どうでしょう、識者のフィードバックが待たれます。
投稿本文の保存
投稿本文が保存されているのはwp_posts.post_contentですが、以下のような理由からHTMLを除去した形式でwp_posts.post_content_filteredに保存します。
- WordPressはデフォルトのままだとpost_content_filteredを使わない
- post_contentはタグ入りだが、imgとかで検索する必然性はおそらくない(alt属性やtitle属性などは検索対象とした方がいいという場合もあるかもしれないが)
- ショートコードなどのように、
the_content
フィルターを通るものも検索対象とした方がよいような気がする(これはサイト次第)
細かい部分はサイトポリシーによるので、適宜変更可能です。
さて、投稿本文をpost_content_filteredに保存するには、次のような方法を取れば良いです。
// 投稿保存アクションにフックをかける add_action(‘save_post’, ’save_post_content_filtered’, 10, 2); /** * フックハンドラ * @global wpdb $wpdb * @param int $post_id 投稿ID * @param WP_Post $post 投稿オブジェクト */ function save_post_content_filtered($post_id, $post){ // 自動保存とかリビジョンだったら何もしない if( wp_is_post_autosave($post) || wp_is_post_revision($post) ){ return; } // 投稿本文のタグを除去して空いているカラムに保存 // ここでwp_update_postを使うと無限ループ発生 $wpdb->update($wpdb->posts, array( 'post_content_filtered' => my_striptags($post->post_content), ), array( 'ID' => $post_id ), array('%s'), array('%d')); } /** * 投稿本文からタグを除去して返す * @param WP_Post $post * @return string */ function my_striptags($post){ // the_contentフィルターを有効にするために、 // 一応ループを作る setup_postdata($post); $content = apply_filters('the_content', $post->post_content); // ループを元に戻す wp_reset_postdata($post); // タグを除去して返す // 特定のタグの中身(rt, rtなど)を取り除きたい場合は // ここで正規表現などを使ってみてください return strip_tags($content); }
これで記事を作成・更新するたびにタグなしテキストがDBに保存されるようになりました。
既存投稿の救済
新たに作られる投稿については上記の処理で問題ないと思いますが、WordPressで全文検索を導入する場合、用意周到に設計してというよりは、「なんかサイト作ったけど遅くなってきちゃったヤベー」というパターンが多いと思います。
その場合、既存のコンテンツのタグなしテキストをどうやって保存するかが問題になるのですが、SQLのバッチ処理なんかだとthe_contentフィルターを通らないので意図しない結果になります。
なので、WordPressの関数郡を利用してこんなバッチを書いてみます。
<?php /** * タグなしテキストをwp_postsテーブルに保存するバッチ処理 * */ // コマンドラインから実行されたのでなければ無視 if( !defined('STDIN') ) { die('このプログラムはPHP CLIでしか実行できません'); } // WordPressルートにあるwp-load.phpを読み込むと、WordPressの関数が全部使える require '/PATH/TO/wp-load.php'; // DBオブジェクト呼び出し global $wpdb; // 投稿を全部取得 // パフォーマンスと相談して、条件は適宜変更可能 $posts = get_posts(array( 'post_type' => 'post', 'post_status' => 'publish', 'posts_per_page' => -1, )); // まだタグなしテキストがないと思われるデータを処理する $counter = 0; foreach($posts as $post){ if( empty($post->post_content_filtered) ){ // タグなしテキストを保存 $stripped_text = my_strip_tags($post); $wpdb->update($wpdb->posts, array( 'post_content_filtered' =>$stripped_text, ), array( 'ID' => $post->ID, ), array('%s'), array('%d')); // 投稿キャッシュを削除しないと場合によっては困る clean_post_cache($post); // メッセージ出力 printf('Post ID: %d %s ... Done', $post->ID, get_the_title($post)); echo PHP_EOL; // カウンター増加 $counter++; } } // 終了メッセージ echo PHP_EOL.'---------------------'.PHP_EOL; printf('%d件の投稿を処理しました。', $counter); echo PHP_EOL;
上記のファイルを保存して、PHPのCLIで実行。
検索クエリを改変
ここまで来たら、あとはWordPressの検索クエリを改変します。
// 検索クエリにフックをかける add_filter('posts_search', ‘my_search_query', 10, 2); /** * 検索クエリを改変する * * @global wpdb $wpdb * @param string $where WHERE節 * @param WP_Query $wp_query * @return string */ function my_search_query($where, &$wp_query){ global $wpdb; if( $wp_query->is_search() ){ // 検索語を半角スペースで連結してAND検索 $query = implode(' ', $wp_query->get('search_terms')); // 全文検索構文を組み立て // ANDでつながないとSQL全体が壊れる $sql = <<<EOS AND ( MATCH(post_content_filtered, post_title, post_excerpt) AGAINST (%s) ) EOS; // プリペアドステートメントでエスケープ $where = $wpdb->prepare($sql, $query); } return $where; }
これで全文検索が有効になります。ためしにWordPressの管理画面から「東京都」と「京都」が入った投稿を用意し、「京都」で検索をかけると、「東京都」の方はひっかかりません。これで関ヶ原の合戦以降続いていた東西戦争に終止符が打たれました。
mroongaなら他にこんなことできるよ!
もっと色々書こうと思ったのですが、疲れたので箇条書きに。詳しくは公式ドキュメントを読んでください。
- 検索スコア(一致度)で並び替え
- ファセット検索(あらかじめ絞り込み検索しておいてユーザーに選ばせる)
- 位置情報(POINT型)も使えるらしい
- スニペット取得(Googleの検索結果で該当語が黄色くなるアレ)
これらをWordPressに取り込むためには、SELECT
やORDER BY
にフィルターをかける必要があります。WordPressのサイト内検索の検索条件をカスタマイズするとかを参考にしてください。
というわけで、mroongaで全文検索してみました。導入にあたっての注意点は次のとおりです。
- Search Everythingなどのプラグインと一緒には使えないので、がんばってください
- 個人のブログで導入したりするのはあんまり意味ないと思われます。よほどの猛者でない限り、投稿が数十万件も行くことはないかと。
- マルチサイトも投稿テーブルが分かれるので、意味ないと思われます。
- 専門用語が多いサイト、やたら造語が連発されるサイト(美魔女とか)では意図しない結果になりますので、辞書をメンテナンスする必要があります。
年内にあと二回ぐらいブログ更新します。終わり。
エキスパートのためのMySQL[運用+管理]トラブルシューティングガイド
価格¥147
順位487,671位
著奥野 幹也
編集奥野 幹也
発行技術評論社
発売日2010 年 6 月 12 日
mroongaについては特に書いてありませんが、MySQLチューニングの仕方とかが載っているので役立つかも。