PHP で MySQL FULLTEXT + MeCab で簡単に全文検索を実装する

大量のデータがあるサイトに検索機能を実装することになりました。LIKE 演算子で %keyword% と検索してみたところ、結果が1分経っても返って来なかったのでで Ngram もしくは MeCab を使った全文検索をできるように仕組みを実装しました。

自分の勉強のために改めて自分で解説してみることにします。初心者の視点から書いてるので、全文検索をやったことがない方やこれから全文検索をしようと思ってる人は入門の参考にしてください。

MySQL での日本語全文検索について

MySQL の FULLTEXT 型とは

そもそも MySQL の FULLTEXT 型というインデックスを使えばそれだけで全文検索をできます。しかし英語のように単語がスペースで区切られてる時だけであり、日本語のように全て繋がっている場合は機能しません。

そのため MeCab などの日本語形態素解析で日本語から単語を抽出してスペース区切りで文章を作り全文検索用のカラムに追加するか、Ngram で変換して全文検索用のカラムに追加することで実現するのが良いです。以下のサイトの説明が参考になりました。

日本語全文検索の実装方法について

この記事では今回 MeCab を使った全文検索をメインに説明させて頂きますが、MeCab より実装が楽な Ngram を使った全文検索については以下のページで詳しく解説されています。Ngram のライブラリも公開されていて参考にさせて頂きました。日本語の全文検索そのものについてはこのページの手順が大変わかり易くて便利です。

MeCab と Ngram の違いについて

Ngram は unigram、bigram、trigram といろいろあり、詳しいことについてはここでは省きますが、文章を細かく変換して追加します。そのため、部分一致検索をすることもできるし、日本語以外の言語にも対応しています。しかし、東京都を京都で引っ掛けてしまう、何も考えずただ変換して追加するためにデータ量が増えるなどのデメリットがあります。

MeCab に関しては文章を解釈して必要な分だけでデータを格納しますので、データをあまり消費しませんし、解釈して格納されているため、東京都を京都で引っ掛けたりすることもありません。効率的な検索と言えます。ただしデメリットとして、辞書に存在しない言葉は解釈されないため、検索での抜けがあったりするため、簡易的ではなく精度の高い検索を目指すためには辞書のパワーアップをしたり、アプリ側で検索キーワードを学習してそれも抜き出す仕組みを実装しても良いかもしれないです。

今回 MeCab で実装した理由

最初はサーバーに MeCab をインストールしなくて良い Ngram での実装をしたのですが、ひとつひとつの記事が数万文字ある長編ストーリーである可能性があるので、Ngram で変換すると飛んでもないデータ量になります。全文インデックスである FULLTEXT は、MyISAM テーブルで、CHAR、VARCHAR、TEXT カラムのみとなっていて、それぞれのカラムは以下のように格納できる最大長が決まっており、一番大きい TEXT でも最大長が 65535 バイトの可変長文字列であるため、私が現在実装しようとしているサイトでは最大長以上のデータ量になる可能性があるため、必要最低限のデータ量で済む MeCab で実装することにしました。

日本語全文検索の仕組みについては以上になります。日本語全文検索の実装は簡単ですが、理解せずにただインストールすればできるという代物ではないので、ここまでで一度勉強してある程度理解できていると良いかもしれません。

MeCab による全文検索を実装する

PHP で MeCab を使えるようにする

まず MeCab のインストールと php_mecab をインストールします。それぞれのインストール方法についても解説記事を書きましたのでインストールしていない人は参考にしてください。

my.cnf の編集をする

デフォルトでは4文字以下のキーワードでは検索されません。そのため /etc/my.cnf を以下のように編集して MySQL を再起動してください。my.cnf の場所は環境によって異なります。

1
2
[mysqld]
ft_min_word_len=1

MySQL のテーブルに全文検索用のカラムを作る

例えば私のサイトだと以下のようになっています。words を全文検索用のカラムとして作成してそこにデータをいれています。

1
2
3
4
5
6
7
8
9
10
11
CREATE TABLE IF NOT EXISTS `wb_posts` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `title` varchar(128) NOT NULL,
  `body` longtext NOT NULL,
  `words` text NOT NULL,
  `created` datetime NOT NULL,
  `updated` datetime NOT NULL,
  PRIMARY KEY (`id`),
  KEY `UPDATED` (`updated`),
  FULLTEXT KEY `words` (`words`)
) ENGINE=MyISAM  DEFAULT CHARSET=utf8 AUTO_INCREMENT=1 ;

MeCab を使うライブラリを作る

以下のクラスは私が作成した簡単に MeCab を使うライブラリです。simple_keyword() で簡単なキーワードの解析結果を配列で返します、match_sql() はマッチ用の SQL を生成します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<?php
class wbMecab
{
    public $mecab;
 
    public function __construct()
    {
        $this->mecab = new MeCab_Tagger();
    }
 
    public function simple_keyword($string)
    {
         $result = mecab_split($string);
         $keywords = Array();
         foreach($result as $key) {
             if(isset($keywords[$key])) {
                $keywords[$key]++;
             } else {
                $keywords[$key] = 1;
             }
         }
         return $keywords;
    }
 
    public function match_sql($string, $column)
    {
        $keywords = $this->simple_keyword($string);
        $words = Array();
        foreach($keywords as $word => $count)
            $words[] = '+' . $word;
 
		return "MATCH(" . $column . ") AGAINST('" . join(' ', $words) . "' IN BOOLEAN MODE)";
	}
    }
}

テーブルにデータを格納する

テーブルにデータを格納する際に以下のように格納します。$mecab->simple_keyword() を使うことでキーワードの配列を取得できます。以下は CakePHP での書き方なので、お使いのフレームワークや SQL を手書きする際はそれに当てはめてください。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$title = 'タイトルです。';
$body = 'ここは本文です。';
$mecab = new wbMecab();
$words = Array();
foreach($mecab->simple_keyword($title . $body) as $word => $count)
    $words[] = $word;
$this->Model->create();
$this->Model->save(
    Array(
        'title' => $title,
        'body' => $body,
        'words' => join(' ',$words),
    )
);

実際にデータを検索する

$mecab->match_sql() を使えば簡単に SQL を生成することができます。これを使って CakePHP 的には以下のように書きます。お使いのフレームワークや SQL を直書きする場合はそれに合わせて書いてみてください。

1
$result = $this->find('first', Array('conditions' => Array($mecab->match_sql($keyword, 'words'))));

まとめ

ベンチマークを取りたかったのですが、ただの LIKE での検索の時は検索すらできなかったのでちょっと取れないのですが、7万件で1秒以内にデータが取れます。元々の文章量がとんでもなく多いので、普通の記事を検索するだけとかであれば爆速だと思います。

もう少し MeCab を上手く使えるようにしたり、辞書をパワーアップしたりすることで精度をアップできたらいいですね。ちなみに最初の全文検索の説明で疲れてしまって、実際の MeCab を使うコードとか書くのがかなり辛かったです。きちんと動かなければ教えてください。

ちなみに可能なら Senna などを入れても良いですし、Fess なども簡単で良いのですが、今回はアプリ側のみで対応するという趣旨で書いています。この記事で全文検索がちょっと分からないという方の助けになれば幸いです。

コメント

コメントは受け付けていません。