Go言語のElasticsearchクライアントに触れての知見共有

ogp

はじめに

検索基盤部の内田です。検索基盤部はZOZOTOWNの商品検索ロジックや検索動線上の各機能の改善に取り組んでいます。検索機能に関連したバックエンド実装にはJavaを使うことが多かったのですが、近年ではGo言語を採用することも増えてきました。

この記事は、Go言語で実装したWeb APIからElasticsearchへの検索処理を実装した際に調べたことをまとめたものです。Go言語でElasticsearchを取り扱うみなさまの助けとなれば幸いです。

2つのElasticsearchクライアント

Go言語のElasticsearchクライアントについて調べると、主に以下の2つのライブラリが使われているのが見受けられます。

elastic/go-elasticsearchは、Elasticsearchを提供するElastic公式のクライアントです。公開されたのは2019年末と比較的最近で、サポートしているElasticsearchのバージョンも6から8系と新しめのものです。今後もElasticsearchのバージョンアップに合わせてアップデートされると思われるため、Go言語でElasticsearchを取り扱う際の有力な候補となるクライアントです。

一方のolivere/elasticはサードパーティ製クライアントであり、2012年に公開されて以来、Go言語でElasticsearchを扱う際の有力な選択肢として長い期間利用されてきました。サポートしているElasticsearchのバージョンは1から7系と幅広く、長い間コミュニティを支えてきたことが伺い知れます。しかし、後発の公式ライブラリの充実に伴い、olivere/elasticは2022年3月の更新を最後に開発が終了し、現在の利用は非推奨となっています。

現在、Go言語のElasticsearchクライアントを利用するならば、公式クライアントであるelastic/go-elasticsearchが最有力候補となります。しかし、公式クライアントの登場は比較的最近でolivere/elasticから主流が移ってまだ日が浅いため、参考となる資料があまり豊富ではありません。また、Elasticが公開しているドキュメントも現時点ではあまり充実していません。

公式クライント elastic/go-elasticsearch についての知見

この節では、elastic/go-elasticsearchを利用して検索処理を実装した際に調べたことを紹介します。執筆時点でのelastic/go-elasticsearchの最新バージョンはv8.9.0です。

2種類のクライアント

elastic/go-elasticsearchを用いて検索処理を実行するには、まずクライアントを生成する必要があります。クライアントを生成する関数にはelasticsearch.Config構造体を渡します。この構造体では、Elasticsearchへの接続や認証、通信に関する設定などを行うことができます。 認証については公式ドキュメントが詳しいのでご参照ください。通信に関する設定については本記事で後述します。

v8.9.0現在、クライアントには以下の2種類があります。基本的に提供されている機能は同じで通信処理なども共通ですが、機能の呼び出し方が異なります。

  • elasticsearch.Client
  • elasticsearch.TypedClient

elasticsearch.Client

elasticsearch.Clientは初期から存在するデフォルトのクライアントです。elasticsearch.NewDefaultClient関数はこちらのクライアントを生成します。提供されているAPIはesapiパッケージで確認できます。パッケージが巨大で、GoDocページが重めになっているのでご注意ください。

// type Client struct {
//     BaseClient
//     *esapi.API
// }

es, _ := elasticsearch.NewClient(elasticsearch.Config{
    // 接続や認証、通信に関する設定をここに書く
})

body := bytes.NewReader([]byte(`
{
    "query": {
        "match_all":{}
    }
}
`))
res, _ := es.Search(
    es.Search.WithContext(ctx),
    es.Search.WithIndex("index-name"),
    es.Search.WithBody(body),
)
// ↓のように書いてもいい
// req := esapi.SearchRequest{
//     Index: []string{"index-name"},
//     Body:  body
// }
// res, _ := req.Do(ctx, es)
defer res.Body.Close()

body, _ := io.ReadAll(res.Body)

// res.Bodyから読みだしたbyte列をjson.Unmarshalなどに渡す

elasticsearch.Clientにはesapi.API構造体へのポインタが埋め込まれているため、esapi.API構造体が持つフィールドや*esapi.API型に紐づいたメソッドを呼び出すことができます。検索の実行に対応するメソッドはSearchです。Searchメソッドを呼び出すと、内部ではesapi.SearchRequest構造体が生成され、そのDoメソッドが呼び出されるようになっています。実装を確認したかぎり*esapi.API型に紐づいたメソッドは基本的にすべて、対応するRequest構造体を生成してそのDoメソッドを呼び出すようになっているようです。そのため、*esapi.API型に紐づいたメソッドの具体的な処理内容や設定可能な項目について知りたいときは、対応するRequest構造体のドキュメントや実装を調べるといいでしょう。埋め込まれた*esapi.APIのメソッドを呼び出すのではなく、Request構造体を直接生成してDoメソッドを呼び出すように実装するのもシンプルでおすすめできます。

Elasticsearchの処理の実行結果は、呼び出したAPIの種類に関わらず*esapi.Response型で返されます。Bodyフィールドからバイト列を読み出し、呼び出したAPIに応じてJSONをパースし各要素にアクセスする必要があります。

elasticsearch.TypedClient

elasticsearch.TypedClientはv8.4.0から追加された新しいクライアント実装です。提供されているAPIはtypedapiパッケージで確認できます。こちらのクライアント実装は公式ドキュメントに専用のページが用意されていて、今後はこちらを推していきたいという雰囲気を感じます。

// type TypedClient struct {
//     BaseClient
//     *typedapi.API
// }

es, _ := elasticsearch.NewTypedClient(elasticsearch.Config{
    // 接続や認証、通信に関する設定をここに書く
})

res, _ := es.Search().Request(&search.Request{
    Query: &types.Query{
        MatchAll: &types.MatchAllQuery{},
    },
}).Index("index-name").Do(ctx)

