Slide 1

Slide 1 text

CUEを使った JSONテンプレート管理 US@Tokyo ML/Search team @pakio 2022/12/02 - mercari.go #20

Slide 2

Slide 2 text

2022 Agenda ● 導入背景 ● CUEについて ○ JSONビルダとしての利用事例 ● 他の生成方法との比較検討 ● まとめ

Slide 3

Slide 3 text

2022 背景

Slide 4

Slide 4 text

2022 導入背景 現在のMercari USの検索周りシステム構成(概略) Microservices BFF … search team Search Service Primary Search Engine Elasticsearch

Slide 5

Slide 5 text

2022 導入背景 GET /_search { "query": { "bool": { "should": [ { "match": { "name.first": { "query": "shay", "_name": "first" } } }, { "match": { "name.last": { "query": "banon", "_name": "last" } } } ], "filter": { "terms": { "name.last": [ "banon", "kimchy" ], "_name": "test" } } } } } Elasticsearch Query DSL ● JSON型 ● 複雑な条件の場合、ネストが深くなる ● 条件の組み合わせによって、ブロックを挿入 する箇所を変えたいケースがある → クエリをコンポーネントで管理しつつ、 それらを組み合わせてクエリを組みたい

Slide 6

Slide 6 text

2022 導入背景 GET /_search { "query": { "bool": { "should": [ { "match": { "name.first": { "query": "shay", "_name": "first" } } }, { "match": { "name.last": { "query": "banon", "_name": "last" } } } ], "filter": { "terms": { "name.last": [ "banon", "kimchy" ], "_name": "test" } } } } } クエリビルダに求める条件 ● 柔軟にJSONが構築できること ● 複雑なクエリになっても可読性が高いこと ● 学習コストが十分に低いこと ● 型安全性が守れること ○ ❌ interface{}の乱用

Slide 7

Slide 7 text

2022 CUE

Slide 8

Slide 8 text

2022 CUE CUEとは? CUE is an open source data constraint language which aims to simplify tasks involving defining and using data. 特徴 ● 基本的にJSONのsuperset ● CUE自体はGoで記述された言語 ○ 文法的にはJSON + Go ● 値のvalidationが詳細に定義可能 ● 各種IDEのサポートが豊富 ● Goのpackageが公式で提供されている Logo from: https://cuelang.org/

Slide 9

Slide 9 text

2022 社内事例 - Kubernetes Configuration Management with CUE https://engineering.mercari.com/en/blog/entry/20220127-kubernetes-configuration-management-with-cue/

Slide 10

Slide 10 text

2022 JSONビルダとしてのCUE

Slide 11

Slide 11 text

2022 JSONビルダとしてのCUE JSON生成までの手順 1. JSONのテンプレートを.cue形式で記述 2. Goのプログラム内で読み込み、CompileBytes()またはCompileString()でcue.Value形式にコンパイル 3. (必要に応じて)プログラムから値のバインディング/マージ等の操作 4. JSON等用途に応じた形式で出力

Slide 12

Slide 12 text

2022 Data binding / validation Goのプログラムから変数のバインディング、また型・値のバリデーションが可能 min?: *0 | number // 0 if undefined max?: number & >min // must be strictly greater than min if defined. minmax.cue type minmax struct { Min int `json:"min"` Max int `json:"max"` } func main() { data := minmax{Min: 10, Max: 100} v := cuecontext.New().CompileBytes(template) filled := v.FillPath(cue.ParsePath(""), data) compiled, err := filled.MarshalJSON() } bind.go Structから 値をバインド

Slide 13

Slide 13 text

2022 Data binding / validation min?: *0 | number max?: number & >min minmax.cue func main() { data := minmax{Min: 10, Max: 100} v := cuecontext.New().CompileBytes(template) filled := v.FillPath(cue.ParsePath(""), data) compiled, err := filled.MarshalJSON() fmt.Printf("err: %v\njson:%s\n", err, compiled) } bind.go err: json:{"min":10,"max":100} out

Slide 14

Slide 14 text

