CreateField Blog

オープンソースを使って個人でWebサービスを開発・運営していたブログ

数百GiBの全文検索用データベースをMroongaのストレージモードにしてはまったこと

前回は、全文検索Webサービスを作ったときにはまったことの第2回として、 Mroongaのラッパーモードからストレージモードに変えた理由という記事を書きました。

今回は、Mroongaのストレージモードにしたことによってはまったことについて書きたいと思います。

Spiderストレージエンジンを使ったMroongaの分散構成の検討

当初、データベースを複数に分割し、Spiderストレージエンジンを使ってデータベースシャーディングする予定でした。

Spiderストレージエンジンは、ストレージエンジンの種類を問わずデータベースを水平分散させることができるMySQL/Mariadbのストレージエンジンです。 Spiderストレージエンジンは、国産で個人の方が開発されたストレージエンジンでMariaDB10.0系にすでにバンドルされています。 Spiderストレージエンジンの開発者の方は、全文検索Mroongaストレージエンジンの開発にも携わられています。

Spiderストレージエンジンは、非常に簡単にデータベースシャーディングすることができ、既存のSQLをそのまま使うことができます。

前回説明したように、ドリルダウン検索*1が非常に便利でWebアプリの機能として組み込みたいと考え、Mroongaをラッパーモードからストレージモードに変更し、全文検索はGroongaのコマンドを使うことにしました。

しかし、SpiderストレージエンジンはSQLベースで分散するため、Groongaのデータベースに直接コマンドを発行することはできません。 当時のGroongaには、分散機能がありませんでした*2

そこで、約400GiBのデータベースを分散させずに良好なパフォーマンスが得られるかという無謀な挑戦がはじまります。

データベースの統合によるカラムサイズ制限

Groongaには、いくつかの制限事項が設けられています。

当時のドキュメントには記載がありませんでしたが、Groongaには1カラムに格納可能なデータサイズの上限値が設けられており上限値は256GiBです。

したがって、約400GiBのデータベースを1カラムに格納することができませんでした。

そこで、苦肉の策として1カラムを3カラムに分割させます。 テーブルには、年度を示すカラムがあったため、1999年以前、2000年~2009年、2010年以降の3つのカラムを作成し、 年度に応じてアプリケーション側で切り替えるようにしました*3

なお、次期GroongaであるGrnxxでは、上記の制限が撤廃できるように設計が検討されているようです。

https://docs.google.com/presentation/d/1R5YqedpDyI9NVNn6f_EkCBZ8miQAldrAxu5-AwjmPas/edit#slide=id.p

データベースの統合によるインデックス構築失敗再び

上記のようにカラムを分割することにより、カラムの上限による制限は回避することができました。

しかしながら、今度はまたインデックス構築に失敗するようになります。

第1回に書いたインデックス構築の失敗とは、また別の原因によるものです。

この事象は、データベースサイズが100GiBぐらいにならないと再現せず、簡単な再現セットを作るのが困難でした。

メーリングリストで相談すると、デバッグ手法を一から教えていただいたり、バックトレースログから推測されるバグの修正パッチなどきめ細やかな対応をしていただきました。

http://sourceforge.jp/projects/groonga/lists/archive/dev/2013-August/001718.html
http://sourceforge.jp/projects/groonga/lists/archive/dev/2013-August/001725.html

しかし、再現に半日かかることもありバックトレースログから原因をなかなか突き止めることができませんでした。

そうしていると、わざわざテストマシンを用意していただけることとなり、当方の100GiB以上の再現可能なデータベースを直接提供して解析していただけることになりました。

http://sourceforge.jp/projects/groonga/lists/archive/dev/2013-September/001739.html

これによりオーバーフローしている箇所を突き止め、バグを修正していただけました。こうして、数百GiBのデータベースでも正常にインデックス構築ができるようになりました。

http://sourceforge.jp/projects/groonga/lists/archive/dev/2013-October/001866.html

以上のようなきめ細やかな対応を全て無償で行っていただきました。

個別環境による検証等は、有償のサポートサービスを契約すべきなのかもしれません。無償にも関わらず、ここまできめ細やかなご対応どうもありがとうございました。

おわりに

以上のようにして、Mroongaのストレージモードで400GiB超のデータベースのインデックス構築ができるようになりました。

しかし、この後の検証によりデフォルトのトークナイザのTokenBigramでは、全文検索のパフォーマンスがあまり芳しくないことが判明します。サイズがでかすぎるので当然といえば当然です。

そこで、400GiB超のデータベースの全文検索のパフォーマンスをできるだけ改善できないかを試行錯誤することになります。

次回は、全文検索のパフォーマンスをできるだけ良くするために試行錯誤したことについて書こうと思っています。投稿まで少し時間が空くと思います。

6/26 記事を追加しました。
Groongaがあまり得意でない類似文書検索に連想検索エンジンGETAssocを使った話

2014-11-29(土)13:30 - 17:30
年に1度のGroongaに関するイベントがあります。Groongaを使っている人、興味がある人は参加してみてはいかがでしょうか。

全文検索エンジンGroongaを囲む夕べ5 - Groonga | Doorkeeper

*1:他の全文検索エンジンやDroongaではファセット検索と呼ばれています。

*2:2014年2月には、Droongaと呼ばれるGroongaの分散システムがメジャーリリースされています。

*3:非常に不格好なのでお勧めしません。

Mroongaのラッパーモードからストレージモードに変えた理由

前回は、全文検索Webサービスを作ったときにはまったことの第1回という記事を書きました。

今回は、Mroongaを使って全文検索Webサービスを作ったときにはまったことの第2回として、ラッパーモードからストレージモードに変えた理由について書きたいと思います。

なお、かなり長く、MySQL、Groongaについて前提知識がないと理解できない部分が多々含まれている可能性があります。

ラッパーモードとは

全文検索Mroongaストレージエンジンでは、全文検索するためにラッパーモードとストレージモードの2つのモードが用意されています。