// resは*search.Response型
// 型付けされているため、res.Hits.Hits[0]のようにして各要素にアクセスできる

elasticsearch.TypedClient構造体にはtypedapi.API構造体へのポインタが埋め込まれています。elasticsearch.Clientの場合と同様に埋め込まれた構造体のフィールドやメソッドにアクセスできます。こちらはメソッドチェーンの形でリクエスト内容の構築と実行ができるようになっています。

こちらのクライアントの特徴は、機能ごとにパッケージが切られて構造体や処理がまとめられていることです。例えば、検索の機能はsearchパッケージにまとめて定義されています。機能ごとに型付けされた構造体の操作でリクエスト内容の構築やレスポンス内容へのアクセスができるため、elasticsearch.Clientと比べるとJSON文字列の扱いを省略できる分シンプルかつ安全に取り扱うことができます。もちろん、io.Reader型を引数として受け取るRawメソッドも用意されているので、従来どおりJSON文字列としてリクエストの内容を設定することもできます。Rawメソッドを利用する場合は、Requestメソッドで渡した内容は無視されます(該当箇所)。

後発の実装なだけあり、elasticsearch.Clientと比べてよく整理されているので、elasticsearch.TypedClientの利用から検討してみるといいと思います。ただし、歴史の浅いelastic/go-elasticsearchの中でもelasticsearch.TypedClientは新しいクライアントであるため、現時点では資料があまりありません。公式ドキュメントやGoDocを読んで自分のやりたい処理に対応するパッケージを調べましょう。

通信処理

通信に関する設定はelastic/go-elasticsearchのelasticsearch.Configで行うことができます。一方、通信処理の実装自体は別ライブラリelastic/elastic-transport-goに切り出されています。

elasticsearch.Clientもしくはelasticsearch.TypedClientを初期化すると、elastic/elastic-transport-goのelastictransport.New関数が呼び出され、elastictransport.Clientが生成されます。生成されたelastictransport.Clientはクライアント構造体の中に格納され、すべての通信処理を担います。elasticsearch.Configで設定したほとんど全ての項目はこのelastictransport.Clientの生成時に利用されます。

elasticsearch.ConfigGoDocには、各フィールドがどのような設定項目なのかがまとめられており、何も設定しなかった場合のデフォルト値も記載されています。しかし、デフォルト値の記載がない一部のフィールドについては、elastic/elastic-transport-goを調べる必要があります。

例えば、コネクションや細かいタイムアウトの設定ができるTransportフィールドについてelastic/go-elasticsearchのドキュメントにはデフォルト値の記載がありません。しかし、elastic/elastic-transport-goを見ると、何も指定されなかった場合にelastictransport.New関数内でhttp.DefaultTransportが使われるようになっていることが分かります(該当箇所)。そのため、デフォルトの挙動を踏襲しつつ一部の設定を変えたい場合は、以下のようにhttp.DefaultTransportを元に生成したTransportの一部の設定を書き換えて利用するのがいいでしょう。

// http.DefaultTransportをコピーする
tr, _ := http.DefaultTransport.(*http.Transport)
t := tr.Clone()

// DefaultTransportの値から変更したい項目を設定する
t.MaxIdleConns = maxIdleConns
t.MaxIdleConnsPerHost = maxIdleConnsPerHost
t.MaxConnsPerHost = maxConnsPerHost
t.IdleConnTimeout = idleConnTimeout

cfg := elasticsearch.Config{
    // 接続や認証、通信に関する設定をここに書く

    Transport: t,
}

Datadog APMとの連携

ZOZOTOWNではサービスの監視にDatadog APMを利用しています。Datadogからは公式のGoクライアントであるDataDog/dd-trace-goが公開されており、その中にelastic/go-elasticsearchと連携するための実装が含まれています。この実装を利用することで、クライアントの内部で行われているElasticsearchとの通信処理をトレースできます。ファイル名にv6と書かれていて不安になりますが、Elasticsearchの7系や8系でも利用可能です。

Elasticsearchとの通信処理のトレースは、NewRoundTripper関数で生成したオブジェクトをelasticsearch.ConfigTransportフィールドに渡すことで実現できます。このRoundTripperもデフォルトではhttp.DefaultTransportを元に生成されます(該当箇所)。先述したようなTransportのカスタマイズを行いたい場合は、下記のようにWithTransport関数を使って元となるhttp.RoundTripperインタフェースを満たす実装を渡す必要があります。

import (
    elastictrace "gopkg.in/DataDog/dd-trace-go.v1/contrib/elastic/go-elasticsearch.v6"
)

t := MyRoundTripper()

cfg := elasticsearch.Config{
    // 接続や認証、通信に関する設定をここに書く

    Transport: elastictrace.NewRoundTripper(
        elastictrace.WithServiceName("service-name"),
        elastictrace.WithTransport(t), // http.DefaultTransportではなく、自分で用意したRoundTripperをベースにRoundTripperを生成させる
    ),
}

まとめ

Go言語におけるElasticsearchクライアントについて紹介しました。改めて、本記事の概要を以下に列挙します。

  • メジャーなクライアントライブラリが2種類ありますが、elastic/go-elasticsearchの利用をおすすめします
  • elastic/go-elasticsearchの中にさらに2種類のクライアント実装がありますが、elasticsearch.TypedClientの利用をおすすめします
  • 通信に関する処理は別ライブラリelastic/elastic-transport-goに切り出されているので、分からないことがあったらこちらの実装を調べると解決することがあります
  • Datadog公式クライアントにはelastic/go-elasticsearchと連携するための実装が含まれているので、これを利用することでクライアント内部の通信処理をトレースできます

おわりに

ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。

corp.zozo.com

カテゴリー