Elasticsearch 超入門 #1 〜IndexとDocument〜

Elasticsearchとは公式サイトに以下記載ある通り、Elastic Stackの中核をなすプロダクトです。

> Elasticsearchは、様々なユースケースを解決する分散型RESTful検索/分析エンジンです。 データを一元的に格納することで、超高速検索や、関連性の細かな調整、パワフルな分析が大規模に、手軽に実行可能になります。Elastic Stackの心臓部となるプロダクトです。

https://www.elastic.co/jp/elasticsearch/

私がこれまでよく使ってきた方法としてはロギングに使用して、調査時に全文検索するという形ですが、その他にも様々な用途で使用ができます。

# ローカル環境構築

今回は気軽に構築する方法として公式dockerイメージを使ってローカル環境を構築する手順を紹介します。

使用するのは公式イメージ

https://hub.docker.com/_/elasticsearch

入門用のリポジトリを用意してますので、まずはクローンしてみてください。

https://github.com/hapoon/introduction-elasticsearch

今回使用するのはクローンしたリポジトリ内の `lesson-01` に入っています。

その中の 

```yaml:compose.yaml
services:
  es01:
    image: elasticsearch:8.4.0
    container_name: es01
    environment:
      - node.name=es01
      - cluster.name=es-docker-cluster
      - discovery.type=single-node
      - xpack.security.enabled=false
    ulimits:
      memlock:
        soft: -1
        hard: -1
    volumes:
      - data01:/usr/share/elasticsearch/data
    ports:
      - 9200:9200
    networks:
      - elasticstack
volumes:
  data01:
    driver: local

networks:
  elasticstack:
    driver: bridge
```

を実行してみましょう。

```shell-session
docker-compose up -d
```

ここまで終えたら一度 http://localhost:9200/ にアクセスしてみましょう。

以下のような情報が表示されていればOKです。

```json
{
  "name" : "es01",
  "cluster_name" : "es-docker-cluster",
  "cluster_uuid" : "piL1NiCDTaqeGWYM2CJeDQ",
  "version" : {
    "number" : "8.4.0",
    "build_flavor" : "default",
    "build_type" : "docker",
    "build_hash" : "f56126089ca4db89b631901ad7cce0a8e10e2fe5",
    "build_date" : "2022-08-19T19:23:42.954591481Z",
    "build_snapshot" : false,
    "lucene_version" : "9.3.0",
    "minimum_wire_compatibility_version" : "7.17.0",
    "minimum_index_compatibility_version" : "7.0.0"
  },
  "tagline" : "You Know, for Search"
}
```

# インデックス作成

現状はまだElasticsearchを起動しただけで何もない状況です。

まずはインデックスを作成していきます。
インデックスは相互に関連づけられたドキュメントの集合です。
インデックスとドキュメントの関係性は以下の図のような感じです。

```mermaid
graph
    subgraph Elasticsearch
        subgraph users[Index:users]
            doc_1[Document:user<br>id:1<br>name:foo<br>age:100]
            doc_2[Document:user<br>id:2<br>name:bar<br>age:10]
        end
        subgraph index2[Index:xxxxs]
            doc_3[Document:xxxx<br>id:1<br>x:12<br>y:34]
        end
        style users fill:pink
        style index2 fill:pink
    end
```

早速 `users` という名前のインデックスを作成してみます。

```shell-session
curl -X PUT 'localhost:9200/users?pretty'
```

```json:実行結果
{
  "acknowledged" : true,
  "shards_acknowledged" : true,
  "index" : "users"
}
```

ちなみに `pretty` をパラメータに付与することでレスポンスのJSONが整形されます。

# インデックス確認

