以下は、LTの資料です。
MacとRabbitの操作に戸惑ってしまい、デモの検証が一部しかできませんでした。デモができなかった分の検証結果を追記しています。
2014/6/27(金)19:00~神戸でGroongaの勉強会をします。Doorkeeperで募集していますので、興味がある方は是非ご参加ください。
以下は、LTの資料です。
MacとRabbitの操作に戸惑ってしまい、デモの検証が一部しかできませんでした。デモができなかった分の検証結果を追記しています。
2014/6/27(金)19:00~神戸でGroongaの勉強会をします。Doorkeeperで募集していますので、興味がある方は是非ご参加ください。
MySQL勉強会 in 大阪(第6回)でLTっていうのをはじめてやってみました。
内容は、「Mroongaを使ったときの MySQLの制限との戦い」です。
資料づくりにほとんど時間をかけられなくてすごい雑です。また、現場でちゃんとLTを見たこともなかったので、趣旨や作法があっているのかもよくわかりません。
うまくしゃべられなかった上に制限時間の5分を数十秒超えちゃってた気がします。
あまり人前での発表は得意ではないですが、無料で普段得られない経験値が得られるので大変お得だと思いました。
LTの後、機材の調整の関係で少し時間があって、いくつか質問があったので覚えている範囲で書いておきます。
Q.ソースをいじってMroongaではいけるけど、その状態でInnoDBはどうなる?
A.ストレージエンジン側の制約をうけると思います。3072バイト制限はInnoDBでは3500バイトって書いてました。インデックス64個制限は、普段InnoDB使っていないのでわからないです。複数インデックス云々は、完全にGroongaのレイヤーで処理させる話なのでInnoDBには関係ないです。
Q.GroongaのコマンドはMySQLとは別にHTTPサーバたててる?
A.Groongaに直接コマンドを発行できるUDF(mroonga_command)があるのでそれを使っています。
Q.最終的にほぼGroongaでやるってことだけどMySQLを介す意味はある?
A.ないです。結局は、Groongaがはやいですよってことだけです。まぁでもやっぱりSQLで操作できるのはすごく楽で、GroongaでJSONをつくってloadさせるのは大変なんでINSERTやUPDATEはSQLでやっています。
2014年4月21日は、第4回Elasticsearch勉強会ですね!
http://elasticsearch.doorkeeper.jp/events/8865
第4回Elasticsearch勉強会は、参加希望者が約200名の大反響なようです。
私は勉強会に参加できないので、C言語で書かれた国産の高速な全文検索エンジンGroongaと、Javaで書かれた世界的に勢いのあるElasticsearchについて性能の比較をしたいと思います。
今回の検証では1台あたりの馬力を比較するためにサーバ1台での全文検索性能について比較しています。
私は、Groonga(Mroonga)の利用暦が約2年であるのに対し、Elasticsearchの利用暦は2日です。このため、Elasticsearchに対するチューニングの不備や公平な比較になっていない点が含まれている可能性があります。
Elasticsearchでの言葉の扱いに慣れていないため、誤った語句を使っている可能性があります。
GroongaはC言語で書かれた国産の高速な全文検索エンジンです。Groongaは、C言語から呼び出すことが可能な全文検索ライブラリとしての機能を有し、また、単体で全文検索サーバとしての機能も有します。
Groongaでは、MySQLのストレージエンジンとしてSQLベースで容易に高速な全文検索が可能なMroongaが提供されています。GroongaやMroongaは、それ単体ではスケールしませんがfluentdのプラグインとして動作するDroongaやSpiderストレージエンジンを使うことによりデータベースシャーディングが可能です。
Elasticsearchは、Javaで書かれた全文検索ライブラリApache Luceneを用いた全文検索、解析サーバです。Elasticsearchは、RESTfullで容易にアクセスすることができ、データを容易にスケールさせることができます。
また、Elasticsearchは、全文検索サーバSolrでも使われている歴史あるApache Luceneを使っていることもあり、非常に高度な全文検索機能、集計機能、豊富なプラグインを有し、多種多様な運用ツールとの連携の実績があります。
最近では、Elasticsearchとfluentdとkibanaを使ったログの可視化等が非常に流行っているようです。
http://dumps.wikimedia.org/jawiki/20131119/ *1
XMLデータサイズ | レコード数 |
---|---|
7.3GiB | 1,766,247 |
Wiki記法等は除去せずそのまま利用します。
7/19追加検証
http://dumps.wikimedia.org/jawiki/20140714/
CPU | メモリ | ディスク |
---|---|---|
4Core | 16GB | SSD 100GB |
7/19追加検証
CPU | メモリ | ディスク |
---|---|---|
Intel(R) Xeon(R) CPU E5620 @ 2.40GHz 1CPU 4Core | 32GB | HDD 2TB |
CentOSにElasticsearchをインストールする方法の手順に沿って、Elasticsearch1.1.1
をインストールします。
ヒープサイズを8GiBに設定し、JVM起動時にメモリを確保するように設定しています。
また、デフォルトのシャード数を1に設定しています。
% vi /etc/init.d/elasticsearch ES_HEAP_SIZE=8g MAX_OPEN_FILES=65535 MAX_LOCKED_MEMORY=unlimited % vi config/elasticsearch.yml http.port: 9200 index.number_of_shards: 1 bootstrap.mlockall: true
ElasticSearchの運用とか (2)を参考にさせていただきました。ありがとうございます。
以下のような簡潔なテーブル構造を作成することを想定します。
id(integer) | title(string) | text(string) |
---|---|---|
150813 | スポーツのプロリーグ一覧 | '''スポーツのプロリーグ一覧'''(スポーツの... |
150815 | Category:スポーツ競技大会 | {{Pathnav|主要カテゴリ|文化|娯楽|スポーツ|... |
上記のテーブル構造に相当するスキーマを作成します。
アナライザーは、Groongaに合わせるためにngram_tokenizerでmin_gram
とmax_gram
を2にしています*2。
なお、Elasticsearchでは文字の正規化やタグ除去、ストップワード除去等フィルタがかなり豊富に用意されていますが、今回は単純な全文検索性能を比較するため、それらを使用しません。
% vi mapping.json { "settings": { "analysis": { "analyzer": { "ngram_analyzer": { "tokenizer": "ngram_tokenizer" } }, "tokenizer": { "ngram_tokenizer": { "type": "nGram", "min_gram": "2", "max_gram": "2", "token_chars": [ "letter", "digit", "punctuation", "whitespace", "symbol" ] } } } }, "mappings": { "text": { "properties": { "id": { "type": "integer" }, "title": { "type": "string", "analyzer": "ngram_analyzer" }, "text": { "type": "string", "analyzer": "ngram_analyzer" } } } } }
スキーマファイルをPOST
します。
curl -XPOST localhost:9200/wikipedia -d @mapping.json
これにより、wikipediaという名前のindex
のスキーマが設定されました。なお、Elasticsearchは、スキーマレスでも動作します。
7/19追加検証
最初の検証では、punctuationとwhitespaceが入っておらず、Groongaと転置索引のトークンの量に違いがありました。追加検証では、Groongaと合わせるためにpunctuationとwhitespaceを有効にしています。
更新は、たとえば、以下のようなPUT
アクセスで行います。
% curl -XPUT 'http://localhost:9200/wikipedia/text/1' -d ' { "id": "1", "title": "title1", "text": "text1" } '
Gistに上げたPHPスクリプトを用いて、WikipediaのXMLデータを1件ずつPUT
で更新します。
Elasticsearchでは、バルクアクセスのAPIや各スクリプト言語のライブラリを用いることもできるようですが、今回は単純に1件ずつCURL
でPUT
するだけにしました。
検索は、たとえば、以下のようなGET
アクセスで行います。title
フィールドとtext
フィールドに対してsearchquery
を全文検索し、一致するid
を0件出力します。ここでは、全文検索性能のみを比較するため、出力数は0にしています。
curl -XGET http://localhost:9200/wikipedia/text/_search -d' { "from" : 0, "size" : 0, "fields": ["id"], "query": { "multi_match" : { "query": "\"searchquery\"", "fields": [ "title", "text" ] } } }'
7/19追加検証
以前の検証結果では、"でくくっていなかったため、OR検索となっていました。追加検証では、"でくくってフレーズ検索としています。Elasticsearchでは、空白が含まれていなくとも、"でくくらないと、アナライザーによって分割されたトークンがOR検索されます。たとえば、「東京都」は、「東京」OR「京都」で検索されます。
Wikipediaのカテゴリのうち5文字以上の日本語のみのカテゴリからランダムに1万件を抽出して全文検索します。
Gistに上げたPHPスクリプトを用いて、1万件のカテゴリを1件ずつGET
で全文検索します。更新と同様に、単純にCURL
でGET
するだけです。
なお、Elasticsearchでの名前体系や扱い方に慣れておらず、不平をつぶやいていたら、@johtaniさんがフィールドの指定方法やカウントの見方を教えてくれました。ありがとうございました。Elasticsearchについて無知なにも関わらず、色々不平をつぶやいて大変失礼しました。
GroongaやDroongaでの名前体系に慣れていると、Elasticsearchの名前体系に慣れるのがなかなか大変でストレスを感じます。たぶん、Elasticsearchに慣れている方がGroongaやDroongaを触ろうとしてもそう感じるんじゃないかなと思います。
Groonga4.0.1
とnginxベースのgroonga-httpd
をインストールします。
% rpm -ivh http://packages.groonga.org/centos/groonga-release-1.1.0-1.noarch.rpm % yum makecache % yum install -y groonga % yum install -y groonga-tokenizer-mecab % yum install -y groonga-normalizer-mysql % yum install groonga-httpd
上記と同様のテーブル構造に相当するテーブル定義を作成します。
トークナイザーはTokenBigram
を設定し、ノーマライザーは設定していません。
こうすると、Groongaでは、一般的なバイグラムのルールに従ってテキストがトークナイズされます。
% vi table.ddl table_create text TABLE_PAT_KEY UInt32 column_create text text COLUMN_SCALAR LongText column_create text title COLUMN_SCALAR LongText table_create text-text TABLE_PAT_KEY ShortText --default_tokenizer TokenBigram column_create text-text index COLUMN_INDEX|WITH_SECTION|WITH_POSITION text title,text
Groongaのテーブルを作成します。
% groonga -n /var/lib/groonga/db/db < table.ddl [[0,1397980056.85291,0.00288963317871094],true] [[0,1397980056.85586,0.00232267379760742],true] [[0,1397980056.8582,0.00235915184020996],true] [[0,1397980056.86058,0.00229644775390625],true] [[0,1397980056.86289,0.00667881965637207],true]
コマンドによってデータベースが新規作成された場合、groonga-httpd
から操作できるようにファイルオーナーをgroonga:groonga
に修正します。groonga-httpd
が起動している場合、すでにgroonga:groonga
でデータベースがつくられていると思います。
% chown groonga:groonga /var/lib/groonga/db/db* % service groonga-httpd restart
これにより、text
という名前のテーブルが作成されました。Groongaでは、MySQLなどと同様に明示的なテーブル定義を必要とします。
更新は、たとえば、以下のようなPOST
アクセスで行います。
% curl -X POST 'http://localhost:10041/d/load?table=text' -d ' [{"_key":1,"text":"text1","title":"title1"}] '
Gistに上げたPHPスクリプトを用いて、WikipediaのXMLデータを1件ずつPOST
で更新します。
Groongaではデータのみを追加した後に全文インデックスを追加することにより最適化された静的インデックス構築ができますが、今回は単純にCURL
で1件ずつPOST
するだけです。
なお、普段はGroongaをMySQLから利用できるMroongaを利用しており、GroongaのHTTPサーバに対してPOST
でうまく更新できないなぁとつぶやいていたら、@ktouさんがnginxベースのgroonga-httpd
を使えばいいことを教えてくれました。ありがとうございました。
検索は、たとえば、以下のようなGET
アクセスで行います。title
カラムとtext
カラムに対してsearchquery
を全文検索し0件出力します。ここでは、Elasticsearch同様、全文検索性能のみを比較するため、出力数は0にしています。また、Groongaでは、ヒット数が0件の場合、自動的に前方一致検索にエスカレーションするという機能があるためこれを抑制しています。
% curl 'http://localhost:10041/d/select?table=text&match_columns=title||text&query=searchquery&limit=0&match_escaltion_threshold=-1'
Wikipediaのカテゴリのうち5文字以上の日本語のみのカテゴリから上記と同じカテゴリ1万件を全文検索します。
Gistに上げたPHPスクリプトを用いて、1万件のカテゴリを1件ずつGET
で全文検索します。更新と同様に、単純にCURL
で1件ずつGET
するだけです。
Elasticsearch | Groonga |
---|---|
8853sec(2h27min) | 21914sec(6h05min) |
いつインデックスが更新されているかまでは追っていないので単純な比較はできないかもしれませんが、シーケンシャルなスクリプトの実行時間で比較すると、Groongaの方が2.5倍ほど遅かったです。
追記
Groongaの方はデータと転置索引の更新の両方が含まれた時間であり、Elasticsearchの方は転置索引の更新の時間は含まれていません。Elasticsearchは、リフレッシュ、フラッシュ、マージなどの転置索引の更新処理がデータの更新とは別に裏で走ります。
※トータルの更新時間には、XMLのパースが含まれています。
7/19追加検証
Elasticsearch | Groonga |
---|---|
6689sec(1h51min) | 20958sec(5h49min) |
Elasticsearch | Groonga |
---|---|
12.7GiB | 18.9GiB |
ディスク使用量は、Elasticsearchの方がコンパクトでGroongaの方が1.5倍ほど大きいですね。 Elasticsearchの方は設定をミスってしまって空白と所定の区切り文字が転置インデックスに入っていないですが、それを差し引いてもGroongaの方が大きいでしょう。
なお、Groongaの場合、インデックスのみを後で静的インデックス構築をすれば、ディスク使用効率が上がり検索性能がさらに向上します。なお、この方法はリアルタイム更新ではないのでここでは比較しません。
6/30追記
Groongaドキュメント読書会1で学んだ事のメモ - Qiitaを参考にすると、Groongaは、インデックスの即時更新のため、インデックス各データの間に隙間があり、データを間に差し込めるようになっているようです。このため、Elasticsearchに比べ、Groongaの方がディスク使用量が大きくなっているものと思われます。その分、インデックス更新にかかる処理コストが低いというメリットがあるようです。
http://groonga.org/ja/blog/2011/07/28/innodb-fts.html
7/19追加検証
Elasticsearch (optimize前) |
Groonga |
---|---|
19.3GiB | 18.9GiB |
punctuationとwhitespaceを有効にしたらElasticsearchの方が大きくなってしまいました。 おそらく、これはマージが終わっていないのでしょう。
以下のようにoptimizeをするとサイズが減少しました。
% curl -XPOST 'http://localhost:9200/wikipedia/_optimize'
Elasticsearch (optimize後) |
Groonga |
---|---|
16.5GiB | 18.9GiB |
種別 | Elasticsearch | Groonga |
---|---|---|
トータル(1万件) | 1050.737sec | 424.797sec |
平均 | 0.105sec | 0.042sec |
最長 | 0.792sec | 0.368sec |
最短 | 0.000992sec | 0.000842sec |
上記を比較すると、全文検索速度はGroongaの方が約2.5倍ほど速いことが判ります。 経験則から言うと、Groongaはこの数倍~10倍ぐらいのデータサイズであっても1台で十分に高速に全文検索できると思います*3。
ただし、Groongaは、単体ではシャーディングすることができません。一方、Elasticsearchは、デフォルトのシャード数が5であることからもわかるように、複数台構成が前提となっており容易にシャーディングすることができます。
また、今回は単一アクセスの検索性能しか試していません。同時実行性能を含めるとどちらが勝っているかは判りません。
7/19追加検証
フレーズ検索でもGroongaの方がElasticsearch(optimize後)よりも約2.8倍ほど速いことが判ります。Elasticsearch(optimize前)であれば約4.3倍ほど速いことが判ります。ちなみにGroongaでは、リアルタイム更新をしても速度劣化はほとんどないため、optimizeという処理はありません。詳しくはこちら。
種別 | Elasticsearch (optimize前) |
Elasticsearch (optimize後) |
Groonga |
---|---|---|---|
トータル (1千件) |
216.161sec | 141.701sec | 50.062sec |
平均 | 0.216sec | 0.141sec | 0.050sec |
最長 | 4.313sec | 1.037sec | 0.339sec |
最短 | 0.000960sec | 0.00317sec | 0.00215sec |
以上、サーバ1台、単一アクセスにおけるGroongaとElasticsearchの性能を比較しました。 まとめると以下のような比率になります。数値が高い方が性能が良いことを示しています。
性能種別 | Elasticsearch | Groonga |
---|---|---|
更新性能 | 2.5 | 1 |
ディスク使用効率 | 1.5 | 1 |
検索性能 | 1 | 2.5 |
7/19追加検証
性能種別 | Elasticsearch (optimize前) |
Groonga |
---|---|---|
更新性能 | 3.13 | 1 |
ディスク使用効率 | 1 | 1.02 |
検索性能 | 1 | 4.31 |
性能種別 | Elasticsearch (optimize後) |
Groonga |
---|---|---|
更新性能 | 3.13 | 1 |
ディスク使用効率 | 1.14 | 1 |
検索性能 | 1 | 2.83 |
上記の比較例では、更新性能、ディスク使用効率ではElasticsearchが勝っており、検索性能ではGroongaが勝っていました。なお、あくまで今回のテストケースによる結果であり、他のテストケースではどうかわかりません。
Elasticsearchは、今、世界的に勢いがあり、容易なスケール機能、豊富な機能、豊富なプラグイン、運用ツールとの連携等、様々なメリットがあります。 アナライザーやフィルターのラインナップは、歴史が長いApache Luceneのライブラリを使っていることもあり、現状、GroongaよりもElasticsearchの方が優れているでしょう。 MoreLikeThisや類似画像検索は、現在のGroongaにはない機能です。
現状、大規模なWebサービス等でサーバを何十台、何百台とたくさん並べて運用する場合は、Elasticsearchの方が利用しやすいかもしれません。
しかし、私は、サーバ1台あたりの全文検索自体の馬力ではGroongaの方が勝っていると考えています。また、弱点だったスケール機能は、2014年2月にDroongaがメジャーリリースされ解消されつつあります。 さらに、Groongaは、MySQLのストレージエンジンであるMroongaを利用することによりSQLベースで非常に容易に高速な全文検索をすることもできます。私はMroongaがあったので、データベースも全文検索の知識もまったくなかった状態からある程度Groongaが扱えるようになりました。
kuromojiトークナイザの検索モードやフィルタ機能程度の利点であれば、多少工夫すればGroongaやMroongaでも補えます。今、Mroongaを使っていて、高度な検索式や豊富な全文検索機能を追加したいと考えている方は、まずは、GroongaやDroongaを検討してもいいと思います。Mroongaをストレージモードで使っているならデータベースそのままでGroongaのコマンドが利用できますし、MySQLの制約からくるインデックスが使用できないことによる速度劣化であれば、Groongaのコマンドに置き換えるだけで10倍以上の検索速度になったりすると思います。詳しくはこちらを参照してください。
上記の比較はあくまで一例にすぎませんが、そこそこ大規模なデータベースに対して少ないサーバ台数で高速な全文検索を実現したいならば、私はGroongaがおすすめだと思っています。
私はGroonga(Mroonga)を使うことにより、データサイズが400GiB超のデータベースを専用サーバ1台でそこそこ実用的な全文検索速度を実現させることができました*4。
データベースの規模や特性に応じて、ElasticsearchとGroongaを使い分けるのも良いかもしれません。
最近、Mroongaを使って全文検索Webサービスを作ったときにはまったことについて、いくつかブログ記事を書きました。GroongaやMroongaに興味があれば、こちらも参考になるかもしれません。
Mroongaを使って全文検索Webサービスを作ったときにはまったこと(第1回)
Mroongaのラッパーモードからストレージモードに変えた理由
数百GiBの全文検索用データベースをMroongaのストレージモードにしてはまったこと
Groongaがあまり得意でない類似文書検索に連想検索エンジンGETAssocを使った話
今後は、Groonga(Mroonga)を使って工夫した点等を書いていきたいと思っています。
7/22追記
GroongaとElasticsearchの転置索引の違いと更新反映速度の差、およびGroongaの静的索引構築とリアルタイム更新時の性能差について検証した記事を追加しました。よければ、こちらもご参照ください。
GroongaとElasticsearchにおける転置索引の違いと更新反映速度について
7/28追記
GroongaをKVSとして利用した場合のベンチマークについて記事を書きました。Groongaは単純なKVSとしてもかなり高速な性能を有しています。
GroongaとTokyoCabinetのHash表のベンチマークについて
9/9追記
ちなみに、Groongaの場合、トークナイザーをTrigramにし、さらに効率化した自作トークナイザ―を使うと検索速度が8倍ほど速くなります(平均 0.0063 sec)。
https://github.com/naoa/groonga-tokenizer-yangram
2015/3/9追記
最新のGroongaで以下のパッチを適用すると、さらに検索速度が1.5倍〜2倍ほど速くなります。
*1:データがやや古いのは、以前、Mroongaの性能をいろいろ検証したときと同じものを使ったためです。
*2:厳密にGroongaに合わせるためには、token_charsにwhitespaceとpunctuationも指定すべきでした。
*3:インデックスが適正に使われていることが前提です。Mroongaの場合、MySQLによる制約でインデックスが利かないケースが多々あります。
*4:アクセスが増えると参照分散させるために台数を増やす必要があると思います。実用的な速度かどうかは、アプリの要求次第によって変わると思います。デフォルトのトークナイザTokenBigramではこのサイズはさばけないと思います。自前でトークナイザを改修したり工夫しています。
旧タイトル:独学Webサービス開発ブログ
新タイトル:CreateField Blog
インターネット上には、たくさんの技術情報やソフトウェアを無償で提供されている方々がたくさんいます。
人から教えを請うていて、独学っていうのは独りよがりな感じがしたので辞めました。
前回は、全文検索Webサービスを作ったときにはまったことの第2回として、 Mroongaのラッパーモードからストレージモードに変えた理由という記事を書きました。
今回は、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を使っている人、興味がある人は参加してみてはいかがでしょうか。
前回は、全文検索Webサービスを作ったときにはまったことの第1回という記事を書きました。
今回は、Mroongaを使って全文検索Webサービスを作ったときにはまったことの第2回として、ラッパーモードからストレージモードに変えた理由について書きたいと思います。
なお、かなり長く、MySQL、Groongaについて前提知識がないと理解できない部分が多々含まれている可能性があります。
全文検索Mroongaストレージエンジンでは、全文検索するためにラッパーモードとストレージモードの2つのモードが用意されています。
(引用) ラッパーモードでは全文検索機能のみGroongaの機能を利用し、データストアはInnoDBなど既存のストレージエンジンを利用します。ラッパーモードを利用することにより、ストレージエンジンとして多くの利用実績のあるInnoDBに全文検索エンジンとして実績のあるMroongaを組み合わせて、高速な全文検索機能付きの信頼性のあるデータベースとして利用できるという特長があります。
ラッパーモードを使うことにより、たとえば、トランザクション対応のInnoDBを使うことができます。全文検索も高速にできてトランザクションも使えるとなるといいことづくめのように見えます。
ドキュメントを比較すると、ラッパーモードは、ストレージモードに比べて、位置情報検索と、レコードIDの取得と、Groongaコマンドの実行と、カラムの刈り込みと、行カウント高速化がないぐらいです。
また、私が使いはじめたときは、割とよくmysqldがクラッシュしてデータベースが破損しやすかったこともあり、ストレージモードではデータファイルがたくさんできて取り扱いにくかったのでMyISAMのラッパーモードでデータベースを作りはじめました。
しかしながら、ストレージモードでは、以下の点が優れていることに気づきラッパーモードからストレージモードに変更しました。
ストレージモードでは、所定の条件の場合、ORDER BY LIMITで全文インデックス以外のインデックスを複数使用して高速に絞込み操作を行うことができます。
なお、この所定の条件は、条件の数だけソースを追加しなければいけないというデメリットがあります。 以前、ユーザが要望したこの条件がわずか一日で追加されて、検索速度が10倍改善されたというエピソードがあります。 Groonga開発チームのこの対応速度は素晴らしいですね。Groonga開発チームのこの対応の良さがMroongaを使う最も大きな利点かもしれません。
ちなみにこの所定の条件に関わらず複数インデックスを使うちょっとした裏技みたいな方法があります。 ただし、これは今のところソースを変更する必要があるというのと、AGAINST句の中でGroongaのクエリ構文を使う必要があります。
ここに試した結果と方法を記載しています。インデックスが使われない場合は、数秒かかっていた検索クエリが0.数秒になっています。
この方法もGroonga開発チームにメーリングリストで教えてもらいました。
インデックスが使われないときのカウントやレコード操作が他のストレージエンジンに比べてストレージモードは段違いに速いです。
具体的に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)
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)
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)
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)
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_index
→grn_obj_set_info
→grn_ii_build
なのに対し、ラッパーモードの場合、ha_mroonga::wrapper_fill_indexes
→grn_column_index_update
→grn_ii_column_update
→grn_ii_update_one
となっていました。ラッパーモードの場合このアップデートがたくさん走るからストレージモードに比べて遅いような気がしました。
(引用) ストレージモードでは、全文検索機能だけではなくデータストアも含めて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をぱっと思いつかなかったので比較していません。
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;
上記のテーブル定義をベクターカラムを使うと以下のように表現することができます。
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)
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をする必要もありそうです。
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で検索しないといけません。これでは、後ろ側のインデックスが使われず遅いですね。
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のクエリ構文が含まれるというイレギュラーな形になってしまいます。
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のselectコマンドでは、上記の所定の条件に縛られずに複数インデックスを使うことができます。 したがって、多数の絞込み条件を自由に追加して、簡単に高速に全文検索することができます。
Groongaでは、上記のように結果セットがJSONとなるので、検索結果とカウントを同時に取得することができます。なお、これは、既存のアプリをそのまま適用できないというデメリットともとらえることができます。
MySQLを使っている方だとわかると思うのですが、MySQLのオフセットは数が大きくなると検索速度が顕著に劣化します。 このため、MySQLではBETWEENで絞り込んだりすると思います。
Groongaのselectコマンドでは、オフセット処理がMroongaストレージモードのSELECT構文よりもかなり速いです。 約1000万件のオフセット速度を比較してみます。
mysql> SELECT COUNT(*) FROM ftext; +----------+ | COUNT(*) | +----------+ | 11549665 | +----------+ 1 row in set (0.00 sec)
mysql> SELECT app_id FROM ftext LIMIT 5 OFFSET 11549660; +---------------+ | app_id | +---------------+ | JP20130513403 | | JP20130513406 | | JP20130513423 | | JP20130513425 | | JP20130513426 | +---------------+ 5 rows in set (8.09 sec)
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で絞り込む必要はありません。
これは、かなり多数のレコードにならないと差がでてきませんが、検索結果が数百万レコードぐらいになると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の学習プログラムに渡すだけです。
アナロジーというのは、ベクトル同士を演算し、A → Bの関係に対し、C→Xに当てはまるXというのを探すことができるようです。
(引用)https://plus.google.com/107334123935896432800/posts/JvXrjzmLVW4
面白いのは、2つのベクトルの差が、2つの単語の関係をよく近似してくれること。 (中略) A B C → X (A → Bの関係に対し、 C → X に当てはまるXを探す)グーグル ヤフー トヨタ → 日産
渋谷 新宿 札幌 → 旭川
警察 泥棒 正義 → くそ
平和 戦争 左 → 右
社員 会社 生徒 → 小学校
空 海 天井 → 床板
生きる 死ぬ 動く → 止まる
兎にも角にも、まずは、実際に試してみた例を示します。特許の全文検索サービスにword2vecを使った類語、類推語検索機能を組み込んで見ました。
以下のページで試すことができます。上部フォーム横にある検索ボタンの左の+ボタンで類義語、類推語を検索することができます。
以下は類語取得例です。
筆記具 ボールペン 筆記 万年筆 消しゴム 水性ボールペン 水性インキ 筆記具用インキ 筆記用具 サインペン 自動車 乗用車 オートバイ 車両 車輌 車輛 二輪車 乗り物 乗物 スマートフォン 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のような属性的な演算がうまくできるものは、見つかりませんでした(思いつきませんでした)。何かおもしろそうなものがあったら、教えてください。
ここからは、実際にやった作業メモです。
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�1234567890,.\"'()();:^/-]/ /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アプリから使うためにサーバが欲しかったので、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