Elaticsearchのディスク容量節約のHow to(Tune for disk usages)

# 1.はじめに
Elasticsearchを利用するに当たり、ディスク使用を効率化してなるべくデータ量を削減していきたい。
公式リファレンスの*How To*には[*Tune for disk usage*](https://www.elastic.co/guide/en/elasticsearch/reference/6.2/tune-for-disk-usage.html#_disable_the_features_you_do_not_need)という記事がある。
これを参考にディスク使用量の削減方法について調査した。

**※ 検索機能や性能用件、稼働環境などとトレードオフになる部分があるため、適宜選択して使用すること。**


比較的影響が小さいと思われるものは下記です。
- 2-2.スコアを考慮しないtextのインデックスにスコアを書き込まない
- 8.Force Merge(_forcemerge API )を使用する
- 10.数値データは最大値に合わせて最小の型を使用する
- 12.登録するドキュメントのフィールドは同じ順番で格納する

# 2.不要な機能を無効にする
## 2-1.検索やフィルタに使用しないフィールドはインデックスを作成しない
Elasticsearchはほとんどのフィールドに対してデフォルトでインデックスを作成する。
例えば、グラフに表示するだけの数値フィールドであってもインデックスを作成している。

このように検索やフィルタに使用しないフィールドは、マッピングを使用してインデックスを安全に無効化することが出来る。

~~~ 変更例：fooという名の数値フィールドのインデックスを無効にする
PUT index
{
  "mappings": {
    "type": {
      "properties": {
        "foo": {
          "type": "integer",
          "index": false
        }
      }
    }
  }
}
~~~

## 2-2.スコアを考慮しないtextのインデックスにスコアを書き込まない
textフィールドにはドキュメントにスコアをつけるため、インデックスに正規化係数(normalization factors)を付与することが出来る。

スコアを気にせず、文字列での検索などしかしないフィールドには、スコアを付与しない設定が可能である。


~~~ 変更例：fooという名のtextフィールドのインデックスに正規化係数を付与しない設定をする
PUT index
{
  "mappings": {
    "type": {
      "properties": {
        "foo": {
          "type": "text",
          "norms": false
        }
      }
    }
  }
}
~~~

## 2-3.フレーズクエリを使用しないtextのインデックスに位置を書き込まない
デフォルトのtextのインデックスには、インデックス頻度(*frequencies*)とインデックスの位置(*positions*、文章中の単語の位置のことだと思う)を格納している。インデックス頻度はインデックスのスコア付けに使用しており、インデックスの位置はフレーズクエリ(*match_phrase*)で使用されている。

対象のtextフィールドに対してフレーズクエリを使用しない場合はインデックス頻度のみを格納するように出来る。

~~~ 変更例：fooという名のtextフィールドのインデックスに頻度のみを格納し、位置を格納しない設定
PUT index
{
  "mappings": {
    "type": {
      "properties": {
        "foo": {
          "type": "text",
          "index_options": "freqs"
        }
      }
    }
  }
}
~~~

### 参考：Match Phrase Query
語順を意識した検索をします。

- Elasticsearch 6.2のリファレンス - Match Phrase Query
https://www.elastic.co/guide/en/elasticsearch/reference/6.2/query-dsl-match-query-phrase.html

- Elasticsearch実践ガイド P.115

## 2-4.上記2-2,2-3の組み合わせ
検索はしたいが、スコアリングを気にせずにフレーズクエリも使用しないフィールドについては、
上記の2,3を組み合わせたインデックスに設定できる。

~~~ 変更例：fooという名のtextフィールドのスコアリングとインデックス位置を無効にする
PUT index
{
  "mappings": {
    "type": {
      "properties": {
        "foo": {
          "type": "text",
          "norms": false,
          "index_options": "freqs"
        }
      }
    }
  }
}
~~~

# 3. 文字列フィールドの動的マッピングを使わない
## 3-1.文字列フィールドの動的マッピングについて
文字列のフィールドの方は*text*と*keyword*の2種類がある。
これらは明示的にマッピングを定義しない場合、文字列を格納する際にはマルチフィールドとして両方のインデックスが作成される。
- text
文字列からなる文章を格納するデータ型。
アナライザによって転置インデックスが作成され、単語による検索が可能。
- keyword
textと同様に文字列を格納する。
ただし、検索する際は完全一致による検索となる。

# 4.シャード数を少なくする(大きなシャードを使用する)
## 4-1.シャードサイズについて
より大きなシャードは小さなシャードよりも効率的にデータを格納することが出来る。
シャードサイズを大きくするには、シャード数を少なくすることで、1シャードあたりのサイズが大きくなる。

***※ ただし、シャードサイズが大きくなればなるほど、完全復旧時に時間がかかるなどの懸念がある***

## 4-2.シャード数の変更方法
既存のインデックスのシャード数を変更する場合、*Rollover API*を使用するか、*Shrink*を使用してシャードを縮退するかなどの方法が必要。
### 4-2-1.Rollover APIを使用する
ロールオーバーAPIはインデックスが大きすぎると判断した場合に、新しいインデックスにエイリアスを切り替える機能。

新しいインデックスのマッピングやエイリアスなどの定義は、一致するテンプレートから取得される。
ただし、ロールオーバーのリクエストにそれらを指定することができ、インデックステンプレートで設定された値に上書きしてインデックスを作成する。

~~~ 例：logs_writeエイリアスのインデックスをロールオーバーする際にシャード数を変更する
PUT /logs-000001
{
  "aliases": {
    "logs_write": {}
  }
}

POST /logs_write/_rollover
{
  "conditions" : {
    "max_age": "7d",
    "max_docs": 1000,
    "max_size": "5gb"
  },
  "settings": {
    "index.number_of_shards": 2
  }
}
~~~


- 詳細はリファレンスを参照してください。
https://www.elastic.co/guide/en/elasticsearch/reference/6.2/indices-rollover-index.html

### 4-2-2.Shrink APIを使用する
Shrink APIはシャード数を縮退する機能である。
元のインデックスと同じ定義のインデックスが作成されるが、インデックスの数は少なくなる。

~~~ 注意点
・新しいインデックスが実行前にあってはならない
・1シャードに2,147,483,519ドキュメントを含めることが出来ない。
・Shrink先のシャード数は、Shrink元のシャード数よりも少ない数の倍数である必要がある(元が8なら、先が4,2,1)
・Shrinkを実行するノードでは、Shrink元のインデックスのコピーを格納するのに十分な領域が必要
・実行前にShrink元のインデックスへの書き込みをブロックしておく必要がある
~~~
Shrink APIはcreate index APIのように設定やエイリアスをパラメータに指定できる。
~~~ 例：ShrinkAPIで新しいインデックスを作成し、エイリアスを設定する
POST my_source_index/_shrink/my_target_index
{
  "settings": {
    "index.number_of_replicas": 1,
    "index.number_of_shards": 1, 
    "index.codec": "best_compression" 
  },
  "aliases": {
    "my_search_indices": {}
  }
}
~~~

- Elasticsearch 6.2のリファレンス Rollover
https://www.elastic.co/guide/en/elasticsearch/reference/6.2/indices-rollover-index.html
- Elasticsearch実践ガイド P.215


#  5._allフィールドを無効にする
_allフィールドはドキュメントのすべてのフィールドのインデックスを付けている。
同時にすべてのフィールドを検索する必要がない場合は、無効にすることができる。

**※ 6.0.0. から_allフィールドは非推奨**
~~~ 例：my_indexのuseの_allフィールドを無効にする
PUT /my_index
{
  "mapping": {
    "user": {
      "_all": {
        "enabled": false
      }
    }
  }
}
~~~

#  6._sourceフィールドを無効にする
_sourceフィールドはドキュメントの元のJSON本体が格納されている。
アクセスする必要がなければ無効に出来る。

**※ 無効にした場合は更新や再インデックス(reindex)が使用できなくなる**

~~~ 例：_souceフィールドへのアクセスを無効にする
PUT tweets
{
  "mappings": {
    "_doc": {
      "_source": {
        "enabled": false
      }
    }
  }
}
~~~

#  7.データの圧縮方式を*best_compression*にする
## 7-1.データの圧縮について
_sourceや格納されているフィールドのデータは圧縮されている。
codecから圧縮方式を変更できる。
デフォルトではLZ4方式で圧縮されているが、より小さく圧縮できる*best_compression*を選択することが出来る。

**※ ただし、LZ4に比べてbest_compressionは検索に時間がかかる**

## 7-2.圧縮方式の変更
圧縮方式の変更は制的パラメータであるためインデックス作成時、またはcloseされているインデックスに大して設定することが出来る。

圧縮方式が変更された後、マージが行われたタイミングで適用される。
Force Mergeを実行し強制的に圧縮方式を変更することも可能。

~~~ 例：best_compressionに変更する
PUT /{index}/_settings
{
    "index" : {
        "index.codec" : "best_compression"
    }
}
~~~

## 7-3.参考
- Elasticsearch 6.2のリファレンス - Static index settingsのindex.codec
https://www.elastic.co/guide/en/elasticsearch/reference/6.2/index-modules.html#index-codec

- Elasticsearch 6.2のリファレンス - Force Merge
https://www.elastic.co/guide/en/elasticsearch/reference/6.2/indices-forcemerge.html

- Elasticsearchの圧縮方式の比較
http://acro-engineer.hatenablog.com/entry/2018/05/08/120000


# 8.Force Merge(_forcemerge API )を使用する
## 8-1.インデックスデータの構成とマージについて
Elasticsearchのインデックスはシャードと呼ばれる単位に分割されて格納されている。シャードは内部的にはLuceneのインデックスと同じ構成となっており、ドキュメントは不変のセグメントという単位にまとめられて保持される。

**※ セグメントは一つの大きなセグメントに格納されているほうが効率的にデータを格納することが出来ます**




## 8-2.Force Mergeとは
通常、マージ処理はリソースの状態などに応じて自動的に実行されるが、Force Mergeは強制的にマージを実行することが出来る。Force Mergeはリクエストのパラメータに応じて、マージ後のセグメント数を決定したり、削除されたセグメントの削除のみを行ったり出来る。

## 8-3._forcemerge API 
### 8-2-1.コマンド
下記のコマンドにてForce Mergeを実行することが出来る。
~~~ 
POST /{indexes}/_forcemerge
~~~

マージを行うインデックスは、カンマ区切りにすることで一度のリクエストで複数指定が可能である。
~~~ 例：index1とindex2を同時にマージする
POST /index1,index2/_forcemerge
~~~

全て(*_all*)のインデックスにマージを行う、インデックス名を省略する。
~~~ 例
POST /_forcemerge
~~~

### 8-2-2.パラメータ
- max_num_segments
マージ後に作成されるセグメントの最大数を指定する。
完全にマージを完了するには1を指定する。
デフォルトでは、マージの必要があるかを判定して実行する。
- only_expunge_delete
削除されたインデックスの削除のみを実行する。true/falseで指定する。
- flush
ForceMergeが完了後、flushを実行するかを指定する。true/falseで指定する。
デフォルトはtrue。
~~~ flushとは
ノードが停止した場合などに備えてドキュメントをディスクに書き出す操作
~~~

- リクエスト例：
~~~ kimchyという名のインデックスを最大100個のセグメントにマージし、マージ後にflushする
POST /kimchy/_forcemerge?only_expunge_deletes=false&max_num_segments=100&flush=true
~~~

### 8-2-3.注意点
- Force Merge実行中はブロックされる
Force Merge実行中は、マージ処理が完了するまで新しいForceMergeの実行をブロックします。
- Force Merge実行中のHTTP接続断時の挙動
HTTP接続が切断された場合は、マージ処理はバックグラウンドで継続される。
この状態でも、マージ処理が完了するまで新しいForceMergeの実行はブロックされる。

# 9.Shrink APIを使用する
シャード数を減らすことが出来る。
Force Merge APIと一緒に使用することで、インデックスのシャード数とセグメント数を大幅に削減できる。

- 4-2-2.Shrink APIを使用する

# 10.数値データは最大値に合わせて最小の型を使用する
数値データの型はディスク容量に対しての影響が大きい。
## 10-1.整数
最大値に応じて最小サイズの型を選択する。
整数を格納する型は下記がある。
- byte：符号付き8ビット整数(-128 ～ 127)
- short：符号付き16ビット整数(-32,768 ～ 32,767)
- integer：符号付き32ビット整数(-2147483648 ～ 2147483647)
- long：符号付き64ビット整数(-9223372036854775808～9223372036854775807)
## 10-2.小数
scaled_floatを使用する方が好ましい。
scaled_floatを使用することが難しい場合は、格納する値に応じて型を選択する。
小数の型には下記がある。
- half_float：16ビット浮動小数点数
- float：32ビット浮動小数点数
- double：64ビット浮動小数点数
- scaled_float：固定倍率の小数点数。scaling_factorに係数を指定する。
実際に格納される値は格納する値に係数をかけた値の小数点以下を四捨五入した整数(long)が格納される。
例えば、scaling_factorが10のとき、2.34を格納しようとした場合、格納される値は23で、使用する場合は2.3となります。
データをディスクに書き込む際に行う圧縮では、浮動小数は圧縮が難しくサイズが大きくなってしまうが、scaled_floatは内部的には整数なので圧縮しやすいためデータサイズを節約出来る。

# 11.インデックスのソートを使って、類似のドキュメントは同じ場所に格納する
## 11-1.インデックスソートによる圧縮率の向上について
Elasticsearchがドキュメント本体を格納する際、圧縮率を向上させるために複数のドキュメントをまとめて圧縮する場合がある。その際に、同じフィールド名を共有することがよくある。
同様のフォーマット、同様のフィールド名を持つドキュメント、同じ値を持つドキュメント、それらをまとめることにって圧縮率が向上し、ディスクが節約することが出来る。

デフォルトではドキュメントの追加された順番に格納される。

## 11-2.インデックスソート
***※ Elasticsearch6.2ではまだベータ版***
デフォルトでは新しいインデックスを作成する際、各セグメントのソート方法については設定されていない。
インデックスソートでは、各セグメント内のドキュメントをソートするために使用するフィールドを定義できる。

***※ネストされたフィールドを含むドキュメントをインデックスソートしようとするとエラーになる***

~~~ 例：index1のusernameの昇順、dateの降順に設定する。
PUT index1
{
    "settings" : {
        "index" : {
            "sort.field" : ["username", "date"], 
            "sort.order" : ["asc", "desc"] 
        }
    },
    "mappings": {
        "_doc": {
            "properties": {
                "username": {
                    "type": "keyword",
                    "doc_values": true
                },
                "date": {
                    "type": "date"
                }
            }
        }
    }
}
~~~
- 補足
初期設定ではインデックスソートがないため、検索時には全てのセグメントを検索する必要があるが、インデックスソートが設定されている場合に、検索時のソートがインデックスソートと同じ指定がされている場合に、検索するセグメントの個数を指定することができ、検索時の性能を向上することが出来る。

- 詳細は公式リファレンスを参照
https://www.elastic.co/guide/en/elasticsearch/reference/6.2/index-modules-index-sorting.html

# 12.登録するドキュメントのフィールドは同じ順番で格納する
複数のドキュメントはがまとめて同じブロックに圧縮されるため、フィールドの順番が常に同じであれば、重複する文字列が長くなり可能性が高くなり、圧縮効率が上がる場合がある。