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"
          }
        }
      }
    }
  }
}
'