個人開発でgo-elasticsearchを用いたので、使い方を整理する

最近個人開発でElasticsearchを利用しまして、それに伴いgolangからアクセスする公式のgoクライアントを使用しました。
その際に感じたのがsearchやinsertあたりしか掲載されていないネット記事が多く感じたので、今回それ以外のとこも含めてまとめてみようと思います。

# 注意点
* この記事のコードはのelasticsearch 8系を想定しています。8系以下のバージョンは別途別の記事を参考にしてください。(個人開発で使用したのは8.3.3)

* 個人開発で用いた際に確認した環境はDockerとwindows11(ローカル上)です。

# how to
# client作成
client作成は[GitHub](https://github.com/elastic/go-elasticsearch)のusageや[qiitaの記事](https://qiita.com/shiei_kawa/items/d992f7fdd4c75906ea0b)に乗っていますので割愛します。

* 一応上記の記事に乗っていない細かいこととして
もしdocker-composeでgoとelasticsearchを別々のイメージで保持しコンテナのnetworkでつなげている場合は、下記のようにConfig構造体のAddresses配列を変更すれば動作します。
```
	cfg := elasticsearch.Config{
		Addresses: []string{
            // elasticsearchのnetworksの名前はesの場合,
			"http://es:9200", // http:// "networks name":port
		},
	}
	client, err := elasticsearch.NewClient(cfg)
	if err != nil {
		panic(err.Error())
	}
	
```

# go-elasticsearchのコードの構造
elasticsearchに対するCURD処理におけるgo-elasticsearchのコードは、下記の２つが主体になっています。
-  〇〇Request 構造体がIndexやSearch、Deleteなど役割ごとに定義
- 役割ごとに定義された〇〇Request構造体のメソッドとして共通してDo関数が存在。Do関数を実行することで実際にelasticsearchにリクエストを送付します。
- 上記２つをパッケージングした関数が存在

なので主な使い方として２種類存在することになり、serachを例にあげると下記のコードになります。
__1. searchRequest構造体から用いる場合__
```
// query はelasticsearchに送付するjsonの内容
if err := json.NewEncoder(&buf).Encode(query); err != nil {
    log.Fatalf("Error encoding query: %s", err)
  }
req := esapi.SearchRequest{
		Index: []string{"test"},
		Body: &buf,
		TrackTotalHits: true,
		Pretty: true,
	}
res, err = req.Do(context.Background(), es)
```
__2. searchRequest構造体とDoメソッドをひとまとめにした関数の場合__
```
// query はelasticsearchに送付するjsonの内容
if err := json.NewEncoder(&buf).Encode(query); err != nil {
    log.Fatalf("Error encoding query: %s", err)
  }
res, err = es.Search(
    es.Search.WithContext(context.Background()),
    es.Search.WithIndex("test"),
    es.Search.WithBody(&buf),
    es.Search.WithTrackTotalHits(true),
    es.Search.WithPretty(),
  )
```
search等の返却された情報からデータを取り出したい場合でも、どちらの手段を用いても返却されるレスポンス(コード上は変数res)の内容に変化はありません。

どちらが良いのかはケースバイケースな気がするので、どちらかの肩を持つことはしませんが、私が個人開発で使用したのは __1のRequest構造体から使用した場合__ のため、以降この記事では1の方法で説明します。

# create(Document追加)
Elasticsearchに対しドキュメントを追加する際は、GitHubのusageにも載っていますが、IndexRequest(またはIndexメソッド)を用います。
```
data, err := json.Marshal(struct{ Title string }{Title: title})
if err != nil {
    log.Fatalf("Error marshaling document: %s", err)
}

// Set up the request object.
req := esapi.IndexRequest{
    Index:      "test",
    DocumentID: strconv.Itoa(i + 1),
    Body:       bytes.NewReader(data),
    Refresh:    "true",
 }

res, err := req.Do(context.Background(), es)
```
基本的にはIndexとDocumentIDを設定し、追加したいドキュメントの構造を定義したオブジェクトをBodyに設定すれば大丈夫です。

戻り値のresはhttp.Responseのラッパーなようになっているので、IsError()で500エラーなどの判定が可能です。またres.Status()でコードを直接確認できます。



# create(Index作成)
ドキュメントを追加した際のコードを同じ方法でIndexを作成することも可能です。
- 作成したいインデックスのsettingやmappingsをmap[string]interface()などで定義します
```
    //個人開発のコードなので、あくまで一例です
    query := map[string]interface{}{
		"settings": map[string]interface{}{
			"number_of_shards":   3,
			"number_of_replicas": 1,
		},
		"mappings": map[string]interface{}{
			"properties": map[string]interface{}{
				"title": map[string]interface{}{
					"type": "text",
				},
				"url": map[string]interface{}{
					"type": "text",
				},
				"text": map[string]interface{}{
					"type": "text",
				},
				"folderId": map[string]interface{}{
					"type": "text",
				},
				"trashed": map[string]interface{}{
					"type": "boolean",
				},
				"date": map[string]interface{}{
					"type":   "date",
					"format": "yyyy-MM-dd",
				},
				"favicon": map[string]interface{}{
					"type": "text",
				},
			},
		},
	}
```
- ドキュメントを追加しないのでDocumentIDは設定せずに構造体を定義し、Doメソッドを実行します
```
    data, err := json.Marshal(query)
	if err != nil {
		return fmt.Errorf("error marshaling documnt: %s", err)
	}
	req := esapi.IndexRequest{
		Index:   "新しく作成したインデックス名",
		Body:    bytes.NewReader(data),
		Refresh: "true",
	}
    res, err := req.Do(context.Background(), es)
    
```
これでドキュメントを追加せずにインデックスを作成することが可能です。

# update
ドキュメントを更新したい場合もこれまでと同様で更新する内容をmap[string]interface()などで定義し、Request構造体の際に更新するドキュメントのIDを設定してからDoメソッドを実行します。
```
    query := map[string]interface{}{
		"doc": map[string]interface{}{
			"folderId": folderID,
			"url":      url,
			"text":     text,
			"favicon":  favicon,
			"trashed":  false, 
		},
	}
    data, err := json.Marshal(query)
	if err != nil {
		return fmt.Errorf("error marshaling documnt: %s", err)
	}
	req := esapi.UpdateRequest{
		Index:      "test",
		DocumentID: "testdoc",
		Body:       bytes.NewReader(data),
		Refresh:    "true",
	}
	res, err := req.Do(context.Background(), es.client)
	if err != nil {
		return fmt.Errorf("error response: %s", err)
	}
```
### 条件を指定してデータを一括更新する場合
sql的には where句で更新対称を指定するパターンです。
この場合も基本的な流れは変わりませんが、使用する構造体がUpdateRequestからUpdateByQueryRequestへ変化します。

```
    query := map[string]interface{}{
		"query": map[string]interface{}{
			"match": map[string]interface{}{
				"folderId": folderId,
			},
		},
		"script": map[string]interface{}{
			"source": "ctx._source.trashed = params.isTrash",
			"lang":   "painless",
			"params": map[string]interface{}{"isTrash": true},
		},
	}
    data, err := json.Marshal(query)

	if err != nil {
		return fmt.Errorf("error marshaling documnt: %s", err)
	}
	req := esapi.UpdateByQueryRequest{
		Index: []string{es.index},
		Body:  bytes.NewReader(data),
	}
	res, err := req.Do(context.Background(), es.client)
	if err != nil {
		return fmt.Errorf("error response: %s", err)
	}
```
# delete(Document)
DocumentIDを指定して削除が可能です。
```
    req := esapi.DeleteRequest{
		Index:      "test",
		DocumentID: "testdoc",
		Refresh:    "true",
	}
	res, err := req.Do(context.Background(), es.client)
	if err != nil {
		return fmt.Errorf("error response: %s", err)
	}
```

# delete(index)
indexを削除する場合はIndicesDeleteRequest構造体を用います。
```
    req := esapi.IndicesDeleteRequest{
		Index: []string{"test"},
	}
	res, err := req.Do(context.Background(), es)
	if err != nil {
		return fmt.Errorf("error response: %s", err)
	}
```

# count
ドキュメントをカウントする場合は CountRequest構造体を用います。
```
    var buf bytes.Buffer
	if err := json.NewEncoder(&buf).Encode(query); err != nil {
		return 0.0, fmt.Errorf("error encoding query: %s", err)
	}
    req := esapi.CountRequest{
		Index: []string{"test"},
		Body:  &buf,
	}
	res, err := req.Do(context.Background(), es.client)
```
カウントされた値はres.Bodyをデコードすることで取得できます。

---
以上長々と簡単な使い方をまとめてみました。
使い方をまとめておいてなんですが、[go-elasticsearch](https://github.com/elastic/go-elasticsearch)のコードはかなり読みやすいので間違いなく使用するには公式のコードを読むことをお勧めします。