(引用) ラッパーモードでは全文検索機能のみGroongaの機能を利用し、データストアはInnoDBなど既存のストレージエンジンを利用します。ラッパーモードを利用することにより、ストレージエンジンとして多くの利用実績のあるInnoDBに全文検索エンジンとして実績のあるMroongaを組み合わせて、高速な全文検索機能付きの信頼性のあるデータベースとして利用できるという特長があります。

ラッパーモードを使うことにより、たとえば、トランザクション対応のInnoDBを使うことができます。全文検索も高速にできてトランザクションも使えるとなるといいことづくめのように見えます。

ラッパーモードとストレージモードの違い

ドキュメントを比較すると、ラッパーモードは、ストレージモードに比べて、位置情報検索と、レコードIDの取得と、Groongaコマンドの実行と、カラムの刈り込みと、行カウント高速化がないぐらいです。

また、私が使いはじめたときは、割とよくmysqldがクラッシュしてデータベースが破損しやすかったこともあり、ストレージモードではデータファイルがたくさんできて取り扱いにくかったのでMyISAMのラッパーモードでデータベースを作りはじめました。

しかしながら、ストレージモードでは、以下の点が優れていることに気づきラッパーモードからストレージモードに変更しました。

ストレージモードの利点その1 所定の条件の場合に全文インデックス以外の複数インデックスを使用した高速な絞込みができる

ストレージモードでは、所定の条件の場合、ORDER BY LIMITで全文インデックス以外のインデックスを複数使用して高速に絞込み操作を行うことができます。

なお、この所定の条件は、条件の数だけソースを追加しなければいけないというデメリットがあります。 以前、ユーザが要望したこの条件わずか一日で追加されて、検索速度が10倍改善されたというエピソードがあります。 Groonga開発チームのこの対応速度は素晴らしいですね。Groonga開発チームのこの対応の良さがMroongaを使う最も大きな利点かもしれません。

ちなみにこの所定の条件に関わらず複数インデックスを使うちょっとした裏技みたいな方法があります。 ただし、これは今のところソースを変更する必要があるというのと、AGAINST句の中でGroongaのクエリ構文を使う必要があります。

ここに試した結果と方法を記載しています。インデックスが使われない場合は、数秒かかっていた検索クエリが0.数秒になっています。

この方法もGroonga開発チームにメーリングリストで教えてもらいました。

ストレージモードの利点その2 インデックスが使用されないときのレコード操作が高速

インデックスが使われないときのカウントやレコード操作が他のストレージエンジンに比べてストレージモードは段違いに速いです。

具体的にInnoDBのラッパーモードとストレージモードで比較してみましょう。

データベースのデータサイズは20GiB強、レコード数は数十万ぐらいです。

まず、以下のように全文インデックスのみでの絞込みが7万8千件ぐらいの場合を比較します*1

mysql> SELECT COUNT(*) FROM ftext WHERE MATCH(title,abstract,description) AGAINST("+画像" in boolean mode);
+----------+
| COUNT(*) |
+----------+
|    78467 |
+----------+
1 row in set (0.04 sec)
  • InnoDBラッパーモード (一時結果が7万8千の場合)
mysql>  SELECT COUNT(*) FROM ftext WHERE MATCH(title,abstract,description) AGAINST("+画像" in boolean mode) AND date LIKE '2010%';
+----------+
| COUNT(*) |
+----------+
|    69644 |
+----------+
1 row in set, 1 warning (19.55 sec)
  • ストレージモード (一時結果が7万8千の場合)
mysql> SELECT COUNT(*) FROM ftext WHERE MATCH(title,abstract,description) AGAINST("+画像" in boolean mode) AND date LIKE '2010%';
+----------+
| COUNT(*) |
+----------+
|    69644 |
+----------+
1 row in set, 1 warning (2.24 sec)

InnoDBラッパーモードに比べると、ストレージモードの方がだいぶ速いですね。 もう少し全文インデックスでの絞り込み件数を少なくし、2万件ぐらいの場合を比較してみます。

mysql> SELECT COUNT(*) FROM ftext WHERE MATCH(title,abstract,description) AGAINST("+画像処理" in boolean mode);
+----------+
| COUNT(*) |
+----------+
|    20742 |
+----------+
1 row in set (0.19 sec)
  • InnoDBラッパーモード (一時結果が2万の場合)
mysql>  SELECT COUNT(*) FROM ftext WHERE MATCH(title,abstract,description) AGAINST("+画像処理" in boolean mode) AND date LIKE '2010%';
+----------+
| COUNT(*) |
+----------+
|    19448 |
+----------+
1 row in set, 1 warning (4.09 sec)
  • ストレージモード (一時結果が2万の場合)
mysql> SELECT COUNT(*) FROM ftext WHERE MATCH(title,abstract,description) AGAINST("+画像処理" in boolean mode) AND date LIKE '2010%';
+----------+
| COUNT(*) |
+----------+
|    19448 |
+----------+
1 row in set, 1 warning (0.68 sec)

このように、ストレージモードはインデックスが使われなくともInnoDBラッパーモードに比べてかなり速いレコード操作を行うことができます。 全文インデックスを使った一時絞込み結果が数万件ぐらいでは、十分実用レベル*2であることがわかります。

さらに、InnoDBのラッパーモードでは、オフラインでのインデックス構築に非常に時間がかかります。 たとえば、数十GiBのテーブルでALTER TABLE ftext ENABLE KEYS;すると、ストレージモードの場合は26分に対し、InnoDBのラッパーモードだと3時間21分かかりました

4/17 追記
少しだけソースを読みました。ストレージモードの場合、ha_mroonga::storage_create_indexgrn_obj_set_infogrn_ii_buildなのに対し、ラッパーモードの場合、ha_mroonga::wrapper_fill_indexesgrn_column_index_updategrn_ii_column_updategrn_ii_update_oneとなっていました。ラッパーモードの場合このアップデートがたくさん走るからストレージモードに比べて遅いような気がしました。

