localhost上にDockerでコンテナ化したElasticsearchクラスタを立てて自分用コマンド検索エンジンを作る
newuniverse
この記事は RECRUIT MARKETING PARTNERS Advent Calendar 2015 の投稿記事です。
こんちには、料理サプリサーバーサイド担当のnewuniverseです!今回は最近自分の個人プロジェクトで勉強しているElasticsearchについての知見を皆さんに共有したいと思います。
TL;DR
目的
Elasticsearchの入門とDockerと組み合わせて使えること目指す。
課題
エンジニアが普段使っているコマンドって忘れがち。検索できればなぁ……。
目標
crtl + r で履歴検索すればいいところを、あえてElasticsearchでコマンド用の検索エンジンをローカル環境で構築してみる。日本語で説明文入力できて、さらに日本語でも検索可能にする。
環境
- MacBook Pro (Retina 13-inch、Early 2015)
- OS X Yosemite
- Docker version 1.9.0, build 76d6bc9
- Elasticsearch 2.0.1
手順
- Elasticsearch officialのDockerfileレポジトリをいじってdocker imageを作る
- localhostでESコンテナを複数立ち上げ、ESクラスタをつくる
- sense上でkuromoji-pluginによる日本語検索、コマンド検索
- ターミナルから使いやすいようにシェルスクリプトを書く
完成予定図
Elasticsearchの概要
複数のElasticsearchサービスを起動しているnodeをクラスタして、スケールアウトのし易い分散検索エンジンを提供してくれます。今回は1コンテナをelasticsearchの1 nodeとしてlocalhost上でElasticsearchクラスタを形成します。
ElasticsearchではJSON形式のドキュメント(Document)としてデータが保存されます。データの持ち方をRDBと比較してみると以下のようになります。
Relational DB | Databases | Tables | Records | Columns |
---|---|---|---|---|
Elasticsearch | Indices | Types | Documents | Fields |
完成予定図の赤の点線がElasticsearchのindexとなり、それらは一つ以上のshardによって構成されます。
shardはElasticsearchの土台となる検索エンジンライブラリApache Luceneのindexに等しいです。入力されたデータはshardsにストアされます。ここにもindexという言葉が出てくるので、混乱しないよう気をつけて下さい。
Elasticsearch Indexは複数のLucene indices(shards)の集まりと考えられ、Elasticsearchはnode間にまたがるshardsをElasticsearchのIndexにします。基本的にshardはprimaryとreplicaの2種類に分かれています。RDBでいうmasterとslaveと考えてもらっても構いません。shardsの管理などはここでは深く言及しませんが、shardsはデフォルトでprimaryとreplicaがnodes間で分散するように配置され、一つのnodeが落ちてもデータを失わないように設計されています(下図参照)。もしprimaryが失われた場合は自動でreplicaがprimaryへと昇格されます。
検索やCRUD操作はRESTfulなAPIが提供されており、一般には9200
ポートにhttpリクエストを送る方式を取ります。
# 例:Elasticseachに検索クエリを投げる
GET /INDEX_NAME/TYPE_NAME/_search
{
"query": {
"match_phrase": {
"hobby": "jogging and climbing"
}
}
}
Dockerfileレポジトリをいじる
それでは目標達成に向けて実際の作業に入ります。Docker HubにはESの公式imageがすでに存在しますが、そのままのimageを使うと必要とするpluginがなかったり設定がデフォルトになるので、公式のDockerfileに少し手を加えます。
git clone https://github.com/docker-library/elasticsearch.git
cd ./docker-library/elasticsearch/2.0
ls
Dockerfile config docker-entrypoint.sh
ls config/
elasticsearch.yml logging.yml
ここでconfig
ディレクトリとdocker-entrypoint.sh
はDockerfile内で使われるとだけ覚えておいてください。それではDockerfile内を覗いていきましょう!
FROM java:8-jre
...
COPY config /usr/share/elasticsearch/config
VOLUME /usr/share/elasticsearch/data
COPY docker-entrypoint.sh /
ENTRYPOINT ["/docker-entrypoint.sh"]
EXPOSE 9200 9300
CMD ["elasticsearch"]
ここでは説明に必要な部分のみ抜粋しています。COPY config /usr/share/elasticsearch/config
では正に先ほどのconfig
ディレクトリ(Elasticsearchの設定ファイル)をコンテナ内環境にコピーしています。imageを作る前にここも先に設定してしまおうということですね。
ENTRYPOINT ["/docker-entrypoint.sh"]
はdocker run
する際に実行されるコマンドがdocker-entrypoint.sh
に記述されています。
今回日本語を扱うため日本語用アナライザーkuromoji plugin、そしてESのクラスタの状態モニタリングや監視ができるmarvel pluginを入れたいと思います。marvelは、ESのデータ可視化ツールKibanaに依存しますので、後ほどKibana専用のdocker imageも作成したいと思います1)もちろんESと同じコンテナ内にインストールしてもいいのですが、Dockerの軽量性を失いたくないのと、Kibanaが落ちてESのnodeも一緒に落ちては困ります。
FROM java:8-jre
...
# marvelをインストール
RUN plugin install license
RUN plugin install marvel-agent
# kuromojiをインストール
RUN plugin install analysis-kuromoji
COPY config /usr/share/elasticsearch/config
...
以上のようにESのプラグインコマンドを実行するように記述しましょう!
次はESのconfigディレクトリ内のelasticsearch.yml
を編集していきます。今回ESのlogは扱いませんのでlogging.yml
の設定は行いません。
network.host: 0.0.0.0
中身がこれだけなので、ここで必要最低限の設定を追記していきます。
# bindするhostとCluster内でnodeがお互いをコネクトする際にpublishするhostを一度に指定している
network.host: 0.0.0.0
# ESでは同じネットワーク内の同じクラスタ名を持つnodeでクラスタを形成する
cluster.name: rmp-advent-es
# nodeを区別するため名前をつけますが、動的に生成したいので環境変数で設定しましょう。
# コンテナを起動する際に環境変数からNODE_NAME=hogeで渡してあげます
node.name: ${NODE_NAME}
# mac環境でdockerを動かす際、virtualboxで仮想マシンを起動し、その環境でコンテナを立てる
# Elasticsearchのクラスタはzen discoveryを使ってnodeを探すので、仮想マシンhostのipを明示的に渡す。
discovery.zen.ping.unicast.hosts: ["192.168.99.100"]
# MacでDocker環境を開くときに付与されるIPがこれです。
docker is configured to use the default machine with IP 192.168.99.100
docker build -t rmp_advent_es:latest ~/docker-library/elasticsearch/2.0
より詳しい設定を知りたいという方はES configurationを参考にしてみてください。
このパートで最後となるKibanaのdocker imageを作ってみましょう。要領はElasticsearchと同じで、こちらもmarvelとsense pluginをインストールしたいので、公式のDockerfileに以下のように追記します。
...
&& tar -xz --strip-components=1 -C /opt/kibana -f kibana.tar.gz \
&& touch /opt/kibana/optimize/.babelcache.json \ #エラー対処
&& chmod 755 /opt/kibana/optimize/.babelcache.json \ #エラー対処
&& chown -R kibana:kibana /opt/kibana \
&& rm kibana.tar.gz
ENV PATH /opt/kibana/bin:PATH
RUN kibana plugin --install elasticsearch/marvel/latest
RUN kibana plugin --install elastic/sense
...
docker build -t rmp_advent_kibana:latest ~/docker-library/kibana/4.2
docker images
REPOSITORY TAG IMAGE ID CREATED VIRTUAL SIZE
rmp_advent_kibana latest 891c4799f8cc 32 minutes ago 294.9 MB
rmp_advent_es latest b12fbe8fd217 41 minutes ago 345.3 MB
java 8-jre 20e756f23350 13 days ago 310.5 MB
debian jessie a604b236bcde 13 days ago 125.1 MB
これで下準備は完了です。
localhostでESコンテナを複数立ち上げ、ESクラスタをつくる
ここまででElasticsearchのnodeを作るためのrmp_advent_es imageとkibanaのrmp_advent_kibana imageが揃いました。あとはこれらのimageからコンテナを起動するだけです。
# 一つ目のElasticsearch node
docker run --name es0 -p 9200:9200 -p 9300:9300 -e "NODE_NAME=node0" -d rmp_advent_es
# 二つ目
docker run --name es1 -p 9201:9200 -p 9301:9300 -e "NODE_NAME=node1" -d rmp_advent_es
# 三つ目
docker run --name es2 -p 9202:9200 -p 9302:9300 -e "NODE_NAME=node2" -d rmp_advent_es
# Kibana
docker run --name kibana -p 5601:5601 -e "ELASTICSEARCH_URL=http://192.168.99.100:9200" -d rmp_advent_kibana
docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
14ec90507af1 rmp_advent_kibana "/docker-entrypoint.s" 5 hours ago Up 5 hours 0.0.0.0:5601->5601/tcp kibana
48febc0fcda2 rmp_advent_es "/docker-entrypoint.s" 5 hours ago Up 5 hours 0.0.0.0:9202->9200/tcp, 0.0.0.0:9302->9300/tcp es2
09b5b588dce9 rmp_advent_es "/docker-entrypoint.s" 5 hours ago Up 5 hours 0.0.0.0:9201->9200/tcp, 0.0.0.0:9301->9300/tcp es1
a8c68d79fdef rmp_advent_es "/docker-entrypoint.s" 5 hours ago Up 5 hours 0.0.0.0:9200->9200/tcp, 0.0.0.0:9300->9300/tcp es0
docker run
... コンテナ起動
--name es0
... コンテナに名前を付与
-p 9200:9200
... ホストの9200をpublishして、コンテナの9200にforward
-p 9300:9300
... クラスタ間のコミュニケーションは9300番台を使うので、nodeとして見つかるためホストの9300番台をコンテナの9300にforwardさせる
-e "NODE_NAME=node0"
... node名を環境変数で渡す
クラスタの状態はブラウザからhttp://192.168.99.100:9200/_cluster/health?prettyにアクセスすることで確認できます。
sense上で日本語検索、コマンド検索
kibanaにsenseをインストールしたので、webクライアントから色々クエリを投げてみましょう。
http://192.168.99.100:5601/app/senseにまずアクセス。
senseを開くと上図のような画面が現れます。Serverというテキスト欄にElasticsearch nodeのアドレスを入れましょう。senseは言わばRubyのirb、Swiftのplaygroundのようなもので、クエリを記述できるエディタ(左)と結果を表示してくれる機能(右)を提供してくれます。
まずはElasticsearchのindex(database)の作成とその設定をします。上図のPUT /rmp {...}
の部分で行っています。queryのパラメータ({}
の部分)はJSON形式で書きます。
主にkuromojiの設定を行っています。設定の解説については省略するのでこちらを参考にして下さい。
次にPUT /rmp/commands/1
部分ではidを指定して、rmp indexの中のcommands typeにcommand
とdescription
というfieldをもつドキュメントを作成しています。
//結果
{
"_index": "rmp",
"_type": "commands",
"_id": "1",
"_version": 1,
"_shards": {
"total": 2,
"successful": 2,
"failed": 0
},
"created": false
}
予めidを持たない、あるいはidを指定したくない場合POST /rmp/commands
のように作成することも可能で、以下のレスポンスを見ると"_id": "AVFrA4-VEhQu5QLbLVdu",
となっていることが確認できます。
//結果
{
"_index": "rmp",
"_type": "commands",
"_id": "AVFrA4-VEhQu5QLbLVdu",
"_version": 1,
"_shards": {
"total": 2,
"successful": 2,
"failed": 0
},
"created": true
}
作成したドキュメントはGET /rmp/commands/1
として取得することが可能です。
//結果
{
"_index": "rmp",
"_type": "commands",
"_id": "1",
"_version": 2,
"found": true,
"_source": {
"command": "ls -la",
"description": "ディレクトリの情報を全部見る"
}
}
最後に簡単な検索を試します。ElasticsearchではQuery DSLを使って検索をかけるのが一般的になります。
//Query DSLテンプレート
"query": {
QUERY_NAME: {
ARGUMENT: VALUE,
ARGUMENT: VALUE,...
}
}
GET /rmp/commands/_search
で「プロセス」という単語にマッチするドキュメントを検索しています。
検索結果はレスポンスの"hits"
に格納されていて、ヒット件数(total
)やヒットしたドキュメントとクエリとの関連度のスコア(_score
)を出してくれます。
全文検索についてはこちらから読み進めていくことをオススメします。
ターミナルから使いやすいようシェルスクリプトを書く
Kibanaのsense pluginから色々とElasticsearchのAPIの使い方を紹介しました。
ただ毎度senseを開いてAPIを叩きに行くのは面倒なので、ここでは簡単なシェルスクリプトを書いて、コマンドでElasticsearchにドキュメントのinsertとsearchができるようにします(もちろんgo言語など好きな言語で処理を書いても構いません)。
#insert.sh
#!/bin/sh
read -p "Please input your command: " input_command
read -p "Please add description: " description
params="
{
\"command\": \"${input_command}\",
\"description\": \"${description}\"
}"
curl -XPOST "http://192.168.99.100:9200/rmp/commands" -d "${params}"
insert.sh
ではcommandとdescriptionのを入力するよう求め、Query parameterを作ってcurl
コマンドでPOSTしています。
#search.sh
#!/bin/sh
params="
{
\"query\":
{
\"multi_match\":
{
\"query\": \"$1\",
\"fields\": [ \"command\", \"description\" ]
}
}
}"
curl -XGET "http://192.168.99.100:9200/rmp/commands/_search?pretty&filter_path=hits.hits._source&_source=description,command" -d "${params}"
search.sh
ではコマンドの第一引数をクエリテキストとして使い、_search
APIの引数に&filter_path=hits.hits._source&_source=description,command
を入れることで、レスポンスに必要な情報だけを出すようにフィルタリングしてあります。
使用した結果を見てみます。
$ ./insert.sh
Please input your command: ssh -i ~/.ssh/id_rsa rmp@123.456.78.9 -p 12345
Please add description: 踏み台へアクセス
{"_index":"rmp","_type":"commands","_id":"AVFrssVbEhQu5QLbLX-1","_version":1,"_shards":{"total":2,"successful":2,"failed":0},"created":true}%
$ ./search.sh 踏み台
{
"hits" : {
"hits" : [ {
"_source":{"description":"踏み台へアクセス","command":"ssh -i ~/.ssh/id_rsa rmp@123.456.78.9 -p 12345"}
} ]
}
}
作業は以上です。検索ライフをエンジョイしていきましょう!w
終わりに
執筆中にElasticsearchの2.1とKibanaの4.3がリリースされていて、『アップデート速っ!』となりました。今回データの永続化などに関して触れられなかったので、そちらに関しては次回あらためてご紹介します。また、今後はクラウド環境などでマルチホストでElasticsearchのクラスタを構築することを試してみようと考えています。こちらにつきましてもいずれご紹介できたらと思っています。
脚注
↑1 | もちろんESと同じコンテナ内にインストールしてもいいのですが、Dockerの軽量性を失いたくないのと、Kibanaが落ちてESのnodeも一緒に落ちては困ります |
---|