読者です 読者をやめる 読者になる 読者になる

CreateField Blog

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

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

Groonga Mroonga 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ではこのサイズはさばけないと思います。自前でトークナイザを改修したり工夫しています。