CreateField Blog

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

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

はじめに

Dockerで簡単に使えるようにしてみた第2弾です。前回は、専門用語を自動抽出してくれるTermExtractをプレーンテキストで簡単に使えるようにしたDockerファイルについて紹介しました。

最近はword2vecが非常に話題になっていますが、word2vecは環境構築周りやテキストの前処理等がなかなかめんどくさいです。

そこで、word2vecと、プレーンテキストをRE2による正規表現フィルタ、ICUによるNFKC正規化(全角文字→半角文字変換等)、MeCabによる分かち書き等を実行してくれる自作のC++プログラムstring-splitterが自動で環境構築されるDockerファイルを作りました。

https://github.com/naoa/docker-word2vec

このDockerファイルには、単語間のベクトル距離が近い類義語っぽいものを出力するdistanceコマンドやking - man + woman = queenといった関係の類推語っぽいものを出力するword-analogyコマンドの他に、 ベクトルを自由に足し引き演算(+-)できるようにしたword2vec-calcコマンドも含まれています。

さらに実験的に、Word2vecの並列実行時の学習速度の改善で紹介されていた並列処理の高速化パッチも自動で適用されるようになっています。

これでDockerを実行できる環境があれば、スクリプトを組まなくとも、
(1)プレーンなテキストをdocker run経由でstring-splitterコマンドで分かち書き、
(2)分かち書きテキストをdocker run経由でword2vecコマンドで学習、
(3)学習済みモデルをdocker run経由でword2vec-calcコマンドでベクトル演算、
の3ステップで簡単*1にword2vecを試すことができます。

また、word2vec-calcコマンドではファイルからの入力やCSV(カンマ区切り)やTSV(タブ区切り)の出力形式も用意しているので、 単語を一気に読み込ませて、全文検索データベース用の同義語データを得るといったことも簡単にできるようになっています。

Dockerのインストール

Dockerが入っていない場合、まずはDockerをインストールしてください。Dockerは今結構旬なプロダクトなので導入に困ることはないんじゃないかなと思います。 CentOSなら以下のように簡単に導入できます。

% rpm --import http://ftp.riken.jp/Linux/fedora/epel/RPM-GPG-KEY-EPEL
% yum localinstall -y http://ftp-srv2.kddilabs.jp/Linux/distributions/fedora/epel/6/x86_64/epel-release-6-8.noarch.rpm
% yum install -y docker-io
% service docker start
% chkconfig docker on

イメージ構築

まずは、Dockerイメージを作成します。コンテナとファイル共有するために/var/lib/word2vecディレクトリを作成しています

% git clone git@github.com:naoa/docker-word2vec.git
% cd docker-word2vec
% mkdir /var/lib/word2vec
% docker build -t naoa/word2vec .

Dockerイメージの構築が完了すると、以下のようにしてコンテナにターミナル接続することができます。

% docker run -v /var/lib/word2vec:/var/lib/word2vec \
  -i -t naoa/word2vec /bin/bash
% exit

Docker越しのコマンドは非常に長くなってしまいますが、たとえば、以下のようにエイリアスを張れば短くすることができます。

% alias word2vec="docker run -v /var/lib/word2vec:/var/lib/word2vec \
  -a stdin -a stdout -a stderr -i naoa/word2vec word2vec"
% alias word2vec-calc="docker run -v /var/lib/word2vec:/var/lib/word2vec \
  -a stdin -a stdout -a stderr -i naoa/word2vec word2vec-calc"
% alias string-splitter="docker run -v /var/lib/word2vec:/var/lib/word2vec \
  -a stdin -a stdout -a stderr -i naoa/word2vec string-splitter"

以下の作業は、コンテナ上ではなくホスト上でエイリアスが張られている前提で進めます。なお、コンテナ上で共有ディレクトリ以外に保存したファイルはコミットしなければコンテナの終了とともに消えるため注意してください。

MeCab辞書のコンテナへの共有

word2vecでより意味のある関係を得たいのであれば、文書のタイトル等抽出しやすい箇所からキーワードを拾い上げたり、前回紹介したTermExtractなどを使って、MeCab辞書をカスタマイズしたほうがいいと思います。
MeCab辞書のカスタマイズが終了したら、MeCab辞書をホストとコンテナの共有フォルダ/var/lib/word2vec/にコピーします。

% cp -rf /usr/lib64/mecab/dic/naist-jdic/ /var/lib/word2vec/naist-jdic

プレーンテキストを分かち書き

