|||||||||||||||||||||

なんぶ電子

- 更新: 

MeCabを使ってMySQLで全文検索

MariaDB 全文検索

複数文書に渡ってテキスト検索をする全文検索。GoogleやYahooのサーチエンジンなどで馴染み深いものでありながら、自分では設定したことがありませんでした。

前回久しぶりMySQLをインストールしてみて、ドキュメントを紐解くことがあったのですが、今になって全文検索用のインデックスがあることを知り、少し試してみたくなりました。合わせてMySQLと姉妹の関係にあるMariaDBの全文検索の対応状況も調べました。

転置インデックス

先のWikipediaには、全文検索時の処理の方法にLinuxのGrepコマンドのようにすべての文章を読み込むか、文章を単語に区切って索引を作るの2種類が紹介されていました。

MySQLの挙動は後者です。索引には「転置インデックス」を使います。

転置インデックスはひとつの単語に対して、該当する文書IDを保存しておくものです。

例えば「索引」という言葉がデータベースの3行目と6行目にあるのなら、「索引=>3,6」をいう感じに記録します。

また近接検索のための単語ごとの位置情報も保持するそうです。

単語の抽出の仕方ですが、MySQLデフォルトの挙動では空白によって文章を区切ることで抽出します。これは英語では機能しますが、単語を空白で区切る文法ではない日本語だとうまく機能しません。日本語で単語を抽出するにはパーサーを利用します。

テーブルの作成

テーブルの作成の方法は、対象にしたいカラムにCHARかVARCHAR、TEXTいずれかを設定した後、FULLTEXT INDEXでインデックスを追加します。

CREATE TABLE test_ft(
seq INT,
content TEXT,
FULLTEXT INDEX(content)
)CHARSET=utf8mb4;

全文検索をする時は次のようにカラムと、検索文字を指定します。

select * from テーブル名 where match(カラム名) against('検索文字');

しかしデフォルト設定では次のように、コマンドを実行すると残念な結果になります。

mysql> insert into test_ft values(1,'カレーライスを食べたい'),
(2,'カツカレーだと少し重い'),
(3,'カレーうどんだと物足りない'),
(4,'やっぱりハヤシライスにしよう');

select * from test_ft where match(content) against('カレー');

Empty set (0.00 sec)

select * from test_ft where match(content) against('カレーライスを食べたい');
+------+------------------------+
| seq  | content                |
+------+------------------------+
|    1 | カレーライスを食べたい |
+------+------------------------+
1 row in set (0.00 sec)

先ほども言ったようにこれは単語を空白文字で区切って管理する為「カレー」という単語は管理されないからです。「カレー ライスを食べたい」という文全体がそのまま登録されてしまいます。

そこで、カレーという言葉やほかの助詞等、単語の前後に空白を入れます。そうするとうまく機能します。

insert into test_ft values(5,'カレー ライス を 食べ たい'),
(6,'カツ カレー だ と 少し 重い'),
(7,'カレー うどん だ と 物足り ない'),
(8,'やっぱり ハヤシライス に し よう');

select * from test_ft where match(content) against('カレー');

+------+----------------------------
| seq  | content                    
+------+----------------------------
|    5 | カレー ライス を 食べ たい     
|    6 | カツ カレー だ と 少し 重い    
|    7 | カレー うどん だ と 物足り ない 
+------+-----------------------------
3 rows in set (0.00 sec)

MeCab

先ほどのように文章を単語毎に空白で区切ればいいのですが、それを自分で実装するのはなかなか難しいです。

文章を単語毎に分割するプログラムのことを形態素解析器というのですが、有名なところに以前このブログでも紹介したことのあるMeCabがあります。

MySQLでは全文検索に、単語抽出用ののパーサーとしてMeCabを設定できるようになっています。そこで、「MySQL 8.0 リファレンスマニュアル」にしたがってMeCabを設定してみます。

まずはMeCabをインストールします。MySQLでプラグインをインストールする際に本体がないと失敗します。