`_cat` は[Compact and Aligned Text API](https://www.elastic.co/guide/en/elasticsearch/reference/8.4/cat.html)の略のようです。
(自分は最初Linuxの `cat` コマンドと同じく `concatenate` の略だと思ってました...)
`indices` は `index` の複数形です。
`v` パラメータは `verbose output` 切り替えのためのオプションで付けることでキー名が表示されます。

```shell-session
curl -X GET 'localhost:9200/_cat/indices?v'
```

```shell-session:実行結果
health status index uuid                   pri rep docs.count docs.deleted store.size pri.store.size
yellow open   users 81oNdJlaTRSANvhC6AFMjg   1   1          0            0       225b           225b
```

できてますね。
続いて `users` インデックスを取得してみると `aliases` や `mappings` が空で `settings` のみ設定されていることを確認できます。
`alias` や `mapping` については初回から扱うと複雑になるので、次回以降に説明します。

```
curl -X GET 'localhost:9200/users?pretty'
```

```json:実行結果
{
  "users" : {
    "aliases" : { },
    "mappings" : { },
    "settings" : {
      "index" : {
        "routing" : {
          "allocation" : {
            "include" : {
              "_tier_preference" : "data_content"
            }
          }
        },
        "number_of_shards" : "1",
        "provided_name" : "users",
        "creation_date" : "1661394180559",
        "number_of_replicas" : "1",
        "uuid" : "81oNdJlaTRSANvhC6AFMjg",
        "version" : {
          "created" : "8040099"
        }
      }
    }
  }
}
```

# ドキュメントの追加

それではドキュメントを作成してみましょう。

```shell-session
curl -X POST 'localhost:9200/users/_doc/?pretty' -H 'Content-Type:application/json' -d '{
  "name": "foo",
  "address": "foo bar",
  "age": 100
}'
```

```json:実行結果
{
  "_index" : "users",
  "_id" : "I1xy4oIBJ9pAjFjtFcni",
  "_version" : 1,
  "result" : "created",
  "_shards" : {
    "total" : 2,
    "successful" : 1,
    "failed" : 0
  },
  "_seq_no" : 0,
  "_primary_term" : 1
}
```

作成されました。
POSTメソッドでid指定なしで作成したので、 `_id` はランダムで生成されています。

# ドキュメントの確認

```
curl -X GET 'localhost:9200/_search?pretty' -H 'Content-Type:application/json' -d '{
  "query": {
    "bool": {
      "must": [
        {"match":{"name":"foo"}}
      ]
    }
  }
}'
```

```json:実行結果
{
  "took" : 38,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 1,
      "relation" : "eq"
    },
    "max_score" : 0.2876821,
    "hits" : [
      {
        "_index" : "users",
        "_id" : "I1xy4oIBJ9pAjFjtFcni",
        "_score" : 0.2876821,
        "_source" : {
          "name" : "foo",
          "address" : "foo bar",
          "age" : 100
        }
      }
    ]
  }
}
```

# ドキュメントの更新

先ほど作成したドキュメントの `age` を `10` に更新してみます。
先ほどPOSTした際のレスポンスで返ってきたIDを指定しjsonの一部を修正してPUTすると更新されます。
下記の `_doc` の後ろは置き換えて実行してください。

```shell-session
curl -X PUT 'localhost:9200/users/_doc/I1xy4oIBJ9pAjFjtFcni?pretty' -H 'Content-Type:application/json' -d '{
  "name": "foo",
  "address": "foo bar",
  "age": 10
}'
```

```json:実行結果
{
  "_index" : "users",
  "_id" : "I1xy4oIBJ9pAjFjtFcni",
  "_version" : 2,
  "result" : "updated",
  "_shards" : {
    "total" : 2,
    "successful" : 1,
    "failed" : 0
  },
  "_seq_no" : 1,
  "_primary_term" : 1
}
```

実際に更新されたか確認してみます。

```shell-session
curl -X GET 'localhost:9200/_search?pretty' -H 'Content-Type:application/json' -d '{
  "query": {
    "bool": {
      "must": [
        {"match":{"name":"foo"}}
      ]
    }
  }
}'
```

`age` が `100` から `10` になっていることが確認できます。

```json:実行結果
{
  "took" : 1001,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 1,
      "relation" : "eq"
    },
    "max_score" : 0.2876821,
    "hits" : [
      {
        "_index" : "users",
        "_id" : "I1xy4oIBJ9pAjFjtFcni",
        "_score" : 0.2876821,
        "_source" : {
          "name" : "foo",
          "address" : "foo bar",
          "age" : 10
        }
      }
    ]
  }
}
```

# ドキュメントの削除

先ほど作ったドキュメントを削除してみましょう。
更新時同様にIDは置き換えてください。

```shell-session
curl -X DELETE 'localhost:9200/users/_doc/I1xy4oIBJ9pAjFjtFcni?pretty'
```

```json:実行結果
{
  "_index" : "users",
  "_id" : "I1xy4oIBJ9pAjFjtFcni",
  "_version" : 3,
  "result" : "deleted",
  "_shards" : {
    "total" : 2,
    "successful" : 1,
    "failed" : 0
  },
  "_seq_no" : 2,
  "_primary_term" : 1
}
```

もう一度 `name:foo` ドキュメントにアクセスしようとすると

```shell-session
curl -X GET 'localhost:9200/_search?pretty' -H 'Content-Type:application/json' -d '{
  "query": {
    "bool": {
      "must": [
        {"match":{"name":"foo"}}
      ]
    }
  }
}'
```

hitしなくなりました。

```json:実行結果
{
  "took" : 401,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 0,
      "relation" : "eq"
    },
    "max_score" : null,
    "hits" : [ ]
  }
}
```

# インデックス削除

ここで最後にインデックスの削除も試してみましょう。

```shell-session
curl -X DELETE 'localhost:9200/users?pretty'
```

```json:実行結果
{
  "acknowledged" : true
}
```

以上で、ローカル環境にElasticsearch環境〜インデックス作成〜ドキュメント操作までの一連の流れとなります。

最後はお片付け。

```shell-session
docker-compose down -v
```

次回へ続く(はず)