今さら聞けないElasticsearchの基本概念・用語のおさらい

# Elasticsearch の基本概念

## Elasticsearch とは

Elasticsearch は、オープンソースの分散型検索・分析エンジンです。Apache Lucene をベースにした全文検索機能を提供し、RESTful API を通じてデータの保存、検索、分析が可能です。スケーラビリティに優れており、大量のデータを高速に処理できることが特徴です。

主な用途として、ログ分析、リアルタイム監視、全文検索、セキュリティ分析などがあり、Elastic Stack（旧 ELK スタック: Elasticsearch、Logstash、Kibana）の中核コンポーネントとして広く活用されています。

私の場合は主に、Railsと連携して web アプリケーションに全文検索機能を追加するために用いています。

## 論理的な概念

Elasticsearch は、データを論理的な概念である以下の要素で管理します。

- ドキュメント
- フィールド
- インデックス
- マッピング

### ドキュメント

- Elasticsearch へ格納する一つの文章の単位をドキュメントと呼ぶ。
- RDB のテーブルの行(レコード)に相当するが、そちらと異なりドキュメントは JSON 形式で格納される。
- ひとつ以上のフィールドによって構成される。

### フィールド

- ドキュメントを内の項目名(key)と値(value)の組をフィールドと呼ぶ。
- RDB のテーブルの列(カラム)に相当する。
- フィールドは以下のようなデータ型を持つ。
  - **text**
    文字列からなる文章を格納するデータ型。
    格納時にアナライザによって文章を構成する各単語に分割され、その単語ごとに転置インデックスが作成される。クエリの際には単語を指定することで検索ができる。
  - **keyword**
    text と同様に文字列を格納するデータ型。
    text との違いは格納時にアナライザによる単語分割処理が行われず、クエリの際には格納された文字列全体とマッチする「完全一致検索」で用いられるということ。(使用例: メールアドレスによるユーザー検索など)
  - **long**, **short**, **integer**, **float**
    数値を表すデータ型。
    数値の大小による比較が可能であり、数値の範囲を指定した range 検索で用いられる。
  - **date**: 日付を表すデータ型。フォーマットを指定して使う。
  - **boolean**: 真偽値を表すデータ型。
  - **object**
    JSON オブジェクトを表すデータ型。ネスト構造を持たせることができる。
    `{ "book": { "title": "Elasticsearch guide", "price": "500"} }`
  - **array**
    配列を表すデータ型は存在せず、全てのフィールドがデフォルトで配列を格納できる。
    `{ "tags": ["elasticsearch", "guide"] }`

:::note info

#### マルチフィールド型

一つのフィールドに同時に複数のデータ型を定義する機能。
例えば、あるフィールドに対して、text 型による部分一致検索と keyword 型による完全一致検索を同時に行うことができる。両方の score を合算することでより精度の高い関連度順で検索結果を返すことができる。
Elasticsearch5.0 以降では明示的なマッピング定義を作成せずに文字列をインデックスに格納すると、デフォルトで text 型と keyword 型のマルチフィールドとして定義される(動的マッピング)。
明示的なマッピング定義を作成する際は、マッピング定義の中でマルチフィールド型を指定する必要がある。

```ruby
analysis_keywords: {
  type: "text",
  fields: {
    keyword: {
      type: "keyword"
    }
  }
}
```

:::

### インデックス

- Elasticsearch におけるドキュメントを保存する場所のこと。
- 例えば、モデルごとにインデックスを作成するなど、RDB のテーブルに相当する。
- 格納したドキュメントによってアナライザによって単語の分割されたり、転置インデックスの情報を構成したりと、さまざまなデータ形式で保存される。
- インデックスが格納される際には、一定数のシャードと呼ばれる単位に分割され、各ノードに分散して保存される。
- インデックスは格納される場所を指す他に、「インデックスする」という動作自体を指すこともある。

:::note info

#### 転置インデックス

文書内に含まれる各単語と、その単語が出現する文書を組み合わせて構成するインデックス。