インストーラー形式なので、特に問題ないと思いますが、文字コード選択の部分だけデータベースに合わせます。ここではUTF-8を選択しました。

MeCab

Windowsでは必要なライブラリや設定ファイルが初期のインストールに含まれますので設定していきます。

ここでは前回同様にd:¥mysql-8フォルダにMySQLバイナリがある前提で話を進めす。

ちなみにインストーラーを使ってインストールした場合、my.iniはc:¥ProgramData¥MySQLにあると思います。

my.iniのloose-mecab-rc-fileのキーでmecabrcの場所を記述します。このファイルはMySQLのルートフォルダからたどって「lib/mecab/etc/mecabrc」にあると思います。mecabrcファイルではdicdirとして辞書ファイルのパスを記述します。

my.ini側の最後に設定しているinnodb_ft_min_token_sizeですが、これは管理する単語の最小の長さです。デフォルト値の3となっていて、3文字未満の言葉は無視されるようになっています。英語だと2文字以下の単語は全文検索の対象にしなくていい場合が多いと思いますが、日本語だとそうはいかないのでこの値を1または2にします。ちなみにmax(最大値)もあるのですがこちらのデフォルト値は84となっています。

dicdirではMeCab用の辞書を設定します。辞書が何をするものなのか気になるようでしたらMeCabで漢字のフリガナを取得した際の記事に書いてありますのでよろしければ合わせてお読みください。

辞書ファイルにはSJIS、UTF-8、EUCとありますが、ここではMySQLに設定した文字コードであるUTF-8のファイルを使います。

my.ini

...
[mysqld]
loose-mecab-rc-file="D:/mysql-8/lib/mecab/etc/mecabrc"
innodb_ft_min_token_size=1

mecabrc

...
dicdir="D:/mysql-8/lib/mecab/dic/ipadic_utf-8"

my.iniの記述を変更し、mecabrcに辞書ファイルを設定したらサーバーを再起動します。

起動時「unknown variable 'loose-mecab-rc-file...」というエラーメッセージが出ますが、問題ありません。これはプラグインをインストールしていないために表示されます。しかしプラグインをインストールするにはこのmecab-rc-fileというエントリーが必要です。そこで、起動時に設定エラーでサーバーの起動が止まらないようにloose-接頭をつけています。

再起動が終わったらrootでログインして、次のコマンドを実行します。windowsなので拡張子はsoでなくdllになります。

mysql>INSTALL PLUGIN mecab SONAME 'libpluginmecab.dll';

show pluginsコマンドでインストールを確認します。

mysql> show plugins;
...
| mysqlx | ACTIVE ...
| mecab | ACTIVE ...
+---------------------------------
46 rows in set (0.00 sec)

インデックスを張りなおすために一度ドロップします。無名でインデックスを作成してしまったので、名前を確認するために、show create tableを実行して取得しています。

また、FULLTEXT INDEXのパーサーにmecabを設定します。

