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


これはGo Advent Calendar 2020の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作成

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

elasticsearch.NewDefaultClient()

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

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は以下のように使用します。


  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がこんな感じだと

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

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メソッドを叩きます。

  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でチェックしてあげてください。


    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など様々なオプションの定義ができることがみて取れますね。


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クライアントの紹介になりました。
少しコード量が多くなってしまう場合もありますが、公式がメンテをしてくれることもあり安心して利用のできるライブラリなので使って損はないと思います。