ストレージモードの利点その3 Groongaコマンドが使える

(引用) ストレージモードでは、全文検索機能だけではなくデータストアも含めてGroongaの機能を利用します。ストレージエンジンのすべての機能をGroongaで実現するため、Groongaが得意としている集計操作が高速です。また、Groongaコマンドで直接データベースを操作できるという特長もあります。

私にとっては、ストレージモードにするとGroongaコマンドで直接操作できるという点が非常に大きかったです。Groongaでは、ドリルダウンという高速な集計操作を行うことができます。

Groongaでは、以下のようにドリルダウン検索することで、全文検索結果に含まれる他のカラムの件数を簡単、且つ、割と*3高速に取得することができます。

mysql> SELECT mroonga_command('select ftext --match_columns title||abstract||description --query データベース --output_columns id --limit 0 --drilldown applicants --drilldown_sortby -_nsubrecs --drilldown_limit 10') as result;
+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| result                                                                                                                                                                                                                                                                                                                                                                                                                                                         |
+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| [[[17701],[["id","ShortText"]]],[[4330],[["_key","ShortText"],["_nsubrecs","Int32"]],  
["F株式会社",495],  
["N株式会社",478],  
["株式会社H",459],  
["M株式会社",330],  
["株式会社T",323],  
["NT株式会社",319],  
["S株式会社",314],  
["C株式会社",308],  
["P株式会社",303],  
["Q株式会社",274]]] |  
+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
1 row in set (0.45 sec)

SQLで同じことをしようとするとかなり大変なクエリになると思います。 なお、同じことをするSQLをぱっと思いつかなかったので比較していません。

ストレージモードの利点その4 ベクターカラムが使える

Groongaでは、ベクターカラムという1カラムに複数の値を格納できる仕組みがあります。このベクターカラムは、Mroongaでも作ることができます。すなわち、1対nの関係を別テーブルにせずに1テーブルで表現することができます。これによりJOINやGROUP BYが不要になります。

たとえば、MySQLでは、以下のようなテーブル定義があったとします。 1つのIDに対して、複数の値がぶら下がる形です。

  • ベクターカラムなしの場合のテーブル定義
CREATE TABLE `ftext` (
  `id` varchar(20) NOT NULL,
  `date` date NOT NULL,
  `title` text NOT NULL,
  `abstract` text NOT NULL,
  `description` longtext NOT NULL,
  PRIMARY KEY (`id`),
  KEY `date` (`date`),
  FULLTEXT INDEX `ftext` (`title`,`abstract`,`description`)
) ENGINE=mroonga DEFAULT CHARSET=utf8;

CREATE TABLE applicants (
  `id` varchar(20) NOT NULL,
  `name` varchar(80),
  PRIMARY KEY (`id`,`name`)
) ENGINE=mroonga DEFAULT CHARSET=utf8;

f:id:naoa_y:20140413160705p:plain

上記のテーブル定義をベクターカラムを使うと以下のように表現することができます。

  • ベクターカラムありの場合のテーブル定義
CREATE TABLE applicants (
  `name` varchar(80) PRIMARY KEY
) ENGINE=mroonga DEFAULT CHARSET=utf8
  COLLATE=utf8_bin
  COMMENT='default_tokenizer "TokenDelimit"';

CREATE TABLE `ftext` (
  `id` varchar(20) NOT NULL,
  `date` date NOT NULL,
  `title` text NOT NULL,
  `abstract` text NOT NULL,
  `description` longtext NOT NULL,
  `applicants` TEXT NOT NULL COMMENT 'flags "COLUMN_VECTOR", type "applicants"',
  PRIMARY KEY (`id`),
  KEY `date` (`date`),
  FULLTEXT INDEX `ftext` (`title`,`abstract`,`description`),
  FULLTEXT INDEX applicants (applicants) COMMENT 'table "applicants"'
) ENGINE=mroonga DEFAULT CHARSET=utf8;

ベクターカラムの場合、半角スペースで区切られた値を渡すことでカラムを更新することができます*4

  • 更新例
mysql > INSERT INTO ftext VALUES("1","2001-01-01","title","abstract","description","name1 name2 name3");

title,abstract,descriptionカラムのいずれかに"装置"が含まれ、且つ、applicantsに"S株式会社"が含まれる件数を集計してみましょう。 上記同様、データベースのデータサイズは20GiB強、レコード数は数十万ぐらいです。

"装置"だけの一時検索結果数は23万7435件です。

mysql> SELECT COUNT(*) FROM ftext WHERE MATCH(title,abstract,description) AGAINST("+装置" in boolean mode) ;
+----------+
| COUNT(*) |
+----------+
|   237435 |
+----------+
1 row in set (0.10 sec)
  • ベクターカラムなしの場合のSQLでのSELECT
mysql> SELECT COUNT(*) FROM ftext
    -> INNER JOIN applicants ON applicants.id = ftext.id
    -> WHERE MATCH(title,abstract,description) AGAINST("+装置" in boolean mode)
    -> AND applicants.name = 'S株式会社';
+----------+
| COUNT(*) |
+----------+
|     3259 |
+----------+
1 row in set (24.81 sec)

ベクターカラムなしのSQLの場合、JOINをする必要があり検索速度が遅いですね。 また、applicants.nameが1IDで複数ヒットする条件ならば、GROUP BYをする必要もありそうです。

  • ベクターカラムありの場合のSQLでのSELECT
mysql> SELECT COUNT(*) FROM ftext
    -> WHERE MATCH(title,abstract,description) AGAINST("+装置" in boolean mode)
    -> AND MATCH(applicants) AGAINST("+S株式会社" in boolean mode);
+----------+
| COUNT(*) |
+----------+
|     3259 |
+----------+
1 row in set (29.49 sec)

