elasticが開発した公式のGo言語ElasticSearchクライアントについてまとめてみる

これは[Go Advent Calendar 2020](https://qiita.com/advent-calendar/2020/go)の8日目の記事です。

先日業務の中でElasticSearchを利用する機会があり、elasticがサポートしている公式のGoクライアントをその際にあまり日本語でまとまっていた情報がなかったので、これを機にまとめてみようと思います。

go-elasticsearch
https://github.com/elastic/go-elasticsearch

# 概要
この公式ライブラリは2019年にリリースされた比較的新しいもので、Elasticの公式のクライアントとして認定され、メンテナンスされています。
https://www.elastic.co/guide/en/elasticsearch/client/index.html

go-elasticsearchクライアントはバージョン6系と7系がありますが、これはそれぞれElasticSearchの6系、7系に対応するものになっているので、使用するElasticSearchのバージョンに合わせて利用するライブラリのバージョンは決定してください。

# 使い方

## Client作成

クライアント作成は２パターンあります。まずNewDefaultClientです。こちらは引数を取らないものですが、 ELASTICSEARCH_URLという環境変数にElasticSearchのエンドポイントURLを入れておくことで自動で設定してくれます。

```go
elasticsearch.NewDefaultClient()

```

elasticsearch.NewClient(Config)は色々とオプションを追加できるクライアントの作成方法になります。Elastic Cloudなどを利用する場合はアドレスではなく、IDでも接続することができます。この場合はELASTICSEARCH_URLに設定された環境変数は無視されます。
CACertで証明書、RetryOnStatusでリトライするStatusの定義なども盛り込むことが可能です。

```go
cert, _ := ioutil.ReadFile("path/to/ca.crt")

cfg := elasticsearch.Config{
  Addresses: []string{
    "http://localhost:9200",
    "http://localhost:9201",
  },
  Username: "foo",
  Password: "bar",
  RetryOnStatus: []int{429, 502, 503, 504},
  CACert: cert,
  Transport: &http.Transport{
    MaxIdleConnsPerHost:   10,
    ResponseHeaderTimeout: time.Second,
    DialContext:           (&net.Dialer{Timeout: time.Second}).DialContext,
    TLSClientConfig: &tls.Config{
      MinVersion:         tls.VersionTLS11,
    },
  },
}

elasticsearch.NewClient(cfg)

```

## Search

検索サジェストなどで用いるSearchは以下のように使用します。

```go

  var buf bytes.Buffer
  query := map[string]interface{}{
    "query": map[string]interface{}{
      "match": map[string]interface{}{
        "title": "test",
      },
    },
  }
  if err := json.NewEncoder(&buf).Encode(query); err != nil {
    log.Fatalf("Error encoding query: %s", err)
  }

  // Perform the search request.
  res, err = es.Search(
    es.Search.WithContext(context.Background()),
    es.Search.WithIndex("test"),
    es.Search.WithBody(&buf),
    es.Search.WithTrackTotalHits(true),
    es.Search.WithPretty(),
  )
  if err != nil {
    log.Fatalf("Error getting response: %s", err)
  }
  defer res.Body.Close()

  if res.IsError() {
    var e map[string]interface{}
    if err := json.NewDecoder(res.Body).Decode(&e); err != nil {
      log.Fatalf("Error parsing the response body: %s", err)
    } else {
      // Print the response status and error information.
      log.Fatalf("[%s] %s: %s",
        res.Status(),
        e["error"].(map[string]interface{})["type"],
        e["error"].(map[string]interface{})["reason"],
      )
    }
  }

```
少し複雑ですが、queryに実際のリクエストで投げるJsonの構造体を参考にmap[string]interface()を定義して、検索する文字列を入れます。

HTTPでリクエスト送る際のJsonBodyがこんな感じだと

```json
{
  "size": 5,
  "query": {
    "bool": {
      "should": [{
        "match": {
          "word.autocomplete": {
            "query": "え"
          }
        }
      }, {
        "match": {
          "word.readingform": {
            "query": "え",
            "fuzziness": "AUTO",
            "operator": "and"
          }
        }
      }]
    }
  },
}'

```

Goで定義するクエリはこんな感じになります。なかなか複雑ですね・・・。

```go
query := map[string]interface{}{
	"query": map[string]interface{}{
		"bool": map[string]interface{}{
			"should": []map[string]interface{}{
				{
					"match": map[string]interface{}{
						"word.autocomplete": map[string]interface{}{
							"query": normalized,
						},
					},
				},
				{
					"match": map[string]interface{}{
						"word.readingform": map[string]interface{}{
							"query":     normalized,
							"fuzziness": "AUTO",
							"operator":  "and",
						},
					},
				},
			},
		},
	},
}

```

そしてその後それをjsonにエンコードし、Searchメソッドの引数にWithBody内に入れ、Searchメソッドを叩きます。

```go
  if err := json.NewEncoder(&buf).Encode(query); err != nil {
    log.Fatalf("Error encoding query: %s", err)
  }

  // Perform the search request.
  res, err = es.Search(
    es.Search.WithContext(context.Background()),
    es.Search.WithIndex("test"),
    es.Search.WithBody(&buf),
    es.Search.WithTrackTotalHits(true),
    es.Search.WithPretty(),
  )

```

他にも多くの引数があることが見て取れます。withSortなどを用いるとSortなども可能となっています。

Searchメソッドのレスポンスはhttp.Responseのラッパーなようになっています。また、IsError()メソットで500エラーなどの判定をすることが可能です、



Searchなどとは異なり、IndexやCreate,Updateは比較的シンプルに記載することができます。基本的にそれぞれのXXRequestという型がgo-elasticのesapiパッケージに用意されているため、そこにリクエストする値を入れてDoメソッドを叩く形になります。

ここもレスポンスはIsErrorでチェックしてあげてください。

```go

	tag := Sample{
		ID:   id,
		Name: name,
	}

	reqByte, err := json.Marshal(tag)
	if err != nil {
		return err
	}

	requestReader := bytes.NewReader(reqByte)

	req := esapi.CreateRequest{
		Body:   requestReader,
		Pretty: true,
	}
	res, err := req.Do(ctx, r.client)
	if err != nil {
		return xerrors.Errorf("failed to update with elastic search. %w", err)
	}

	if res.IsError() {
		return xerrors.Errorf("failed to update with elastic search. Not ok. %s", res.Status())
	}
	defer res.Body.Close()

```
様々なオプションもその型の中で定義することが可能です。試しにUpdateRequest型を見てみましょう。基本的なリクエストのボディをBodyに格納する形になりますがHeaderやPrettyなど様々なオプションの定義ができることがみて取れますね。

```go

type UpdateRequest struct {
	Index        string
	DocumentType string
	DocumentID   string

	Body io.Reader

	Fields              []string
	IfPrimaryTerm       *int
	IfSeqNo             *int
	Lang                string
	Parent              string
	Refresh             string
	RetryOnConflict     *int
	Routing             string
	Source              []string
	SourceExcludes      []string
	SourceIncludes      []string
	Timeout             time.Duration
	Version             *int
	VersionType         string
	WaitForActiveShards string

	Pretty     bool
	Human      bool
	ErrorTrace bool
	FilterPath []string

	Header http.Header

	ctx context.Context
}

```

以上がelasticがサポートするElasticSearchのGoクライアントの紹介になりました。
少しコード量が多くなってしまう場合もありますが、公式がメンテをしてくれることもあり安心して利用のできるライブラリなので使って損はないと思います。