CreateField Blog

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

国産の全文検索エンジンGroonga vs 世界的流行のElasticsearch

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とは

GroongaはC言語で書かれた国産の高速な全文検索エンジンです。Groongaは、C言語から呼び出すことが可能な全文検索ライブラリとしての機能を有し、また、単体で全文検索サーバとしての機能も有します。  

Groongaでは、MySQLのストレージエンジンとしてSQLベースで容易に高速な全文検索が可能なMroongaが提供されています。GroongaやMroongaは、それ単体ではスケールしませんがfluentdのプラグインとして動作するDroongaSpiderストレージエンジンを使うことによりデータベースシャーディングが可能です。

Elasticsearchとは

Elasticsearchは、Javaで書かれた全文検索ライブラリApache Luceneを用いた全文検索、解析サーバです。Elasticsearchは、RESTfullで容易にアクセスすることができ、データを容易にスケールさせることができます。

また、Elasticsearchは、全文検索サーバSolrでも使われている歴史あるApache Luceneを使っていることもあり、非常に高度な全文検索機能、集計機能、豊富なプラグインを有し、多種多様な運用ツールとの連携の実績があります。

最近では、Elasticsearchとfluentdとkibanaを使ったログの可視化等が非常に流行っているようです。

検証データ

  • 日本語Wikipedia - Articles, templates, media/file descriptions, and primary meta-pages.

http://dumps.wikimedia.org/jawiki/20131119/ *1

XMLデータサイズ レコード数
7.3GiB 1,766,247

Wiki記法等は除去せずそのまま利用します。

7/19追加検証

http://dumps.wikimedia.org/jawiki/20140714/

検証環境

  • さくらのクラウド ×1台
CPU メモリ ディスク
4Core 16GB SSD 100GB

7/19追加検証

  • さくらの専用サーバ ×1台
CPU メモリ ディスク
Intel(R) Xeon(R) CPU E5620 @ 2.40GHz 1CPU 4Core 32GB HDD 2TB

Elasticsearchの検証手順

環境構築

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_grammax_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件ずつCURLPUTするだけにしました。

検索手順

検索は、たとえば、以下のような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で全文検索します。更新と同様に、単純にCURLGETするだけです。

なお、Elasticsearchでの名前体系や扱い方に慣れておらず、不平をつぶやいていたら、@johtaniさんがフィールドの指定方法やカウントの見方を教えてくれました。ありがとうございました。Elasticsearchについて無知なにも関わらず、色々不平をつぶやいて大変失礼しました。

GroongaやDroongaでの名前体系に慣れていると、Elasticsearchの名前体系に慣れるのがなかなか大変でストレスを感じます。たぶん、Elasticsearchに慣れている方がGroongaやDroongaを触ろうとしてもそう感じるんじゃないかなと思います。

Groongaの検証手順

環境構築

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

Groongaの自作トークナイザーの紹介 - Qiita

2015/3/9追記
最新のGroongaで以下のパッチを適用すると、さらに検索速度が1.5倍〜2倍ほど速くなります。

頻出トークンとレアトークンを一緒に検索したときの性能向上パッチ (groonga-dev,03095) - Groonga - fulltext search engine. (グルンガ) - SourceForge.JP

*1:データがやや古いのは、以前、Mroongaの性能をいろいろ検証したときと同じものを使ったためです。

*2:厳密にGroongaに合わせるためには、token_charsにwhitespaceとpunctuationも指定すべきでした。

*3:インデックスが適正に使われていることが前提です。Mroongaの場合、MySQLによる制約でインデックスが利かないケースが多々あります。

*4:アクセスが増えると参照分散させるために台数を増やす必要があると思います。実用的な速度かどうかは、アプリの要求次第によって変わると思います。デフォルトのトークナイザTokenBigramではこのサイズはさばけないと思います。自前でトークナイザを改修したり工夫しています。

ブログタイトルを変更しました。

旧タイトル:独学Webサービス開発ブログ
新タイトル:CreateField Blog

インターネット上には、たくさんの技術情報やソフトウェアを無償で提供されている方々がたくさんいます。
人から教えを請うていて、独学っていうのは独りよがりな感じがしたので辞めました。

数百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あたりを勉強しつつ、新しく全文検索を使ったサービスを作りたいなぁと思っています。