ベクターカラムに対しては、MATCH ... AGAINSTで検索しないといけません。これでは、後ろ側のインデックスが使われず遅いですね。

  • ベクターカラムありの場合のSQLでのSELECT(AGAINST句でGroongaのクエリ構文使用) 上記の複数インデックスが使えるちょっとした裏技を使ってみます。
mysql> SELECT COUNT(*) FROM ftext
    -> WHERE MATCH(title,abstract,description) AGAINST("+装置 +applicants:@S株式会社" in boolean mode);
+----------+
| COUNT(*) |
+----------+
|     3259 |
+----------+
1 row in set (0.08 sec)

この方法ならSQLでも複数インデックスが使えて速いですね。 ただし、ソースをいじる必要があり、Groongaのクエリ構文が含まれるというイレギュラーな形になってしまいます。

  • ベクターカラムありの場合のGroongaのselectコマンド
mysql> SELECT mroonga_command("select ftext --match_columns title||abstract||description --query '装置 +applicants:@S株式会社' --output_columns id --limit 0") as result;
+---------------------------------+
| result                          |
+---------------------------------+
| [[[3259],[["id","ShortText"]]]] |
+---------------------------------+
1 row in set (0.29 sec)

Groongaのselectコマンドでは、結果がJSONで返ってきます。複数インデックスが使われていて高速に結果が取得されていますね。

MySQLのクエリチューニングは非常に奥が(闇が)深いのでSQLの方は、もっと速くする方法があるのかもしれません。 Groongaのselectコマンドでは、さほどチューニングが不要ということも利点かもしれません。

このように、ストレージモードは速度的なメリットとGroongaコマンドが使えるというメリットがあります。

ここで、MroongaストレージモードのSQLのSELECT構文とGroongaのselectコマンドを比較するとさらに以下の利点があります。

Groongaの利点その1 自由に複数インデックスを使って高速な絞込みができる

Groongaのselectコマンドでは、上記の所定の条件に縛られずに複数インデックスを使うことができます。 したがって、多数の絞込み条件を自由に追加して、簡単に高速に全文検索することができます。

Groongaの利点その2 カウントが検索クエリの結果と同時に取得できる

Groongaでは、上記のように結果セットがJSONとなるので、検索結果とカウントを同時に取得することができます。なお、これは、既存のアプリをそのまま適用できないというデメリットともとらえることができます。

Groongaの利点その3 オフセットがストレージモードよりもかなり速い

MySQLを使っている方だとわかると思うのですが、MySQLのオフセットは数が大きくなると検索速度が顕著に劣化します。 このため、MySQLではBETWEENで絞り込んだりすると思います。

Groongaのselectコマンドでは、オフセット処理がMroongaストレージモードのSELECT構文よりもかなり速いです。 約1000万件のオフセット速度を比較してみます。

mysql> SELECT COUNT(*) FROM ftext;
+----------+
| COUNT(*) |
+----------+
| 11549665 |
+----------+
1 row in set (0.00 sec)
  • MroongaストレージモードのSELECT構文*5
mysql> SELECT app_id FROM ftext LIMIT 5 OFFSET 11549660;
+---------------+
| app_id        |
+---------------+
| JP20130513403 |
| JP20130513406 |
| JP20130513423 |
| JP20130513425 |
| JP20130513426 |
+---------------+
5 rows in set (8.09 sec)
  • Groongaのselectコマンド
mysql> SELECT mroonga_command("select ftext --output_columns app_id --limit 5 --offset 11549660") as result;
+-----------------------------------------------------------------------------------------------------------------------------------+
| result                                                                                                                            |
+-----------------------------------------------------------------------------------------------------------------------------------+
| [[[11549665],[["app_id","ShortText"]],  
["JP20130513403"],  
["JP20130513406"],  
["JP20130513423"],  
["JP20130513425"],  
["JP20130513426"]]] |  
+-----------------------------------------------------------------------------------------------------------------------------------+
1 row in set (0.25 sec)

Groongaのselectコマンドでは、約1000万件の末尾でも0.数秒で取得できますね。これなら、わざわざBETWEENで絞り込む必要はありません。

Groongaの利点その4 カウントがストレージモードよりもわずかに速い

これは、かなり多数のレコードにならないと差がでてきませんが、検索結果が数百万レコードぐらいになると1秒ぐらいの差がでてきます。 ただ、数百万レコードで1秒ぐらいなのであまり影響はない範囲だと思います。

Qiitaに試した結果があります。

おわりに

かなり長くなってしまいました。あまりまとまっておらず、判りづらかったと思います。

種類 利点
ストレージモード 所定の条件の場合に全文インデックス以外の複数インデックスを使用した高速な絞込みができる (ソースをいじってAGAINST句にGroongaのクエリ構文を使うと自由に複数インデックスが使える)
ストレージモード インデックスが使用されないときのレコード操作が他のストレージエンジンに比べて高速 全文インデックスでの一時絞込み結果が数万件ぐらいであれば実用レベル
ストレージモード Groongaコマンドが使える ドリルダウンが使える
ストレージモード ベクターカラムが使える ベクターカラムを使うとJOINやGROUP BYが不要
Groonga 自由に複数インデックスを使って高速な絞込みができる
Groonga 結果がJSONなのでカウントが検索クエリの結果と同時に取得できる
Groonga オフセットがストレージモードよりもかなり速い 1000万件の末尾でも0.数秒
Groonga カウントがストレージモードよりもわずかに速い 数百万件で1秒

以上のような利点から私はMroongaをストレージモードにし、更新やメンテナンスでの簡単な検索はMroongaのSQLで楽にデータ操作しつつ、全文検索はGroongaのselectコマンドというスタイルが最もフィットしました。

最初は、Groongaは敷居が高く入りづらかったですが、Mroongaをストレージモードで使っているうちにGroongaについても慣れてきました。Groongaは、いろいろいじれて面白いです。

