Share
Embed
新卒が解説するDMMの検索proxy APIの解体書
24新卒でDMM.comへ入社し、データ基盤開発部への配属されました。本資料では弊チームの持つ検索proxy APIの開発方法について説明します。
社外向けイベントでの登壇資料です
Event: https://dmm.connpass.com/event/335033/
Hinata
November 19, 2024
Transcript
32 slides
-
© DMM © DMM CONFIDENTIAL 新卒が解説するDMMの 検索proxy-APIの解体書 ML基盤
菊地 ひなた 開発統括本部 ML基盤チーム 菊地 ひなた 2024/11/05 -
© DMM 自己紹介 名前 : 菊地 ひなた 経緯 : 24新卒で合同会社
DMM.com に入社 ML基盤チームに所属 趣味 : 料理とお酒が大好きです 最近は魚を捌いたり、野山でキウイを刈ったりしました🥝 2 -
© DMM 目次 1. 弊社サービスと検索proxy 2. 検索proxyの満たすべき条件と詳細 3. 使用技術:goa 4.
検索proxyにおける使われ方用途 5. まとめ 3 -
© DMM 弊社サービスと検索proxy 検索api ( search-api ) の前面でリクエスト・レスポンスに対し変更を行 う
⇒ 検索体験の改善だけでなく様々な施策を担う (例: reranking, a/b test id …) 4 👇ここ -
© DMM 条件を満たすために必要なこと 1. リクエスト・レスポンス仕様がsearch-apiと一致する必要がある 2. クライアントである事業サービスにかかる工数を最小限に 3. 検索改善施策が失敗した際に各施策を撤退・換装
1. search-apiのreq/responseを漏らさずに現状の機能を維持 2. 施策の追加・安定稼働を担保しつつ工数を最小限に抑える 6 -
© DMM 7 [1] https://goa.design/ Goa[1] - 既存APIの利用もあるスキーマ駆動開発パッケージ - API定義/client/server/documentの自動生成が可能
⇒ドメインロジックの実装に集中できる - Goで書ける独自のDSL(Domain Specific Language) -
© DMM Goa[1] 8 [1] Goa :: Design 1. search-apiのreq/responseを漏らさずに現状の機能を維持
2. 施策の追加・安定稼働を担保しつつ工数を最小限に抑える - 既存APIの利用もあるスキーマ駆動開発パッケージ - API定義/client/server/documentの自動生成が可能 ⇒ドメインロジックの実装に集中できる - Goで書ける独自のDSL(Domain Specific Language) -
© DMM GoaのDSL[1] 9 [1] Goa :: Design [2] デザイン概要|Goa
v3 入門 1. API クライアントやopenAPIドキュメントの生成に必 要 2. Service Method groupの定義部分、REST APIのリソー ス・gRPCのサービス定義に相当 3. Type payloadなどの定義 API Service Type ├── Title ├── Description ├── Extend ├── Description ├── Docs ├── Reference ├── Version ├── Security ├── ConvertTo ├── Docs ├── Error ├── CreateFrom ├── License ├── GRPC ├── Attribute ├── TermsOfService ├── HTTP ├── Field ├── Contact ├── Method └── Required ├── Server │ ├── Payload └── HTTP │ ├── Result │ ├── Error │ ├── GRPC │ └── HTTP └── Files [2] 04デザイン概要より -
© DMM GoaのDSL[1] 10 [1] Goa :: Design [2] デザイン概要|Goa
v3 入門 1. API クライアントやopenAPIドキュメントの生成に必 要 2. Service Method groupの定義部分、REST APIのリソー ス・gRPCのサービス定義に相当 3. Type payloadなどの定義 API Service Type ├── Title ├── Description ├── Extend ├── Description ├── Docs ├── Reference ├── Version ├── Security ├── ConvertTo ├── Docs ├── Error ├── CreateFrom ├── License ├── GRPC ├── Attribute ├── TermsOfService ├── HTTP ├── Field ├── Contact ├── Method └── Required ├── Server │ ├── Payload └── HTTP │ ├── Result │ ├── Error │ ├── GRPC │ └── HTTP └── Files [2] 04デザイン概要より -
© DMM 検索proxyにおけるgoaのDSL: API 11 // API describes the global
properties of the API server. var _ = API("searchproxy-api", func() { … Title("検索proxy-api") Description("検索APIのproxy") Server ("app", func() { Host("localhost", func() { URI("http://localhost:8080 ") }) … }) }) Server : クライアントのリクエストを受け付けるプロセス 内部にホストとURIを定義可能 👈 -
© DMM 検索proxyにおけるgoaのDSL: Type 12 var searchResponse = Type("search_response", func()
{ Description("search APIのレスポンス") Attribute("meta", Any, func() { Meta("struct:field:type", "json.RawMessage", "encoding/json") Meta("struct:tag:json", "meta,omitempty") }) … search-apiと一致させるためにとても便利 APIの受け取るペイロード・レスポンス型などを記述 👈 -
© DMM 検索proxyにおけるgoaのDSL: Service 13 var _ = Service("search", func()
{ … Method ("search video contents ", func() { Description("動画の検索API") Payload(searchPayload ) Result(searchResponse ) HTTP(func() { GET("/video/contents") Response(StatusOK, func() { ContentType("application/json") }) Response("NotFound", StatusNotFound) Response("Unauthorized", StatusUnauthorized ) … }) }) … } 👈ここら辺がさっきのTypeに一致 -
© DMM 弊チームのdesignパッケージ階層説明 14 - design/ - service/ - base.go
- 各エンドポイントの親パスの定義(Service) - search.go - エンドポイントの定義・検索proxy/search-apiの接続 (Service, Type) - design.go - 実APIの定義(API) エンドポイント数が少ないので下記の構成でgoaを記述している -
© DMM goaによる自動生成 15 - gen/ - search/ - service.go
- searchサービスのエンドポイント定義 - types.go - searchサービスのデータ型定義 - http/ - search/ - client/ - client.go - searchサービスのHTTPクライアント - endpoint.go - search サービスの各メソッドをエンドポイントとしてラップする+middle wareの処理 - types.go - searchサービスのHTTPクライアント用データ型 - server/ - server.go - searchサービスのHTTPサーバー - types.go - searchサービスのHTTPサーバー用データ型 - openapi.json - openapi.yaml … 記述が終わったらgenコマンドによって対応する実装を自動生成する 生成物はgenディレクトリ内に格納される goa gen <design package path> -
© DMM 自動生成後の流れ 16 service.goに記述されるinterfaceを実装 ⇒ サービスの実装 var _ =
Service("search", func() { … Method ("search video contents", func() { Description("動画の検索API") … }) … } type Service interface { // 動画の検索API SearchVideoContents (context.Context, *SearchPayload) (res *SearchResponse, err error) …} goa gen -
© DMM DMMの検索proxy API 17 機能の一部は以下の通り。幅広い事業領域に向けて検索proxyを提供している // proxy of search
service endpoint type Service interface { // 動画の検索API SearchVideoContents(context.Context, *SearchPayload) (res *SearchResponse, err error) // 同人誌の検索API SearchDoujinContents(context.Context, *SearchPayload) (res *SearchResponse, err error) // dmmブックスのトップページの一般向け書籍を並べ替えるAPI SearchBookTopContents(context.Context, *SearchPayload) (res *SearchResponse, err error) // 一般向け書籍の検索API SearchBookContents(context.Context, *SearchPayload) (res *SearchResponse, err error) … // dmmtvのコンテンツ検索API SearchDmmtvContents(context.Context, *SearchPayload) (res *SearchResponse, err error) // dmm通販のコンテンツ検索API SearchMonoContents(context.Context, *SearchPayload) (res *SearchResponse, err error) } -
© DMM 自動生成後の流れ 18 これらのinterfaceはendpoint.goで公開されている type Service interface { //
動画の検索API SearchVideoContents (context.Context, *SearchPayload) (res *SearchResponse, err error) …} // Endpoints wraps the "search" service endpoints. type Endpoints struct { SearchVideoContents goa.Endpoint …} // NewEndpoints wraps the methods of the "search" service with endpoints. func NewEndpoints(s Service) *Endpoints { return &Endpoints{ SearchVideoContents : NewSearchCoJpVideoContentsEndpoint (s), …} goa gen -
© DMM 初めてGoaに触れて 19 今回はチームが持っている資産を解読して説明する機会をもらえました 普段の業務では既存実装に触れる機会が少ないためとても勉強になりました Goa自体初めて触ったものの、思った以上に読みやすかった です 初めてjoinした自分でもそう感じられる点で有益なツールだと思います
チーム内からは 「コントローラーやミドルウェアの実装部分はわざわざgoaを使わないほうが実装に手間取 られない可能性がある 」 という意見が挙がっていました -
© DMM goaによる自動生成とService,Methodの対応 21 - gen/ - search/ - service.go
- types.go - http/ - search/ - client/ - client.go - endpoint.go - types.go - server/ - server.go - types.go - openapi.json - openapi.yaml … // API describes the global properties of the API server. var _ = API("searchproxy-api ", func() { … Title("検索proxy-api ") Description("検索APIのproxy ") Server("app", func() { Host("localhost", func() { URI("http://localhost:8080 ") }) … }) }) var searchResponse = Type("search_response ", func() { Description ("search APIのレスポンス ") Attribute("meta", Any, func() { Meta("struct:field:type ", "json.RawMessage ", "encoding/json ") Meta("struct:tag:json ", "meta,omitempty ") }) … var _ = Service("search", func() { Method("search video contents ", func() { Description("動画の検索API ") Payload(searchPayload ) Result(searchResponse ) HTTP(func() { GET("/video/contents ") Response(StatusOK, func() { ContentType("application/json ") }) … }) }) … } design/design.go design/service/search.go design/service/search.go -
© DMM goaによる自動生成とTypeの対応 22 - gen/ - search/ - service.go
- types.go - http/ - search/ - client/ - client.go - endpoint.go - types.go - server/ - server.go - types.go - openapi.json - openapi.yaml … // API describes the global properties of the API server. var _ = API("searchproxy-api ", func() { … Title("検索proxy-api ") Description("検索APIのproxy ") Server("app", func() { Host("localhost", func() { URI("http://localhost:8080 ") }) … }) }) var searchResponse = Type("search_response ", func() { Description ("search APIのレスポンス ") Attribute("meta", Any, func() { Meta("struct:field:type ", "json.RawMessage ", "encoding/json ") Meta("struct:tag:json ", "meta,omitempty ") }) … var _ = Service("search", func() { Method("search video contents ", func() { Description("動画の検索API ") Payload(searchPayload ) Result(searchResponse ) HTTP(func() { GET("/video/contents ") Response(StatusOK, func() { ContentType("application/json ") }) … }) }) … } design/design.go design/service/search.go design/service/search.go -
© DMM goaによる自動生成とAPI,Serverの対応 23 - gen/ - search/ - service.go
- types.go - http/ - search/ - client/ - client.go - endpoint.go - types.go - server/ - server.go - types.go - openapi.json - openapi.yaml … // API describes the global properties of the API server. var _ = API("searchproxy-api ", func() { … Title("検索proxy-api ") Description("検索APIのproxy ") Server("app", func() { Host("localhost", func() { URI("http://localhost:8080 ") }) … }) }) var searchResponse = Type("search_response ", func() { Description ("search APIのレスポンス ") Attribute("meta", Any, func() { Meta("struct:field:type ", "json.RawMessage ", "encoding/json ") Meta("struct:tag:json ", "meta,omitempty ") }) … var _ = Service("search", func() { Method("search video contents ", func() { Description("動画の検索API ") Payload(searchPayload ) Result(searchResponse ) HTTP(func() { GET("/video/contents ") Response(StatusOK, func() { ContentType("application/json ") }) … }) }) … } design/design.go design/service/search.go design/service/search.go -
© DMM 施策 24 ML基盤と協業するMLエンジニアたちが自由にパラメータを触れるように定義されている 例として、コンテンツ並び替えでは距離計算・推論を選べる必要がある experiment: typed_config:
… embedding_vector: metric_kind: … repository: user_vector: … content_vector: … query_reranking: word_vector: … query_weight: 0.5 rerank_threshold: 60 -
© DMM goaによる自動生成物の説明[1] 25 type SearchResponse struct { Meta
json.RawMessage `json:"meta,omitempty"` … } var searchResponse = Type("search_response", func() { Description("search APIのレスポンス") Attribute("meta", Any, func() { Meta("struct:tag:json", "meta,omitempty") }) … DSLで記述された内容に対応して定義される -
© DMM gRPC+protobuf 1. 速度などのパフォーマンスを落とさずに現状の機能を維持 2. 施策の追加・安定稼働を担保しつつ工数を最小限に抑える 26 [1]
https://github.com/bufbuild/protoc-gen-validate 1. 施策ごとにproto定義 2. buf generatenによるprotoとGo言語の自動読み替えコードの自動生成 3. pluginにprotoc-gen-validateを使うことでgrpc=>goのvalidationも自動生 成 [1] -
© DMM 各ツールの利点: protobuf 27 (experimentは実際に利用するのがMLエンジニア 試作の開発過程で必要になるパラメータを柔軟にバリデーションできる点が良い .
├── measure_1 │ ├── measure_1.pb.go │ ├── measure_1.pb.validate.go │ ├── measure_1.proto │ └── README.md ... └── measure_N ├── README.md ├── measure_N.pb.go ├── measure_N.pb.validate.go └── measure_N.proto buf.gen.yaml version: v1 plugins: - name: go out: . opt: - paths=source_relative - name: validate out: . opt: - lang=go - paths=source_relative buf.yaml version: v1 deps: - buf.build/envoyproxy/protoc-gen-validate lint: use: - DEFAULT breaking: use: - FILE ☝protoとGOのやりとりの バリデーションコード 👈protoとGOの読み替え -
© DMM 各ツールの利点: goa 28 goa genで一度にクライアント・サーバーコード+grpc+swaggerが作れる - gen/
- search/ - service.go - searchサービスのエンドポイント定義 - types.go - searchサービスのデータ型定義 - http/ - search/ - client/ - client.go - searchサービスのHTTPクライアント - types.go - searchサービスのHTTPクライアント用データ型 - server/ - server.go - searchサービスのHTTPサーバー - types.go - searchサービスのHTTPサーバー用データ型 - openapi.json - openapi.yaml - grpc/ … - grpc/ - search/ - client/ - client.go - searchサービスのgRPCクライアント - types.go - searchサービスのgRPCクライアント用データ型 - server/ - server.go - searchサービスのgRPCサーバー - types.go - searchサービスのgRPCサーバー用データ型 -
© DMM 他手法との比較 29 connect 1. Go意外にもサポート充実(Kotlin, Swift, Node)
2. 2022~ openapi-generator 1. Goa よりも薄くてシンプル 2. yamlを書く必要あり swagger editor使えば良さそうではあるが、、 -
© DMM 他手法との比較 30 swaggo 1. コードからyamlを生成。 2. openapiがv2まで
3. コード変更した後にコメントを変更 忘れると実態の内容と乖離する -
© DMM 31 - 発表の壁打ちに付き合ってくださった上長の河西さん 深いチームプロダクトへの理解とGoの知識、貴重なお時間を本当にありが とうございました。 - 理不尽な 中間レビュー(✖N回)にお付き合いいただいた平賀さん/松本さん
お二人の優しいお心遣いと幅広い観点・知識により、自分の微塵子みたい な知識を総動員して最大限の発表資料を作成できました。本当にありがと うございますorz 感謝と謝罪とお祈り