2022 func main() { data := minmax{Min: 10, Max: 5} v := cuecontext.New().CompileBytes(template) filled := v.FillPath(cue.ParsePath(""), data) compiled, err := filled.MarshalJSON() fmt.Printf("err: %v\njson:%s\n", err, compiled) } bind.go Data binding / validation min?: *0 | number max?: number & >min minmax.cue 不正な値 err: cue: marshal error: max: invalid value 5 (out of bound >10) json: out

Slide 15

Slide 15 text

2022 Reduce boilerplate in your data list/mapを渡してイテレーション #name: { first: string last: string } #names: [... #name] #names: [ {first: "jiro", last: "sasaki"}, {first: "taro", last: "tanaka"}, ] attendees: [ for name in #names { {"name": "\(name.first) \(name.last)"} }, ] { "attendees": [ { "name": "jiro sasaki" }, { "name": "taro tanaka" } ] }

Slide 16

Slide 16 text

2022 Reduce boilerplate in your data 複数のCUEを結合 { term: {"name": "taro tanaka"} } term.cue { query: {} } query.cue { function_score: { query: {} random_score: {} } } function_score.cue

Slide 17

Slide 17 text

2022 Reduce boilerplate in your data 複数のCUEを結合 ※ FillPathで指定するValue同士は同一のcuecontextから生成される必要あり func mergeCue() { ctx := cuecontext.New() term := ctx.CompileBytes(termTemplate) query := ctx.CompileBytes(queryTemplate) score := ctx.CompileBytes(scoreTemplate) merged := query.FillPath(cue.ParsePath("query"), term) compiled, _ := merged.MarshalJSON() merged = score.FillPath(cue.ParsePath("function_score.query"), term) compiled, _ = merged.MarshalJSON() } merge.go

Slide 18

Slide 18 text

2022 CUE その他サポートされているオペレーション : ● Ifによる条件分岐 ● 各種論理演算子・算術演算(math packageの一部) ● len()を用いた配列長判定 ● nullが渡された場合のデフォルト値定義

Slide 19

Slide 19 text

2022 比較/検討

Slide 20

Slide 20 text

2022 パフォーマンス検証 他のJSON生成方法と比較した際のパフォーマンスについて検証 比較対象 ● encoding/json ● text/template シナリオ ● 1層のみのシンプルなJSON ● 5層のネスト、リストありなJSON ● testing.Bによるベンチマークでns/opとallocs/opを比較

Slide 21

Slide 21 text

2022 検証結果 → us単位なので許容範囲なものの、かなり低速 かつencoding/json, text/templateと比較して、複雑度の増加に対する速度の低下が顕著

Slide 22

Slide 22 text

2022 総合的な検討 encoding/json text/template cue-lang/cue 可読性(小規模) ○ ○ ○ 可読性(大規模) ☓ ○ ○ 開発効率 △ ☓ ○ 学習コスト ○ △ △ 型安全性 ○ ○ ○ 処理パフォーマンス ○ ○ ☓

Slide 23

Slide 23 text

2022 総合的な検討 クエリビルダに求める条件 ● 柔軟にJSONが構築できること ● 複雑なクエリになっても可読性が高いこと ● 学習コストが十分に低いこと ● 型安全性が守れること encoding/json text/template cue-lang/cue 可読性(小規模) ○ ○ ○ 可読性(大規模) ☓ △ ○ 開発効率 △ ☓ ○ 学習コスト ○ △ △ 型安全性 ○ ○ ○ 処理パフォーマンス ○ ○ ☓ WINNER👑 → 全て条件を満たした上で、複雑になった場合の可読性・開発効率から CUEを選択

Slide 24

Slide 24 text

2022 まとめ

Slide 25

Slide 25 text

2022 まとめ ● Mercari USの検索ではElasticsearch向けのJSONビルダとしてCUEを利用している ● シンタックスハイライトやIDEによるオートコンプリートなどの恩恵を受けた効率的な開発を実現 ● ただし、パフォーマンスについての懸念が若干あるため、ユースケース毎に検証は必要そう

Slide 26

Slide 26 text

2022 Thanks!