Upgrade to Pro — share decks privately, control downloads, hide ads and more …

新卒が解説するDMMの検索proxy APIの解体書

Hinata
November 19, 2024

新卒が解説するDMMの検索proxy APIの解体書

24新卒でDMM.comへ入社し、データ基盤開発部への配属されました。本資料では弊チームの持つ検索proxy APIの開発方法について説明します。
社外向けイベントでの登壇資料です
Event: https://dmm.connpass.com/event/335033/

Hinata

November 19, 2024
Tweet

Other Decks in Programming

Transcript

  1. © DMM © DMM CONFIDENTIAL 新卒が解説するDMMの 
 検索proxy-APIの解体書 
 ML基盤

    菊地 ひなた 
 開発統括本部 ML基盤チーム 菊地 ひなた 2024/11/05
  2. © DMM 自己紹介
 名前 : 菊地 ひなた
 経緯 : 24新卒で合同会社

    DMM.com に入社
 ML基盤チームに所属
 趣味 : 料理とお酒が大好きです
 最近は魚を捌いたり、野山でキウイを刈ったりしました🥝
 2
  3. © DMM 弊社サービスと検索proxy
 検索api ( search-api ) の前面でリクエスト・レスポンスに対し変更を行 う
 


    ⇒ 検索体験の改善だけでなく様々な施策を担う
 (例: reranking, a/b test id …)
 
 4 👇ここ

  4. © DMM Goa[1]
 8 [1] Goa :: Design 1. search-apiのreq/responseを漏らさずに現状の機能を維持


    2. 施策の追加・安定稼働を担保しつつ工数を最小限に抑える
 - 既存APIの利用もあるスキーマ駆動開発パッケージ
 - API定義/client/server/documentの自動生成が可能
 ⇒ドメインロジックの実装に集中できる
 - Goで書ける独自のDSL(Domain Specific Language) 

  5. © 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デザイン概要より
  6. © 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デザイン概要より
  7. © 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を定義可能
 👈 

  8. © 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の受け取るペイロード・レスポンス型などを記述
 👈

  9. © 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に一致

  10. © DMM 弊チームのdesignパッケージ階層説明
 14 - design/
 - service/
 - base.go

    - 各エンドポイントの親パスの定義(Service)
 - search.go - エンドポイントの定義・検索proxy/search-apiの接続
 (Service, Type)
 - design.go - 実APIの定義(API)
 エンドポイント数が少ないので下記の構成でgoaを記述している

  11. © 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>

  12. © 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 

  13. © 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)
 }

  14. © 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 

  15. © 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

  16. © 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

  17. © 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

  18. © 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 

  19. © 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で記述された内容に対応して定義される

  20. © 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]
  21. © 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の読み替え

  22. © 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サーバー用データ型
  23. © DMM 他手法との比較
 29 connect 
 1. Go意外にもサポート充実(Kotlin, Swift, Node)


    2. 2022~
 
 
 openapi-generator 
 1. Goa よりも薄くてシンプル
 2. yamlを書く必要あり
 swagger editor使えば良さそうではあるが、、 

  24. © DMM 他手法との比較
 30 swaggo 
 1. コードからyamlを生成。
 2. openapiがv2まで


    3. コード変更した後にコメントを変更 
 忘れると実態の内容と乖離する 
 
 

  25. © DMM 31 - 発表の壁打ちに付き合ってくださった上長の河西さん
 深いチームプロダクトへの理解とGoの知識、貴重なお時間を本当にありが とうございました。
 - 理不尽な 中間レビュー(✖N回)にお付き合いいただいた平賀さん/松本さん


    お二人の優しいお心遣いと幅広い観点・知識により、自分の微塵子みたい な知識を総動員して最大限の発表資料を作成できました。本当にありがと うございますorz
 感謝と謝罪とお祈り