例:
ドキュメント 1: イベントが東京で開催される。
ドキュメント 2: 東京マラソンに参加する。

| 単語     | ドキュメント ID |
| -------- | --------------- |
| 東京     | 1, 2            |
| イベント | 1               |
| マラソン | 2               |

:::

### マッピング

- ドキュメント内の各フィールドのデータ構造やデータ型を記述した情報。
- ただし、事前にマッピング定義を作成しなくても、Elasticsearch が各フィールドのデータ型を推測して格納してくれる。

```bash: マッピング定義を追加更新するコマンド
curl -XPUT "localhost:9200/my_index/_mapping" \
-H "Content-Type: application/json" -d'
{
  "properties": {
    "additional_comment": { "type": "text" }
  }
}'
```

:::

## 物理的な概念

Elasticsearch が動作する物理的なサーバは以下の要素で構成される。

- ノード
- クラスタ
- シャード
- レプリカ

### ノード

- Elasticsearch が動作する各サーバのこと。
- 各物理サーバに 1 ノードが基本だが、1 台の物理サーバに複数のノードを配置することも可能。
- 各ノードは設定ファイルで指定できる一意のノード名を持つ。

### クラスタ

- 協調して動作するノードのグループ。
- 各ノードは設定ファイルで指定できる一意のクラスタ名を持ち、同じクラスタ名を持つノードグループを見つけるとお互いにメッセージを送信しあい、自律的にクラスタを形成する。異なるクラスタ名を設定すれば、２つのクラスタを形成することも可能。

### シャード

- 異なるノードで保持された、インデックスを分割した各部分のこと。
- シャードの数はインデックス作成時に指定できるが、**作成後に増やすことはできない。**

### レプリカ

- シャード、ノードの可用性を高めるために、各シャードは自動的に複製される仕組みがある。この複製をレプリカと呼ぶ。
- 複製されるオリジナルのシャードをプライマリシャードと呼び、複製されたシャードをレプリカシャードと呼ぶ。インデックス更新時は必ず前者に反映されてから、逐次レプリカシャードを複製するようになっている。
- 可用性を担保するために、プライマリシャードとレプリカシャードは自動的に異なるノードに割り当てられる機能がある。
- 検索処理はレプリカに対しても並列で行われるため、レプリカを構成することで検索性能が向上するメリットもある。

## システム構成

### ノードの種別

Elasticsearch には以下の 4 種類のノード種別(属性)が存在する。１台で複数の属性を持たせることもできるし、一つの属性のみを持たせた専用ノードを構成することも可能(デフォルトでは各ノード全てが Master, Data, Ingest の複数属性を持つ)。

- Master ノード
- Data ノード
- Ingest ノード
- Coordinating ノード

### Master(Master-eligible) ノード

- クラスタ管理を行うノード。クラスタは必ず一台の Master ノードを持つ。
- **ノードの参加・離脱の管理**: 定期的に全ノードに対して ping を送信し、返信の有無で生死判定を行う。
- **クラスタメタデータの管理**: クラスタ内のノード構成情報、インデックス・マッピング設定情報、シャード割り当てとステータス関連情報を管理し、各ノードに伝達する。
- **シャードの割り当て・再配置**: 新しいシャードの割り当てや、既存シャードの再配置を行う。
- Master ノードが停止した場合に、新しい Master に昇格できる候補として準備されたノードを Master-eligible ノードと呼ぶ。

### Data ノード

- Elasticsearch におけるデータの格納、クエリへの応答、内部的に Lucene のインデックスを管理するノード。
- インデックス格納時は格納対象となるシャード番号を決定して、そのシャードを格納するノードへ処理をルーティングする。
- クエリ時は、クエリ内容に対応したシャードを持つ全てのノードへルーティング(scatter)し、そのレスポンスを集約して(gather)クライアントへ返却する。

### Ingest ノード

