はじめに
検索基盤部の内田です。検索基盤部は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.Config
のGoDocには、各フィールドがどのような設定項目なのかがまとめられており、何も設定しなかった場合のデフォルト値も記載されています。しかし、デフォルト値の記載がない一部のフィールドについては、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.Config
のTransport
フィールドに渡すことで実現できます。この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では、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。