string-splitterコマンドを使ってプレーンテキストをword2vecで学習させる用のテキストファイルに変換します。
デフォルトの設定では、<>タグ、改行コード、一部の記号(\,.;:&^/-#'"()[]{}])が除去され、NFKC正規化+アルファベットの大文字小文字変換されてから分かち書きされます。
ここではコンテナからでも読み取れるように分かち書き済みテキストは共有フォルダ/var/lib/word2vec/wakati.txtに出力されるようにしています。

% cat example/open_source.txt | \
  string-splitter --mecab_dic /var/lib/word2vec/naist-jdic \
  > /var/lib/word2vec/wakati.txt

また、カスタマイズしたMeCab辞書を使用するために--mecab_dicオプションで/var/lib/word2vec/naist-jdicを指定しています。

string-splitterコマンドは、このほかMeCabを使って日本語の活用形を基本形に戻したり英語版WordNetを使って英語の複数形や過去形を基本形に戻したりするオプションも含まれています。

詳細はGitHubを参照してください。

分かち書き済みテキストをトレーニング

分かち書き済みテキスト/var/lib/word2vec/wakati.txtを読み込んで、/var/lib/word2vec/learn.binに学習済みモデルファイルを出力します。

% word2vec -train /var/lib/word2vec/wakati.txt -output /var/lib/word2vec/learn.bin \
  -size 200 -window 5 -negative 0 -hs 1 -sample 1e-3 -threads 12 -binary 1

学習モデルでベクトルの演算

トレーニングが完了するとコンテナ上のword2vec-calcコマンドで/var/lib/word2vec/learn.binを読み込ませることによりベクトル演算することができます。 なお、word2vec-calcコマンドではdistanceコマンド等と違い、--file_pathでファイルを指定するようになっているため、注意してください*2

word2vec-calcコマンドでは、単語半角スペース演算子(+-)半角スペース単語・・・で自由にベクトルの足し引きができるようになっています。たぶん100個ぐらいつなげられると思います。単語1個の場合はdistanceコマンド相当、単語2 - 単語1 + 単語3にするとword-analogyコマンド相当になると思います。

試しに前回構築した特許文書を学習させたモデルを使って、ベクトルの足し算をしてみます。なお、ここでは、echoコマンドを使ってコンテナの標準入力に渡していますが対話的に実行することも可能です。

% echo "データベース + 車両" | word2vec-calc --file_path /var/lib/word2vec/learn.bin --output 1
>
Word: データベース  Position in vocabulary: 1228

Word: 車両  Position in vocabulary: 305
0.743926        車両データ
0.739224        運行情報データベース
0.722270        走行履歴記憶部
0.712590        走行履歴データ
0.712389        自車両情報取得部
0.711377        走行経路情報
0.709190        車両運行情報
0.708623        中古車データベース
0.708502        状況認識部
0.707862        自車両情報

「データベース」に「車両」を足すことによって、車両関係のデータベースに関連するぽいような言葉が得られました。

echo "筆記具 + 消す" | word2vec-calc --file_path /var/lib/word2vec/jpa_abs.bin --output 1
>
Word: 筆記具  Position in vocabulary: 6017

Word: 消す  Position in vocabulary: 22672
0.674807        消しゴム
0.657548        筆記
0.655728        筆記用具
0.644496        万年筆
0.620492        描線
0.616586        マーカーペン
0.606570        サインペン
0.602623        化粧直し
0.597417        消せる

「筆記具」に「消す」を足すと最上位には「消しゴム」が現れました。それ以外はあまりそれっぽくないのでたまたまかもしれませんが、以下のように「消しゴム」は「筆記具」からの距離が4位なので少なくとも「筆記具」に「消す」を足すことで「消しゴム」に近づいているものと思われます。

echo "筆記具" | word2vec-calc --file_path /var/lib/word2vec/jpa_abst5.bin --output 1
>
Word: 筆記具  Position in vocabulary: 6017
0.784545        ボールペン
0.765932        筆記
0.763255        万年筆
0.754146        消しゴム
0.732143        水性ボールペン
0.720880        水性インキ

今度はパソコンの部品の「ディスプレイ」と「ハードディスク」と「ネットワーク」と「キーボード」を足してみます。

echo "ディスプレイ + ハードディスク + ネットワーク + キーボード" | word2vecalc --file_path /var/lib/word2vec/jpa_abst5.bin --output 1
>
Word: ディスプレイ  Position in vocabulary: 1262

Word: ハードディスク  Position in vocabulary: 4647

Word: ネットワーク  Position in vocabulary: 547

Word: キーボード  Position in vocabulary: 3742
0.736513        デスクトップ
0.711561        コンピュータ装置
0.705553        パソコン
0.702222        インタフェース
0.699718        コンピュータ用
0.689817        マルチウインドウ
0.687212        インターフェース
0.685114        携帯型コンピュータ
0.682025        コンピュータ機器
0.678986        コンピュータ本体
0.676609        画面共有システム
0.676334        プログラマブル表示器
0.674826        デスクトップ型
0.672900        コンピュータ
0.671351        汎用コンピュータ

上位にパソコンぽいものが得られましたね。

「動画」から「動き」を引いて見ます。

% echo "動画 - 動き" | word2vec-calc --file_path /var/lib/word2vec/jpa_abst5in --output 1
>
Word: 動画  Position in vocabulary: 2047

Word: 動き  Position in vocabulary: 1561
0.491828        静止画ファイル
0.480281        動画ファイル
0.456786        動画データ
0.456166        映像ファイル
0.451632        再生時間取得手段

「消しゴム」と同じように最上位以外は動画要素満々ですが、以下のように「静止画データ」は「動画」から7位でしたので、一応「動き」が減算されて、「静止画ファイル」が上位表示されたように見えます。

% echo "動画" | word2vec-calc --file_path /var/lib/word2vec/jpa_abst5.bin --output 1
>
Word: 動画  Position in vocabulary: 2047
0.742014        動画データ
0.740587        動画像データ
0.732553        動画ファイル
0.707877        静止画と動画
0.705789        高精細静止画
0.699955        映像
0.697672        映像データ
0.687976        静止画データ

このようにword2vec-calcコマンドでは、単語の足し算、引き算を自由に組み合わせることができます。 このほかword2vec-calcコマンドでは、オプションで出力を正規表現でフィルタさせたりCSVやTSVで出力できるようにしたりしています。
また、ファイルから一括で解析させることもできるので、全文検索データベース用の同義語(シノニム)ファイルを作ったりしやすくしています。

詳細はGitHubを参照してください。

おわりに

Dockerファイルを作っておけば、C言語やC++でライブラリに依存しまくってプログラムを作ったとしても、 簡単に環境構築できるため、非常に便利だなぁと思いました。

上記で実験した特許のデータはコーパスサイズ6.4Gとあまり規模を大きくしていないので、さらにコーパスを大きくしたらどうなるかも実験したいと思っています。 特許のデータは全体で数百GiBになるので分野を区切ったり色々実験したいと思います。

次は連想検索エンジンGETAssocのDockerファイルを作る予定です。 GETAssocは、word2vecのスキップグラムのように単語間のつながりまでは考慮していませんが、 文書に含まれる単語の共起表現から連想されるワードを高速、且つ、結構な精度で抽出することができます。 また、類似文書検索や文書から簡単に専門的な文書を検索することができます(参考)。

連想検索エンジンGETAssocはインデックス(NWAM)を作るのがめんどうだったり、データストアがなかったりするのですが、 このあたりは国産の超高速な全文検索エンジンGroongaと連携させて楽にできるようにしたいと考えています。

*1:Dockerコンテナでの作業イメージが沸かない方には簡単じゃないかもしれません。また、バージョンによってはホストの標準入力をコンテナの標準入力に渡すのがうまくいかないかも。

*2:なぜ、このようにしたかと言うと、単語の演算式を標準入力から渡せるようにしたかったからです。

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

はじめに

品詞のつながりや出現頻度、学習情報から複合語らしきキーワードを自動で抽出するPerlモジュールTermExtractが公開されています。

これを利用すれば、形態素解析済みのテキストを食わせるだけでそこそこそれらしい専門用語をたくさん得ることができます。

このTermExtractは、ソースからインストールする必要があったり、EUC環境であったり形態素解析後のデータを入力に必要としたりなかなかめんどくさいです。

そこで、MeCabとTermExtractが自動で環境構築されるDockerファイルを作りました。

https://github.com/naoa/docker-termextract

このDockerファイルでは、正規表現フィルタや形態素解析、コスト推定などを自動でやってくれるPerlスクリプトも自動で導入されるようになっています。

これでDockerを実行できる環境があれば、プレーンなテキストをパイプでdocker runコマンドに渡すだけで、簡単に専門用語を抽出することができます。

また、形態素解析辞書の出力形式も用意しているので、出力結果を簡単に形態素解析辞書に組み込むこともできます。

Dockerのインストール

Dockerが入っていない場合、まずはDockerをインストールしてください。Dockerは今結構旬なプロダクトなので導入に困ることはないんじゃないかなと思います。 CentOSならepelリポジトリを有効にしてyum install -y docker-ioだけで導入できます。

イメージ構築

まずは、Dockerイメージを作成します。コンテナとファイル共有するために/var/lib/termextractディレクトリを作成しています。

% git clone git@github.com:naoa/docker-termextract.git
% cd docker-termextract
% mkdir /var/lib/termextract
% docker build -t naoa/termextract .

Docker上のTermExtractを実行して専門用語を抽出

echoコマンドやcatコマンドを使って、パイプ越しにプレーンテキストをdocker runコマンドに渡します。

% echo "印刷用紙を複合機で印刷する。" | \ 
  docker run -v /var/lib/termextract:/var/lib/termextract \
  -a stdin -a stdout -a stderr -i naoa/termextract termextract_mecab.pl
複合機                                298.54
印刷用紙                              14.01

Dockerコンテナ内に環境構築済みのTermExtractによって、「複合機」と「印刷用紙」という複合語を得ることができました。

数値は、重要度を示しています。学習機能を自動でオンにしているため、複数回実行すると重要度はどんどんあがっていきます。学習機能の詳細は本家サイトをご確認ください。学習用のDBは、/var/lib/termextract/stat.db/var/lib/termextract/comb.dbに作成されます。分野を変える場合やリセットする場合は、ファイルを移動するなり削除するなりしてください。

7/10追記

コマンドが長くてめんどくさいですが、エイリアスをはれば短縮できます。常用的に使う場合は、~/.bashrcなどの起動スクリプトに書けばよいです。

% alias termextract="docker run -v /var/lib/termextract:/var/lib/termextract \
  -a stdin -a stdout -a stderr -i naoa/termextract termextract_mecab.pl"

正規表現フィルタ

TermExtractでは、品詞のつながりや出現頻度、学習情報から複合語らしきものを結合しようとします。しかしながら機械的な処理なので誤って結合されることも結構あります。

形態素解析前にプレーンテキストから除去する文字列の正規表現パターンが記載されたファイル/var/lib/termextract/pre_filter.txtと、複合語抽出後に除外させる正規表現パターンが記載されたファイル/var/lib/termextract/post_filter.txtを用意しています。 TermExtractを実行しながらこれらのファイルを編集して適宜フィルタルールを調整することにより極力ノイズを減らすことができます。

出力形式

出力形式には、以下のように専門用語と重要度を出力してくれるモード、直接、IPAdic辞書の形式で出力してくれるモードを用意しています。

--output 説明
1 または 引数なし 専門用語+重要度
2 専門用語のみ
3 カンマ区切り
4 IPAdic辞書形式(自動コスト推定)
5 IPAdic辞書形式(文字列長)

せっかく自動コスト推定までしてくれる環境を作ったので、ついでにTermExtractを使わずに行ごとの単語リストをコスト推定してくれるモードも用意しています。

オプション 説明
--no_term_extract 専門用語抽出せずにコスト推定のみ実行

IPAdic辞書形式(自動コスト推定)

--output 4を指定するとMeCabで自動コスト推定したIPAdic形式の文字列が出力されます。

% echo "印刷用紙を複合機で印刷する。" | \
  docker run -v /var/lib/termextract:/var/lib/termextract \
  -a stdin -a stdout -a stderr -i naoa/termextract termextract_mecab.pl --output 4
複合機,1285,1285,7336,名詞,一般,*,*,*,*,複合機,*,*,ByTermExtractEst
印刷用紙,1285,1285,7336,名詞,一般,*,*,*,*,印刷用紙,*,*,ByTermExtractEst

これを利用すれば、プレーンテキストを入力するだけで、自動コスト推定までしたIPAdic辞書を簡単に追加することができます。

なお、自動コスト推定のアルゴリズムはよくわかっていませんが、自動推定によって追加した用語は推定コストが割り振られているため文脈によっては必ず区切られるとは限りません。

既存の形態素解析結果を極力壊したくない場合は、こちらを利用する方がいいかもしれません。

GroongaやElasticsearch、Solrなどの全文検索では、単語の前後の出現位置もマッチさせる完全転置索引方式をとっているため、コストを考慮せずに単純に用語を追加しただけの辞書を用いると検索漏れが多くなる惧れがあります。 全文検索では極力精度がおちないように慎重にチューニングするかNgramと併用するなど工夫が必要です。

MeCabには、既存の学習済みモデルと少量の学習データ(1行分の正解の形態素解析結果ぐらい)を使った再学習機能も備わっています。

再学習機能は、少量とはいえ自前で正解の形態素解析結果をつくる必要があるため、多数の用語ごとに学習データを準備するのはかなり大変です。しかし、ちゃんとドメイン適応したい場合は多少工数をかけてでも手動で学習モデルをつくったほうがいいかもしれません。

IPAdic辞書形式(文字列長)

--output 5を指定すると文字列長に応じてコストが設定されたIPAdic形式の文字列が出力されます。

% echo "印刷用紙を複合機で印刷する。" | \
  docker run -v /var/lib/termextract:/var/lib/termextract \
  -a stdin -a stdout -a stderr -i naoa/termextract termextract_mecab.pl --output 5 
複合機,0,0,-14500,名詞,一般,*,*,*,*,複合機,*,*,ByTermExtractLen
印刷用紙,0,0,-16000,名詞,一般,*,*,*,*,印刷用紙,*,*,ByTermExtractLen

これを利用すれば、プレーンテキストを入力するだけで、用語の文字列長に応じてコストが極端に低くなるように設定されたIPAdic辞書を簡単に追加することができます。

この場合、ほぼ既存の形態素解析辞書のコストよりも低くなるため、文脈によらず追加した用語が優先的に切りだされるようになります。

なお、この方法はすでに存在する用語を押しのけて切り出されるようになるので、短い用語や一般的な用語を追加するのはお勧めできません。

テキストマイニング系では多少の誤りは統計的に淘汰されることにより影響が低いと思われますので、多少ノイズがあっても専門用語を多数追加したほうがよい結果が得られるかもしれません。

IPAdic辞書形式(複合語抽出なし)

--no_term_extractを指定すると形態素解析、専門用語抽出をせずにMeCabで自動コスト推定したIPAdic形式の文字列が出力されます。

% echo "印刷用紙" | docker run -v /var/lib/termextract:/var/lib/termextract \
  -a stdin -a stdout -a stderr -i naoa/termextract termextract_mecab.pl --no_term_extract
印刷用紙,1285,1285,7336,名詞,一般,*,*,*,*,印刷用紙,*,*,ByMeCabEst

その他のオプション

その他、出力件数や重要度の閾値等を設定することができます。TermExtractのサンプルスクリプトにある設定項目はほぼすべてコマンドオプションにマッピングしています。 詳しくは、GitHubを参考にしてください。

MeCab辞書への追加

形態素解析速度を損なわないように、システム辞書へ追加することをおすすめします。

システム辞書への追加は、辞書ソースフォルダに辞書形式のテキストファイルを設置して再ビルドします。 なお、追加辞書ファイルの文字コードに注意です。このDockerの環境では、utf-8でテキストが書き出されるのでeuc-jpに戻す必要があります。

自動コスト推定した辞書の追加例

% docker run -v /var/lib/termextract:/var/lib/termextract -i -t naoa/termextract /bin/bash
% echo "印刷用紙を複合機で印刷する。" | termextract_mecab.pl --output 4 > /mecab-ipadic-2.7.0-20070801/user.csv
% cd /mecab-ipadic-2.7.0-20070801
% nkf -e --overwrite user.csv
% make clean
% ./configure --with-charset=utf8; make; make install
# echo "印刷用紙を複合機で印刷する。" | mecab
印刷用紙        名詞,一般,*,*,*,*,印刷用紙,*,*,ByTermExtractEst
を      助詞,格助詞,一般,*,*,*,を,ヲ,ヲ
複合    名詞,サ変接続,*,*,*,*,複合,フクゴウ,フクゴー
機      名詞,接尾,一般,*,*,*,機,キ,キ
で      助詞,格助詞,一般,*,*,*,で,デ,デ
印刷    名詞,サ変接続,*,*,*,*,印刷,インサツ,インサツ
する    動詞,自立,*,*,サ変・スル,基本形,する,スル,スル
。      記号,句点,*,*,*,*,。,。,。
EOS

「印刷用紙」と「複合機」を追加していますが、「複合機」では分割されませんでしたね。MeCabの自動コスト推定の結果によって設定されたコストでは、この文脈では分割されませんでした。この文脈で正しく分割して欲しい場合は、正解の形態素解析結果を作って再学習させる必要があります。

文字列長に応じた低コスト辞書の追加例

% docker run -v /var/lib/termextract:/var/lib/termextract -i -t naoa/termextract /bin/bash
% echo "印刷用紙を複合機で印刷する。" | termextract_mecab.pl --output 5 > /mecab-ipadic-2.7.0-20070801/user.csv
% cd /mecab-ipadic-2.7.0-20070801
% nkf -e --overwrite user.csv
% make clean
% ./configure --with-charset=utf8; make; make install
% echo "印刷用紙を複合機で印刷する。" | mecab
印刷用紙        名詞,一般,*,*,*,*,印刷用紙,*,*,ByTermExtractLen
を      助詞,格助詞,一般,*,*,*,を,ヲ,ヲ
複合機  名詞,一般,*,*,*,*,複合機,*,*,ByTermExtractLen
で      接続詞,*,*,*,*,*,で,デ,デ
印刷    名詞,サ変接続,*,*,*,*,印刷,インサツ,インサツ
する    動詞,自立,*,*,サ変・スル,基本形,する,スル,スル
。      記号,句点,*,*,*,*,。,。,。
EOS

こうすると、「印刷用紙」、「複合機」ともに優先して分割されるようになります。 ただし、この方法は、短い用語や一般的な用語が追加されないように注意した方がいいと思います。

Wikipediaでの用語抽出例

Wikipediaからオープンソースのカテゴリを持つページを抽出したテキストファイル6.4MBからTermExtractにより専門用語を抽出してみます。

なお、Wikipediaからのカテゴリを持つページの抽出は、以前、全文検索エンジンGroongaのRubyバインディングRroongaを使ったベンチマークプログラムで作成したデータベースから抽出しました。

% time cat example/open_source.txt | docker run -v /var/lib/termextract:/var/lib/termextract -a stdin -a stdout -a stderr -i naoa/termextract termextract_mecab.pl
real    0m57.540s
user    0m0.015s
sys     0m0.020s

解析結果の上位100位は以下の通りです。全データは、こちらから参照することができます。

% head -100 open_source_words.txt
オープンソース               3271565.53
ソースコード                  1148379.84
Linux                      1145244.41
Windows                              719143.07
フリーソフトウェア          528618.23
ファイルシステム             330097.36
プラグイン                      231639.42
Java                          224278.60
公式サイト                      178292.62
ソースファイル                119766.41
Linuxカーネル                    108254.88
ソフトウェア開発              99021.32
FreeBSD                       96416.05
Python                          85005.73
開発環境                          82837.95
BSDライセンス                    80794.42
設定ファイル                    79932.39
Webサイト                          76738.43
コンピュータシステム        71030.63
フレームワーク                 70935.05
Debian                       68897.67
コンピュータゲーム           68737.58
拡張機能                          66684.10
開発チーム                       64753.75
公式リリース                    62297.61
Unix系OS                             58288.22
Apache                           57019.52
Mozilla                          55182.90
処理系                             53580.87
Webシステム                       52866.46
Firefox                          50441.02
ファイル名                       49379.71
Webサーバ                          48128.29
管理画面                          46835.77
デスクトップ環境              46306.85
文字列                             44828.24
ゲーム機                          44209.68
Google                          44127.48
ファイル形式                    44066.47
開発プロジェクト              43722.48
仮想化                             42989.77
ゲームエンジン                 42329.06
ウェブブラウザ                 41486.02
Perl                          41174.69
Webアプリケーション           40369.26
フリーソフト                    40277.61
Debianプロジェクト              40133.80
Play                          39835.14
Apacheソフトウェア財団        39716.86
webページ                          36313.45
国際化                             35435.52
GNOME                           32105.18
コードベース                    30556.86
Webページ                          30230.03
GNUプロジェクト                 30071.87
文字コード                       29706.67
バックエンド                    27899.38
プロジェクト管理              27781.24
画面開発                          27641.52
著作権                             26789.01
ブラウザ                          26203.04
Unix                            25665.32
機能拡張                          25403.47
UNIX                          24603.10
デスクトップ                    24601.07
リンクコンピュータ           24400.02
サブプロジェクト              24241.51
Ruby                           24157.42
BSD系OS                              23188.30
アドベンチャーゲーム        23091.86
コマンドライン                 22857.34
作曲家                             22567.23
ファイルフォーマット        22391.98
変更点                             22186.33
Linuxシステム                     21900.20
ベースシステム                 21424.94
Unix系                        20711.56
ゲームプログラミング        20629.42
HTML                        20533.24
暗号化                             20299.04
編集機能                          19913.35
エンコード                       19837.66
オープンソース実装           19693.71
FLOSS                        19645.75
Windows用ゲーム                   19281.47
初期化                             19231.87
計算ソフト                       19102.70
クライアント                    19063.37
Emacs                          19013.35
研究プロジェクト              18959.65
開発終了                          18501.36
標準ソフト                       18411.54
オープンソース化              18354.86
MySQL                          18147.87
日本語化                          18143.13
Cライブラリ                      18091.82
ウェブサイト                    17744.98
機能開発                          17733.23
ブログ                             17712.72
データソース                    17667.48

経験則的に正規表現でフィルタをかけたおかげもありますが、重要度の上位はそこそこ良い感じにとれているのではないでしょうか。 なお、重要度が低い部分は結構ノイズがおおかったりするので、適宜閾値等を調整したり取捨選択したりする必要があると思います。

同様に、Wikipediaからファミリーコンピュータ用ソフトのカテゴリを持つテキストファイル11MBからTermExtractにより専門用語を抽出してみます。

%  time cat example/famicon.txt | docker run -
v /var/lib/termextract:/var/lib/termextract -a stdin -a stdout -a stderr -i naoa
/termextract termextract_mecab.pl > famicon_words.txt

real    1m1.901s
user    0m0.022s
sys     0m0.042s

解析結果の上位100位は以下の通りです。全データは、こちらから参照することができます。

# head -100 famicon_words.txt
ゲームオーバー                225391.74
コンピュータゲーム          207293.26
アクションゲーム             191859.12
ゲームソフト                   173559.54
アーケードゲーム              82471.63
攻撃力                             78720.54
ゲームシステム                 76020.80
シューティングゲーム        58060.15
バーチャルコンソール        47166.23
ゲーム画面                       47020.95
敵キャラクター                 41591.97
難易度                             38140.79
野球ゲーム                       33310.86
ゲームクリア                    31195.55
ステージクリア                 30310.27
ファミコンソフト              28704.64
アドベンチャーゲーム        27880.07
ゲーム作品                       25405.97
ゲーム機                          24178.18
ボスキャラクター              22827.36
ゲーム内容                       22718.59
ゲームモード                    22519.06
ボス戦                             19458.30
Aボタン                            19204.68
ボーナスステージ              18773.07
家庭用ゲーム機                 18195.87
プレイ                             16706.88
タイトル画面                    16570.34
残機                           14833.45
シリーズ作品                    14581.82
敵機                           14208.63
敵キャラ                          13983.64
戦闘機                             12714.64
Bボタン                            12165.97
ニンテンドーDS                  11585.81
ゲーム開始                       11576.77
ゲーム雑誌                       11135.99
パズルゲーム                    11131.51
ゲームシリーズ                 10931.45
ゲームボーイ                    10911.38
専用ソフト                       10823.23
キャラクターゲーム           10125.81
テレビゲーム                     9729.73
ゲームオリジナル               9713.91
ゲームブック                     8578.78
ボスキャラ                        8438.66
アクションゲーム画面         8421.34
敵弾                         8256.72
登場キャラクター               8193.54
攻撃アイテム                     8107.03
防御力                              7834.61
ゲーム終了                        7511.86
ファミリートレーナー         7510.19
ボス敵                              7395.66
攻略本                              7162.74
登場人物                           7125.86
戦闘力                              7112.11
仕事カード                        6867.87
ボスキャラクターボス         6849.71
攻撃ボタン                        6795.73
ゲームデザイン                  6549.28
ジャンプ力                        6441.60
ゲームデータ                     6272.59
パックマン                        6256.40
マリオシリーズ                  6251.48
守備力                              5980.39
ゲームソフトナムコ            5954.83
ファミコン用ソフト            5871.01
ゲームソフトサン電子         5733.77
移植作品                           5698.47
ゲームスタート                  5677.25
人同時プレイ                     5628.80
ナムコゲーム                     5594.78
体当たり                           5571.98
獣人                           5546.24
アクションステージ            5507.70
ハゲ丸                              5492.77
障害物                              5465.10
シリーズ作                        5406.64
ステージ構成                     5246.22
変更点                              5239.89
ステージ開始                     5236.79
スタートボタン                  5235.63
ゲームバランス                  5186.70
キャラクターデザイン         5149.77
ゲーム化                           5125.85
人対戦                              5061.89
赤影                            4954.96
対戦相手                           4934.96
PCゲーム                            4851.10
PCエンジン                         4830.22
ゲーム発売                        4825.91
ゲームソフトセガ               4792.47
マップ画面                        4769.02
リメイク                           4753.66
ボーナス点                        4691.56
攻撃方法                           4580.24
ステージボス                     4379.08
PC用ソフト                         4378.88
宝箱                             4226.46

「ゲームソフトセガ」、「ゲームソフトナムコ」、「ボスキャラクターボス」などあまり正しくない結合のされ方も含まれていますが、ある程度はそれらしい用語も抽出できていますね。 他のオプションを組み合わせたり学習情報によっては、うまく解析できるようになるかもしれません。

おわりに

このような一時的な解析環境の構築は一度やった後は、結構忘れがちなのでDockerで環境を作っておくと便利だなぁと思いました。

文書構造的に容易に抽出可能なタイトルやキーワードなどから固有名詞を抽出する方法が知られています。 たとえば、Wikipediaのタイトルやはてなキーワードなどがよく知られていますね。

よりキーワードを集めたい方は、TermExtractを使ったキーワード抽出を試してみるといいのではないのでしょうか。

最近は、word2vecなどに興味がある人が多いと思いますが、学習の前には用語を抽出して辞書に追加した方が面白い結果が得られると思いますよ。

word2vecや連想検索エンジンGETAssocのDockerファイルも近日中に作ろうと思っています。

7/14 記事を追加しました。

word2vecとプレーンテキストをフィルタや正規化をしつつ分かち書きしてくれるstring-splitterコマンドとベクトルを自由に足し引き演算(+-)できるようにしたword2vec-calcコマンドをDockerファイルdocker-word2vecで簡単に使えるようにしました。また、アナロジー以外のベクトル演算をした実験結果を紹介しています。

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

全文検索エンジンGroongaユーザ勉強会@神戸を主催しました

2014/06/27(金)に全文検索エンジンGroongaユーザ勉強会@神戸を主催しました。

開催のきっかけ

草の根Groongaイベントのお誘いを受けて、関西圏でもGroongaのイベントがあるといいなと思い、神戸でも開催してみることにしました。

会議室の確保

人の集まり具合がどうなるかわからなかったので、三宮近辺でできるだけ費用が抑えられるというポイントで会場を探しました。WordBench神戸などが利用されているような貸会議室ビジネスしているところだと、平日夜間でも1万円を超えるところが多かったです。人が数十人来ることがほぼ確定しているプロダクトの勉強会なら千円徴収すればなんとかなりますが、小規模が想定される勉強会での利用は難しかったです。

そこで、公民館など市営の施設の会議室をこちらから探してみました。公民館等の場合、だいたい10数名規模の部屋で数千円といったところでした。

その他、インターネットで探して見ると、KIITOが1時間につき500円で時間単位の貸し出しOKで、且つ、比較的、予約も空いていたので利用することにしました。無料で備品も貸出しており、なかなか穴場なんじゃないかなぁと思いました。ただ、費用は安く抑えられましたが、駅からやや遠いのが難点でした。

告知

DoorkeeperやTwitter、メーリングリストなどで告知しました。また、近隣のRuby関西GDG神戸のAngularJSの勉強会などで簡単な開催のお知らせをしました。

当日の様子

19:00を超えると、入り口がclosedって札が立つのを知らなくて、入るのに戸惑った人がいて申し訳ありませんでした。

平日ということもあって、19:30ぐらいになるまでなかなか人が集まりませんでしたが、最終的には、当日キャンセルなしで合計10名が参加していただけました。ありがとうございました。

今回の勉強会では、Groongaの紹介と事例紹介を発表しました。

Groongaの紹介では、私が説明しながら、Groongaの開発に携わっている須藤(@ktou)さんが適宜、補足や質問に答える形で進めました。開発に携わっている方から直接、詳細な情報が聞けるいい機会になったんじゃないかなと思います。

事例紹介では、特許の全文検索サービスPatentFieldを紹介しました。当サービスでは、PHPからMroongaを使っていますが、Groongaの機能をすべて使いたかったので、全文検索はUDFを使ってGroongaのコマンドを使っています。基本的にGroongaのほぼすべての機能を使っているので、実際に使っている様子と工夫した点を紹介しました。

参加していただいた@shigeponさんが、ブログに勉強会の感想を書いてくれています。よければ、こちらもご参照ください。

懇親会

勉強会終了後、終電間近まで懇親会が行われました。懇親会では、Groonga関連の話もGroongaに関係ない話も盛り上がってた思います。@soundkitchenさんからdocker-mroongaについて紹介がありました。MroongaをDockerを使って簡単にイメージ作るにはこれしかない!と熱弁されていました。Dockerを使って、Mroongaを簡単にサーバにデプロイしたい方は是非使ってみるといいのではないでしょうか。

感想・反省点

進行がなかなかうまくいかず、発表を用意していただいた方もいらっしゃったのですが、時間を確保できなくて申し訳ありませんでした。

実際にコマンドをうちながら説明したいなぁと思ったのですが、現場では案外パッとでてこないので、事前に準備しておいたほうがよかったなぁとおもいました。

また、平日ということもあり、なかなか19時丁度に全員集まるのは難しかったので、今後、開催することがあれば、できるだけ休日に開催してみようかなと思っています。

勉強会の主催経験がなく拙い進行でしたが、参加いただいた皆様、および、わざわざ東京から来ていただいた須藤(@ktou)さん、どうもありがとうございました!

Groongaの今後への期待

当日できなかったGroongaの今後への期待の資料です。この資料は、メーリングリストで案内があった[groonga-dev,02439] Groongaの今後の開発への協力のお願いへの回答です。

61st Ruby/Rails勉強会@関西でLTしました

以下は、LTの資料です。

MacとRabbitの操作に戸惑ってしまい、デモの検証が一部しかできませんでした。デモができなかった分の検証結果を追記しています。

2014/6/27(金)19:00~神戸でGroongaの勉強会をします。Doorkeeperで募集していますので、興味がある方は是非ご参加ください。

http://koberoonga.doorkeeper.jp/events/11578

「Mroongaを使ったときの MySQLの制限との戦い」という内容で初LTしました

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でやっています。

国産の全文検索エンジン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

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