- Elasticsearch 内部でデータ変換・加工を行うノード。
- このノード上で処理フロー(pipeline)を定義することで、送信されたデータを前処理できる。
- pipeline は、必ずしも定義する必要はない。

### Coordinating ノード

- クライアントからのリクエストハンドリングのみを担当する(Data ノードと違い保持はしない)。
- リクエスト処理の負荷分散や、検索結果のマージ・aggregation など負荷のかかる処理を Data ノードに行わせたくない場合に配置される。

### ノード属性の設定方法

```yml: elasticsearch.yml
# 全てfalseにするとcoordinatingノードになる
node.master: true  # Master(Master-eligible) ノード
node.data: true    # Data ノード
node.ingest: true  # Ingest ノード
```

### Master ノードの選出のための注意点

Master-eligible ノードを偶数台で構成すると、通信不良などでクラスタが分断された際にそれぞれのクラスタで Master ノードが独立して稼働してしまう(スプリットブレイン)。
そのような状態を防ぐために、以下のような対処が必要。

- Master-eligible ノードを**3 以上の奇数台**で構成する。
- elasticsearch.yml で `discovery.zen.minimum_master_nodes` を **Master-eligible ノードの過半数** に設定する。

これにより、過半数の Master-eligible ノードが集まるグループでのみ Master ノードが選出されるようになる。
例えば、3 台の Master-eligible ノードで構成されたクラスタでは、過半数の 2 台のノードが集まるグループでのみ Master ノードが選出される。
なお、デフォルトでは`discovery.zen.minimum_master_nodes`は 1 に設定されているので注意。

### ノード検知とクラスタ形成のメカニズム(discovery)

分散環境においてクラスタにノードを参加させたり、Master ノードを選定するメカニズムを **discovery** と呼ぶ(デフォルトでは Zen discovery)。

```yml: Zen discovery使用のためのelasticsearch.ymlの設定例
cluster.name: my-cluster
discovery.zen.ping.unicast.hosts: ["master1", "master2", "master3"]
discovery.zen.minimum_master_nodes: 2
```

#### Zen discovery のフロー

1. クラスタ名を設定し、同名のクラスタ名を持つノード同士でクラスタを形成する。
2. `discovery.zen.ping.unicast.hosts`にこのノードから接続を行う Master-eligible ノードの IP アドレスかホスト名を設定する(複数ある場合は上記のように配列で記述)。
3. クラスタ上ですでに Master ノードが稼働しているかどうかで処理が分かれる。
   - すでに Master ノードがいる場合
     このノードは Master ノードに認知され、クラスタに参加する。
   - まだ Master ノードがいない場合
     このノードが Master-eligible ノードであれば他の Master-eligible ノードと調停を行い、 Master ノードを選定後にクラスタを形成する(ただし、`discovery.zen.minimum_master_nodes`で設定された過半数ノードが集まっている場合)
     このノードが Master-eligible ノードでなければ上記のクラスタ形成を待って、新しいクラスタへ参加する。

注意点として、`discovery.zen.ping.unicast.hosts`のデフォルト値は["127.0.0.1", "[::1]"]となっており自ノード以外にクラスタ参加のリクエストが投げられない。これは不用意に他のノードとクラスタを形成しないようにするため。
複数ノードでクラスタを形成するためには必ずこのパラメータを設定変更する必要がある。

### シャード分割数の設定方針

シャード数はインデックス作成後に分割数を変更できないため、作成時にある程度方針を決めておく必要がある。
将来的に、データサイズもノード数も増える可能性があるのなら、ノード数よりもシャード数が多い設定としておくのが望ましい。
この状態にすると一部のノードに複数シャードがあるために検索クエリの際に重複するシャードの分だけ多少のオーバーヘッドが見込まれるが、それほど大きなものではない。それよりもデータ数が増大した場合にノード拡張による負荷分散が行えなくなる方がデメリットが大きいと考えられる。
もし、インデックスにこれ以上格納できなくなるような状態に陥ったら、新たにインデックスを作成するか、既存のインデックスを作り直す(再インデックス)。