Slide 1

Slide 1 text

© DMM © DMM CONFIDENTIAL 新卒が解説するDMMの 
 検索proxy-APIの解体書 
 ML基盤 菊地 ひなた 
 開発統括本部 ML基盤チーム 菊地 ひなた 2024/11/05

Slide 2

Slide 2 text

© DMM 自己紹介
 名前 : 菊地 ひなた
 経緯 : 24新卒で合同会社 DMM.com に入社
 ML基盤チームに所属
 趣味 : 料理とお酒が大好きです
 最近は魚を捌いたり、野山でキウイを刈ったりしました🥝
 2

Slide 3

Slide 3 text

© DMM 目次
 1. 弊社サービスと検索proxy
 2. 検索proxyの満たすべき条件と詳細
 3. 使用技術:goa
 4. 検索proxyにおける使われ方用途
 5. まとめ
 3

Slide 4

Slide 4 text

© DMM 弊社サービスと検索proxy
 検索api ( search-api ) の前面でリクエスト・レスポンスに対し変更を行 う
 
 ⇒ 検索体験の改善だけでなく様々な施策を担う
 (例: reranking, a/b test id …)
 
 4 👇ここ


Slide 5

Slide 5 text

© DMM 検索proxyの満たすべき条件
 以下の要件が存在する
 1. リクエスト・レスポンス仕様がsearch-apiと一致する必要がある
 2. クライアントである事業サービスにかかる工数を最小限に
 3. 検索改善施策が失敗した際に各施策を撤退・換装
 5

Slide 6

Slide 6 text

© DMM 条件を満たすために必要なこと
 1. リクエスト・レスポンス仕様がsearch-apiと一致する必要がある
 2. クライアントである事業サービスにかかる工数を最小限に
 3. 検索改善施策が失敗した際に各施策を撤退・換装
 
 
 1. search-apiのreq/responseを漏らさずに現状の機能を維持
 2. 施策の追加・安定稼働を担保しつつ工数を最小限に抑える
 6

Slide 7

Slide 7 text

© DMM 7 [1] https://goa.design/ Goa[1]
 - 既存APIの利用もあるスキーマ駆動開発パッケージ
 - API定義/client/server/documentの自動生成が可能
 ⇒ドメインロジックの実装に集中できる
 - Goで書ける独自のDSL(Domain Specific Language)

Slide 8

Slide 8 text

© DMM Goa[1]
 8 [1] Goa :: Design 1. search-apiのreq/responseを漏らさずに現状の機能を維持
 2. 施策の追加・安定稼働を担保しつつ工数を最小限に抑える
 - 既存APIの利用もあるスキーマ駆動開発パッケージ
 - API定義/client/server/documentの自動生成が可能
 ⇒ドメインロジックの実装に集中できる
 - Goで書ける独自のDSL(Domain Specific Language) 


Slide 9

Slide 9 text

© 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デザイン概要より

Slide 10

Slide 10 text

© 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デザイン概要より

Slide 11

Slide 11 text

© 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を定義可能
 👈 


Slide 12

Slide 12 text

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


Slide 13

Slide 13 text

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


Slide 14

Slide 14 text

© DMM 弊チームのdesignパッケージ階層説明
 14 - design/
 - service/
 - base.go - 各エンドポイントの親パスの定義(Service)
 - search.go - エンドポイントの定義・検索proxy/search-apiの接続
 (Service, Type)
 - design.go - 実APIの定義(API)
 エンドポイント数が少ないので下記の構成でgoaを記述している


Slide 15

Slide 15 text

© 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 


Slide 16

Slide 16 text

© 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 


Slide 17

Slide 17 text

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


Slide 18

Slide 18 text

© 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 


Slide 19

Slide 19 text

© DMM 初めてGoaに触れて
 19 今回はチームが持っている資産を解読して説明する機会をもらえました
 普段の業務では既存実装に触れる機会が少ないためとても勉強になりました
 
 Goa自体初めて触ったものの、思った以上に読みやすかった です
 初めてjoinした自分でもそう感じられる点で有益なツールだと思います
 
 チーム内からは
 「コントローラーやミドルウェアの実装部分はわざわざgoaを使わないほうが実装に手間取 られない可能性がある 」
 という意見が挙がっていました


Slide 20

Slide 20 text

© DMM ご静聴ありがとうございました 


Slide 21

Slide 21 text

© 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


Slide 22

Slide 22 text

© 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


Slide 23

Slide 23 text

© 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


Slide 24

Slide 24 text

© 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 


Slide 25

Slide 25 text

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


Slide 26

Slide 26 text

© 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]

Slide 27

Slide 27 text

© 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の読み替え


Slide 28

Slide 28 text

© 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サーバー用データ型

Slide 29

Slide 29 text

© DMM 他手法との比較
 29 connect 
 1. Go意外にもサポート充実(Kotlin, Swift, Node)
 2. 2022~
 
 
 openapi-generator 
 1. Goa よりも薄くてシンプル
 2. yamlを書く必要あり
 swagger editor使えば良さそうではあるが、、 


Slide 30

Slide 30 text

© DMM 他手法との比較
 30 swaggo 
 1. コードからyamlを生成。
 2. openapiがv2まで
 3. コード変更した後にコメントを変更 
 忘れると実態の内容と乖離する 
 
 


Slide 31

Slide 31 text

© DMM 31 - 発表の壁打ちに付き合ってくださった上長の河西さん
 深いチームプロダクトへの理解とGoの知識、貴重なお時間を本当にありが とうございました。
 - 理不尽な 中間レビュー(✖N回)にお付き合いいただいた平賀さん/松本さん
 お二人の優しいお心遣いと幅広い観点・知識により、自分の微塵子みたい な知識を総動員して最大限の発表資料を作成できました。本当にありがと うございますorz
 感謝と謝罪とお祈り


Slide 32

Slide 32 text

© DMM 32 - 聞いてくださっているみなさま
 新卒配属数ヶ月でみなさまのgo欲を満たせる発表ができるとは思っていま せんが、ちょっとした余興としてお楽しみいただけたのであれば幸いです。 今後もDMM.comの社員として精進しますので、どうか見捨てないで…
 感謝と謝罪とお祈り