私は個人で開発していたのでGroongaのコマンドを使うのは特に問題がなかったですが、Groongaコマンドまでいってしまうと完全にSQLを逸脱してしまうので、 保守性や属人性を考えると、一般的なシステム構築現場では、採用できない・提案できないことがあるかもしれません。 また、Groongaコマンドは、結果セットがJSONになるので既存のアプリをそのまま使うこともできません。

それでも私は、単純なカラムを全文検索するという場合を除き*6、がっつり全文検索したいならば、ストレージモードをおすすめします。 堅牢性と高速性を両方狙ってもあまりいいことはありません。この2つはベクトルの向きが違います。Mroongaは、正直割と壊れるときは壊れます*7。 全文検索用のデータベースは、壊れても泣かないぐらいの心づもりでいると良いかもしれません。 私は、全文検索用のデータベースの他にデータ保持用のデータベースを作るか他のストレージエンジンとレプリケーションをしたほうがいいと思っています。

次回は、ストレージモードにしたことによってはまったことや改良点について書く予定です。

4/15 記事を追加しました。
ストレージモードにしてはまったことについて説明しています。
数百GiBの全文検索用データベースをMroongaのストレージモードにしてはまったこと - CreateField Blog

4/16 追記
MroongaのInnoDBラッパーモードの採否について以下の記事も参考になると思います。転置インデックスにはトランザクションが効きません。
日々の覚書: MroongaのラッパーモードでInnoDBを使う落とし穴

6/26 記事を追加しました。
Groongaがあまり得意でない類似文書検索に連想検索エンジンGETAssocを使った話

2014-11-29(土)13:30 - 17:30
年に1度のGroongaに関するイベントがあります。Groongaを使っている人、興味がある人は参加してみてはいかがでしょうか。

全文検索エンジンGroongaを囲む夕べ5 - Groonga | Doorkeeper

*1:全てのクエリにおいて、クエリキャッシュは効いておらず、毎クエリごとにmysqldを再起動しています。

*2:検索速度はサーバスペック、ディスク速度に応じて変わると思います。

*3:ドリルダウン検索は割と高速ですが、全文検索の速度と比べると検索結果がかなり多い(数百万件クラス)と件数の積み上げに結構時間がかかります。

*4:半角スペース以外にしたい場合は、default_tokenizerを変えればよいです。

*5:InnoDBのオフセットはストレージモードのオフセットよりもさらに遅いと思います。

*6:全文インデックスのみを使った全文検索ならラップしているストレージエンジンは関係なく高速に全文検索できると思います。

*7:とはいっても、動きはじめて安定すれば、更新もせずに勝手に壊れるというのはないです。

日米特許のデータを使ってword2vecを試してみた

はじめに

去年あたりから流行っているらしいword2vecが面白そうだったので日本特許の要約データと米国特許の要約データを使って試してみました。

word2vecは、類語やアナロジー(類推)等を取得することができます。

word2vecの使い方は非常に簡単で、空白区切りのテキストデータをword2vecの学習プログラムに渡すだけです。

アナロジーというのは、ベクトル同士を演算し、A → Bの関係に対し、C→Xに当てはまるXというのを探すことができるようです。

(引用)https://plus.google.com/107334123935896432800/posts/JvXrjzmLVW4
面白いのは、2つのベクトルの差が、2つの単語の関係をよく近似してくれること。 (中略) A B C → X (A → Bの関係に対し、 C → X に当てはまるXを探す)

グーグル ヤフー トヨタ → 日産
渋谷 新宿 札幌 → 旭川
警察 泥棒 正義 → くそ
平和 戦争 左 → 右
社員 会社 生徒 → 小学校
空 海 天井 → 床板
生きる 死ぬ 動く → 止まる

実装例

兎にも角にも、まずは、実際に試してみた例を示します。特許の全文検索サービスにword2vecを使った類語、類推語検索機能を組み込んで見ました。

以下のページで試すことができます。上部フォーム横にある検索ボタンの左の+ボタンで類義語、類推語を検索することができます。

PatentField | 無料特許検索

類語取得例

以下は類語取得例です。

筆記具 ボールペン 筆記 万年筆 消しゴム 水性ボールペン 水性インキ 筆記具用インキ 筆記用具 サインペン
自動車 乗用車 オートバイ 車両 車輌 車輛 二輪車 乗り物 乗物
スマートフォン PDA 携帯情報端末 パソコン pda カーナビ 携帯型コンピュータ
ラーメン うどん 味噌汁 スープ 麺類 麺 玉子
情報処理装置 情報処理システム データ処理装置 コンピュータ装置 多機能周辺装置 情報処理プログラム
煩雑 繁雑 面倒 煩わしい 煩瑣 手間
円滑 スムーズ スムース 確実 容易 迅速 速やか

割と類語らしきものがとれていますね。結構いい感じです。ちなみに日本特許の要約データには、ほぼ商標が入っていないはずなので、商品名とかはとれません。

また、学習させたコーパスには、行頭に出願人(会社名)をつけているので、今度は会社名で類語を取得してみます。

任天堂株式会社 株式会社コナミデジタルエンタテインメント 株式会社ソニー・コンピュータエンタテインメント 株式会社バンダイナムコゲームス 株式会社国際電気通信基礎技術研究所 株式会社タイトー 株式会社セガ 株式会社スクウェア・エニックス
ヤフー株式会社 楽天株式会社 株式会社日立ソリューションズ 株式会社野村総合研究所 NECフィールディング株式会社 株式会社日立システムズ 富士通モバイルコミュニケーションズ株式会社 ソフトバンクモバイル株式会社
グーグル・インコーポレーテッド ヤフー!インコーポレイテッド ▲ホア▼▲ウェイ▼技術有限公司 アマゾンテクノロジーズインコーポレイテッド グーグルインコーポレイテッド アリババ・グループ・ホールディング・リミテッド
パナソニック株式会社 シャープ株式会社 ソニー株式会社 パナソニック電工株式会社 松下電器産業株式会社 三星電子株式会社 三菱電機株式会社
トヨタ自動車株式会社 日産自動車株式会社 本田技研工業株式会社 マツダ株式会社 株式会社デンソー 富士重工業株式会社 ダイハツ工業株式会社 日野自動車株式会社 アイシン精機株式会社

