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

Go Night Talks – After Conference 登壇資料 Hikari

Go Night Talks – After Conference 登壇資料 Hikari

Transcript

  1. ⾃⼰紹介 © UPSIDER, Inc. 2 Hikari Kosuda • 2021/10からUPSIDERにJOIN ◦

    その前に副業でもちょこっと参加 • GoはUPSIDERで初めて使い始めました ◦ それまではPython/Javaなど • 決済基盤開発チームで、カード発⾏や決済処理などを開発 • ワーケーションを割とやってます ◦ 最後に⾏ったのは5⽉の宮城県(松島と気仙沼) @kosuda-h @ksdev_xxx
  2. クレカ決済の加盟店名の難しさ © UPSIDER, Inc. 5 なんのこっちゃわからん名前の決済が来ることも • XXX⾜⽴インター店 → XXX ADACHIINTATEN •

    Facebook広告 → FACEBK *XYZ1234abc (←ランダム⽂字列) • FooJapan → HuuZIYAP ※実際の加盟店名ではない。なお、実際はもっとわかりにくいものも存在する。
  3. それでも加盟店は同定したい! © UPSIDER, Inc. 6 このような機能には必須! • 利⽤先制限 ◦ Amazon専⽤カード、広告専⽤カード、etc…

    • 不正利⽤対策 ◦ 過去に不正利⽤でしか使われていない加盟店名パターンでブロック • ポイント付与 ◦ 〇〇広告で利⽤したらポイント1.5倍!キャンペーンなど
  4. 加盟店の同定⽅法 © UPSIDER, Inc. 7 決済情報にある、名前とIDを利⽤する • 名前(今回の主題) ◦ 加盟店が⾃由に決めるもの

    ◦ 毎回同じとは限らない → ある程度、パターンでマッチさせたい ▪ オーソリ(承認要求)とクリアリング(売上確定)で変わったり ▪ サービスに関するIDがついていたり e.g. FACEBK *XYZ1234abc ▪ チェーン店など、店名がついていたり e.g. XXX ADACHIINTATEN • ID ◦ 加盟店を管理する、アクワイアラという管理会社によって定められる ◦ 毎回同じとは限らない ▪ 名前と同じ問題が、⼤体ある
  5. 加盟店名パターンの⼀致チェック⽅法 © UPSIDER, Inc. 8 正規表現だと間に合わないので、前⽅⼀致に絞る! Goの正規表現の速度が〜〜〜という話ではない 多くの加盟店に対し、それぞれ複数の名前パターンがありうる → 毎回の決済ごとに、1000個とかの⽐較を回せないのが問題   もし、1個が0.001秒でも全体で1秒かかる

      決済処理全体の⽬標時間が1秒なので、これは許容できない 多くの加盟店では、前⽅⼀致で判定可能なことに着⽬ 前⽅⼀致なら、アルゴリズムを⼯夫することで、O(N)ではなくO(n)にできる ※N = 名前パターン数、n = 名前パターンの最⼤⽂字数 とした場合
  6. 効率的な前⽅⼀致の⼿法 © UPSIDER, Inc. 9 Trie(トライ)⽊というデータ構造を⽤いる トライ木(Trie木) の解説と実装【接頭辞(prefix) を利用したデータ構造】 https://algo-logic.info/trie-tree/

    名前パターンを挿入する時は、 rootから該当文字のnodeを辿っていく 存在しない場合は文字nodeを追加する 最後のnodeにマーキングする 名前からマッチするパターンを検索する時は、 1⽂字⽬から順にこの⽊を辿るだけでどのパターン に該当するかわかる! root node 例えば「firemap」をこの木で検索すると、 「firema」まで辿ることができる。 最後に通った◎の位置から、「fire」のパターンに一致 するとわかる node: 各頂点。今回だと文字に対応する。 root: 木の大元のnode。ここから文字を辿る。
  7. Trie⽊の実装(定義‧初期化‧Insert) © UPSIDER, Inc. 10 // 初期化はシンプルに、rootを用意するだけ func InitTrie() (*Trie,

    error) { return &Trie{ root: &TrieNode{children: map[rune]*TrieNode{}}, expectedChars: ExpectedCharsEBCDIC, }, nil } // 名前パターンのInsert func (t *Trie) Insert(pattern string, patternID string) { current := t.root for _, r := range pattern { // 次の文字のノードが既にあれば、移動する if v, ok := current.children[r]; ok { current = v continue } // まだなければ、ノードを作って移動する current.children[r] = &TrieNode{children: map[rune]*TrieNode{}} current = current.children[r] } // Insertする名前パターンの全文字分ノード移動したら、最終地点にIDをセット current.patternID = patternID } // ノード(節点、頂点) type TrieNode struct { children map[rune]*TrieNode patternID string // 名前パターンIDを持てる } // 木の本体 type Trie struct { root *TrieNode expectedChars string } // 使える文字列は定義しておくとGood // 今回は、加盟店名にありうる文字列のみにする var ExpectedCharsEBCDIC = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTU VWXYZ0123456789 !?.,:;<>()¥+-*/=&%@#|'\"`_"
  8. Trie⽊の実装(検索) © UPSIDER, Inc. 11 func (t *Trie) GetMatchedIDs(key string)

    ([]string, error) { matchedPatternIDs := make([]string, 0, len(key)) current := t.root // rootからスタート! for _, r := range key { // 次の文字に対応するノードがあれば進む v, found := current.children[r] if !found { // 対応するノードがなければ終わり break } current = v // 道中、名前パターンのIDを見つけたら拾っておく if current.patternID != "" { matchedPatternIDs = append(matchedPatternIDs, current.patternID) } } // マッチした名前パターン群を返す。実際の運用では、最後にマッチしたIDを参照する return matchedPatternIDs, nil }
  9. Goでよかったこと © UPSIDER, Inc. 12 • 実装 ◦ シンプルで書きやすい ◦

    標準のベンチマークテストで⼗分な確認ができた(Go 1.17当時でも) • 運⽤ ◦ 加盟店名検索をマイクロサービス化したかったため、相性◯ • その他 ◦ 今後、カナなどの名称を扱いたい場合もruneによるUnicode対応が容易
  10. まとめ © UPSIDER, Inc. 13 決済における加盟店の同定、Goでどう解決する? • 正規表現だと、どうしても間に合わない...! • Trie⽊での前⽅⼀致なら、クレカ決済の制約にも耐えられるパフォーマンスに!

    • Goだとシンプルに書けて、マイクロサービスとの相性も◯でいいね! スペシャルサンクス スライド内のGopherくんを描いてくれた、 弊社のMiki Masumotoさん( :@m_miki0108)
  11. Appendix:ベンチマークテスト © UPSIDER, Inc. 14 O(n)とは⾔ったが、実際どのくらいのパフォーマンスが出るのか? func Benchmark_Search(b *testing.B) {

    trieTree, _ := partialmatch.InitTrie() insertTimes := 10000 // Trie木に入れるランダムワードの個数 // ①10~20文字のワードをランダム生成して、木に Insertする patterns, ids := createRandomWordsAndIDs(trieTree, b, insertTimes) patternHolder := createMerchantInfos(patterns, ids) _ = trieTree.Insert(patternHolder) // ② ①のいずれかのワードを選び、一致 &不一致のワードをランダム生成 // ↑をb.N回繰り返す (ベンチマーク package側で調整してくれる ) wordsToFind := createRandomWordsToFind(trieTree, b, b.N, patterns) b.ResetTimer() // ③計測開始! for i := 0; i < b.N; i++ { word := wordsToFind[i] _ = trieTree.GetMatchedIDs(word) // ④検索! } } go test -bench=Benchmark_Search -benchmem … Benchmark_Search-10 513416 2276 ns/op 4686 B/op 10 allocs/op … 10~20文字 * 1万語のTrie木に対し、大量のランダム検索を 行った。 結果、1回あたり2276 nsで検索できると計測された! ※スペースの関係で、ランダム生成関数の実装詳細は省略した