mysql> SHOW CREATE TABLE test_ft;
...
 CREATE TABLE `test_ft` (
   `seq` int DEFAULT NULL,
   `content` text,
   FULLTEXT KEY `content` (`content`)
...

mysql> ALTER TABLE test_ft DROP INDEX content;
...
mysql> ALTER TABLE test_ft ADD FULLTEXT INDEX(content) WITH PARSER mecab;

先ほどと同じようにカレーを検索してみます。

mysql> select * from test_ft where match(content) against('カレー');
+------+----------------------------+
| seq  | content                    |
+------+----------------------------+
|    2 | カツカレーだと少し重い     |
|    3 | カレーうどんだと物足りない |
+------+----------------------------+
2 rows in set (0.00 sec)

1の「カレーライスを食べたい」が表示されないのは、「カレーライス」までを1単語として判別されたからです。

Nグラム

合わせて、Nグラムでの設定の方法もMySQL:「12.10.8 ngram 全文パーサー」を参考に紹介しておきます。NグラムはMeCabと違って、単語という概念ではなくNに入る文字単位で文章を分割してインデックスを作成します。

その区切りを何文字にするかをmy.iniに設定します。これは1~10の間で設定が可能です。

my.ini

...
[mysqld]
...
ngram_token_size=2
...

たとえば「abcd」という文字があった時の結果は次のようになります。

  • ngram_token_size=1

    a,b,c,d

  • ngram_token_size=2

    ab,bc,cd

  • ngram_token_size=3

    abc,bcd

  • ngram_token_size=4

    abcd

1-2,3-4と単純に文字を分割するわけではないところがわかりにくい点です。

またNグラムの処理では空白は除去されます。分かりやすいように空白をアンダーバーに変えて説明すると、先の参考サイトではngram_token_size=2の時、「ab_cd」は「ab」と「cd」に、「a_bc」は「bc」になるという説明でした。

この説明の補足をすると、「ab_cd」の時は、「ab」、「b_」、「_c」、「cd」となり空白を含むものを除去すると、「ab」、「cd」となります。

「a_bc」の時は、「a_」、「_b」、「bc」となり空白を含むものを除去すると「bc」となります。

管理テーブル群

全文検索インデックスがどのように管理されているか知りたい場合があると思います。これらの情報は、 「15.15.4 InnoDB INFORMATION_SCHEMA FULLTEXT インデックステーブル」によれば、INFORMATION_SCHEMAにある、INNODB_FTから始まるテーブル群が全文検索の情報が確認できるテーブルだそうです。INNODBの接頭からもわかるように、INNODBに限定されます。これらのテーブルには次のようなものがあります。

  • INNODB_FT_CONFIG

    全文検索に関係するメタデータが入っています。

  • INNODB_FT_DELETED

    パフォーマンスの為に、削除したドキュメントIDを一時的にリストしておくテーブルです。

  • INNODB_FT_BEING_DELETED

    メンテナンス中に、INNODB_FT_DELETEDのスナップショットが入るテーブルで、ユーザー向けの用途は少ないものです。

  • INNODB_FT_DEFAULT_STOPWORD

    デフォルトストップワードのリストです。ストップワードについては次で説明します。

  • INNODB_FT_INDEX_TABLE

    倒置インデックスの情報です。

  • INNODB_FT_INDEX_CACHE

    新しく挿入されたデータのインデックス情報が含まれます。キャッシュがいっぱいになるか他インデックスが再構成時に統合されます。

主に参照するのは、倒置インデックスが入っているINNODB_FT_INDEX_TABLEとそのキャッシュであるINNODB_FT_INDEX_CACHEだと思います。これらのテーブルの構造はどちらも同じで、次のようになっています。

  • WORD

    単語

  • FIRST_DOC_ID

    単語が出現する最初のドキュメントID

  • LAST_DOC_ID

    単語が出現する最後のドキュメントID

  • DOC_COUNT

    全体を通して単語が出現する回数

  • DOC_ID

    ドキュメントID。元のデータのIDか自動生成された番号です。

  • POSITION

    出現箇所ですが、これは相対的な番号で絶対的な位置を示すものではありません。

INNODB_FT_DEFAULT_STOPWORD以外のデーブルは通常は空です。クエリを実行する前に、innodb_ft_aux_table変数に、対象となるデータベース名/テーブル名を指定することで中にデータが格納されます。変数の設定は、全文検索用のテーブルに行を挿入後した後で問題ありません。

mysql> SET GLOBAL innodb_ft_aux_table = 'db_name/table_name';

テーブルにデータが挿入されたばかりの場合はCACHE側にデータが蓄積されていると思います。サーバーが再起動される等内部で「OPTIMIZE TABLE」が実行されると_CACHEにあった内容は、_TABLE側に移動します。

もし手動でOPTIMIZE TABLEを発行したい場合は、「SET GLOBAL innodb_optimize_fulltext_only=ON」として、OPTIMIZE TABLEの挙動を制限して実行します。

mysql> SELECT * FROM INFORMATION_SCHEMA.INNODB_FT_INDEX_CACHE;
...
33 rows in set (0.000 sec);

mysql> SET GLOBAL innodb_optimize_fulltext_only=ON
...
mysql> OPTIMIZE TABLE 対象テーブル名
...
mysql> SELECT * FROM INFORMATION_SCHEMA.INNODB_FT_INDEX_CACHE;
Empty set (0.000 sec)

mysql> SELECT * FROM INFORMATION_SCHEMA.INNODB_FT_INDEX_TABLE;
...
33 rows in set (0.000 sec);

ストップワード

ストップワードは単純に言うと全文インデックスさせない言葉のリストです。INNODB_FT_DEFAULT_STOPWORDには英語における冠詞「a」など、インデックスさせても効果が見込めないものがデフォルトで設定されています。

このストップワードの挙動は、先ほどのNグラムの時にだけ若干挙動がかわります。通常は単語に完全一致した場合にインデックスしないのですが、Nグラムで区切る場合は、区切った文字列にストップワードが含まれていた場合(部分一致)で除外となります。

MeCabで切り分ける場合不要だと思われる格助詞や接続詞などををストップワードにすると精度が上がりそうです。Nグラムで運用する場合は部分一致で消されてしまうので微妙なところだと思います。たとえば、格助詞の「は」をストップワードとして除外した場合、ひらがなで書かれた「はなまる」「はなび」といった単語は原型を失ってしまいます。

オリジナルのストップワードリストを作るには、INNODB_FT_DEFAULT_STOPWORDと同じ構成のテーブルを作って、ストップワードを挿入し、最後にテーブル名をinnodb_ft_server_stopword_table変数にセットするだけです。テーブルを設置するデータベースは、ユーザー領域のどこでもいいようです。

mysql> CREATE TABLE innodb_ft_ft_my_stopwords(value VARCHAR(30)) ENGINE = INNODB;

mysql> INERT INTO innodb_ft_ft_my_stopwords VALUES('が'),('の'),('を'),('に')...;

mysql>SET innodb_ft_server_stopword_table = 'database_name/innodb_ft_ft_my_stopwords';

自然言語全文検索

先ほど紹介したMATCH...AGAINSTの検索の方法は自然言語全文検索というモードでの検索方法でした。

AGAINSTに文字列を渡す際、モードを指定しないと、AGAINST ('カレー' IN NATURAL LANGUAGE MODE)とするのと同じことで、「自然言語全文検索」モードとなります。

この時、ORDERを指定しなければ、表示される結果は関連性の高い順になります。

関連性のスコアを知りたい場合は、次のようなクエリを実行することもできます。scoreカラムに目的の値が入ります。

SELECT seq, MATCH (content) AGAINST ('カレー' IN NATURAL LANGUAGE MODE) AS score FROM articles;

単一の単語でなく複数の単語が含まれていることを条件にしたい場合はをダブルクォートを使います。

...AGAINST ("カレー ライス" IN NATURAL LANGUAGE MODE)

こうすると文中に「カレー」と「ライス」という複数の単語を含む行を検索します。それぞれの単語はどこにいてもいいですが、出現する順序は入力した通りでなければいけません。単語はMySQLの実装によって分割されます。,や.は単語に含まれる文字とは認識されないようです。先の例と「"カレー, ライス"」は検索時には同値になります。

「'カレー ライス'」とシングルクォートで囲んだ場合はそれ全体を単語として検索します。

ブール全文検索

全文検索時のもうひとつの方法にブール全文検索というものがあります。

この検索では検索条件を自然言語全文検索より柔軟に設定ができます。

単語の前に+を付けるとAND条件になります。

SELECT seq, MATCH (content) AGAINST ('+カレー +ライス' IN BOOLEAN MODE) AS score FROM articles;

単語の前に-を付けると否定のAND条件になります。次の場合カレーという文字を含み、ライスという文字は含まないものを探します。

... AGAINST('+カレー -ライス' IN BOOLEAN MODE) ...

なにもつけない時は、存在すれば評価が高くなります。次の例はカレーという文字を含み、ライスという文字はなくてもいいですが、あった場合は評価が上がります。

... AGAINST('+カレー ライス' IN BOOLEAN MODE) ...

なくてもよくて、存在した場合に評価を下げたい場合は~を使います。

.. AGAINST('+カレー ~ライス' IN BOOLEAN MODE) ...

()でグループを作れます。次の例では「カレー、うどん」か「カレー、ライス」いずれかの2語を含む文字が取得されます

... AGAINST('+カレー +(うどん ライス)' IN BOOLEAN MODE) ...

>と<で特定の単語に対する。貢献度の調整もできます。「カレー、うどん」に対する評価を上げ、「カレー、ライス」対する評価を下げます

... AGAINST ('+カレー +(>うどん <ライス)' IN BOOLEAN MODE) ...

他には、検索語に「'カレー*'」というように、ワイルドカードを設定したり、「'"カレー ライス"'」とすることで2語で構成する単語全体を指定したり、@数値で単語間の距離を指定することもできます。距離の指定の仕方は次のようにします。

... AGAINST('"word1 word2 word3" @8' IN BOOLEAN MODE) ...

ここで用いられる+や-、@などはBOOLAN MODEで単語を構成する文字としては利用できないので注意が必要です。

関連性や、評価の上下という説明がありましたが、これは先ほどの自然言語全文検索でも登場したスコアのことです。自然言語同様に、対象カラムとして MATCH ... AGAINST (...IN BOOLEAN MODE)を設定すれば、スコアがでます。

ブール全文検索では、自然言語全文検索と違いデフォルトではスコアの順に表示されません。そうしたい場合は、カラムに指定してあるなら、別名をつけてそれをORDERで記述するか、直接ORDERにMATCH ... AGAINST (...IN BOOLEAN MODE)と書くこともできます。関連性の高い順に表示する場合は、DESCのキーワードも必要です。

評価の計算式については先のMySQLブール全文検索のリンクにて記述があります。

MariaDB

筆者はMariaDBを常用しているので、そちらの方で日本語の全文検索ができないかと調べてみました。ネット上では、MariaDBでは日本語の全文検索は使えないという話になっていますが、この「使えない」は「使い物にならない」の意味のようです。MariaDBでは、MeCabやNグラムのパーサーがないだけで、全文検索自体は日本語でも使えるようです。MariaDB10.6において、先に示したように自力で日本語をスペース区切りにしたカラムに全文インデックスを張れば意図した抽出ができることを確認しました(トップ画像)。

換言すると、本文とは別にインデックス用のカラムを持てばMariaDBで日本語の全文検索は可能だということです。ただ、実際使ってみた場合どれだけ有用性があるかは未知数なので、後日試してみたいと思います。

ちなみに、MariaDBのストレージエンジンをMroongaにすることで、日本語の全文検索も可能になるということでしたがMariaDB 10.2.11を境にパッケージされなくなっているようです。さらにWindows環境においては、近年のバージョンのMariaDBでMroongaストレージエンジンを使いたい場合は、Mroongaが提供しているカスタムされたMariaDBを使うか自力でビルドしないといけないようです。

筆者紹介


自分の写真
がーふぁ、とか、ふぃんてっく、とか世の中すっかりハイテクになってしまいました。プログラムのコーディングに触れることもある筆者ですが、自分の作業は硯と筆で文字をかいているみたいな古臭いものだと思っています。 今やこんな風にブログを書くことすらAIにとって代わられそうなほど技術は進んでいます。 生活やビジネスでPCを活用しようとするとき、そんな第一線の技術と比べてしまうとやる気が失せてしまいがちですが、おいしいお惣菜をネットで注文できる時代でも、手作りの味はすたれていません。 提示されたもの(アプリ)に自分を合わせるのでなく、自分の活動にあったアプリを作る。それがPC活用の基本なんじゃなかと思います。 そんな意見に同調していただける方向けにLinuxのDebianOSをはじめとした基本無料のアプリの使い方を紹介できたらなと考えています。

広告