ほぼ同じ業界の会社名が現れていますね。

なお、特許情報の書誌データに出現する会社名を追加した形態素解析辞書を使っているので株式会社まで入った正式名称で類語検索する必要があります。出願人名はこちらで検索することができます。

米国特許の要約データから英語のモデルも作成しました。

フレーズ処理で変なくっつきかたしているのもありますが、英語の方もうまくいく例が結構ありました。しかし、うまく前処理ができていないせいか日本語よりは精度が落ちる感じです。

smartphone smart_phone smartphones cellular_telephone pda pda_laptop assistant_pda
ipad iphone ipod iphone_xae
browser web_browser web_page browse webpage applet html
car vehicle automobile passenger railway
twitter facebook facebook_xae stock_quote yahoo facebook_twitter myspace blogging newsfeeds blogger
google ebay yahoo yahoo! stock_quote twitter facebook

類推語取得例

以下は、類推語取得例です。

印刷→プリンタ 通信→(通信制御装置 LAN データ通信 無線通信回線 無線通信網 ゲートウェイ装置 通信機器)

動詞V→主語S 動詞Vとすることで、動詞Vに対応する主語Sが取得されています。この例では、通信する物が取得されていますね。

プリンタ→印刷 カメラ→(撮影 撮像 被写体 撮像カメラ ステレオ撮影 テレビカメラ)

逆に、主語S→動詞V 主語Sとすることで、主語Sに対応する動詞Vが取得されています。この例では、カメラに対応する動詞として、撮影、撮像が取得されていますね。

日本語は、主語と動詞のベクトル差が同じぐらいなのかもしれません。

king - man +woman = queenのような属性的な演算がうまくできるものは、見つかりませんでした(思いつきませんでした)。何かおもしろそうなものがあったら、教えてください。

ここからは、実際にやった作業メモです。

C言語版word2vecをダウンロード、コンパイル

https://code.google.com/p/word2vec/

CentOS6.4では、makefileのCFLAGSの-Ofastオプションがうまく使えなかったのでfastオプションを除去して-O2を追加してmakeしました。

4/23追記
以下を参考にすると、速度差が結構あるらしく、GCCのバージョンを見直したほうがいいのかもしれません。

http://www.pc-koubou.jp/blog/word2vec.php

前処理

sedを使って、コーパスに対してアルファベット大文字を小文字に統一、HTML/XMLタグ、特殊文字の除去等の前処理をします。

% sed -i -e "s/[\t&#12345678901234567890,.\"'()();:^/-]/ /g" jpa_abst.csv
% sed -i -e 's/<[^>]*>/ /g' jpa_abst.csv
% tr A-Z a-z < jpa_abst.csv > jpa_abst2.csv

英語コーパスの基本形戻し

英語の場合、ingや複数形、過去形などの活用形をWordNetを使って基本形に戻します。

以下は、WordNetライブラリを使った作業用プログラムです。
https://gist.github.com/naoa/9997134

% yum install glib2 glib2-devel wordnet wordnet-devel
% gcc -I/usr/include/glib-2.0 -lglib-2.0 -lWN wn_morph.c -o wn_morph
% ./wn_morph us_abst2.csv

英語コーパスのフレーズ処理

word2vecにはフレーズを抽出するプログラムが含まれているようなので、これを使ってフレーズ抽出します。

% time ./word2phrase -train us_abst2.csv -output us_abst_phrase.csv -threshold 500 -debug 2
Starting training using file us_abst2.csv

Words processed: 400900K     Vocab size: 18319K
Vocab size (unigrams + bigrams): 11162427
Words in train file: 400962342
Words written: 400900K
real    6m40.781s
user    6m34.243s
sys     0m6.251s

日本語コーパスの分かち書き

日本語の文書は、単語ごとに空白区切りとなっていないため、MeCabを使って分かち書きをします。 辞書データは、専門用語が含まれている方が望ましいです。今回は、特許の機械翻訳辞書や特許文書から機械的に専門用語を抽出して、naist-jdicに専門用語を追加した辞書を使って分かち書きをしています。(あまり洗練されていなく、名詞以外も含まれちゃっています。)

% mecab -d /usr/lib64/mecab/dic/naist-jdic/ -Owakati jpa_abst.csv > jpa_abst2.csv 2>&1 &

トレーニング

オプションは、demo-word.shとdemo-phrases.shを参考に以下のようにしました。

  • 日本
% time ./word2vec -train jpa_abst4.csv -output jpa_abst5.bin -size 200 -window 5 -negative 0 -hs 1 -sample 1e-3 -threads 12 -binary 1
Starting training using file jpa_abst4.csv
Vocab size: 667502
Words in train file: 1049391660
Alpha: 0.000025  Progress: 99.90%  Words/thread/sec: 18.22k
real    123m17.701s
user    961m9.457s
sys     0m13.122s
コーパスサイズ 6.4G
モデルサイズ 523M
  • 米国
% time ./word2vec -train us_abst_pharase5.csv -output us_abst_pharase5.bin -cbow 0 -size 300 -window 10 -negative 0 -hs 1 -sample 1e-3 -threads 12 -binary 1
Starting training using file us_abst_pharase5.csv
Vocab size: 552645
Words in train file: 392879338
Alpha: 0.000007  Progress: 99.98%  Words/thread/sec: 6.76k
real    122m56.767s
user    968m55.401s
sys     0m5.715s
コーパスサイズ 2.2G
モデルサイズ 640M

類似演算とアナロジー演算するWebサーバの作成

