Elasticserachのgetting startedを読んでみた
クラスターとノードについて
Elasticserachはクラスターの構築を想定しています。クラスターはクラスター名で識別し、クラスター内のノードを識別するためのUUIDはクラスター参加時にランダムな値が生成されます。ノード起動時に参加可能なクラスターが存在しない場合、デフォルトのクラスター名である"elasticsearch"でクラスターを自動で作成します。ローカルマシンでノード一つだけで動かす場合は、ノードが参加するデフォルトのクラスター名が"elasticsearch"なのでそれに参加します(シングルノードクラスタ)。
クラスターの構築として気になる点としてメタ情報をどこで持ってどう管理しているのかがあると思いますが、以下のドキュメントとやり取りをみて見ると各ノード上のディレクトリ(デフォルトは$ES_HOME/data)にメタデータを保存といった感じなのでしょうか。
https://www.elastic.co/guide/en/elasticsearch/reference/current/modules-node.html
https://discuss.elastic.co/t/where-cluster-metadata-is-stored/44540/12
とりあえずシングルノードクラスタで動かしたいと思うのですが、クラスターを構築する場合はHigh Availability化どうするか、負荷分散はどうなるのかなど色々調査して考えなければいけないのかと思います。
各ノードについて
ノードには以下の種類がある。
- マスターノード
- データノード
- ingestノード
- tribノード
で、それぞれの役割は
マスターノード
node.masterがtrue(デフォルト=true)に設定されているノードからマスタノードが選出される。クラスターを制御するためのノード。
データノード
node.dataの設定値がtrue(デフォルト=true)に設定されているノードがデータノードになる。データを保持し、CRUD、検索、集約などのデータ関連操作を実行する。
ingestノード
node.ingestがtrue(デフォルト)に設定されているノードがingestノードになる。ingestノードは、インデックスする前にElasticsearch自体でデータ変換/加工を行います。ElasticsearchはJson形式でデータを受け取る必要があり、今まではfulentdやlogstashなどのETLツールを使ってJson形式に変換したデータをElasticserachに渡していたようですが、ingestノードを使うとそれらの機能をElasticserach側で行えるようになるとのことでした。
tribノード
tribノードは複数のクラスタを横断して検索やその他の操作を実行できる特別なノード。
インデックス
インデックスはRDBでいうデータベースのようなもので、多少類似した特性をもつドキュメントの集合になります。先ほどのtribノードを使ってクラスターを横断しての操作を想定する場合、クラスター全体で異なるインデックス名をつけるように気をつけなければいけません。
タイプ
インデックス内で使われるドキュメントのカテゴリ、パーティションを区別して保持するためのようなものであって以前までは一つのインデックスに複数のタイプを持つことができたがElasticsearchの6系からは一つのインデックスに一つのタイプまでとなっているのでインデックスの方で検索を意識してドキュメントを区別する方向になるのかと思う。以前まではParent-Child タイプを使ってインデックス内のタイプでjoinすることができたが、複数タイプが不可になった6系からはjoinタイプが追加され、これによりインデックス間のタイプでjoinのようなことができるようになっている。
ドキュメント
インデックス -> タイプ内で保持するデータをドキュメントといってJSON形式でデータを保持します。ドキュメント数はインデックス作成のパフォーマンスに影響があり、パフォーマンスを測定する場合はバルクAPIを使って大量のドキュメントを登録した時の性能からインデックス内のドキュメント数を決めれば良い感じなのでしょうか。
https://www.elastic.co/jp/blog/performance-considerations-elasticsearch-indexing
シャード & レプリカ
Elasticsearchのインデックスは大量データの保持を想定しノード間でインデックスを分散して保持できるようにしています。このインデックスの断片のことをシャードと言ってインデックスを作成する時にシャードの数を設定します。インデックスの設計をするときは最初にボリューム感を把握しておかないの後々困りそうに思います。 シャーディングは以下の点から重要と言われています。 - コンテンツボリュームを水平に分割/拡大縮小することができる - シャード間で(複数のノード上にある可能性がある)オペレーションを分散および並列化できるため、パフォーマンス/スループットが向上します ファイルの分散 & 処理の分散を行う仕組みなので運用を行う上で重要そう。運用中にシャードの数を変更したくなる場合が考えられますが簡単に調べた限りだとできなそうです。 Elasticsearchはスキーマレスを謳っているようですが、ドキュメントが増えてきた後のパフォーマンスのことを考えるとデータのマッピングを定義したりとか色々考えなければいけないのかも知れないです。
シャードにどのようにデータが分散されるかはElasticsearchに管理されています。 クラスタ内のノードが故障やメンテナンスでアクセスできなくなった場合に備えてシャードの複製(レプリカ)を持つことが推奨されています。 レプリカは以下の理由により推奨されています。 - シャード/ノードに障害が発生した場合の高可用性を提供する - すべてのレプリカで検索を並行して実行できるため、検索ボリューム/スループットをスケールアウトできる。
レプリカの方でも処理を分散してくれるのでHigh Availabilityだけでなく性能の面でも影響のある設定になる。レプリカの数はシャードと違ってインデックスを作った後からでも変更できる。
デフォルトの設定ではシャードの数が5でレプリカの数が1となっています。つまり、ノードが2つある場合プライマリのシャードが5個とシャード毎でプライマリとは別のノードにレプリカがあって計5個ある。
一つのノードで複数のシャードを保持できるようだがノードに対してプライマリとレプリカをどう保持させるのかは分からなかった(例えばこのノードはシャードを優先して保持させるとか)が、以下にはそれっぽいことがのっていた。
https://www.elastic.co/guide/en/elasticsearch/guide/current/replica-shards.html
インストール
シングルノードクラスタで簡単に動きを確認したいだけなのでdockerで入れてみたいと思います。
https://www.elastic.co/guide/en/elasticsearch/reference/current/docker.html
dockerイメージの取得
$ docker pull docker.elastic.co/elasticsearch/elasticsearch:6.3.0 省略 $ docker images REPOSITORY TAG IMAGE ID CREATED SIZE docker.elastic.co/elasticsearch/elasticsearch 6.3.0 56d3dc08212d 7 days ago 783MB
dockerイメージの起動
docker run -p 9200:9200 -p 9300:9300 -e "discovery.type=single-node" docker.elastic.co/elasticsearch/elasticsearch:6.3.0 別ターミナル $ docker ps -a CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 739bd2c81da2 docker.elastic.co/elasticsearch/elasticsearch:6.3.0 "/usr/local/bin/dock…" About a minute ago Up About a minute 0.0.0.0:9200->9200/tcp, 0.0.0.0:9300->9300/tcp tender_visvesvaraya
linuxでdockerを動かす場合はプロセスあたりのスレッド数を変更したいのでカーネルパラメータを変更する必要がある。 macの場合はxhyve virtual machineに接続した後の設定変更となる。
$ screen ~/Library/Containers/com.docker.docker/Data/com.docker.driver.amd64-linux/tty linuxkit-025000000001:~# sysctl -w vm.max_map_count=262144
xhyve virtual machineからはCtrl-a k y
で抜けることができる。
それから以下のdocker-compose.ymlを作成する
version: '2.2' services: elasticsearch: image: docker.elastic.co/elasticsearch/elasticsearch:6.3.0 container_name: elasticsearch environment: - cluster.name=docker-cluster - bootstrap.memory_lock=true - "ES_JAVA_OPTS=-Xms512m -Xmx512m" ulimits: memlock: soft: -1 hard: -1 volumes: - esdata1:/usr/share/elasticsearch/data ports: - 9200:9200 networks: - esnet elasticsearch2: image: docker.elastic.co/elasticsearch/elasticsearch:6.3.0 container_name: elasticsearch2 environment: - cluster.name=docker-cluster - bootstrap.memory_lock=true - "ES_JAVA_OPTS=-Xms512m -Xmx512m" - "discovery.zen.ping.unicast.hosts=elasticsearch" ulimits: memlock: soft: -1 hard: -1 volumes: - esdata2:/usr/share/elasticsearch/data networks: - esnet volumes: esdata1: driver: local esdata2: driver: local networks: esnet:
それから事前に実行したdocker run -p 9200:9200 -p 9300:9300 -e "discovery.type=single-node" docker.elastic.co/elasticsearch/elasticsearch:6.3.0
を終了し、docker-compose.ymlを作成したディレクトリでdocker-compose up
を実行する。
$ docker-compose up $ docker ps -a CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES ce6c11cbf676 docker.elastic.co/elasticsearch/elasticsearch:6.3.0 "/usr/local/bin/dock…" 3 minutes ago Up 3 minutes 9200/tcp, 9300/tcp elasticsearch2 a323142519a3 docker.elastic.co/elasticsearch/elasticsearch:6.3.0 "/usr/local/bin/dock…" 3 minutes ago Up About a minute 0.0.0.0:9200->9200/tcp, 9300/tcp elasticsearch 739bd2c81da2 docker.elastic.co/elasticsearch/elasticsearch:6.3.0 "/usr/local/bin/dock…" About an hour ago Exited (130) About a minute ago tender_visvesvaraya
疎通確認
9200ポートでポートフォワードしているのでローカルの環境から以下のコマンドを実行して疎通確認してみる。
$ curl http://127.0.0.1:9200/_cat/health 1529730461 05:07:41 docker-cluster green 1 1 0 0 0 0 0 0 - 100.0% $ curl -u elastic:testpassword localhost:9200/_cat/nodes 172.18.0.3 35 96 48 0.80 0.51 0.27 mdi - X3pZjfr 172.18.0.2 31 96 19 0.80 0.51 0.27 mdi * pleiZk3
起動中のコンテナに接続
$ docker ps -a CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES ce6c11cbf676 docker.elastic.co/elasticsearch/elasticsearch:6.3.0 "/usr/local/bin/dock…" 6 hours ago Up 5 hours 9200/tcp, 9300/tcp elasticsearch2 a323142519a3 docker.elastic.co/elasticsearch/elasticsearch:6.3.0 "/usr/local/bin/dock…" 6 hours ago Up 5 hours 0.0.0.0:9200->9200/tcp, 9300/tcp elasticsearch 739bd2c81da2 docker.elastic.co/elasticsearch/elasticsearch:6.3.0 "/usr/local/bin/dock…" 7 hours ago Exited (130) 6 hours ago tender_visvesvaraya $ docker exec -it elasticsearch /bin/bash
Elasticsearchの操作
ヘルスチェック
$ curl http://127.0.0.1:9200/_cat/health 1529735365 06:29:25 docker-cluster green 2 2 0 0 0 0 0 0 - 100.0%
クラスター名docker-clusterがstatus greenのことが確認できます。
ノードのチェック
$ curl http://127.0.0.1:9200/_cat/nodes?v ip heap.percent ram.percent cpu load_1m load_5m load_15m node.role master name 172.18.0.3 49 96 1 0.04 0.08 0.12 mdi - X3pZjfr 172.18.0.2 27 96 1 0.04 0.08 0.12 mdi * pleiZk3
インデックスのチェック
$ curl http://127.0.0.1:9200/_cat/indices?v health status index uuid pri rep docs.count docs.deleted store.size pri.store.size
インデックスが作られていない状態だとこのような結果になります。
インデックスの作成
$ curl -X PUT http://127.0.0.1:9200/customer?pretty { "acknowledged" : true, "shards_acknowledged" : true, "index" : "customer" } $ curl http://127.0.0.1:9200/_cat/indices?v health status index uuid pri rep docs.count docs.deleted store.size pri.store.size green open customer u9pFJC6vSOiGXHrvUuYPfA 5 1 0 0 1.1kb 460b
一つ目のコマンドでcustomerというインデックスを作成している。二つ目のコマンドで作られたインデックスを表示している。シャードが5個でレプリカ1の設定で現在のドキュメント数が0なことが確認できる。
ドキュメントの追加
$ curl -v -H "Accept: application/json" -H "Content-type: application/json" -X PUT 'http://localhost:9200/customer/_doc/1?pretty' -d '{ "name" : "John Doe" }' * Trying ::1... * TCP_NODELAY set * Connected to localhost (::1) port 9200 (#0) > PUT /customer/_doc/1?pretty HTTP/1.1 > Host: localhost:9200 > User-Agent: curl/7.54.0 > Accept: application/json > Content-type: application/json > Content-Length: 23 > * upload completely sent off: 23 out of 23 bytes < HTTP/1.1 201 Created < Location: /customer/_doc/1 < content-type: application/json; charset=UTF-8 < content-length: 222 < { "_index" : "customer", "_type" : "_doc", "_id" : "1", "_version" : 1, "result" : "created", "_shards" : { "total" : 2, "successful" : 2, "failed" : 0 }, "_seq_no" : 0, "_primary_term" : 1 } * Connection #0 to host localhost left intact
Content-type: application/json
をヘッダーにつけないとエラーになる
ドキュメントの確認
$ curl http://localhost:9200/customer/_doc/1?pretty { "_index" : "customer", "_type" : "_doc", "_id" : "1", "_version" : 1, "found" : true, "_source" : { "name" : "John Doe" } }
インデックスの削除
$ curl -X DELETE http://127.0.0.1:9200/customer?pretty { "acknowledged" : true } $ curl http://127.0.0.1:9200/_cat/indices?v health status index uuid pri rep docs.count docs.deleted store.size pri.store.size
ドキュメントの変更
$ curl -H "Content-type: application/json" -X PUT 'http://localhost:9200/customer/_doc/1?pretty' -d '{ "name" : "John Doe" }' { "_index" : "customer", "_type" : "_doc", "_id" : "1", "_version" : 1, "result" : "created", "_shards" : { "total" : 2, "successful" : 2, "failed" : 0 }, "_seq_no" : 0, "_primary_term" : 1 } $ curl -H "Content-type: application/json" -X PUT 'http://localhost:9200/customer/_doc/1?pretty' -d '{ "name" : "Jane Doe" }' { "_index" : "customer", "_type" : "_doc", "_id" : "1", "_version" : 2, "result" : "updated", "_shards" : { "total" : 2, "successful" : 2, "failed" : 0 }, "_seq_no" : 1, "_primary_term" : 1 } $ curl http://127.0.0.1:9200/customer/_doc/1 {"_index":"customer","_type":"_doc","_id":"1","_version":2,"found":true,"_source":{ "name" : "Jane Doe" }}
またID指定なしで登録する場合は以下のようになる
$ curl -H "Content-type: application/json" -X POST 'http://localhost:9200/customer/_doc/?pretty' -d '{ "name" : "John Doe" }'
ドキュメントの更新
$ curl -H "Content-type: application/json" -X POST 'http://localhost:9200/customer/_doc/1/_update?pretty' -d '{"doc": { "name": "Jane Doe", "age": 20 }}' { "_index" : "customer", "_type" : "_doc", "_id" : "1", "_version" : 3, "result" : "updated", "_shards" : { "total" : 2, "successful" : 2, "failed" : 0 }, "_seq_no" : 5, "_primary_term" : 1 }
JSONのフォーマットを変更しての更新もできる。 また、以下のように既存の値を使った更新もできる
$ curl -H "Content-type: application/json" -X POST 'http://localhost:9200/customer/_doc/1/_update?pretty' -d '{"script": "ctx._source.age += 5" }' { "_index" : "customer", "_type" : "_doc", "_id" : "1", "_version" : 4, "result" : "updated", "_shards" : { "total" : 2, "successful" : 2, "failed" : 0 }, "_seq_no" : 6, "_primary_term" : 1 } {"_index":"customer","_type":"_doc","_id":"1","_version":4,"found":true,"_source":{"name":"Jane Doe","age":25}}
ドキュメントの削除
$ curl -X DELETE 'http://localhost:9200/customer/_doc/1?pretty'
バルクAPI
$ curl -H "Content-type: application/json" -X POST 'http://localhost:9200/customer/_doc/_bulk?pretty' -d ' {"index":{"_id":"1"}} {"name": "John Doe" } {"index":{"_id":"2"}} {"name": "Jane Doe" } ' {"index":{"_id":"1"}} {"name": "John Doe" } {"index":{"_id":"2"}} {"name": "Jane Doe" } ' { "took" : 33, "errors" : false, "items" : [ { "index" : { "_index" : "customer", "_type" : "_doc", "_id" : "1", "_version" : 1, "result" : "created", "_shards" : { "total" : 2, "successful" : 2, "failed" : 0 }, "_seq_no" : 10, "_primary_term" : 1, "status" : 201 } }, { "index" : { "_index" : "customer", "_type" : "_doc", "_id" : "2", "_version" : 1, "result" : "created", "_shards" : { "total" : 2, "successful" : 2, "failed" : 0 }, "_seq_no" : 2, "_primary_term" : 1, "status" : 201 } } ] } $ curl -H "Content-type: application/json" -X POST 'http://localhost:9200/customer/_doc/_bulk?pretty' -d ' {"update":{"_id":"1"}} {"doc": { "name": "John Doe becomes Jane Doe" } } {"delete":{"_id":"2"}} '
バルクAPIは開業を挟む必要がある。
データの検索
まずサンプルデータを投入しておく
$ wget https://raw.githubusercontent.com/elastic/elasticsearch/master/docs/src/test/resources/accounts.json $ curl -H "Content-Type: application/json" -XPOST "localhost:9200/bank/_doc/_bulk?pretty&refresh" --data-binary "@accounts.json" $ curl "localhost:9200/_cat/indices?v" health status index uuid pri rep docs.count docs.deleted store.size pri.store.size green open customer 6S9pdchzSBmr5ztVVlHAvg 5 1 2 0 22.4kb 11.2kb green open bank fwwsrUjcSFaWmv2QtYZ5QA 5 1 1000 0 989.9kb 498.8kb
ソートして表示
以下ではaccount_numberでソートして表示している。レスポンスとして10件帰って来る。
$ curl http://localhost:9200/bank/_search?q=*&sort=account_number:asc&pretty
同様のリクエストをQueryDSLで以下のように行える。
$ curl -H "Content-type: application/json" -X GET 'http://localhost:9200/bank/_search' -d ' { "query": { "match_all": {} }, "sort": [ { "account_number": "asc" } ] } '
limit offsetは以下のようにfrom, sizeで設定する。
$ curl -H "Content-type: application/json" -X GET 'http://localhost:9200/bank/_search' -d ' { "query": { "match_all": {} }, "sort": [ { "account_number": { "order": "desc" } } ], "from": 10, "size": 10 } '
select対象を絞りたい場合は_source
で指定する
$ curl -H "Content-type: application/json" -X GET 'http://localhost:9200/bank/_search' -d ' { "query": { "match_all": {} }, "_source": ["account_number", "balance"] } '
where条件一致
$ curl -H "Content-type: application/json" -X GET 'http://localhost:9200/bank/_search' -d ' { "query": { "match": { "account_number": 20 } } } '
matchでwhere条件をした場合は、スペース区切りで複数単語指定した場合、どれか一つの単語に一致したものを返します。以下はmillまたはlaneが含まれるものを返します。
$ curl -H "Content-type: application/json" -X GET 'http://localhost:9200/bank/_search' -d ' { "query": { "match": { "address": "mill lane" } } } '
match_phraseでの条件指定の場合は、指定した単語が含まれるものを返します。以下は"mill lane"が含まれるものを返します。
$ curl -H "Content-type: application/json" -X GET 'http://localhost:9200/bank/_search' -d ' { "query": { "match_phrase": { "address": "mill lane" } } } '
mustで指定した場合は中のmatchの条件が全てTrueの場合Trueになる。
$ curl -H "Content-type: application/json" -X GET 'http://localhost:9200/bank/_search' -d ' { "query": { "bool": { "must": [ { "match": { "address": "mill" } }, { "match": { "address": "lane" } } ] } } } '
shouldで指定した場合は中のmatchの条件の一つでもTrueになればTrueになる。
$ curl -H "Content-type: application/json" -X GET 'http://localhost:9200/bank/_search' -d ' { "query": { "bool": { "should": [ { "match": { "address": "mill" } }, { "match": { "address": "lane" } } ] } } } '
not条件はmust_notで指定する。
$ curl -H "Content-type: application/json" -X GET 'http://localhost:9200/bank/_search' -d ' { "query": { "bool": { "must_not": [ { "match": { "address": "mill" } }, { "match": { "address": "lane" } } ] } }, "_source": ["address"] } '
bool直下の条件を複数指定できる、以下はmust, must_notの条件が共にTrueのものを返す。
$ curl -H "Content-type: application/json" -X GET 'http://localhost:9200/bank/_search' -d ' { "query": { "bool": { "must": [ { "match": { "age": "40" } } ], "must_not": [ { "match": { "age": "40" } } ] } }, "_source": ["age", "state"] } '
以下の条件では残高(balance)が20000以上30000以下のものでfilterをかけます。
$ curl -H "Content-type: application/json" -X GET 'http://localhost:9200/bank/_search' -d ' { "query": { "bool": { "must": { "match_all": {} }, "filter": { "range": { "balance": { "gte": 20000, "lte": 30000 } } } } } } '
集計
以下のQueryDSLはstate.keyword毎で件数をカウントします。
$ curl -H "Content-type: application/json" -X GET 'http://localhost:9200/bank/_search' -d ' { "size": 0, "aggs": { "group_by_state": { "terms": { "field": "state.keyword" } } } } '
以下はkeyword毎でbalanceの平均を出します。orderで平均の高い順で表示します。
$ curl -H "Content-type: application/json" -X GET 'http://localhost:9200/bank/_search' -d ' { "size": 0, "aggs": { "group_by_state": { "terms": { "field": "state.keyword", "order": { "average_balance": "desc" } }, "aggs": { "average_balance": { "avg": { "field": "balance" } } } } } } '