Webアプリから使うためにサーバが欲しかったので、Qiitaにあったpythonを使ったWebサーバにword2vecのpythonインターフェースのcosineとanalogyの機能を付け加えました(Qiitaの著者様に感謝です。)。なお、pythonをはじめて触ったこともあって割りと適当です。

https://gist.github.com/naoa/10005117

pythonのパッケージ管理ソフトとword2vecのpythonインターフェースをインストールして、サーバ実行

% yum install python-pip python-setuptools numpy
% pip install word2vec
% python word2vec_server.py jpa_abst4.bin 8000

これで以下のようにGETアクセスをすると、JSONが得られるようになります。

curl "127.0.0.1:8000?c=test&n=10"
curl "127.0.0.1:8000?pos=king%20woman&neg=man&n=10"

おわりに

自然言語処理の知識はほとんどありませんでしたが、上記のようにword2vecを簡単に利用することができました。非常に簡単に学習できるにも関わらず、類義語はなかなかの精度で得られています。類推は、使いどころが難しいなぁと思いました。そのうち、もう少し分野を絞ったら、どうなるか試してみたいと思います。

本来の動きかどうかはわかりませんが、訳語が抽出できるようなパターンもあるようです。

http://naoyat.hatenablog.jp/entry/2013/09/11/002941

特許の文献では日英対訳を取得することも容易なので、日英対訳コーパスでword2vecをうまく利用できないかなぁとぼんやりと思っています。

また、アナロジーとは異なるベクトル演算ができないかとかも気になっています。

全文検索エンジンGroongaを使って、特許の全文検索システムを作るときにも思ったのですが、自然言語処理は非常に面白いなぁと思います。

惜しむらくは、大学時代に自然言語処理が面白いことに気づきたかったなぁ。。

おまけ

あまり出現しないワードだと、へんてこな答えが返ってくることがあります。

サラリーマン 山奥 お年 貧困 出掛ける いただける

特許情報的には、サラリーマンは山奥でお年をめしていて貧困なようです。

記事を追加しました。

word2vecをDockerでプレーンテキストから簡単に使えるようにしました

複合語などの専門用語を自動抽出するTermExtractをDockerで簡単に使えるようにしました。word2vecで解析する前にこれを使って、形態素解析辞書に用語を追加してみてはいかがでしょうか。

専門用語を自動抽出するTermExtractをDockerで簡単に使えるようにしました

全文検索エンジンGroongaからword2vecを簡単に使えるプラグイン - CreateField Blog

Groongaからword2vecを使って類似文書を取得してみる - CreateField Blog

参考

https://plus.google.com/107334123935896432800/posts/JvXrjzmLVW4
http://naoyat.hatenablog.jp/entry/2013/09/05/230947
http://naoyat.hatenablog.jp/entry/2013/09/11/002941
http://antibayesian.hateblo.jp/entry/2014/03/10/001532
http://saiyu.cocolog-nifty.com/zug/2014/02/word2vec-1867.html
http://kensuke-mi.hatenablog.com/entry/2014/01/25/072210

Mroongaを使って全文検索Webサービスを作ったときにはまったこと(第1回)

前回のエントリに書いたように、1年半ほどをかけて、独学で特許の全文検索サービスを開発しました。

PatentField | 無料特許検索

最初は、MySQLを使ったこともない状態だったこともあり、かなり紆余曲折しました。Groonga開発チームの懇切な対応もあって、専用サーバ1台で最大で1千万レコード超、400GiB以上のサイズのテキストデータを高速に検索できるようになりました。

今後、何回かにわけて、Mroonga(Groonga)を使って全文検索Webサービスを作ったときにはまったこと、学んだことを全て書き出したいと思います。

全文検索エンジンMroongaとは?

Mroongaは全文検索エンジンであるGroongaをベースとしたMySQL用のストレージエンジンです。Mroongaは、MySQLが使える人であれば、簡単に高速な全文検索機能が使えます。MariaDB10.0系にもバンドルされる予定のようです。

MyISAMラッパーモードでのデータベース構築

最初、データベースをMyISAMで構築したこともあり、ドキュメント(ストレージモード/ラッパーモード)を比較したところ、Groongaのコマンドの実行とカウントの高速化がないぐらいだったため、ラッパーモードでいけるかなぁと思い、ラッパーモードで全文検索を試してみました。

インデックス構築に失敗(mmap編)

数百GiBぐらいのテーブルを10分割し、数十GiBぐらいのテーブルを作成し、インデックス構築をしてみました。しかしながら、以下のようなメッセージがでて、インデックス構築できませんでした。

mmap(4194304,551,432017408)=Cannot allocate memory

これについては、groonga-devのメーリングリストで相談しつつ、vm.max_map_countのカーネルパラメータを見直すことで、このメッセージの発生を抑制することができました。今は、このドキュメントに対応方法の詳細が記載されています。

4/15 追記

このドキュメントに記載のように、データベースがメモリサイズを超える場合、vm.overcommit_memoryの設定をしておくことが推奨されています。

vm.overcommit_memory = 1

インデックス構築に失敗(long token編)

Mroongaでは、転置インデックスを作成するためのパーサ(Groongaでは、トークナイザと呼ばれる)として、デフォルトでTokenBigramが用いられます。

通常、Bigramと言えば、2文字ごとにトークンが作成されると思われますが、Mroongaでは、このドキュメントに記載の通り、連続したアルファベット、記号については、1つのトークンとして扱われます。

また、Mroonga(Groonga)では、1つのキーの最大サイズが4096Byteという制限があります。

このため、当時は、アルファベット、記号が4096個以上連続する場合、この制限にひっかかり、インデックス構築できませんでした(今は、4096個以上連続したとしても警告扱いでインデックス構築には失敗しません。)。

これについて、groonga-devのメーリングリストで相談すると、すぐにTokenBigramSplitSymbolAlphaDigit使えばいいよ!という回答が得られました。

非常に親切だなぁと思ったのを覚えております。

TokenBigramSplitSymbolAlphaDigitは、記号、アルファベットについても、2文字で切り出すトークナイザです。これにより、4096Byte以上となるトークンが発生せずに、インデックス構築ができるようになりました。

TokenBigramSplitSymbolAlphaDigitとTokenBigramの検索性能差

アルファベットは、日本語よりも種類が少ないため、アルファベットのBigramでの異なり語数は、たったの26*26種類です。このため、数十GiBクラスの英文のテキストデータをTokenBigramSplitSymbolAlphaDigitでトークナイズすると、TokenBigramに比べて、検索性能が顕著に劣化することが判りました。

TokenBigramSplitSymbolAlphaDigitは、トークンを短く切って一致率を上げるかわりに、検索性能の劣化が生じます。これは、トレードオフの関係となるため、仕方がありません。

しかしながら、上述のように、4096個以上連続するアルファベットが含まれる場合は、インデックス構築することができませんでした。

groonga-devのメーリングリストで相談すると、too long tokenを警告メッセージにして、4096Byte以上の長いトークンについては無視するように改修してくれました。

これもものすごい速い対応で感動した覚えがあります。

これにより、数十GiBのデータベースについては、インデックス構築に失敗せず、MyISAMのラッパーモードで高速に全文検索できるようになりました。

その後、「ストレージモードの検討」、「複数条件の絞り込み」、「Spiderストレージエンジンを使ったデータベースシャーディングの検討」、「ドリルダウン検索の検討」、「テーブルの統合」、「テーブルの統合によるカラムサイズ制限」、「テーブルの統合によるインデックス構築失敗再び」、「トークナイザのカスタマイズ」、「ノーマライザのカスタマイズ」、「スニペットがうまく取得できない場合がある」、等、様々な問題にぶちあたります。

それは、またの機会に書き連ねたいと思います。

おわりに

Groonga、Mroongaは日本で活発に開発が行われており、毎月29日にバージョンアップされています。

最近、全文検索エンジンとしては、Elasticsearchなどが流行ってきているようですが、Mroongaには、MySQLのストレージエンジンで簡単に全文検索が使えるというメリットもあり、Groongaも、他の全文検索エンジンに負けないほど高速なはずです(Lucene系の全文検索エンジンと比較したことがないからわかりませんが速いはず。)。

また、先月には、Droongaと呼ばれる分散型の全文検索システムもメジャーリリースされています。

困ったことがあれば、groonga-devのメーリングリストで相談すると、ものすごく親切に対応してくれるはずです。(Twitterでつぶやくだけでも、どこからともなく、親切な方がやってきて助け舟が入る可能性も。。私が知っている範囲なら、私に聞いてもらっても大丈夫です。)

全文検索システムを使いたい場合は、Groonga、Mroongaがおすすめです。

足りない部分があるなら、どんどん提案したり、改修していけたらいいなぁと思います。日本製のGroongaがもっと普及して欲しいと思っています。

全文検索システムを使いたい場合は、Groonga、Mroongaがかなりおすすめです。

4/13 記事を追加しました。
ストレージモードとGroongaの利点について説明しています。
Mroongaのラッパーモードからストレージモードに変えた理由 - CreateField Blog

4/15 記事を追加しました。
ストレージモードにしてはまったことについて説明しています。
数百GiBの全文検索用データベースをMroongaのストレージモードにしてはまったこと - CreateField Blog

6/26 記事を追加しました。
Groongaがあまり得意でない類似文書検索に連想検索エンジンGETAssocを使った話

2014-11-29(土)13:30 - 17:30
年に1度のGroongaに関するイベントがあります。Groongaを使っている人、興味がある人は参加してみてはいかがでしょうか。

全文検索エンジンGroongaを囲む夕べ5 - Groonga | Doorkeeper

独学で特許の全文検索サービスを開発しました

はてなブログ初投稿です。

 大学の授業でC言語をかじった程度のサラリーマンですが、1年半ほどをかけて、独学で特許の全文検索サービスを開発しました。

 PatentField | 無料特許検索

 1年半前は、データベースもサーバサイドの言語もJavaScriptもまったく触ったことがなく、Ajaxって何?ってぐらいの技術レベルでしたが、ようやく先月公開することができました。

 まだまだ未完成ですが、最大で1千万レコード以上、400GiB以上のサイズのテキストデータを高速に全文検索することができます。

 

このサービスでは、ただ公報データを全文検索するだけではなく、整理標準化データと呼ばれる権利の死活情報等を含む数十種類の項目を組み合わせて検索することができます。これにより、一般の利用者が特許を侵害していないかどうかを確認し易く、また、特許期限切れのフリ―な技術情報を簡単に参照できるようにしています。

また、特許の世界では、日本で出願した特許出願が翻訳され、同じ発明の内容が様々な国で外国特許出願されます。この同じ発明の内容の特許出願をパテントファミリーといいます。このサービスでは、日本出願のパテントファミリー情報を独自に蓄積しており、パテントファミリーの有無に応じた絞り込みも可能となっています。

たとえば、米国に特許出願されている日本特許出願を検索し、日本特許出願と、対応する米国特許出願を比較すれば、技術用語の対訳表現を抽出することができます。これにより、特許明細書の技術者や翻訳者が技術用語を簡単に調べることができるようにしています。

このサービスでは、サーバサイドの言語にPHP、データベースにMySQL、全文検索エンジンにMroonga(Groonga)、連想検索エンジンにGETAssocを使用しています。

今後について

このブログでは、主に、独学でWebサービス開発、運営するにあたり、つまったこと、調べたこと、感じたこと、等を記録していきたいと思います。

なお、単純な技術メモについては、MediaWikiをつかった以下のWikiでまとめています。

CreateField

今後は、上記サービスの運営、改善をしつつ、Droonga、Ruby、Rails、Bootstrapあたりを勉強しつつ、新しく全文検索を使ったサービスを作りたいなぁと思っています。