TECH PLAY

株式会社ZOZO

株式会社ZOZO の技術ブログ

974

はじめに こんにちは、MA部の松岡( @pine0619 )です。MA部ではマーケティングオートメーションシステムの開発・運用に従事しています。 ZOZOTOWNでは、マーケティングオートメーションシステム(以下、MAシステム)を使い、メールやLINE、アプリプッシュ通知といったチャネルへのキャンペーンを配信しています。 MA部では、複数のMAシステムが存在しており、MAシステムそれぞれに各チャネルへの配信ロジックが記述されていました。これにより、現状の運用保守ならびに今後の改修コストが高いかつ、使用している外部サービスのレートリミットの一元管理が出来ていないなどの問題を抱えていました。そのため、外部サービスへのリクエスト部分をチャネルごとにモジュールとして切り出し、複数のMAシステムから共通で使える配信基盤を作成しました。 また、社内の他チームの持つシステムからのキャンペーン配信の要望があったため、全社共通で利用できる配信基盤を作成しました。 その中でも本記事ではメール配信基盤にフォーカスし、以下についてご紹介します。 システム構成 マーケティングメールを配信する際の考慮ポイント 複数の社内システムから利用される基盤作成の考慮ポイント メール配信基盤の監視について このメール配信基盤は注文完了などのトランザクションメールは対象外で、マーケティングメールのみを扱うシステムとなっています。 なお、アプリプッシュ通知やLINEの配信基盤については別の記事で紹介する予定です。 目次 はじめに 目次 メール配信基盤の作成の背景・目的 既存のマーケティングオートメーションシステムでの課題 他の社内システムからの利用希望 メール配信基盤のシステム構成 配信基盤へのリクエスト メール配信処理の流れ マーケティングメールを配信する際の考慮ポイント 配信の可能時間チェック 配信の有効期限チェック 配信の重複制御 メールアドレスの解決 配信対象者の分割 配信リストテーブルのテーブルサイズの取得 分割数の決定 パーティションを使ってテーブルを分割する 分割テーブルごとにファイルへエクスポートする メール配信サービスへのリクエストの流量制限 テスト配信 複数の社内システムから利用される基盤作成の考慮ポイント リクエスト元の識別およびデータ管理について メール配信基盤の監視について 今後の展望 まとめ メール配信基盤の作成の背景・目的 既存のマーケティングオートメーションシステムでの課題 導入部分でも触れたように、現在MA部では複数のMAシステムを使って配信しています。配信は特定のユーザーセグメント向けのマス配信と、個別のユーザーに最適化されたパーソナライズ配信の2種類が存在しています。この2種類の配信に加えて、チャネルによっては独立したシステムになっているという理由から複数のMAシステムの開発・運用をしています。 メール配信では外部のメール配信サービス(以下、メール配信サービス)を使用しています。このメール配信サービスを使った配信ロジックがパーソナライズ配信、マス配信の両方のMAシステムに入っていました。配信ロジックが2つのMAシステムに存在していることで、現状の運用保守ならびに今後の改修において工数がかかってしまうなどの問題がありました。 また、一般的に外部サービスにはリクエスト制限があります。このメール配信サービスにもリクエスト制限がありますが、現在のMAシステムでは、メール配信サービスへのリクエスト流量制限の一元化が出来ていませんでした。そのため、両方のシステムから同時に大量のメール配信をリクエストした際に、リクエスト制限を超過してしまい、配信遅延やエラーが発生するという問題もありました。 上記の課題を解決するため、配信基盤としてMAシステムから配信ロジックをモジュールとして切り出すことで、配信ロジックならびにリクエスト流量制限を一元化することにしました。 他の社内システムからの利用希望 上記で述べた課題の他に、社内の他チームの持つ社内システムからのキャンペーン配信の要望もありました。しかし、配信ロジックがMAシステム内に書かれていたため、そのままだと他の社内システムからの配信に対応出来ない状況でした。 この課題を解決するために、全社共通で利用できるメール配信基盤を作成することにしました。 メール配信基盤のシステム構成 今回作成したメール配信基盤についてご紹介します。 メール配信基盤では、ワークフローエンジンの Digdag を採用しています。既存のMAシステムでDigdagを採用しており、今回の配信基盤の開発でリソースを流用できるかつ、チームメンバーがDigdagに慣れており開発・運用コストを削減できるため採用を決めました。 以下がシステムの全体構成および処理の流れとなっています。 メールの配信内容は、メールデザインのテンプレートと、キャンペーンやユーザごとに動的な商品などのパラメータにより決まります。このテンプレートとパラメータを指定してメール配信サービスへ配信リクエストをすることで、配信時にテンプレートへパラメータが埋め込まれた状態でメール配信されます。 パラメータの詳細については以下のコンテンツパートを参照してください。 techblog.zozo.com 配信基盤へのリクエスト 配信基盤へ配信リクエストをする前に、社内システム側でメール配信サービスへのテンプレートのアップロードと、BigQueryの配信対象者テーブルを作成します。以下の図でいうと赤枠の部分になります。この配信対象者テーブルは、テンプレートのパラメータ、ユーザーID、メールアドレスに紐づいたIDを含みます。現状は社内システムから直接メール配信サービスへテンプレートをアップロードしていますが、こちらは後々、メール配信基盤を経由するように変更し、社内システムのメール配信サービスへの依存を無くす予定です。 その後、社内システムからの配信リクエストをAPIで受信します。以下の図でいうと赤枠の部分になります。この配信リクエストでは前述したテンプレートの情報や配信対象者テーブル名などを受け取ります。 次にリクエスト内容を保存するためのリクエストテーブルにリクエスト内容をINSERTして、社内システムにレスポンスを返します。その後、Digdagによるマイクロバッチでリクエストテーブルからリクエスト内容を取得し、配信処理しています。また、配信処理の他に配信履歴の確認や、分析目的で使用される配信実績テーブルへ実績を書き込みます。 ZOZOではBigQueryを全社共通のデータ基盤として利用しています。このデータ基盤上にメール内容の元となる商品データや会員データなどがあり、それらのデータを利用して配信します。また、メール配信基盤の配信実績データと、データ基盤上のデータを組み合わせて分析したいという要件もありました。以上の理由から、データ基盤とメール配信基盤間で円滑にデータを受け渡すために、メール配信基盤ではデータの管理にBigQueryを採用しています。 メール配信処理の流れ メール配信処理は以下の流れとなっています。 配信処理は、起動・配信前処理・配信処理を担う3つのワークフローから構成されています。各ワークフローの役割と実行タイミングについては以下となっています。 ワークフロー 役割 実行タイミング 起動  配信対象のリクエストの取得 配信の有効期限チェック 配信前処理ワークフローの起動 3分おきに定期実行 配信前処理 重複制御 メールアドレスの取得 配信リスト作成 起動ワークフローから起動される 配信処理 メール配信サービスへのリクエスト 配信実績の書き込み 5分おきに定期実行 配信前処理と配信処理を分けているのは、後述するメール配信サービスへのリクエストの流量を制限するためです。 マーケティングメールを配信する際の考慮ポイント マーケティングメールを配信する際に考慮すべき点と、メール配信基盤においてそれをどう実現したかについてご紹介します。 配信の可能時間チェック メール配信基盤では、一律な配信の可能時間を設定しています。メール配信基盤や外部サービスでの障害などにより配信遅延が発生し、深夜帯にメール配信される場合が考えられます。しかし、深夜帯は多くのユーザーが活動時間外ということもあり、場合によってはメルマガ購読停止やクレームなどに繋がる恐れがあります。 そのためメール配信基盤では、Digdagの スケジュール機能 を利用し、以下のようにスケジュール設定することで配信の可能時間を定めています。 schedule: cron>: '*/5 8-22 * * *' 配信の有効期限チェック 配信の可能時間チェックとは別に、社内システムからリクエストを受け取る際に配信の有効期限を受け取るようにしています。有効期限を受け取る理由は、配信リクエストを受け付けてから一定の時刻を過ぎた場合は配信しないようにするためです。 たとえばセール最終日のお知らせメールを送る場合について考えてみます。メール配信基盤や外部サービスでの障害などにより、セール終了時間までに配信完了しない場合が考えられます。その際にそのままメールを配信してしまうと、セールは終了しているため、事実と異なるメールがユーザーに届いてしまいます。それを防ぐために配信の有効期限をチェックし、有効期限切れの配信は送らないようにしています。 起動ワークフローでリクエストテーブルから配信対象を取得するクエリが以下です。このクエリで有効期限(expires_at)を過ぎていないかつ、statusが配信処理前を示すWAITINGになっているリクエストを取得することで、配信の有効期限チェックを実現しています。 SELECT * FROM `リクエストテーブル` WHERE expires_at > CURRENT_TIMESTAMP () AND status = ' WAITING ' ORDER BY expires_at ASC 前述の通り、メール配信基盤は必ず届かないと困るようなトランザクションメールではありません。そのため誤情報をユーザに送ってしまうことのほうが良くないため、有効時間を過ぎた場合は配信しないようにしています。 配信の重複制御 マーケティングメールにおいて重複配信は、会社への不信感や、メルマガ購読停止、クレームに繋がってしまう可能性があります。メール配信基盤では、配信リクエスト完了後の処理が失敗し処理全体をリトライした場合や、社内システムから同一リクエストを複数回受け取った場合に、重複配信の可能性がありました。そのため、重複制御の処理を入れることで配信の重複を防いでいます。 重複制御は以下の流れになっています。 配信対象者テーブルは社内システムから連携されるテーブルです。この配信対象者テーブルでdeduplication_idというメール1通単位ごとに一意となるIDを受け取ります。このdeduplication_idと重複制御用テーブルを使って重複制御を行います。重複制御用テーブルには、既に配信処理されたdedeplication_idを保存しています。重複除外のため、配信対象者テーブル内のdeduplication_idが重複制御用テーブルに存在しているかどうかをチェックします。存在している場合は、該当のdeduplication_idを持つレコードを除外した重複除外済みテーブルを作成します。その後、重複制御用テーブルを更新しています。後続の配信処理ではこの重複除外済みテーブルを配信対象としてメールを配信します。 メールアドレスの解決 メール配信では配信対象者のメールアドレスが必要となります。メールアドレスは個人情報に当たるため、本当に必要な場合を除いて、参照できる状態は好ましくありません。そのため、社内システムと配信基盤間ではメールアドレスに紐づいたemail_idを使って配信対象者を連携し、配信基盤側でemail_idに紐づいたメールアドレスを全社共通のデータ基盤から取得しています。これにより社内システムと配信基盤間で個人情報を受け渡す必要が無くなります。 会員メール情報テーブルのメールアドレスカラムに関してはアクセス制御されています。メール配信基盤で使用するサービスアカウントにはあらかじめメールアドレスカラムへのアクセス権限を付与しているため、email_idに紐づいたメールアドレスが取得できます。 配信対象者の分割 メール配信サービスへのリクエストでは、配信対象者のメールアドレスおよび、キャンペーンやユーザごとに必要なパラメータのリストを連携する必要があります。この配信リストの連携ではファイルサイズ上限が定められています。ZOZOTOWNのマーケティングメールでは1回のキャンペーンで1000万通程度のメールを配信することもあり、何も考えずに配信リストを作成・連携するとファイルサイズ上限を超過してしまいます。そのため、ファイルサイズ上限を超過しないように、配信リスト作成時に配信対象者を適切に分割する必要がありました。 配信対象者のリストはBigQueryに配置されると説明しました。配信時にはそのデータをCloud Storageにエクスポートし、そのファイルをメール配信サービスへ連携します。BigQueryからCloud Storageへのエクスポート時に、エクスポートするファイルのサイズを制限する方法は Google Cloudのドキュメント を参考にしました。 配信基盤では、以下のステップで配信対象者を分割し、配信リストを作成しています。 配信リストテーブルのテーブルサイズの取得 分割数の決定 パーティションを使ってテーブルを分割する 分割テーブルごとにファイルへエクスポートする 各ステップについてそれぞれご紹介します。 配信リストテーブルのテーブルサイズの取得 前述した重複制御・メールアドレスの解決の処理後、配信リストの元となる配信リストテーブルが作成されます。この配信リストテーブルを適切に分割し、ファイルへエクスポートするために、まずは配信リストテーブルのテーブルサイズを知る必要があります。テーブルサイズは INFORMATION_SCHEMA 内の パーティションビュー の TOTAL_LOGICAL_BYTES から取得可能で、以下のクエリにて取得しています。 SELECT total_logical_bytes FROM `{project}.{dataset}.INFORMATION_SCHEMA.PARTITIONS` WHERE table_name = 配信リストテーブル 分割数の決定 上記で取得した配信リストテーブルのテーブルサイズと、メール配信サービスのファイルサイズ上限を元に分割数を決定します。メール配信基盤ではDigdagでPythonスクリプトを呼び出しており、以下のコードで分割数を決定しています。 file_size_limit_mb = 900 # ファイルサイズ上限 table_size_bytes = 配信リストテーブルのテーブルサイズ table_size_mb = table_size_bytes / ( 1024 * 1024 ) partition_count = int (table_size_mb // file_size_limit_mb) + 1 パーティションを使ってテーブルを分割する 分割数が決定した後は、以下のクエリでパーティション用のidであるpartition_idを各レコードへランダムに割り振ります。 CREATE OR REPLACE TABLE `パーティションテーブル` PARTITION BY RANGE_BUCKET(partition_id, GENERATE_ARRAY( 0 , {partition_count}, 1 )) CLUSTER BY partition_id AS ( SELECT *, CAST ( FLOOR (n*RAND()) AS INT64) AS partition_id FROM `配信リストテーブル` ) その後、partition_idごとにテーブルを作成することで配信対象者を分割しています。 CREATE OR REPLACE TABLE `分割テーブル _ {partition_id}` AS ( SELECT * EXCEPT(partition_id) FROM `パーティションテーブル` WHERE partition_id = {partition_id} ) 分割テーブルごとにファイルへエクスポートする partition_idごとに分割したテーブルをCloud Storageにエクスポートします。メール配信基盤では、 EXPORT DATAステートメント ではなく、 クライアントライブラリ を使ってエクスポートしています。 EXPORT DATAステートメントでは、エクスポート先のCloud StorageのURIの指定で「単一のURI」が使用できず、「単一のワイルドカードURI」のみ対応しています。「単一のワイルドカードURI」の場合、以下のドキュメントの通り、複数のファイルに自動で分割され、ファイルサイズが一定ではなくなってしまいます。そのため、クライアントライブラリを使ってエクスポートしています。 エクスポートされるデータが最大値の 1 GB を超えそうな場合は、単一のワイルドカード URI を使用します。データは、指定したパターンに基づいて複数のファイルに分割されます。エクスポートされたファイルのサイズは一定ではありません。 引用: テーブルデータを Cloud Storage にエクスポートする  |  BigQuery  |  Google Cloud メール配信サービスへのリクエストの流量制限 メール配信基盤では、外部のメール配信サービスを利用してメールを配信しています。前述したようにメール配信サービスへのリクエスト流量を制限しない場合、メール配信サービスのパフォーマンス悪化により、配信遅延やエラー発生の懸念がありました。 そのため配信の前処理と実際にメール配信サービスへリクエストする配信処理との間に配信キューを挟み、配信リクエストの流量を制限することで上記の課題を解決しました。 ここでは Pub/Sub のようなメッセージングサービスではなく、BigQueryをキューとして使っています。これは、今後配信の優先度付けをする予定のため、条件でデータが取得できるデータベースの方が適していると判断しました。また、リクエストテーブルや配信対象者テーブルなどをBigQueryで管理しており、データの分散や運用対象リソースが増えるのを防ぎたくCloudSQLなど他のデータベースは利用しませんでした。 テスト配信 ユーザーへメールを送る前に、表示崩れや文字化けが無いかどうかを確認するため、施策担当者へテストメールを送りたいという要件がありました。そのため、テスト配信用のワークフローとユーザーテーブルを別途作成しました。 通常配信とテスト配信のワークフローを分けているのは、テスト配信では重複制御などの処理が不要なためです。テスト配信ワークフローでは、不要な処理を省略することで処理時間を短縮し、施策担当者がテストメールを受け取るまでの待ち時間を減らすことができました。 また、テストユーザーテーブルを用意しているのはテスト配信時に一般のユーザーへの誤配信を防ぐためです。通常の配信ではデータ基盤上の会員メール情報テーブルからメールアドレスを取得しますが、テスト配信ではテストユーザテーブルからメールアドレスを取得します。これによりテスト時は一般ユーザーの情報が入ったテーブルを参照する必要が無くなるため、誤配信を防ぐことができます。 複数の社内システムから利用される基盤作成の考慮ポイント 今回のメール配信基盤は、複数の社内システムからの利用を考慮する必要がありました。その際の考慮ポイントについてご紹介します。 リクエスト元の識別およびデータ管理について メール配信基盤が複数の社内システムから利用されるにあたって、どの社内システムからリクエストされたのかを識別する必要がありました。そのため配信基盤ではリクエスト元の識別にsourceという概念を導入しています。配信基盤ではsourceごとに作成したデータセット内へ配信実績テーブルやテストユーザテーブル、重複制御用テーブルを配置しています。以下の画像でいうと「ma_batch」がsourceに該当しています。 このようにsourceごとのデータセットに分けた理由は権限管理の容易さにあります。BigQueryではデータセット単位のアクセス制御が可能です。これによりテーブルごとの権限付与が不要となります。また、全sourceで共通なテーブルにしてしまうと、レコード数によってはクエリコストが増えてしまう可能性があることから、このような設計にしました。 メール配信基盤では Terraform によるインフラ管理をしており、以下のようにデータセットに対してviewer権限を付与しています。 resource "google_bigquery_dataset_iam_member" "bigquery_data_viewer_ma_batch" { for_each = toset (local.bigquery_data_viewer_ma_batch_members) project = local.project member = each.key dataset_id = "ma_batch" role = "roles/bigquery.dataViewer" } メール配信基盤の監視について メール配信基盤ではDigdagのワークフローが失敗した場合に、Slack通知および PagerDuty による電話通知をしており、オンコール当番が気付ける仕組みになっています。しかし、これだけだとシステム監視として不十分なため、以下の監視を導入しています。 監視内容 目的 有効期限切れにより配信されなかった配信の監視 未配信および配信遅延の検知 配信キュー内の未配信件数の監視 未配信検知および配信遅延の検知 重複配信の監視 重複配信の検知 配信成功率の監視 配信異常の確認 今後の展望 冒頭でも触れた通り、ZOZOTOWNのMAシステムには「マス配信」と「パーソナライズ配信」の2つがあります。現在MA部ではMAシステムのリプレイスを進めており、現在「マス配信」のリプレイスが完了し、「パーソナライズ配信」をリプレイスしています。MAシステムのリプレイスについて詳しくは以下の記事をご覧ください。 techblog.zozo.com 「パーソナライズ配信」の中にはリアルタイム性を求められるリアルタイム配信があります。現状の配信基盤では配信リクエスト順に処理しており、1000万通規模のメールを配信し終わるまでに時間がかかります。この1000万通のメール配信中にリアルタイム配信をリクエストされた場合、現状だとこの1000万通のメール配信が完了してからリアルタイム配信の配信が行われます。配信完了までのスピードがもっと早ければ現状の配信基盤でも対応できますが、メール配信サービスにはリクエスト上限があるため、これ以上の配信の速度向上は見込めません。 そこで配信の優先度を付け、優先度順に配信することでリアルタイム配信にも対応できるシステムにしていきたいと考えています。 また、今後の配信基盤の全体の展望として、他チャネルの拡充や改善を進め、ZOZOとお客様をつなぐコミュニケーションの窓口となる基盤を作り上げていきたいと考えています。 まとめ 本記事ではメール配信基盤について紹介しました。メール配信基盤の誕生により、メール配信サービスへの流量制限の一元化や、配信ロジックの集約による保守・運用コストの削減、複数の社内システムからの配信を実現できました。本記事が同じような状況・課題を持つ方、新たに配信基盤を作る方への参考になれば幸いです。 MA部では上記で挙げた理想の配信基盤を一緒に作り上げてくれる方を募集中です。ご興味のある方は、ぜひ以下のリンクからぜひご応募ください。 hrmos.co hrmos.co
アバター
はじめに こんにちは、技術本部SRE部フロントSREブロックの柳田です。オンプレミスとクラウドの構築・運用に携わっています。 ZOZOTOWNでは、既存システムのリプレイスプロジェクトを進行中です。リプレイス過渡期の現在、オンプレミスのネットワークとAWSのデータセンターを直接専用線で接続し、安定した高速通信を実現するDirect Connect(以降、DX)を利用しています。各サービスのマイクロサービス化に伴い、オンプレミスとクラウド間の通信量が増えた為、DX10Gの回線が逼迫する問題に直面しました。 本記事では、この回線逼迫の課題をどのように解決したかについて紹介します。 目次 はじめに 目次 回線逼迫の課題 ZOZOTOWNへのアクセスが困難 今後のリプレイスプロジェクトが遅延する可能性 DX10GからDX100Gへの移行 ステップ1:DX100Gの利用申請(クラウド) ステップ2:DX100G用のスイッチ導入(オンプレミス) ステップ3:AWS Transit Gatewayの導入(クラウド) Transit Gateway導入の経緯 TransitVIFとDXGWを用意 DXGWとTGWの紐付け IaCによるTGWの作成と管理 本番移行 移行前準備 移行実施 まとめ 回線逼迫の課題 DX10Gの回線逼迫により、以下の課題が出てきました。 ZOZOTOWNへのアクセスが困難 今後のリプレイスプロジェクトが遅延する可能性 ZOZOTOWNへのアクセスが困難 DX10Gが回線逼迫することで、性能低下、アクセスの難しさ、信頼性の低下が生じます。具体的には、データ転送の遅延、レイテンシーの増加によるユーザーエクスペリエンスの低下、サイトやサービスへのアクセス障害、予期せぬダウンタイムによるサービス信頼性等、様々な影響が考えられます。 これらの問題は、ZOZOTOWNだけではなく同様に回線を利用している弊社のサービスであるWEARにも発生しうるものでした。 今後のリプレイスプロジェクトが遅延する可能性 ZOZOTOWNはリプレイスプロジェクトが現在進行中であり、リプレイスが進むにつれて、オンプレミスとクラウド間の通信が増えていました。このままDX10Gを使用し続ける場合、回線の飽和が生じてしまいます。この状況はリプレイスプロジェクトに遅延をもたらす可能性が高いです。リプレイスプロジェクトが遅延すると、ZOZOTOWNの成長が遅れるとともに、サイトの進化にも悪影響を及ぼすことが考えられました。 DX10GからDX100Gへの移行 上記2つの課題を対処するために、DX10GからDX100Gへの移行を実施しました。 このアップグレードにより、性能低下やアクセス困難、信頼性の低下といった問題を解決し、ZOZOTOWNのサービス品質を大幅に向上できると考えられました。 ZOZOTOWNのDX10Gネットワーク構成を簡潔に示すと、以下になります。 DX100Gへの移行準備は、以下の3ステップにて実施しました。それぞれのステップについて説明します。 DX100Gの利用申請(クラウド) DX100G用のスイッチ導入(オンプレミス) AWS Transit Gatewayの導入(クラウド) ステップ1:DX100Gの利用申請(クラウド) AWSマネジメントコンソールからDX100Gの利用申請を行います。 まず、AWS Direct Connectサービスにアクセスし、必要なデータセンターを選択して100G回線を指定します。この過程で、Letter of Authorization (LOA) をダウンロードする必要があり、これは回線事業者への申請に必要な公式文書です。LOAを回線事業者へ提出することで、物理的な接続設定を開始します。この手続きは、AWS環境とオンプレミス環境間の高速接続を確立するための重要なステップです。 詳しくはこちらをご参照下さい。 docs.aws.amazon.com ステップ2:DX100G用のスイッチ導入(オンプレミス) オンプレミス側の必要な項目について以下にまとめます。 データセンター機器のレンタル 100G通信が可能なONUとデータセンター機器を、回線事業者およびデータセンターからレンタルしました。 レンタルに関する詳細情報や契約条件は、提携している回線事業者やデータセンターに直接お問い合わせいただくことをお勧めします。 スイッチ/ケーブルの準備 弊社のスイッチは10Gまでの通信しかサポートしていなかった為、10G以上の通信を実現するためには、QSFPに対応したSFPモジュールの準備が必要でした。 MPOケーブルの準備が必要です。これは、10Gを超える通信に適しており、複数のファイバーを効率的に扱えます。 マネージド機器とスイッチの結線 準備したONUとマネージドスイッチを、弊社の既存スイッチに結線します。 実際に用意したSFPモジュールとMPOケーブルを結線する際の緊張感は、とんでもないものでした。 この段階のネットワーク構成は以下の通りです。 ステップ3:AWS Transit Gatewayの導入(クラウド) DX100Gへの移行に伴い、Transit Gateway(TGW)を導入しました。 TGWにより、複数のVPCとオンプレミスネットワーク間の通信が簡素化され、管理が大幅に効率化されます。TGWの導入は、ネットワークの拡張性と柔軟性を向上させます。 このステップは、ZOZOTOWNのインフラを次のレベルへと引き上げる重要な進展でした。それぞれについて詳しく説明します。 Transit Gateway導入の経緯 リプレイスが進むにつれ、新規VPCを追加する度にDirect Connect Gateway(DXGW)とVirtual Private Gateway(VGW)を紐付ける作業に多大な労力を要していました。 この組み合わせでは、紐づけ可能なVPCの数が最大で10までと限定されており、さらにVPC間での直接通信が不可能であるという制約がありました。 これらの課題を解決するために、TGWへの移行を決定しました。TGWを採用することで、紐付け可能なVPCの数が5000まで増加し、異なるAWSアカウント間でのVPC間通信が可能になります。この移行はネットワーク管理の効率化と柔軟性の向上を実現しました。 TransitVIFとDXGWを用意 オンプレミスとクラウド間の通信にTGWを使用する為、Transit Virtual Interface (TransitVIF)の作成が必須です。 しかし、重要な注意点があります。TransitVIFは既存のDXGWには紐付けられません。そのため、新しいDXGWを準備し、そこにTransitVIFを紐付ける手順が必要になります。また、AS(Autonomous System)番号の重複にも注意してください。 DXGWとTGWの紐付け DXGWとTGWの紐付け作業はAWSマネジメントコンソールから実施します。 IaCによるTGWの作成と管理 Infrastructure as Code (IaC)を用いてTGWの作成、そのアタッチメント、およびアソシエーションとプロパゲーションを実施しました。 この作業により、TGWのデプロイメントが自動化され、複数のVPCやオンプレミスネットワーク間での統合通信が容易になりました。IaCの採用により、これらの作業をコードベースで管理し、迅速かつ一貫性のある環境構築を実現しました。 この段階のネットワーク構成は以下の通りです。 本番移行 オンプレミスとクラウド環境の準備が整ったため、次はDX100Gへの移行プロセスについて説明します。 移行前準備 DX10GからDX100Gへの移行において、通信断やサイト停止を最小限に抑えるため、移行前に可能な限り多くの準備作業を行いました。 特に、TGWアタッチメントのアソシエーションとプロパゲーションは、既存の通信に影響を与えず事前に実施できる作業として挙げられます。これらの作業を先行することで、移行プロセスの影響を最小化しました。 また、重要な点として、各VPCのルートテーブルをTGWに向ける変更をしなければ、既存の通信へ影響を及ぼすことはありません。 移行実施 いよいよ移行作業です。 ZOZOTOWNはその巨大なシステム規模から、関連する多くのシステムとの連携が必要であり、サイト停止の調整が不可欠でした。このため、DX100Gへの移行作業は数十分で完了予定だったものの、全体としては約4時間のサイト停止時間を確保する必要がありました。約4時間かかる理由として、以下の要素がありました。 ZOZOTOWNと関連システムの停止/開始作業 DX100G切り替え後、ZOZOTOWN全体の動作検証 移行失敗時の切り戻し時間(バッファ) 移行作業としては以下作業を実施しました。 ルートテーブルのターゲット切り替え(クラウド) 移行対象のVPCに関連するルートテーブルのターゲットをVGWからTGWに変更します。 ZOZOTOWNはネットワークもIaC管理の為、CI/CDパイプラインを通じてルートテーブルのターゲットをVGWからTGWに変更しました。 この自動化されたアプローチにより、ネットワーク設定の更新と展開を迅速かつ効率的に行うことができました。 ファイアウォール機器のスタティックルート切り替え(オンプレミス) オンプレミスとクラウドの通信にファイアウォール機器を使用しています。 ファイアウォール機器のスタティックルートがDX10G回線を指している為、DX100G回線へ向けてルーティングを更新します。 これにより、通信がDX100G回線を経由するようになり、高速で安定した接続が確保されます。 上記の作業を実施した結果、6個のVPCを20分程度でDX100Gに切り替えることができました。また、VGWの関連付け解除は、移行後の対応で問題ありません。 この過程で、継続的な接続確認のためにpingやtracertコマンドを活用し、新しい回線への正確な切り替わりを監視しました。また、より詳細なネットワークパフォーマンス分析のためにmtrコマンドも利用可能です。これらのツールを駆使することで、移行作業の安全性とスムーズな実行を確保しました。適切なモニタリング手法の選択は、環境に応じて異なりますが、移行プロセスの成功には欠かせません。 まとめ 本記事では回線逼迫を解決するために、DX10GからDX100Gへの移行方法を紹介しました。 DX10GからDX100Gへの移行は、オンプレミスからクラウドまで多岐にわたる作業が必要で、非常に手間がかかりました。 本作業は、幅広い技術と計画が求められ、チーム全体での協力が不可欠でした。特に、dev/stg環境での移行手順の事前検証や移行リハーサルが、スムーズな移行を実現するための大きな要因となりました。 結果として、この移行はZOZOTOWNのインフラを大幅に強化し、将来の拡張性とパフォーマンスの向上を実現させることができました。 DX100Gを検討している方がいれば、ぜひ参考にしてください。 ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください! corp.zozo.com
アバター
こんにちは。検索基盤部の橘です。検索基盤部では、ZOZOTOWNのおすすめ順検索の品質向上を目指し、機械学習モデル等を活用しフィルタリングやリランキングによる検索結果の並び順の改善に取り組んでいます。 ZOZOTOWN検索の並び順の精度改善の取り組みについては以下の記事をご参照ください。 techblog.zozo.com 検索基盤部では新しい改善や機能を導入する前にA/Bテストを行い効果を評価しています。A/Bテストの事前評価として、オフラインの定量評価と定性評価を実施しています。これらの評価によりA/Bテストの実施判断をしています。 おすすめ順検索のフィルタリング処理の効果検証として導入したオフライン定量評価の方法については以下の記事をご参照ください。 techblog.zozo.com 以前の記事で紹介したオフライン評価を日々運用する中で、幾つか課題点が見つかりました。本記事では、その課題点と改善方針について紹介します。 目次 目次 ZOZOTOWNのおすすめ順検索の商品ランキングロジックにおけるフィルタリング処理 フィルタリング処理における定量評価と定性評価 A/Bテストにおける定量評価と定性評価の位置付け 定量評価について 定性評価について オフライン評価で起こった問題 問題の原因分析アプローチ CVログ取得時と評価時の検索インデックス内容の差異 問題原因の分析 定量評価の改善 まとめ おわりに ZOZOTOWNのおすすめ順検索の商品ランキングロジックにおけるフィルタリング処理 ZOZOTOWNのおすすめ順検索の商品のランキングロジックは2つのフェーズに分けられます。 フィルタリング処理 : 再現率を高めることを目的にルールベースのロジックや軽量な機械学習モデルを用いて商品のフィルタリングを行います。 リランキング処理 : フィルタリング時のスコアトップN件に絞って「ランキング学習」と呼ばれる手法の機械学習モデルを用いた並び替え処理を行います。 フィルタリング処理における定量評価と定性評価 A/Bテストにおける定量評価と定性評価の位置付け 以下にA/Bテスト実施までのフローを示します。 オフラインの定量評価及び定性評価は、事前評価としてA/Bテストの前に実施します。 定量評価について 以前のテックブログ に記載している定量評価について抜粋してご紹介します。定量評価では、以下の評価指標により新ロジックの性能の良さを評価します。 評価指標 評価したい内容 評価結果の解釈 例 コンバージョンカバー率 コンバージョンの大幅な悪化がないこと 評価値が高いほどコンバージョンの悪化が少ない 新商品表示率 新ロジックが旧ロジックとは異なる商品を多く表示できるか 評価値が高いほど新ロジックは旧ロジックとは異なる商品を多く表示できる RBO(rank biased overlap) 新ロジックと旧ロジックの検索結果の並び順がどの程度類似しているか 評価値が低いほど新ロジックは旧ロジックの検索結果の並び順と異なる 0.8(0から1の値をとる) 定性評価について 次におすすめ順でのフィルタリング処理における定性評価について説明します。ZOZOでは社内で開発しているツールを用いて検索結果を定性評価しています。以下は、おすすめ順のフィルタリング処理の定性評価の流れです。 評価依頼者が評価対象の検索キーワードを評価者に割り振る 評価者がツールに評価対象の検索キーワードを入力する ツールが検索結果を新旧ロジックから取得し、それぞれの検索結果の差集合にあたる商品を画面に出力する 評価者は新旧ロジックそれぞれの結果のどちらが良いかを評価し、評価依頼者に結果を返却する 評価の流れのイメージは以下の通りです。 定性評価ツールの画面出力イメージは以下の通りです。 差集合にあたる商品のみを比較することで新旧ロジックの優劣を明確にします。 評価依頼者は複数の評価者による評価結果を集計し、新旧ロジックどちらが良いかの判定とA/Bテストに進むかを判定します。 現状の定量評価と定性評価は大幅に指標が上昇または下降していないかを確認する役割が大きいですが、理想的にはロジックの真の性能と定量評価の結果、定性評価の結果は相関することが望ましいです。もし性能の悪い新ロジックが定量評価で悪い評価結果になると、定性評価を実施する前に施策をストップします。 オフライン評価で起こった問題 前章のオフライン評価を用いたA/Bテストの事前評価を進めていくにあたり起こった問題について説明します。 定量評価の結果は良好であった新ロジックについて定性評価を引き続き実施したところ、定性評価の結果が悪いという結果になりました。以下の図に定量評価と定性評価の結果を示しています。 新ロジックでのみ抽出される商品(新ロジック抽出商品)のコンバージョン(CV)ログが存在することについては後ほど説明します。 定量評価では、コンバージョンカバー率と新商品表示率ともに高い値であり、新ロジックは良い結果となっています。 定性評価では、旧ロジックの方が良い検索結果となっている件数が多く、新ロジックは悪い結果となっています。 上に記載した通り本来は定量評価と定性評価の結果は相関するのが望ましいので上記は望ましくない結果であり、定量評価の方法もしくは定性評価の方法に改善すべき点があります。 双方の評価が相関しない原因を分析し、改善方針を検討しました。 問題の原因分析アプローチ この定量評価と定性評価の乖離の原因を分析するにあたり、どのようにアプローチしていけばよいでしょうか? 実際の分析アプローチとして、先ほどの図の定量評価の結果部分にあった『新ロジック抽出商品のCVログ』を活用しました。 この定量評価の指標を導入した当時は、新ロジック抽出商品のCVログは存在しない想定でした。検索結果に表示される商品は旧ロジックでのみ抽出される商品であるため、新ロジックでのみ抽出される商品のCVログは存在しないと想定していたためです。 しかし、実際にはCVログの中には新ロジック抽出商品のCVログが存在しました。この原因として、CVログ取得時と評価時の検索インデックス内容の差異の例をご紹介します。 CVログ取得時と評価時の検索インデックス内容の差異 以下の図のように、ZOZOTOWNの商品検索システムのインデックスは逐次更新されています。商品の売り切れや新商品の発売などに応じてインデックス内の商品が入れ替わります。よってCVログ取得時(ユーザーの検索行動時)は同じ旧ロジックを扱っていても検索結果は変化することがあります。 対して、評価時は旧ロジックによって表示された商品をログから集計せず特定のインデックスを使用し検索結果を抽出します。 このCVログ取得時と評価時のインデックスの違いが、CVログ取得時との検索結果の差分を発生させている原因です。 以上から、過去のインデックスから旧ロジックにより抽出された商品は、評価時のインデックスでは新ロジック抽出商品となりえることがわかりました。 問題原因の分析 定性評価の事後分析を実施し、先ほど説明したCVログを利用して新ロジック抽出商品ごとの過去のCV率を集計し、旧ロジック抽出商品のCV率と比較しました。新ロジック抽出商品と旧ロジック抽出商品のCV率のヒストグラムを作図すると以下のようになっていました(実際とは異なりますが、同様の傾向を持つデータです)。 この図から読み取れる新旧ロジックの特徴は以下の通りです。 新ロジック抽出商品はCV率のばらつきが比較的大きい。CV率が比較的高い商品を多く含むが、CV率が0の商品も多く含む。 旧ロジック抽出商品はCV率のばらつきが比較的小さい。 新ロジック抽出商品におけるCV率が0の商品を確認したところ、検索キーワードと無関係で検索意図に合わない商品となっていることがわかりました。 つまり、新ロジック抽出商品はユーザーの検索意図に合わない商品が多く含まれており、定性評価時にそれが原因で悪い評価結果になったことがわかりました。 ユーザーの検索意図に合わない商品が検索結果に多く含まれる場合、ユーザーは検索結果に対し満足せず再検索や検索から離脱する可能性があります。さらにはZOZOTOWNの検索機能やサービス自体に疑念を持つ場合も考えられます。 上記のリスクに関して、eコマースサイトの検索機能の改善ガイドラインを提供する Baymard Instituteのブログ でも、検索意図に合わない結果がサイトの離脱を招くと指摘されています。 以上より、検索意図に合わない商品数を確認できるよう定量評価を改善することにしました。 定量評価の改善 ある程度の量の新ロジック抽出商品のCVログが取得出来れば検索キーワードごとの新ロジック抽出商品のCV率が求められます。それを利用し、定量評価の時点でCV率が極端に低い(検索意図に合わない)商品数を新旧ロジックで比較することにしました。 この比較により検索意図に合わない商品の多さを定量評価の時点で把握でき、検索意図に合わない商品が多い傾向の新ロジックの検討を定量評価の時点で中止できます。 まとめ 本記事ではZOZOTOWNのおすすめ順検索の精度改善におけるオフライン評価を運用していた中で生じた課題について説明し、分析アプローチや評価方法のブラッシュアップの事例をご紹介しました。 引き続きオフライン評価を実施し評価方法のブラッシュアップを重ねていく予定です。 おわりに ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com
アバター
はじめに こんにちは、検索基盤部の広渡です。検索基盤部では、検索クエリのサジェスト(以下、サジェスト)の改善を行なっています。ここでサジェストは一般的に「Query Auto Completion」と呼ばれる、検索クエリを入力した際に入力の続きを補完したキーワードを提示する機能を指します。 ZOZOTOWNにおいては検索クエリを入力したとき、最大10件の検索クエリのサジェスト(以下、サジェストリスト)が表示されます(なお、ランキングを考慮しない場合はサジェスト集合と呼ぶこととします)。また、サジェストリストのランキングはユーザーの行動ログを用いて計算されたスコアによって決定されます。サジェストの具体的な説明や過去の改善事例は以下の記事を参照してください。 techblog.zozo.com techblog.zozo.com サジェストリストをチーム内で定性評価したところ、類似したサジェストが多く表示されることによる、多様性の乏しさを指摘する声が多くありました。多様性の乏しいサジェストリストの具体例は、「ゾゾ」「ZOZO」など表記揺れした同じ意味のサジェストや、「パンツ」「パンツ レディース」など性別のみ加わっただけのサジェストを含むものです。このような多様性の乏しいサジェストリストは、ユーザーに価値のあるサジェストが含まれず、結果的にユーザーの入力文字数が増えてしまい、検索体験の質を下げてしまう可能性があるため重要な課題です。 本記事では、サジェスト集合の多様性度を計測するdiversityと、サジェストリストの多様性度を計測するD#-nDCGについて紹介し、それらの指標とゴール指標との相関についての検証結果を解説します。ゴール指標は後ほど詳しく説明しますが、CTR(Click Through Rate)とCVR(Conversion Rate)です。多様性評価指標とゴール指標の相関を調査する目的は、多様性評価指標がZOZOTOWNのサジェスト改善に有効かを調べるためです。有効な指標を用いて新規サジェスト手法を計測することで、実際のユーザーの反応を見る前に改善が見込めるかどうかを判断でき、改善サイクルの効率化につなげられます。 目次 はじめに 目次 多様性評価指標 サジェスト集合の多様性度を表す指標 多様性を考慮したランキング指標 α-nDCG D#-nDCG ゴール指標と多様性評価指標の相関 測定方法 結果 まとめ おわりに 参考文献 多様性評価指標 ここでは、ランキングを考慮しないサジェスト集合の多様性度を表す指標について紹介し、その後、多様性を考慮したランキング指標について紹介します。 サジェスト集合の多様性度を表す指標 まず、 Ma, Hao et al, AAAI 2010 1 で提案されたdiversityと呼ばれるサジェスト集合の多様性度を測る指標について紹介します。 入力クエリ が与えられた際のサジェスト集合を とし、 番目のサジェストを とします。サジェスト集合 に対する多様性度は Ma, Hao et al, AAAI 2010 の定義に従うと、以下のように定義されます。 ここで であり、 は 番目のサジェスト と 番目のサジェスト との距離を表します。 このサジェスト間の距離 として、様々な定義が提案されています。 Ma, Hao et al, AAAI 2010 では、サジェスト経由で表示されたWebページのうち、クリックされた各ページに関連する検索クエリの集合のコサイン類似度を用いています。 Zhu, Xiaofei et al, WWW 2011 2 では、サジェストを経由して表示された上位10件のWebページのうち重複した割合で定義しています。 サジェスト間の距離が遠ければ遠いほど の値が大きくなり、サジェスト集合は多様であると判断できます。 多様性を考慮したランキング指標 diversityではサジェスト間の距離をもとに多様性度を測定していましたが、ランキングは考慮されていませんでした。ここでは多様性を考慮したランキング指標である、α-nDCGとD#-nDCGについて紹介します。ここからの説明は 情報アクセス評価方法論 3 を参考にしています。 α-nDCGとD#-nDCGは、サジェストリストがユーザーの多様な検索意図に適合するかを評価できるようにnDCG(normalized Discounted Cumulative Gain)を拡張したものです。ここでいう多様な検索意図とは、例えば"パンツ"を入力したユーザーはズボンを探している場合もあれば、下着を探している場合もあるなど、同一の検索クエリに対してユーザーの求める情報が異なることを指しています。 α-nDCG 本節では、 Clarke, Charles L.A. et al, SIGIR 2008 4 で提案されているα-nDCGについて紹介します。α-nDCGは、すでに検索された意図の利得を減衰させる特徴があります。 まず、 番目のサジェスト が 番目の意図 に適合するとき 、しないとき とします。 ここで、意図 に適合すると判断された 番目までのサジェストの個数を で表します。 このとき、 番目までサジェストリストを評価するための指標α-DCG(Discounted Cumulative Gain)は以下のように表されます。 ここで はパラメータであり、 情報アクセス評価方法論 によると通常0.5に設定されます。 そして理想的なランキングのα-DCGを求め、正規化したものがα-nDCGです。 α-nDCGによる評価は、 Zhu, Xiaofei et al, WWW 2011 , Cai, Fei et al, Foundations and Trends 2016 5 , Cai, Fei et al, ACM 2016 6 でも採用されています。 しかし、理論的には理想的なランキングを求める計算はNP完全であると Clarke, Charles L.A. et al, SIGIR 2008 で述べられています。 D#-nDCG α-nDCGにおけるNP完全を回避できる指標が、 Sakai, Tetsuya et al, SIGIR 2011 7 で提案されているD#-nDCGです。 D#-nDCGはサジェストリストを評価するD-nDCGと、検索意図の再現性を評価する意図再現率I-recの2つの要素から構成されています。まず、D-nDCGについて説明します。 検索窓に入力されたキーワード に対する意図 の確率を意図確率 とし、 番目のサジェスト の意図 に対する利得を とします。意図確率 のイメージとしては、"パンツ"が検索窓に入力されたとき、70パーセントがズボン、30パーセントが下着を意図しているといったものです。 このとき、 番目までのサジェストリストを評価するための指標D-DCGは以下のように表されます。 この式の分子 はグローバル利得と呼ばれます。 グローバル利得は、サジェスト が入力キーワード に適合する確率 を近似したものです。 このグローバル利得によりソートすることで理想的なランキングを求め、正規化したものがD-nDCGです。 次に、 Zhu, Xiaofei et al, WWW 2011 で提案されている意図再現率I-recについて説明します。 まず、 を、 番目までのサジェストリストの中で意図 に適合したものがあれば1、なければ0を表す変数とします。先ほどの を用いると、 で表されます。 このとき、 番目までのサジェストリストの意図再現率I-recは以下のように表されます。 ここまでで求めたD-nDCGと意図再現率I-recを線形結合させたものがD#-nDCGであり、以下のように表されます。 はパラメータであり0.5に設定されることが多いようです。 ゴール指標と多様性評価指標の相関 ここからは、上記で紹介した多様性評価指標であるdiversityとD#-nDCGが通常のnDCGと比べてゴール指標に相関するのか調査します。 ゴール指標であるCTR(Click Through Rate)とCVR(Conversion Rate)について説明します。CTRは(サジェストリストのクリック数/サジェストリストの表示数)で、CVRは(サジェストリストを経由して商品詳細に遷移した数/サジェストリストクリック数)としています。 α-nDCGではなくD#-nDCGを採用した理由は、先ほど説明にもある通りα-nDCGで生じるNP完全が回避できるためです。 本章では、各指標の計測方法を説明し、結果について述べます。 測定方法 まず、diversityの計測方法を説明します。 サジェスト間の距離 を計測するために、「サジェストを経由して遷移した商品の類似性」に着目しました。具体的な定義としては、遷移先商品の頻度をベクトル で表現し、そのコサイン類似度を用いました。 次に、D#-nDCGの計測方法を説明します。 はじめに、入力キーワードの意図 を定義する必要があります。α-nDCGを採用していた Cai, Fei et al, ACM 2016 では、クエリを通じてクリックされたURLを ODP(Open Directory Project) に基づき分類することで意図としていました。 ここから、ZOZOTOWNにおける商品URLも何らかの粒度で分類する必要がありました。クエリを通じてクリックされた商品URLをブランドとカテゴリーの組み合わせにより分類することで意図と定義しました。例えば、ユーザーがブランド「Hoge」のスニーカー商品をクリックするとURLは brand/hoge/shoes/sneakers/ になります。このURLからユーザーの検索意図は「Hoge スニーカー」と分類し定義しました。 この定義に基づき、意図確率 は、検索窓に文字列 を入力した後に検索された意図を集計することにより求めました。また、 番目のサジェスト の意図 に対する利得 は、サジェストを経由してクリックした商品が意図に適合していれば1、していなければ0としました。 最後に、nDCGの計測方法を説明します。 を 番目サジェストに対する利得としたとき、nDCGにおけるDCGは以下の式で表されます。 ここで利得 は、以下のようにサジェストを経由して商品詳細に遷移した回数によって定義しました。 利得=3: 100回以上 利得=2: 10回以上100回未満 利得=1: 1回以上10回未満 利得=0: 上記以外 ZOZOTOWNで実際にある1日で表示されたサジェストを使用して実験をしました。実験にあたっては、はずれ値の影響を抑えるために、以下に該当するサジェストリストのフィルタリング処理を事前に行いました。 CTRやCVRが0もしくは1 同じ商品を除き商品詳細に遷移した回数が3回未満 検索回数が5回未満 結果 以下の表にそれぞれの指標におけるCTR, CVRの相関係数を示します。有効数字3桁となるように四捨五入してあります。 nDCG diversity D#-nDCG CTR -0.00257 -0.153 0.253 CVR 0.177 -0.0353 0.219 D#-nDCGにおいては、弱い相関ではありますがnDCGよりも相関するという結果になりました。このことから、他の2指標と比較すると、D#-nDCGの値が高くなれば、ユーザーの検索体験の質が向上する傾向にあると言えます。 特に、CTRについてはnDCGとの差が大きいという結果になりました。これは、D#-nDCGはnDCGと比べ、クリックされた情報だけでなく、入力キーワードの意図をどの程度再現できるかを考慮している特徴が影響していると考察しました。 diversityがnDCGと比べ相関が弱くなった原因として、下記の影響が考えられます。 ZOZOTOWNではユーザーごとに異なる検索結果を表示しているため、いかなるサジェスト集合でも、サジェスト間の距離が大きく計測され多様性度も高くなる傾向にある。 入力キーワードによっては、多様なサジェストが表示される必要のない場合を考慮できない。例えば、入力キーワードが特定の商品名の場合は、遷移先の商品の多様性が乏しいためサジェスト集合の多様性度は低くなるが、CTRは低くならない。 まとめ 本記事では、サジェストにおける多様性評価指標と、それらの指標とCTR/CVRとの相関について紹介しました。実際に多様性評価指標を使うと、通常のnDCGに比べてCTR/CVRと相関することが分かりました。 引き続きサジェストの評価指標を調査し、さらにオフライン精度評価体制を整えていきます。 おわりに ZOZOでは検索エンジニア・MLエンジニアのメンバーを募集しています。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com 参考文献 Ma, Hao and Lyu, Michael and King, Irwin. (2010, January). Diversifying Query Suggestion Results. In Proceedings of the Twenty-Fourth AAAI Conference on Artificial Intelligence (pp. 1399-1404). ↩ Zhu, Xiaofei and Guo, Jiafeng and Cheng, Xueqi and Du, Pan and Shen, Hua-Wei. (2011, March). A Unified Framework for Recommending Diverse and Relevant Queries. In Proceedings of the 20th International Conference on World Wide Web (pp. 37–46). ↩ 酒井 哲也.(2015, 6月). 情報アクセス評価方法論ー検索エンジンの進歩のためにー. コロナ社 (pp. 73-82). ↩ Clarke, Charles L.A. and Kolla, Maheedhar and Cormack, Gordon V. and Vechtomova, Olga and Ashkan, Azin and Buttcher, Stefan and MacKinnon, Ian. (2008, July). Novelty and diversity in information retrieval evaluation. In Proceedings of the 31st annual international ACM SIGIR conference on Research and development in information retrieval (pp. 659–666). ↩ Cai, Fei and de Rijke, Maarten. (2016). A Survey of Query Auto Completion in Information Retrieval. In Foundations and Trends in Information Retrieval (pp. 1-92). ↩ Cai, Fei and Reinanda, Ridho and Rijke, Maarten De. (2016, September). Diversifying Query Auto-Completion. In ACM Transactions on Information Systems (pp. 1-33). ↩ Sakai, Tetsuya and Song, Ruihua. (2011, July). EvaLuating diversified search results using per-intent graded relevance. In Proceedings of the 34th international ACM SIGIR conference on Research and development in Information Retrieval (pp. 1043–1052). ↩
アバター
はじめに こんにちは、技術本部SRE部カート決済SREブロックの遠藤・金田です。 普段はSREとしてZOZOTOWNのカート決済機能のリプレイスや運用を担当しています。本記事では自作のコマンドラインツールをSlack + AWS Chatbot + AWS Lambdaを使用してChatOps化した事例をご紹介します。「日々の運用業務をコマンドラインツールを実装して効率化したものの今ひとつ広まらない」「非エンジニアにも使えるようにしたい」と考えている方の参考になれば幸いです。 目次 はじめに 目次 背景・課題 ChatOpsとは AWS ChatBotとは 構成 AWS ChatBot チャットツール側の設定 Slack Workflow Lambda 実装のポイント ChatBotのアクセス制御 User Roleの運用方法 ガードレールポリシー コマンドラインツールのLambda関数化 コマンドラインツールについて local環境での実行とPod起動コマンドの共通化 cgroupによる実行環境の判定 ツールのコードを再利用したLambda関数の実装 main関数の差し替え 実行結果の通知方法 標準出力をキャプチャする実装 ChatOps化の効果 環境構築や準備が不要 本番環境の権限が不要 実行履歴が残る さいごに 背景・課題 私たちの日々の運用業務の1つに過熱登録という作業があります。 福袋や限定品など、ユーザーから大量のアクセスが来る商品のことを弊社では「過熱商品」と呼んでいます。過熱商品は販売開始のタイミングで大量のアクセスが発生します。ZOZOTOWNのカート機能では過熱商品の販売によりアクセスが急増した際にも過熱商品ではない商品の購入に支障が出ないようにシステムを構築しています。 詳しくは以下のテックブログをご覧ください。 techblog.zozo.com 「過熱登録」とは、上記で紹介している、カート投入された商品が過熱商品であるかを判断するためのデータベース(以下:DB)に、その商品情報を登録する作業を指します。 このプロセスでは、商品ごとに一意のIDを用いてDB内に登録していきます。 過熱登録に使用するIDは商品のサイズや色毎に発行されるIDを使用しており、1つの商品に紐づく全てのIDをDBから抽出しそれらを個別に登録する必要があるため、手作業で行うと手間のかかる面倒な作業でした。 上記の課題を解決するため、DBから関連情報を抽出し一括で過熱登録するコマンドラインツールをGo言語で実装し運用していましたが、以下のような別の課題が見えてきました。 ツールを使用するための事前準備が必要 本番環境に接続する権限が必要 実行履歴が残らない コマンドラインツールのため非エンジニア向けではない これらの課題を解決する手法としてChatOpsの採用を決め実装しました。 ChatOpsとは ChatOpsとは、Slackなどのチームコラボレーションツールと自動化を組み合わせたもので、システム運用における運用業務を自動化するものです。ChatOpsはDevOps文化と密接に関わっており、作業の透明性や効率性を高めることができます。 AWS ChatBotとは AWS ChatBotとは、Slackなどのチャットツールと連携してAWSリソースを管理、監視、操作するためのAWSサービスです。 以下のような特徴が挙げられます。 通知とアラート:AWSのサービスやリソースに関するイベントやアラートをリアルタイムでチャットツールに通知できます。 アクションの実行:ChatBotを使用してAWSリソースを操作できます。awsコマンドで実施できることは一通り実行できます。 アクセス制御:IAMを使用して、特定のチャンネル、ユーザーにのみAWSリソースへのアクセスを許可します。これによりセキュリティを維持しながらチーム全体でのAWSリソースの管理が可能になります。 また、2024/2時点で以下の3つのチャットツールと連携が可能です。 Amazon Chime Slack Microsoft Teams 構成 ChatOpsのシステム構成は以下の通りです。 ChatOps用のSlackチャンネルからワークフローを実行し、対象の商品IDなど必要な情報を入力します。新しいスレッドが作成され、AWS ChatBotに対してコマンドが実行されます。ChatBot経由でAWS Lambdaを実行し、過熱登録ツールが実行されます。 AWS ChatBot ZOZOではチャットツールとしてSlackを使用しているため、本記事ではSlackとChatBotの連携方法について記載します。 はじめに、SlackとAWS ChatBotを連携する必要があり、Slackワークスペースに対して、AWS ChatBotアプリのアクセスを許可します。 その後、AWS ChatBotのリソースを作成します。 AWS ChatBotのCloudFormationは非常にシンプルです。WorkspaceIdとChannelIdを指定して、チャンネル単位で連携します。AWS ChatBotには後段のAWS Lambdaを実行するRoleを付与しています。 Resources : ZozoCartChatOpsChatbot : Type : AWS::Chatbot::SlackChannelConfiguration Properties : SlackWorkspaceId : !Ref TargetWorkspaceId SlackChannelId : !Ref TargetChannelId ConfigurationName : !Ref ConfiguredChannelName IamRoleArn : !GetAtt ZozoCartChatOpsChatbotRole.Arn LoggingLevel : ERROR ZozoCartChatOpsChatbotRole : Type : AWS::IAM::Role Properties : RoleName : zozo-chatops-chatbot AssumeRolePolicyDocument : Version : '2012-10-17' Statement : - Effect : Allow Principal : Service : - chatbot.amazonaws.com Action : - sts:AssumeRole ManagedPolicyArns : - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole Policies : - PolicyName : zozo-chatops-chatbot-policies PolicyDocument : Version : '2012-10-17' Statement : - Effect : Allow Action : - 'lambda:invokeAsync' - 'lambda:invokeFunction' Resource : - !GetAtt ZozoChatOpsLambda.Arn チャットツール側の設定 ChatBotリソースが作成されたら、ChatBotを利用したいSlackチャンネル上で以下のコマンドを実行しAWSアプリを追加します。 /invite @aws これでSlackからAWS ChatBotを利用する準備が整いました。 Slackの場合、パブリックチャンネル/プライベートチャンネルのどちらでも連携可能です。権限設定については後述しますが、管理をシンプルにするためプライベートチャンネルを採用し、作業者のみをチャンネルに招待して利用を開始しました。 Slack Workflow Slackにはワークフローという自動化機能があり、社内でも多く活用しています。作業をより簡単にするため、ChatOpsでもSlackワークフローを使って簡易化を検討しました。 自動化する作業ごとにワークフローを作成します。ワークフローを実行するとスレッド上でawsコマンドが投稿され、AWS ChatBotへ命令を渡すことができます。 aws lambda invoke コマンドを実行して任意のLambda関数を実行します。 Lambda ChatOpsから任意の処理を実行する基盤としてAWS Lambdaを採用しました。選定理由としては以下が挙げられます。 ChatBotから実行できワークフローで指定したパラメータを渡せる 今後様々な用途でChatOpsを利用することを考え、柔軟に実装できる コストが安い ChatOpsは運用者がコマンドを実行しない限り発動しないため、実行時間での課金であるAWS LambdaはChatOpsの実行基盤として相性が良いです。 ChatBotを経由して以下のコマンドを実行することでLambda関数を実行します。 @aws lambda invoke --payload '{JSON形式で過熱商品IDなどを渡す}' --function-name [Lamda関数名] --invocation-type Event --region ap-northeast-1 --payload オプションでLamdbaへ任意のパラメータを渡すことができるため、ワークフロー内で入力された情報をLambdaに渡すことができます。 実装のポイント ChatBotのアクセス制御 AWS ChatBotの権限設定には2つの設定範囲があります。 Channel Role User Role Channel Roleは、SlackチャンネルごとにIAM Roleを付与します。そのSlackチャンネルに参加するメンバーは同じ権限でChatBotにコマンドを送ることができます。 User Roleは、Slackユーザーごとに個別のIAM Roleを付与できます。チャンネル内のメンバーごとに、異なる権限を与えたい場合にはこちらが適しています。 理想としては、ChatOps用のSlackチャンネルをパブリックチャンネルにして、一部のメンバーのみが過熱登録できる状態が望ましいです。そのためUser Roleを使いたいと考えましたが、後述する問題がありSlackチャンネルはプライベートチャンネルにしてChannel Roleを使って運用しています。 User Roleの運用方法 ChatBotの設定でUser Roleを有効化することで、Slackユーザーごとに個別のIAMロールを付与できます。 しかし、User Roleの設定は各ユーザーがAWSのマネジメントコンソールにアクセスして、ChatBotの画面からSlackへ連携する必要があります。 この連携設定はCloudFormationで管理できません。AWSへの権限がある程度必要になるため、AWSへの権限を持っていないユーザーはそもそもこの設定ができません。 また、設定時に付与するIAM Roleを自分で設定する必要があり、統制を取ることが難しいと感じました。そのため、一旦プライベートチャンネルとChannel Roleでの運用としています。 ガードレールポリシー 上記のロール設定とは別にAWS ChatBotにはガードレールポリシーという設定があります。ガードレールポリシーで権限を制限することで、その範囲を超えて権限が付与されることを防ぎます。 ガードレールポリシーはデフォルトでAdministratorAccessとなっているため、制限がない状態です。ChatBotからアクセスする想定がないリソースはここで権限を制限しておくと安心です。 これにより、誤ってAWS ChatBotに強い権限が付与されてしまう事故を防ぐことができます。 コマンドラインツールのLambda関数化 コマンドラインツールについて 過熱登録用コマンドラインツール(以下、過熱登録ツールと記載します)についても簡単に触れておきたいと思います。 過熱登録の作業を効率化するために実装したGo言語製ツールで、機能毎にサブコマンドとして実装しています。 機能(サブコマンド) 処理内容 商品情報の取得 指定されたIDに紐づく商品情報をDBから取得して出力する。 過熱登録 1つの商品に紐づく全てのIDをDBから取得して過熱登録用のAPIサーバに登録リクエストを行う。 過熱商品の検知と自動登録 カート投入のログから過熱商品の発生を検知して自動で過熱登録する。ArgoWorkflowから定期的に実行する。 local環境での実行とPod起動コマンドの共通化 過熱登録ツールの各機能は主にDBサーバからの情報取得とEKS上で稼働しているAPIサーバへのリクエストで構成されます。 アクセス権限やDB接続情報などの秘密情報を管理する観点から、DBサーバやAPIサーバへのアクセスはlocal環境で実行する過熱登録ツールからは行わず、EKS上にPodを起動して行う構成としています。 Pod起動に使用するコンテナイメージは過熱登録ツールのバイナリを /usr/local/bin に配置したイメージをECRに登録して使用しています。 以下のDockerファイルでイメージをビルドしています。 FROM golang:1.21.4 WORKDIR /go/src/hotitem-tool COPY . . RUN go mod tidy # GOOSとGOARCHの設定以外はソースコードも含めlocal環境用のビルドと同一の設定でビルド RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o /usr/local/bin/hotitem-tool . CMD [ "hotitem-tool" ] 過熱登録ツールはlocal環境で起動されたらPod起動の処理を行い、コンテナ環境で起動されたら各種サーバへのアクセスを行いビジネスロジックを実行する構成になっています。 local環境用/コンテナ環境用といったようにツールの実装を分けてしまうと管理が煩雑になってしまいます。そのため過熱登録ツール内部で起動された環境がlocal環境か/コンテナ環境かを判別して、Podを起動するか/ビジネスロジックを実行するかを分岐しています。 下記は商品情報の取得処理を実行する場合のツール起動コマンドとPod起動設定です。 local環境でのコマンド実行例です。 hotitem-tool get-goodsinfo --all 1234 上記コマンドで実行した場合、過熱登録ツールは以下のマニフェストに相当するspec設定で client-go を利用してPodを起動します。 apiVersion : v1 kind : Pod name : hotitem-tool namespace : <namespace> spec : containers : - name : hotitem-tool image : <ECRにpushした過熱登録ツールコンテナイメージのURI> command : - hotitem-tool - get-goodsinfo - --all - 1234 ツール起動時に指定されたサブコマンド/オプション/引数の値を全て引き継いでコンテナの起動コマンドを設定してPodを起動しています。Pod起動によってコンテナ環境で実行された過熱登録ツールは、commandに設定されたサブコマンドや引数に応じたビジネスロジックを実行します。 このような構成にすることで実装を分けずにツール単体でクライアント/サーバ型のアプリケーションと近い構成を実現しています。 cgroupによる実行環境の判定 ツールがどの環境で起動されたかの判定にはcgroupを利用しています。 cgroup(Control Group)はLinuxカーネルの機能の1つで、システムリソースを使用するプロセスをグループ化し、グループに対してのリソース使用量を制限できる機能です。cgroupはコンテナの実現に重要な役割を担っておりKubernetesにおいてもnodeリソースを管理するために利用されています。 /proc/<pid>/cgroup を確認すると <pid> のプロセスがどのコントロールグループに属しているかがわかるようになっています。 下記は開発環境のEKSで稼働しているコンテナのcgroupを実際に出力してみた結果です。コンテナのメインプロセスはpid:1で起動されるためpidは1を指定しています。 / # cat /proc/1/cgroup 11:cpuset:/kubepods.slice/kubepods-pod1f9bc0a5_a5bc_4707_b55e_f9accda568af.slice/cri-containerd-7e3aab58c075f4d2b7e00a7f91d5bc9ed827fb141f0cc2a95ab25083c71d0f6e.scope 10:hugetlb:/kubepods.slice/kubepods-pod1f9bc0a5_a5bc_4707_b55e_f9accda568af.slice/cri-containerd-7e3aab58c075f4d2b7e00a7f91d5bc9ed827fb141f0cc2a95ab25083c71d0f6e.scope 9:freezer:/kubepods.slice/kubepods-pod1f9bc0a5_a5bc_4707_b55e_f9accda568af.slice/cri-containerd-7e3aab58c075f4d2b7e00a7f91d5bc9ed827fb141f0cc2a95ab25083c71d0f6e.scope 8:pids:/kubepods.slice/kubepods-pod1f9bc0a5_a5bc_4707_b55e_f9accda568af.slice/cri-containerd-7e3aab58c075f4d2b7e00a7f91d5bc9ed827fb141f0cc2a95ab25083c71d0f6e.scope 7:perf_event:/kubepods.slice/kubepods-pod1f9bc0a5_a5bc_4707_b55e_f9accda568af.slice/cri-containerd-7e3aab58c075f4d2b7e00a7f91d5bc9ed827fb141f0cc2a95ab25083c71d0f6e.scope 6:net_cls,net_prio:/kubepods.slice/kubepods-pod1f9bc0a5_a5bc_4707_b55e_f9accda568af.slice/cri-containerd-7e3aab58c075f4d2b7e00a7f91d5bc9ed827fb141f0cc2a95ab25083c71d0f6e.scope 5:memory:/kubepods.slice/kubepods-pod1f9bc0a5_a5bc_4707_b55e_f9accda568af.slice/cri-containerd-7e3aab58c075f4d2b7e00a7f91d5bc9ed827fb141f0cc2a95ab25083c71d0f6e.scope 4:blkio:/kubepods.slice/kubepods-pod1f9bc0a5_a5bc_4707_b55e_f9accda568af.slice/cri-containerd-7e3aab58c075f4d2b7e00a7f91d5bc9ed827fb141f0cc2a95ab25083c71d0f6e.scope 3:devices:/kubepods.slice/kubepods-pod1f9bc0a5_a5bc_4707_b55e_f9accda568af.slice/cri-containerd-7e3aab58c075f4d2b7e00a7f91d5bc9ed827fb141f0cc2a95ab25083c71d0f6e.scope 2:cpu,cpuacct:/kubepods.slice/kubepods-pod1f9bc0a5_a5bc_4707_b55e_f9accda568af.slice/cri-containerd-7e3aab58c075f4d2b7e00a7f91d5bc9ed827fb141f0cc2a95ab25083c71d0f6e.scope 1:name = systemd:/kubepods.slice/kubepods-pod1f9bc0a5_a5bc_4707_b55e_f9accda568af.slice/cri-containerd-7e3aab58c075f4d2b7e00a7f91d5bc9ed827fb141f0cc2a95ab25083c71d0f6e.scope / # cgroupは階層構造を持っており、上記の出力でも確認できる通りKubernetesで起動されるコンテナのcgroupは必ず kubepods.slice/ というユニットに属します。 以上のことを利用し、過熱登録ツールでは以下の実装で実行環境を判定しています。 func IsInPods() bool { _, err := os.Stat( "/proc/1/cgroup" ) if err != nil { return false } return grep( "(kubepods)" , "/proc/1/cgroup" ) } func grep(pattern, filename string ) bool { file, err := os.Open(filename) if err != nil { return false } defer file.Close() scanner := bufio.NewScanner(file) for scanner.Scan() { line := scanner.Text() if match, _ := regexp.MatchString(pattern, line); match { return true } } if err := scanner.Err(); err != nil { return false } return false } ツールのコードを再利用したLambda関数の実装 過熱登録に必要な機能は、過熱登録ツールに実装済みであるため、実装済みの機能はChatOps用に再実装することなくそのまま活かしたい、ソースコードも同一リポジトリで管理したいと考えました。 Go言語でLambda関数を作成するには aws-lambda-go を使って以下のようなmain関数とハンドラ関数を実装します。 package main import ( "context" lambdago "github.com/aws/aws-lambda-go/lambda" ) type Option struct { Name string Value string } type Options []Option // 引数で受け取るペイロードは独自形式で定義できます type Payload struct { Command string Options Options Arguments string } func main() { lambdago.Start(handler) } func handler(ctx context.Context, event Payload) ( string , error ) { switch event.Command { case "get-goodsinfo" : return executeGetGoodsInfoCommand(event) case "regist" : return executeRegistCommand(event) } return "" , nil } 〜略〜 実装が完了したら以下の手順でLambda関数を作成します。 コンテナイメージをビルド ECRにpush Lambda関数の作成オプションでコンテナイメージを選択し、ECRにpushしたコンテナイメージのURIを設定 過熱登録ツールがlocal環境側で行う処理は、EKSにPodを起動して実行結果を受け取り出力することだけです。 Lambda関数化しても行う処理は同様で、EKSにPodを起動して実行結果を受け取り出力できれば良いです。そのためmain関数をコマンドラインツール用の実装からLambda関数用の実装に切り替えることができれば、既存実装をそのまま活かしてLambda関数化できます。 main関数の差し替え Go言語には実行対象の環境毎に実装を切り替えることができる ビルド制約 (ビルドタグとも呼ばれます)という機能があります。 ビルド制約は基本的にはOS/ARCHといったビルド時に設定する実行環境の情報を使ってビルドに含む/含まないといった制御ができる機能ですが、独自タグの定義による制御もできます。 本記事ではLambda関数用のビルドに含みたいファイルは、ファイルの先頭に //go:build lambda_handler というビルドタグを定義します。元のmain関数が実装してあるファイルの先頭にはLambda関数用のビルド時に含まれないように //go:build !lambda_handler を定義します。 そして go build コマンドのオプションに -tags lambda_handler を設定することで独自タグの定義が有効になりmain関数の差し替えを実現しています。 Lambda関数用のコンテナイメージをビルドするDockerfileは以下のように記述しています。 FROM golang:1.21.4 AS lambda-builder WORKDIR /go/src/hotitem-tool COPY . . RUN go mod tidy RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o /usr/local/bin/hotitem-tool -tags lambda_handler . # Lambda関数実行時にawsコマンドでeks clusterの設定をするためaws-cliのイメージをベースにする FROM --platform=linux/amd64 amazon/aws-cli:2.14.1 AS lambda-handler WORKDIR /etc/ # awsコマンドでeks clusterのconfig設定をした後にハンドラ関数を実行するshellをentrypointに設定 COPY startup.sh . RUN chmod 755 startup.sh COPY --from=lambda-builder /usr/local/bin/hotitem-tool . ENTRYPOINT [ "/bin/sh" , "-c" , "./startup.sh" ] startup.sh の実装です。ハンドラ関数からPod起動ができるようにツール実行前にtokenを取得してKubeConfigを更新しています。 #!/bin/sh if [ -z " $AccountID " ] || [ -z " $ClusterName " ]; then echo " Error: environment variables are not set. " exit 1 fi export KUBECONFIG =/tmp/kubeconfig aws eks get-token --cluster-name " arn:aws:eks:ap-northeast-1: $AccountID :cluster/ $ClusterName " aws eks update-kubeconfig --name " $ClusterName " ./hotitem-tool " $@ " 過熱登録ツールは各機能をサブコマンドで実装しており、main関数を実装しているファイルはコマンドの定義だけです。そのためmain関数を実装しているファイルのみ差し替えることで既存実装に手を加えることなくLambda関数化できました。 実行結果の通知方法 過熱登録ツールは元々コマンドラインツールとして実装した関係で、実行結果の出力は標準出力で行っています。ChatOps化するにあたって実行結果はSlackに通知する必要がありました。 しかし、ChatBotの欠点として、タイムアウト値を10秒から変更できないことが挙げられます。 処理に10秒以上かかる場合、ChatBotによってレスポンスを返すことができないため、処理の中でWebhookによってレスポンスを通知するという方法をとりました。 そのため、AWS Lambdaの実行は -invocation-type オプションで非同期(Event)としています。 Lambda関数の標準出力は自動的にCloudWatchのロググループに /aws/lambda/<LambdaFuntionName> の形式で保存されます。 CloudWatchAlerm/AmazonSNS/AWS Chatbotを組み合わせてSlack通知する方法もありますが、本記事では以下の理由からツール側でSlack通知する方針にして実装しました。 元々ArgoWorkflowで実行した処理結果のSlack通知をツール側で実装済みだった Slack通知する際のフォーマットをツール側で定義・管理したい Slack通知する実装は既にツール側に存在していましたが、Lambda関数から実行した場合、実行結果を標準出力ではなくSlack通知へ切り替える実装をする必要があります。 真っ先に思いつく方法は各機能の引数にLambdaからの実行かどうかのフラグを持たせて標準出力とSlack通知を分岐させる方法です。しかしフラグを用いた方法だと既存実装に手を加える必要があり修正箇所も多くなるため、本記事では別の方法を採用しました。 具体的には以下のような標準出力をキャプチャする処理を実装し、ハンドラ関数のはじめにInit関数の呼び出しと終了時にPostする処理を呼び出すようにしています。 こうすることで各機能の実装に手を加えることなく標準出力をSlack通知に差し替えています。 標準出力をキャプチャする実装 //go:build lambda_handler package lambda import ( "bytes" "fmt" "io" "os" "regist-hotitems/slack" // Slackへのpostとフォーマットする関数を実装した独自モジュール ) type StdoutCapture struct { orgStdout *os.File bufChan chan string writer *os.File reader *os.File } func (s *StdoutCapture) Init() error { var err error s.orgStdout = os.Stdout s.reader, s.writer, err = os.Pipe() if err != nil { return err } os.Stdout = s.writer s.bufChan = make ( chan string ) go func () { var b bytes.Buffer io.Copy(&b, s.reader) s.bufChan <- b.String() }() return nil } func (s *StdoutCapture) Close() { s.writer.Close() s.reader.Close() os.Stdout = s.orgStdout } func (s *StdoutCapture) PostWebhookForGetGoodsInfo(title, url string ) error { s.writer.Close() s.reader.Close() os.Stdout = s.orgStdout // 標準出力をパースしてSlack通知用のフォーマットに変換する関数の呼び出し mes, err := slack.GenerateLambdaGetGoodsInfoNotificationMessage(title, <-s.bufChan) if err != nil { return fmt.Errorf( "failed to generate message: %w" , err) } if err := slack.PostWebhook(url, mes); err != nil { return fmt.Errorf( "failed to post webhook: %w" , err) } return nil } ハンドラ関数の先頭で標準出力をキャプチャする処理を初期化して、 defer で終了時にSlackへpostする処理を実行する。 //go:build lambda_handler package main import ( 〜略〜 "regist-hotitems/lambda" ) 〜略〜 func handler(ctx context.Context, event Payload) ( string , error ) { switch event.Command { case "get-goodsinfo" : return executeGetGoodsInfoCommand(event) case "regist" : return executeRegistCommand(event) } return "" , nil } func executeGetGoodsInfoCommand(event Payload) ( string , error ) { capture := lambda.StdoutCapture{} if err := capture.Init(); err != nil { return "" , fmt.Errorf( "failed to init stdout capture: %w" , err) } defer capture.PostWebhookForGetGoodsInfo( "get-goodsinfo result" , os.Getenv( "WebhookURL" )) 〜略〜 標準出力差し替えの詳細については ZOZO AdventCalender 2023 に投稿した記事で解説しているので、興味がある方はご覧ください。 qiita.com ChatOps化の効果 すでに本番環境の運用をChatOpsに切り替えており、ChatOps化したことであらゆるメリットを実感しています。 環境構築や準備が不要 今までは、運用業務を実施するメンバーがローカル環境を構築する必要がありました。運用ツールをアップデートするたびに、ツールをPullして再ビルドする必要がありましたがそれらが不要になりました。 本番環境の権限が不要 今までは、本番環境に対する権限を作業者に付与する必要がありましたが、AWS ChatBotを活用することで、本番環境の権限を直接払い出す必要がなくなりました。 また、作業に必要な権限が明確になる効果もあると感じました。 実行履歴が残る 誰がいつ何の作業をしたかを自動的に履歴として残すことができます。 また、そのままチャット上で会話に繋がるため、作業の共有やコミュニケーションが促される効果もあると感じました。 さいごに 本記事で紹介した過熱登録ツールを皮切りに、今後も様々な運用作業をChatOps化していこうと思います! ZOZOTOWNでは、SREとして自動化やインフラ構築に携わってくれるメンバーを募集しています! corp.zozo.com
アバター
こんにちは、MA部の齋藤( @kyoppii13 )です。 ZOZOTOWNでは、プッシュ通知やLINE、メールでのキャンペーン配信を実施しています。キャンペーン配信の例としては、お気に入り商品の在庫数が少なくなったときにプッシュ通知を送るといったものです。LINEやメールといった配信チャネル以外にも、キャンペーンごとにセグメントや実施タイミングも様々で、システムも配信キャンペーンの種類によって複数存在している状況でした。そのため運用保守のコストが大きくなっていました。また、キャンペーンの内容を変更するために開発側での工数が発生している状況でした。 そこでキャンペーン配信を効率的に実施するため社内向けのマーケティングプラットフォーム「ZOZO Marketing Platform(ZMP)」を開発しました。 本記事では、マーケティングプラットフォームの開発にあたって考慮した点とアーキテクチャについて紹介します。 ZOZOTOWNでのキャンペーン配信 セグメント コンテンツ 配信タイミング その他設定 これまでの運用フロー 課題 運用・保守コストの肥大化 新規開発にリソースが割けない キャンペーンを簡単かつ柔軟に追加・修正できない 新しいプラットフォームに求められること 開発フェーズと実現できること アーキテクチャと開発の進め方 既存システム バッチ配信 リアルタイムイベント配信 既存アーキテクチャ ZMPのアーキテクチャ アーキテクチャの考慮点 耐障害性 管理データの特性と処理特性 複数チームでの開発 拡張性 機能の統一 概念モデル セグメント コンテンツ オファー キャンペーン 各モジュールについて 管理画面 管理画面を使ったZMPの運用フロー MA基盤 MAモジュール MAマネージャー 配信基盤 開発の結果 開発を振り返って 今後の展望 まとめ 最後に ZOZOTOWNでのキャンペーン配信 ZOZOTOWNでは様々なキャンペーン配信を実施しています。キャンペーン配信をする際は以下の項目を検討する必要があります。 概要 説明 セグメント 誰に配信するか コンテンツ どのようなデザインでどのような情報を配信するか 配信タイミング どんなイベントをトリガーとするか その他設定 ABテスト、最適化などの設定 セグメント セグメントとは配信対象者のグループです。このセグメントは全ユーザーの場合もあれば、性別や年齢などのユーザー属性で絞り込む場合もあります。 コンテンツ コンテンツとはユーザーに配信する内容です。コンテンツは配信チャネルによって異なります。例えば、プッシュ通知の場合はタイトルと本文、画像を設定します。一方で、メールの場合はHTMLを設定します。 配信タイミング 配信タイミングとはキャンペーン発火のタイミングです。トリガーの種類によって、「バッチ配信」と「リアルタイムイベント配信」の2つに分かれます。バッチ配信は時間をトリガーとして配信するものです。例えば、新しいセールが始まるとき全ユーザーに通知するといったキャンペーンがあります。 リアルタイムイベント配信はユーザーの行動や商品在庫をトリガーとして配信するキャンペーンです。例えば、あるアイテムをお気に入り登録しているユーザーに対して、そのアイテムの在庫数が残り1つになったら通知する配信です。 バッチ配信は特定のユーザーセグメントに対して一括で送信するため「マス配信」とも呼んでいます。 その他設定 その他にはABテストを実施するか、配信時間を最適化するかなどを設定します。配信時の最適化は例えば配信する時間帯をユーザーごと最適なタイミングにする、配信数が増えすぎないように調整するといったものがあります。 これまでの運用フロー MA部では様々なキャンペーン特性に応じて複数のシステムを運用・管理していました。 バッチ配信ではワークフローエンジンのDigdagを利用したり、LINE Friendship Manager(LFM)やMail Banner Manager(MBM)といった社内ツールを自分たちで開発して利用しています。 リアルタイムイベント配信はリアルタイムマーケティングシステム(RTM)を利用しています。こちらも自分たちで開発したツールになります。 このように様々存在するキャンペーンを特性ごとに異なるシステムを利用して、担当者は以下のフローで運用していました。 各システムの詳しい説明については以下のテックブログを参照ください。 techblog.zozo.com techblog.zozo.com techblog.zozo.com techblog.zozo.com 課題 現状のキャンペーン配信では以下のような問題点がありました。 運用・保守コストの肥大化 新規開発にリソースが割けない キャンペーンを簡単かつ柔軟に追加・修正できない 運用・保守コストの肥大化 キャンペーンは様々な種類が存在しており、キャンペーン特性によってシステムも異なっていました。前の章でも説明したとおり、リアルタイムイベント配信はRTM、バッチ配信はワークフローエンジンのDigdagのように分かれていました。配信システムもチャネルや配信タイミングで分かれています。このようにシステムが分かれているのは、その時々で部分最適化しながら開発してきたためです。また、システムパフォーマンスを維持するための定期的なメンテナンスも必要です。中には古くから運用されているものもあり、属人化も顕著になっていました。 新規開発にリソースが割けない 運用・保守コストが大きいため、新規開発にリソースが割けない状況でした。また古くから運用されているシステムは特殊な技術スタックを採用しているものも多く、新規での人材採用が困難になり、新しいメンバーの育成にもコストがかかっていました。 また、古いシステムにはその仕様からテストやロールバックが困難なものもありました。そのため、新規の改修が難しく、開発工数も大きくなっていました。 キャンペーンを簡単かつ柔軟に追加・修正できない キャンペーンは新たに追加したり、既存キャンペーンの内容を修正したりもします。このキャンペーンの追加・修正作業は配信するキャンペーンの種類によっては管理画面が存在し、施策担当者のみで作業が完結するものもあります。一方で、管理画面が存在せずキャンペーン追加・修正時に開発側での工数が発生しているものもありました。例えば、RTMがその1つです。結果として、キャンペーンの企画から実施までを施策担当者だけで完結できず、キャンペーン実施までのリードタイムが長くなっていました。 また、前述の課題から新規開発にリソースが割けない状況であるため、最適化ロジックの追加修正もできず、最適なキャンペーンが実施できていない状況でした。 ここまで述べた課題を解決するものが必要でした。 新しいプラットフォームに求められること 前章で述べた課題を解決するために新規プラットフォームの構築を計画しました。プラットフォームに求められることは以下です。 システムがシンプルで、運用・保守や機能開発の工数を低減できる 既存システムと機能やパフォーマンスが変わらない 施策担当者(非エンジニア)だけでキャンペーンの追加・修正ができる 1つ目はシステムの運用・保守や新たな機能追加の工数を削減し、エンジニアの負荷を低減するためです。これによって、前述の運用保守コストの課題を解決し、結果として新規開発のリソースに人員を割けます。 2つ目は既存システムでのキャンペーン内容が変わったりやパフォーマンスが落ちたりしては、これまでのキャンペーンをユーザに提供できなくなってしまいます。そのため既存システムでできることはそのままである必要があります。 3つ目は施策担当者だけでキャンペーンを実施できるようにし、キャンペーン実施までのリードタイムを短くするためです。これによって、施策担当の部門が主体となってキャンペーンを実施できるようになり、キャンペーンの効果を高めたり、実施回数を増やしたりできます。 これらの要件を満たすプラットフォームを開発コストを抑えて実現するために、最初にマーケティングツールSaaSの導入を検討しました。しかしながらZOZOTOWNは会員数が1000万人を超え、多くの訴求をします。扱うデータも大規模であり、リアルタイムイベント配信ではリアルタイム性が求められます。キャンペーン経由での売上も非常に大きいため、安定的に稼働する必要もあります。 外部SaaSのみでの実現だけではなく、一部システムは自社開発しハイブリッドアーキテクチャとして開発した場合でも検討しました。しかし、ZOZOTOWNで必要な高いパフォーマンスと安定的な稼働が求められるシステムを実現できないとの結論になりました。 このような経緯から自社開発に至りました。そして作成したプラットフォームがZOZO Marketing Platform(ZMP)です。 開発フェーズと実現できること 自社開発にあたって、まず開発フェーズを設定しました。 フェーズ 実現できること 1 管理画面上からのバッチ配信キャンペーンの登録 2 管理画面上からのリアルタイムイベント配信キャンペーンの登録と配信最適化 3 他システムの統合 フェーズ1ではZMPを新規で開発し、画面上からバッチ配信キャンペーンを設定・配信できるようにします。 フェーズ2ではフェーズ1で作成したZMPに機能追加をする形でリアルタイムイベント配信と配信最適化ができるようにします。 フェーズ3では他社内ツールを統合し、統合的なプラットフォームとして利用できるようにします。 記事執筆の時点ではフェーズ1までが完了しています。 フェーズ1では、新規のシステム開発とキャンペーン処理の整理・共通化をします。ZMPをゼロから開発しバッチ配信ができるようにします。キャンペーン処理の整理・共通化では、キャンペーンごとに独自の処理を持っていたものを共通化し、キャンペーンの設定や配信を共通化できるようにします。 アーキテクチャと開発の進め方 ZMPのアーキテクチャについて説明する前に、既存システムとアーキテクチャについて説明します。 既存システム 既存のアーキテクチャは配信の種類によって異なり、大きく分けてバッチ配信とリアルタイムイベント配信があります。配信フローと配信特性が異なるため、システムも異なっています。 バッチ配信 バッチ配信は特定のユーザーセグメントに対して、決まった時間に送信するものです。以下のフローで配信されます。 順番 概要 1 時間トリガー発火 2 セグメント作成 3 コンテンツ作成 4 最適化処理 5 配信処理 設定した時間になったタイミングでトリガーが発火し、対象となるセグメントを作成します。 次にコンテンツを作成します。コンテンツは対象者全員で共通の場合もあれば、デザインのフォーマットは同じでも掲載される情報が異なる場合もあります。例えば、おすすめアイテムの通知はユーザーごとに掲載される商品が異なります。 次に最適化処理です。直近でキャンペーン配信が多ければ配信をしない通数最適化などをします。そして、最後に配信処理を実施します。 リアルタイムイベント配信 リアルタイムイベント配信は在庫状況の変動などをトリガーにする配信です。以下のフローで配信されます。 順番 概要 1 ユーザー行動や商品情報の変化を検知 2 キャンペーン判定 3 セグメント抽出 4 最適化処理 5 コンテンツ作成 6 配信処理 まずユーザーの行動や商品情報の変化を検知します。在庫が2つから1つに減少したなどの変化です。 次に変化のイベントをもとにキャンペーンの判定を実施します。例えば「在庫数が少なくなりました」とメッセージを配信するキャンペーンがあります。キャンペーンごとにトリガー条件が決まっており、これを判定するのがキャンペーン判定です。 次にセグメント抽出です。キャンペーン判定の結果、そのキャンペーンの対象となるユーザーを抽出します。在庫が少なくなった商品をお気に入り登録しているユーザーを抽出するといった処理です。 次に最適化処理です。開封されやすいチャネルを選択するチャネル最適化、開封されやすい時間に配信する時間最適化などの最適化をします。そして、最後に配信処理を実施します。 既存アーキテクチャ 既存システムで説明したように、配信フローや配信特性の違いから、既存アーキテクチャは以下のようになっています。 バッチ配信とリアルタイムイベント配信でシステムが異なっており、配信チャネルによってもシステムが分かれているような状態でした。また、配信に必要なデータを全社基盤から連携したり、連携したデータを元に配信データを作成したりするシステムも存在しています。 ZMPのアーキテクチャ 既存アーキテクチャをリプレイスするために考えたZMPのアーキテクチャは以下です。 フェーズ1だけでなくZMP全体としてのゴールを達成するためのアーキテクチャとなっています。 アーキテクチャは大きく分けて管理画面、MA基盤、配信基盤の3つのモジュールに分かれます。 管理画面モジュールは社内の施策担当者がキャンペーンの設定をするための画面です。 MA基盤モジュールは「MAマネージャー」と「MAモジュール」の大きく2つに分かれます。MAマネージャーは管理画面への管理画面のAPIを提供します。管理画面で必要なデータや設定されたキャンペーンパラメータは単一のDBに保存されます。MAモジュールは管理画面から設定されたキャンペーンを元に、配信に必要なデータの準備とキャンペーンを発火します。 配信基盤モジュールはMA基盤モジュールで生成されたデータを配信する部分です。 これら以外のモジュールとしてBigQuery上に構築された全社基盤からの変更データを連携するデータポンプが存在しています。データポンプはリアルタイムデータ連携システムで、必要なデータの変更を検知して、MAのシステムへリアルタイムで連携するシステムです。このシステムは既に開発済みでした。 データポンプの詳細については以下のテックブログをご覧ください。 techblog.zozo.com また、配信に必要なZOZOTOWNのデータはすべてBigQuery上に集約しています。ログデータもリアルタイムでBigQueryに連携しています。このBigQueryのデータを使って集計だけではなく、セグメントやコンテンツを作成します。 アーキテクチャの考慮点 このようなモジュール分割になった理由は以下です。 耐障害性 データ特性と処理特性 複数チームでの開発 拡張性 機能の統一 耐障害性 ZOZOTOWNでの配信では耐障害性が重要となります。非常に多くの配信をしているため、少しの間でも配信が止まってしまうと、売上影響が大きく機会損失となってしまいます。そこで障害が発生した場合に配信影響が最小となるようにしました。 まず、障害発生時に各モジュールで利用しているインフラやツールでの処理に影響があるかを検討しました。下記の処理について、障害発生時にどの処理で影響があるかを検討しました。 処理 モジュール 概要 配信設定 管理画面 管理画面からのキャンペーン設定 セグメント作成 MA基盤 セグメント作成処理 コンテンツ作成 MA基盤 コンテンツ作成処理 配信処理(メール) 配信基盤 メール配信処理 配信処理(プッシュ) 配信基盤 プッシュ配信処理 配信処理(LINE) 配信基盤 LINEの配信処理 管理画面で設定されたキャンペーンの情報はMA基盤のDBに保存され、トリガーによってセグメント作成とコンテンツ作成をし、配信処理を呼び出して配信します。 配信設定、セグメント作成、コンテンツ作成、配信処理が独立したシステムで存在しているので、前段のシステムで障害が発生して使用できなくなっても、後続のシステムまで処理が届いていれば処理が可能です。また、前段で処理されたデータが必要なのにシステムが使用できない場合、最終手段として手動でデータを用意して後続の処理を呼び出すなどの対応が可能となります。例えば、コンテンツ作成を手動でして、配信処理を手動で呼び出すなどの対応です。 管理データの特性と処理特性 管理画面から設定したキャンペーン情報は単一のDBに保存します。次のデータモデルで説明しますが、データには依存関係が多く存在しており、また、開発もMA部に閉じています。そのため一貫性を担保しやすくするのを優先し、マイクロサービス化はせずに単一のDBとしました。 MA基盤モジュール内ではバッチ配信とリアルタイムイベント配信の処理は別となっています。これは配信特性やフローが異なり、ボトルネックの箇所や要件が異なるためです。そのためここに関しては分割しています。 複数チームでの開発 MA部は複数の開発チームで構成しています。モジュールごとに責任を分け、各チームがモジュールを境界として開発を分担できるメリットがあります。障害試験や負荷試験もモジュールごとに実施ができます。 拡張性 将来的にMA基盤モジュール以外に独立させたいモジュールが必要でかつ、管理画面での管理が必要になったとします。その都度、管理画面を作ると部分最適化された管理画面が複数作られてしまいます。今回、管理画面を独立化させて、図のように管理画面を1つに統一できます。 機能の統一 既存のシステムでは、配信処理がアプリケーションごとに分かれており、処理が重複して存在していました。配信に利用する外部SaaSではクオータ制限もあり、配信処理を統一しないとフロー制御も難しい状況でした。また、配信基盤については、社内の他システムからも利用する可能性がありました。そのため、配信処理をモジュールとして独立させました。 概念モデル 今回、開発を進めるにあたって、最初に開発者全員で概念モデルを考えました。ここでいう概念モデルとは既存の配信フローを整理して考えたモデル構造です。概念モデルを最初に決定することで、共通認識を持って進められ、チームを分割しても開発を進められました。 既存の配信フローにおけるバッチ配信とリアルタイムイベント配信についてはそれぞれ前の章で述べました。2つの配信に共通するのは、「だれ」に「なに」を「どのように」送るかです。 このような共通点から考えたのが次の概念モデルです。 セグメント セグメントは「だれ」に相当する部分で、配信対象ユーザーグループです。既存の配信ではBigQueryのビューでセグメントを定義していましたが、将来的にGUIで作成する要件がありました。具体的にどのようなツールを使うかは現時点では未定ですが、例えばLookerの Explore を使ってGUIでの柔軟な条件指定でデータ抽出・可視化ができるようにするなどが考えられます。このように複数のセグメント定義方法に対応する必要がありました。そこでセグメントソースの概念を導入し、セグメント定義方法が増えても対応できるようにしました。 コンテンツ 次にコンテンツです。これは「なに」に相当する部分で、配信する内容です。コンテンツは配信チャネルごとに設定する項目が変わるので、コンテンツの下に各チャネルと対応するモデルがあります。 コンテンツにはデザインフォーマットは同じでも、内容となるデータをユーザーごとに変えたい場合があります。例えば対象セグメントのユーザーごとにお気に入りブランドの商品画像と価格を表示する場合です。このようなコンテンツは既存の配信においては、任意の記号(ここでは%%)で囲まれた文字列であるタグをコンテンツに記述し、配信時に実行するクエリで取得したデータを埋め込む形で対応していました。HTMLのメールコンテンツであれば、以下のようなHTMLをコンテンツに埋め込みます。 < p > GOODS_ID_1:%%GOODS_DETAIL_ID_1%% </ p > < p > GOODS_NAME_1:%%GOODS_NAME_1%% </ p > < p > PRICE_1:%%PRICE_1%% </ p > < p > GOODS_2:%%GOODS_DETAIL_ID_2%% </ p > < p > GOODS_NAME_2:%%GOODS_NAME_2%% </ p > < p > PRICE_2:%%PRICE_2%% </ p > このような共通して利用するデザインパーツをマージタグと呼ばれる概念で定義できるようにしました。動的パラメーターを含むHTMLなどのデータをその名前(マージタグ名)とともに定義します。上の例だと商品ID、商品名、価格を2つ表示するパーツになります。以下のようなyamlで定義します。nameがマージタグ名、descriptionがマージタグの説明、valueがマージタグの値です。 name : '{# goods_list #}' description : 商品リスト value : | <p>GOODS_ID_1:%%GOODS_DETAIL_ID_1%%</p> <p>GOODS_NAME_1:%%GOODS_NAME_1%%</p> <p>PRICE_1:%%PRICE_1%%</p> <p>GOODS_2:%%GOODS_DETAIL_ID_2%%</p> <p>GOODS_NAME_2:%%GOODS_NAME_2%%</p> <p>PRICE_2:%%PRICE_2%%</p> このマージタグ名をメールテンプレートなどのコンテンツに記述すると対応するvalueが動的に埋め込まれます。 マージタグでの値(例:GOODS_ID_1)をどのように取得するかはコンテンツマージタグパラメータで定義できるようにしています。 name : 商品リストのコンテンツマージタグパラメータ description : 商品リストのコンテンツマージタグパラメータ schema : name : goods_list key : email_id columns : - name : email_id type : INTEGER description : EMAIL ID example : 123456789 - name : GOODS_DETAIL_ID_1 type : INTEGER description : 商品詳細ID1 example : 1 - name : GOODS_NAME_1 type : STRING description : 商品名1 example : "かっこいいシャツ" - name : PRICE_1 type : STRING description : 価格 example : "10000" - name : GOODS_DETAIL_ID_2 type : INTEGER description : 商品詳細ID2 example : 2 - name : GOODS_NAME_2 type : STRING description : 商品名2 example : "かっこいいシャツ" - name : PRICE_2 type : STRING description : 価格 example : "10000" query : | SELECT a.email_id AS email_id, b.GOODS_DETAIL_ID_1 AS GOODS_DETAIL_ID_1, b.GOODS_NAME_1 AS GOODS_NAME_1, b.PRICE_1 AS PRICE_1, b.GOODS_DETAIL_ID_2 AS GOODS_DETAIL_ID_2, b.GOODS_NAME_2 AS GOODS_NAME_2, b.PRICE_2 AS PRICE_2, FROM GoodsListTagle また、マージタグやマージタグパラメータで定義された値の中にはキャンペーンによって変える値があります。例えば、タイムセールの終了日時や表示する商品の条件(表示する商品の件数や最低割引率)などです。先のHTMLの例だと、場合によっては件数を10件や20件にしたい場合があります。このような値を画面上で設定できるようにZMPプリセットの概念も導入しました。以下のような定義をあらかじめしておきます。 key : mail_min_goods_num_5_through_10 name : 最小商品数(メール用) description : | 対象商品の最小商品数を指定する項目です。 type : INTEGER input : from : offer type : select default : 5 options : - label : 5 value : 5 - label : 6 value : 6 - label : 8 value : 8 - label : 10 value : 10 マージタグには関連するZMPプリセットのキー名を定義します。そうすると、先のZMPプリセットの例の場合は管理画面で最低件数をセレクトボックスで選択できるようになります。 これらのコンテンツで使用するパラメータ(マージタグ、マージタグパラメータ、ZMPプリセット)はyamlファイルで定義します。このyamlファイルをCI/CDによってDBに反映できます。この仕組みによって、DBを直接操作せずにエンジニア以外でもコンテンツで使用するパラメータの定義ができるようになっています。 オファー 次にオファーです。これは、セグメントとコンテンツを組み合わせてそれを「どのように」配信するかを設定する部分です。定義したセグメントとコンテンツを指定し、いつ配信するのかのトリガーを指定します。トリガーはバッチ配信とリアルタイムイベント配信で設定項目が異なります。前の章でも説明したように、バッチ配信の場合は「時間」を基準にリアルタイムイベント配信の場合は「ユーザ行動や商品データの変更」がトリガーとなります。 施策によっては、複数チャネルへの配信や同一チャネルでのABテストを実施します。また、将来的には配信以外にもポイント付与やWeb接客といった施策もZMPで設定できるようにしたいです。そこで、オファーに直接コンテンツを紐づけるのではなく、アクションを間に導入しています。これにより、配信だけではない様々な施策をZMPでしたいとなったときに、その施策に対応するモデルを開発・導入してアクションに紐づけられます。 キャンペーン 最上位のキャンペーンはオファーをまとめるものです。キャンペーンによってはコンテンツを変えたり、対象者を変えたり内容を変えて配信します。そのように同一のキャンペーンであっても内容が異なる配信をまとめるためのものです。 次の章では各モジュールについて説明します。 各モジュールについて ZMPを構成する各モジュールについての詳細と責務について説明します。ここではフェーズ1までに完成している部分について主に説明します。 管理画面 管理画面モジュールは管理画面を提供するモジュールです。 管理画面を独立したモジュールとして開発した理由は2つあります。 1つ目の理由は、フロントエンドとバックエンドで開発の責務を分けてチーム分割をできるようにするためです。 2つ目の理由は、管理画面を伴う機能拡張の際に単一の管理画面アプリケーションで管理できるようにするためです。例えばフェーズ3で他の社内ツールをZMPに統合する際、先に管理画面だけをZMPに統合する手段が取れます。また、将来的にキャンペーン設定だけではなく、配信基盤のパラメータ設定もしたい場合でも、拡張性で説明したように管理画面アプリケーションを増やさずに対応できます。 管理画面を使ったZMPの運用フロー 管理画面ではフェーズ1終了時点で以下のようなフローで配信を実施します。 バッチ配信キャンペーンのお気に入りブランドの新着アイテムを配信する場合を考えます。 最初に最上位概念のキャンペーンを作成します。キャンペーン名と担当者を設定します。 次にセグメントを作成します。セグメントの設定項目は名前とセグメントソースです。セグメントソースとして今回はBigQueryのビューを使用します。BigQueryでビュー作成後にそれをセグメントに設定します。 次にコンテンツを作成します。今回はメールで配信する場合を想定します。また、内容として対象者ごとにお気に入りしているブランドの新着アイテムを10件表示します。メールコンテンツの設定項目は名前、タイトル、デザイン(HTML版・テキスト版)です。デザインはスマートフォン・PC向けのHTML版とフィーチャーフォン向けのテキスト版が必要です。このHTML版のデザイン作成に外部SaaSを利用します。対象者ごとに新着アイテムを出し分けるためにマージタグを利用します。 最後にオファー作成です。設定項目としては名前、セグメント、コンテンツ、トリガー条件です。セグメントとコンテンツはここまでで作成したものを設定します。トリガー条件はいつ配信するかです。今回はバッチ配信のため時間を設定します。 このようなフローで設定が可能です。作成したセグメントやコンテンツは使い回せるため、これまでの運用と比べて効率的に配信設定ができるようになります。 フェーズ2ではこの管理画面でリアルタイムトリガーの設定ができるようにします。 管理画面ではセグメント・コンテンツ・キャンペーンを設定しますが、管理画面の要件の1つに、GUIでのセグメント作成とメールコンテンツ作成がありました。 セグメント作成では、既存の運用においてはSQLを書く必要があります。しかし、担当者によってはSQLを書くための学習コストが発生したり、テーブル定義を理解したりする必要があります。これらを解消するために画面上から設定できる必要がありました。この理由からセグメント作成のツールの導入が必要でした。 メールコンテンツの作成では既存のフローでは施策内容を元にデザイナーがHTMLを書いて、それを施策担当者が確認し、適宜修正をして確定するフローでした。しかし、工数が多く発生していたので、これを解消するために施策担当者が自らデザインできるようにする必要がありました。この理由からコンテンツ作成ツールの導入が必要でした。 これらの機能は一般化されている部分であり、自分たちで開発するよりも既存のものを利用したほうが良いと判断し、外部SaaSを導入しました。一般化されている部分のため、導入したツールが利用できなくなっても他ツールで代用可能です。また自社開発よりも開発工数が削減出来ます。 今回導入したメールコンテンツ作成のSaaSではメールHTMLを部分パーツのブロックで定義・編集できる機能があります。例えば、フッターバナーのHTMLを1つのブロックで定義しておき、様々なテンプレートでの再利用を可能にします。この機能を応用し、運用効率の向上に繋がりました。施策担当者が掲載内容の一部をブロック単位で変更できるようになり、また、ブロックの組み合わせで新しいコンテンツを作成できるようになりました。SaaS導入にあたって、施策担当者とデザイナー共同で既存のHTMLメールデザインを全てSaaS上でパーツ化しました。 フェーズ1終了の現時点では、メールコンテンツ作成のみ外部SaaSの導入が完了しています。 MA基盤 MA基盤モジュールは管理画面のAPIの提供と配信に必要なデータの準備とキャンペーンを発火します。 MA基盤モジュールはその中にMAマネージャー、MAモジュールを持ちます。MAマネージャーは管理画面が使用するAPIを提供します。MAモジュールは管理画面から設定されたキャンペーン情報をもとにデータを準備し、配信処理をトリガーします。 MAモジュール MAモジュールの中には全社データ基盤からのデータ連携、キャンペーンごとに必要なセグメント・コンテンツ作成処理、チャネルごとのコンテンツ生成処理が含まれます。 バッチとリアルタイムイベントキャンペーンで処理を分けているのはキャンペーン特性や処理の違いがあって共通化が難しく、ボトルネックの箇所や要件が異なるためです。 配信の直前までの処理として、バッチ配信では時間のトリガーが発火した後にセグメント作成、コンテンツ作成となります。一方でリアルタイムイベント配信の場合はデータを連携し、条件が満たされた場合にセグメントとコンテンツを作成します。どちらも必要であれば最後に最適化を実施します。このように処理の流れが異なります。 また、配信のボトルネックの箇所や要件も異なります。バッチ配信は大量のユーザーに向けて1度に配信します。大量のユーザーに対してコンテンツを作成するのでボトルネックになりやすいです。リアルタイムイベント配信の場合は、データの変更を検知してすぐに配信する必要があるため、リアルタイム性が求められます。 このようなキャンペーン特性の違いからモジュールを分割しています。フェーズ1終了時点では、バッチ配信のモジュールであるMAバッチのみ作成が完了しています。 MAマネージャー MAマネージャーは管理画面のAPIを提供するアプリケーションとDBを持ちます。MAマネージャーは管理画面とMAモジュールの間に入りハブの役割を持ちます。 単一のDBにしているのは前で説明した通りデータの一貫性を保つためです。 MAマネージャーはMAモジュールへデータを一方向に流れるように設計しています。MAモジュールからは参照しないようにし、MAマネージャーで障害が起きても、配信処理に影響が出ないようにするためです。 配信基盤 配信基盤はMA基盤モジュールで生成されたデータを配信する部分です。 ここを独立させている理由は2つあります。 1つ目は他モジュールで障害が発生してもデータさえあれば配信できるようにするためです。管理画面やMAマネージャーが使えなくなった場合でも、すでに設定されているキャンペーンについては独自でトリガーを実行し配信処理はできるようにしています。 2つ目は将来的に全社向けの配信基盤とできるようにするためです。今はZMPだけでの利用となっていますが、将来的に他のシステムや部門からも利用できるようにするためです。 開発の結果 現時点ではフェーズ1のバッチ配信キャンペーンのみが設定・配信できる状態ですが、キャンペーン実施を施策担当者のみでできるようになり、これまで発生していた開発やデザイナーでの工数を削減できました。また、既存のDigdagなどにおける処理は、キャンペーンごとに定義された大きなワークフローとなっており、処理が重複し複雑になっていました。それをZMPへの構成に載せ替え、処理を共通化しシンプルな形でリプレイスも出来ました。 運用保守については、運用保守のコストが大きいRTMの移行ができていないため、大きなコスト改善はできていないものの、バッチ配信の部分についてはコスト削減ができました。 機能ごとにモジュール分けができたため、障害発生時でも原因追及がしやすくなり、チームごとに柔軟に開発ができるようになりました。 集計情報を施策担当者と整理したうえで1つのプラットフォームに載せ替えることで、集計情報をまとめられ、分析しやすい環境になりました。 開発を振り返って フェーズ1終了後に開発メンバーでKPTを実施しました。開発における進め方で良かった点や改善点を振り返りました。 良かった点としては、品質の担保ができたことと開発しやすさが挙げられました。 品質の担保については、障害試験・負荷試験の実施や、仕様やリリースフローについてCTO室レビューの実施で担保できたことが理由としてあります。 開発しやすかった理由としては、概念モデルを最初に決定し、大きな後戻りがなかったためです。現状の仕様や要件を調査し、実装フェーズへ入る前に開発メンバーと施策担当者で概念モデルについて念入りに認識合わせをし、決定出来たためです。 また、開発モジュールの分割でチームごとに実装を進められ、仕様決定までの経緯や開発フローについてドキュメントを残したことで途中参画メンバーでもスムーズに開発に入れたと意見もありました。 今後の展望 現在はフェーズ1の開発までが終了しており、2024年1月から運用開始しています。 現在はフェーズ2以降の開発に着手するための準備と、フェーズ1でやり残した細かいタスクやZMPユーザーからのフィードバックを受けて改善に取り組んでいます。フェーズ2までが完了すれば、リアルタイム配信システムを退役でき、運用保守コストの大きな改善が期待できます。フェーズ3までが完了すれば、他システムとの統合が完了し、全てのキャンペーンがZMPで管理できるようになります。 まとめ マーケティングプラットフォームの開発にあたって考慮した点とアーキテクチャについて紹介しました。このプラットフォームの作成によって、施策担当者やマーケティング担当者でキャンペーンの運用ができるようになりました。また、開発側においてもキャンペーン実施時に発生する開発コストやシステムの運用保守コストを削減できました。本記事が皆様の参考になりましたら幸いです。 最後に ZOZOでは一緒にプロダクトを開発してくれるエンジニアを募集しています。ご興味のある方は下記リンクからぜひご応募ください! corp.zozo.com
アバター
はじめに こんにちは、CISO部の 兵藤 です。日々ZOZOの安全のためにSOC対応を行なっています。 本記事ではサイバー脅威インテリジェンスプラットフォーム「OpenCTI」からMicrosoft Sentinelへの脅威インテリジェンスの取り込みについて紹介します。また、この内容については以下の「Azureで織りなすOpenCTI構築」に続く内容となっています。 techblog.zozo.com 目次 はじめに 目次 背景と概要 構築 連携に使用するプレイブック プレイブックを使用するための準備 Sentinel側のデータコネクタ SSLサーバー証明書 アクセス制御 インジケータの登録 Send to Security Graph API Read Stream- OpenCTI Indicators カスタムコネクタ OpenCTI-IndicatorsStream 運用 分析ルールの作成 取り込むインテリジェンスの選定 検知精度 まとめ おわりに 背景と概要 以前投稿したように、SOC対応を実施するにあたって各個人で調査収集していた情報を1つのプラットフォームで自動収集、管理し、ゆくゆくはSIEMと連携したいと考えていました。そのプラットフォームとしてOpenCTI( ver 5.11 )を導入しています。 また、ZOZOのSOCでは普段の検知対応においてSIEMを活用しています。具体的にはMicrosoftのSentinelです。この2つを連携し、自動で脅威インテリジェンスを用いた検知の仕組みを構築しました。 Sentinelにも「Microsoft Defender 脅威インテリジェンス」といったDefenderの脅威インテリジェンスを取り込むSentinelのデータコネクタは存在します。ですが、ZOZO独自で活用しているインテリジェンスやX(旧Twitter)にてリアルタイム公開される新鮮なフィッシング情報等インテリジェンスを用いる場合などでOpenCTIはとても便利です。 上記のようにアレンジした脅威インテリジェンスを用いた検知を自動で行うようにOpenCTIのインテリジェンスをSentinelに食わせました。 構築 連携に使用するプレイブック 連携のために、Microsoftが用意してくれているソリューションを利用します。執筆時点(2024年2月22日)ではSentinelのコンテンツハブから取得でき、プレイブックが4つあります。 また、OpenCTI公式のドキュメント 1 からもこのコンテンツがリンクされています。 4つのプレイブックは以下の通りです。 OpenCTIのインテリジェンスをSentinelに食わせるには Read Stream- OpenCTI Indicators と Send to Security Graph API - Batch Import (OpenCTI) の2つを使用します。 プレイブックを使用するための準備 Sentinel側のデータコネクタ このプレイブックは Microsoft Graph API を利用してインテリジェンスをテナントに作成します。このインテリジェンスをSentinel(Log Analytics)上の脅威インテリジェンスとして確認し検知ルールに組み込むためには、Sentinel上でデータコネクタを作成する必要があります。 このデータコネクタはコンテンツハブの「Threat Intelligence」から取得できます。 必要なデータコネクタはSentinel上で「脅威インテリジェンス プラットフォーム - 非推奨になっています (プレビュー)」といった表示になっており、Microsoftからは非推奨とされています。 ですが、公式ドキュメント 2 には以前の名称ではありますが、以下のようにこのデータコネクタを使用するように記載されています。 このデータコネクタを起動しておかないと、Microsoft Graph APIでのリクエストはエラーなく登録されますが、Sentinelでその情報を活用できない状態になります。 この状態に自分はとても悩まされました。このデータコネクタの後継である「Threat Intelligence Upload Indicators API (Preview)」を起動しただけではダメな状況でした。2つのデータコネクタを起動して取り込みに成功しています。 SSLサーバー証明書 プレイブックからOpenCTIへのリクエストはHTTPSなので、SSLサーバー証明書が必要です。以前のOpenCTIの構成ではAzure Load Balancer(以降ALB)を使用してAzure Key Vaultから配布される証明書を使用していました。 以前の構成ではSSLサーバー証明書の管理が面倒であり、かつSOC関係者やシステムしかアクセスしないOpenCTIに対してそこまで大規模なロードバランサーは必要ないと考えました。そのため、ALBを使用せずにMicrosoft側で管理されているSSLサーバー証明書を使用できるサービスにリプレースしました。 SSLサーバー証明書管理を省力化でき、小規模運用でき、アクセス制御が効くものとしてAzure Web App for Containers(以降Web App)でnginxを立てることを選択しました。Web AppはデフォルトでHTTPS通信のエンドポイントを提供してくれます。また、マシンスペックもごく小規模で運用でき、コンテナでのデプロイも容易です。仮想サブネットワーク統合でのリプレースもスムーズに進みました。 アクセス制御 アクセス元の制御については「受信トラフィックの構成」の項目から「アクセス制限付きで有効」を構成することによって実現できます。 Netwotk Security Group(以降NSG)と同様にサービスタグやIPアドレスを指定できます。これによってMicrosoft Entra ID(旧Azure Active Directory)からのSAML認証も許可できます。nginxの設定でもIP制限はできますが、Microsoft Entra IDからのアクセスをサービスタグで一括許可したい場合はこちらを構成しておくと便利です。 インジケータの登録 OpenCTIからのインテリジェンスを取り込むためには、OpenCTI側で取り込む対象となるSTIX形式のインジケータを登録する必要があります。以下のようにインジケータがあることを確認しておきます。 Send to Security Graph API 実際にプレイブックを利用してOpenCTIからインテリジェンスを取り込むLogic Appを組みます。 Send to Security Graph API - Batch Import (OpenCTI) プレイブックを最初に実行します。このプレイブックを利用して作成される OpenCTI-ImportToSentinel のLogic Appが以降の使用する Read Stream- OpenCTI Indicators で必要になるので、先にこちらを作成しておかなければなりません。 以下のようなLogic Appができるはずです。 また、このLogic Appにインジケータを作成する 3 ためのロール( ThreatIndicators.ReadWrite.OwnedBy )を付与する必要があります。 詳しい付与方法は公式の GitHub を参照してもらえればわかりますが、以下のようにテナント接続後にPowershellを用いてロールを付与します。 $MIGuid = "<Logic App の Managed ID>" $MI = Get-AzureADServicePrincipal -ObjectId $MIGuid $GraphApIAppId = "00000003-0000-0000-c000-000000000000" $PermissionName = "ThreatIndicators.ReadWrite.OwnedBy" $GrphAPIServicePrincipal = Get-AzureADServicePrincipal -Filter "appId eq ' $GraphApIAppId '" $AppRole = $GrphAPIServicePrincipal .AppRoles | Where-Object { $_ .Value -eq $PermissionName -and $_ .AllowedMemberTypes -contains "Application" } New-AzureAdServiceAppRoleAssignment -ObjectId $MI .ObjectId -PrincipalId $MI .ObjectId -ResourceId $GrphAPIServicePrincipal .ObjectId -Id $AppRole .Id Microsoft Entra IDの OpenCTI-ImportToSentinel 表記のエンタープライズアプリケーションを確認します。アクセス許可の項目で以下のようにロールが付与されていることを確認できれば成功です。 Read Stream- OpenCTI Indicators 次に Read Stream- OpenCTI Indicators プレイブックを利用してOpenCTIからインテリジェンスを取得するための2つ目のLogic Appを組みます。 カスタムコネクタ このプレイブックを利用するためにはカスタムコネクタを作成する必要があります。このカスタムコネクタはOpenCTIへGraphQLを利用してインテリジェンスを取得するためのものです。 以下のようにプレイブックの説明欄にリンクがあるので、そこからデプロイが可能です。 デプロイ後はカスタムコネクタのリソースとは別でAPI接続のリソースができます。このAPI接続にOpenCTIで利用するAPIキーを設定します。 ここで注意すべきは、APIキーの記載方法です。AzureのAPI接続にAPIキーを登録する場合は以下の注意書きがあり、 Bearer から記載する必要があります。 OpenCTI-IndicatorsStream プレイブックを実行すると、以下のようなLogic App(OpenCTI-IndicatorsStream)ができるはずです。 これでOpenCTIからインテリジェンスを取得するための大枠ができました。ですが、これだけでは動きません。 Until hasNextPage is false 内部の Parse JSON Indicators のアクションなど、最新のOpenCTIのGraphQLのレスポンスに対応していないため、スキーマを修正する必要があります。 具体的にはスキーマ内部の creators が creator になっていたり、この creators の形式を array に変更したり、 indicator_types の type 項目を編集したりと、レスポンスによって様々です。 レスポンスの例についてはOpenCTIの /graphql 階層で GraphQL Playground があるので、Logic Appのクエリ結果を以下のように確認できます。OpenCTIの ver 5.12 以上ではクエリの filters 等が異なりますので 公式ドキュメント を参照ください。 このGraphQL Playgroundを活用して、インジケータがOpenCTIから返されるレスポンスをサンプルとして取得すれば、Logic Appのスキーマの修正が容易です。以下の「サンプルのペイロードを使用してスキーマを作成する」から自動的にスキーマを生成してくれます。 上記のようにエラーが出るアクションごとにスキーマを修正し切れば、OpenCTIからのインテリジェンスを取得するためのLogic Appが完成します。このAppが最後に Send to Security Graph API へ送信することで、OpenCTIからのインテリジェンスをSentinelへ食わせることができます。 運用 分析ルールの作成 OpenCTIからのインテリジェンスを取り込むことができたら、次はそれを活用するための分析ルール(検知するためのルール)を作成します。このルールについては先述したコンテンツハブの「Threat Intelligence」から取得できます。 この分析ルールには大体の欲しいKQLの書き方が揃っているので独自で作成することは少ないですが、Microsoft製品以外のログ分析は当然自力で書くことになると思います。 例えばSWG製品を導入している場合は、そのログと取り込んだ脅威インテリジェンスを組み合わせて検知ルールを作成することになります。以下にIPv4アドレスのIOC情報と突合させるためのKQLの例を示します。 let dt_lookBack = 2h; let ioc_lookBack = 30d; let IP_Indicators = ThreatIntelligenceIndicator | where TimeGenerated >= ago(ioc_lookBack) | summarize LatestIndicatorTime = arg_max(TimeGenerated, *) by IndicatorId | where Active == true and ExpirationDateTime > now() | where isnotempty(NetworkIP) or isnotempty(EmailSourceIpAddress) or isnotempty(NetworkDestinationIP) or isnotempty(NetworkSourceIP) | extend TI_ipEntity = iff(isnotempty(NetworkIP), NetworkIP, NetworkDestinationIP) | extend TI_ipEntity = iff(isempty(TI_ipEntity) and isnotempty(NetworkSourceIP), NetworkSourceIP, TI_ipEntity) | extend TI_ipEntity = iff(isempty(TI_ipEntity) and isnotempty(EmailSourceIpAddress), EmailSourceIpAddress, TI_ipEntity) | where ipv4_is_private(TI_ipEntity) == false and TI_ipEntity !startswith "fe80" and TI_ipEntity !startswith "::" and TI_ipEntity !startswith "127."; IP_Indicators | join kind=innerunique ( SWG_log() | where TimeGenerated >= ago(dt_lookBack) | extend SWG_TimeGenerated = TimeGenerated | where isnotempty(DstIpAddr) | where LogSummary !startswith "Block" ) on $left.TI_ipEntity == $right.DstIpAddr | where SWG_TimeGenerated < ExpirationDateTime | summarize SWG_TimeGenerated = arg_max(SWG_TimeGenerated, *) by IndicatorId, DstIpAddr | project SWG_TimeGenerated, Description, ActivityGroupNames, IndicatorId, ThreatType, DstIpAddr, User, Hostname, ExpirationDateTime, ConfidenceScore, Type | extend timestamp = SWG_TimeGenerated 上記KQLは例のためにSWGでのログをSentinelに以下のようなテーブル形式で取り込んでいると仮定しています。1例なので、実際の環境に合わせて修正する必要があります。 TimeGenerated LogSummary DstIpAddr DstPort User Hostname SrcIpAddr SrcPort 2024-02-22T09:10:30.8885957Z Block Risk Connection 192.0.2.1 443 user01 HOST01 192.0.2.2 4444 上記はIPでの分析例なのでURLやファイルハッシュなどのIOC情報を突合させる場合はそれぞれ別々の分析ルールを作成することになります。 これらの分析ルールによって自動で取得した脅威インテリジェンスを活用して検知できます。 取り込むインテリジェンスの選定 自分たちがOpenCTIで収集している脅威インテリジェンスは膨大にあります。作成されるインジケータをなんでも取り込んでしまうと過検知の量も膨大になります。人材も豊富で大規模なSOCであればそれでも対応できるかもしれませんが、自分たちのSOCではそれは難しいです。また、IPアドレスでのIOC情報はCDNのIPアドレスなども多く含まれているため、鮮度が落ちやすい情報も多いです。 ある程度の検知精度を出すためにインテリジェンスの信頼度、鮮度、作成元などの情報でSentinelに取り込むインテリジェンスを選定します。 選定する場合はOpenCTIに取り込む元々のインテリジェンスを制限するのではなくLogic Appでフィルタリングを行います。あくまでOpenCTIは情報の泉として活用したいからです。 Logic Appでのフィルタリングは以下のように Until hasNextPage is false 内部の Switch 2 アクションの各ケースに追加します。追加内容は制御アクションです。 制御アクションとして x_opencti_score の値によってインテリジェンスを取り込むかどうかを判断する「条件」(if文に相当)を設定します。以下の例だと信頼度が50未満の場合はIPv6のインテリジェンスを取り込まないように設定しています。 検知精度 セキュリティの観点から詳しいことをこの場で記載できませんが、検知精度としてはいい具合にZOZOの環境にチューニングできていると感じています。OpenCTIからのインテリジェンスを活用して検知することで、これまで検知できていなかった脅威を見つけることができています。 まとめ OpenCTIからのインテリジェンスをSentinelに食わせ、検知する仕組みや運用法を紹介しました。OpenCTIを用いた自動でのインテリジェンス検知運用の記事は事例が少ないので、参考になれば幸いです。 ZOZOではこれからも脅威インテリジェンスを逐次収集し、意思決定プロセスに必要なインテリジェンスの活用に努めていき、ZOZOの安全性の向上を図っていきたいと考えています。 おわりに ZOZOでは、一緒に安全なサービスを作り上げてくれる仲間を募集中です。ご興味のある方は、以下のリンクから是非ご応募ください! corp.zozo.com OpenCTI-Ecosystem ↩ 脅威インテリジェンスプラットフォームをMicrosoft-Sentinelに接続する ↩ 脅威インテリジェンスインジケータを作成する ↩
アバター
はじめに こんにちは、ZOZOMO部OMOバックエンドブロックの長野です。普段はZOZOMOのサービスである ブランド実店舗の在庫確認・在庫取り置き (以下、店舗在庫連携)の開発・保守を担当しています。 店舗在庫連携はAWS上にシステムを構築しており、システムにはAWSの各サービスを利用しています。AWS上で構築するシステムは、マルチAZなどの冗長構成をとることで可用性を高めることができます。しかし、実際に障害が起こった際に、意図していなかった箇所でシステムが停止してしまう可能性は否定しきれません。 OMOバックエンドブロックでは、このような未知の障害を防ぐためのアプローチとしてカオスエンジニアリングを実施しました。本記事ではカオスエンジニアリングの説明とチームで行った結果を紹介します。 目次 はじめに 目次 カオスエンジニアリングとは 1. 定常状態を定義する 2. 仮説を立てる 3. 実験する 4. 検証する カオスエンジニアリングの目的 チームで実施したこと 仮説を立てる 実験する 検証する カオスエンジニアリングを実施して良かったこと まとめ カオスエンジニアリングとは カオスエンジニアリングは、稼働しているシステムに対して意図的に障害を発生させることでシステムの堅牢性・回復力を高める手法です。特にクラウド上で構築されるシステムは、各コンポーネント同士が疎になっていて、障害時の挙動の予想がより困難になっています。そこで、意図的に障害を発生させることで、システムの未知の挙動を発見することが期待できます。 カオスエンジニアリングは主に4つのステップで構成されます。 定常状態を定義する 仮説を立てる 実験する 検証する それぞれのステップについて説明します。 1. 定常状態を定義する 障害を注入する前にシステムの通常時の振る舞いを定義します。 定常状態は、障害発生中にシステムが問題なく稼働していることの確認や、停止状態からの復帰の判断に利用します。スループット・エラーレート等のメトリクスやSLOの値などが用いられます。 2. 仮説を立てる 定常状態を破壊できる事象を考え、その事象が発生してもシステムが停止しないという仮説を立てます。 いきなり仮説を立てることが難しい場合には、先ず起こりうる事象を考えるところから始めるのがやりやすいと思います。仮説を立てる中で課題を発見した場合は、実験の対象にはせずに改善します。 カオスエンジニアリングは未知の発見が目的になるので、既知となった問題は実験対象として扱いません。 3. 実験する 定常状態を破壊できる事象を実際に発生させます。 カオスエンジニアリングによる実験は、本番環境で実施することが推奨されますが、まずは開発環境で実施することをおすすめします。仮説が正しいことを開発環境で確認した後に、本番環境で実施するようにした方が安心出来ますね。 4. 検証する 仮説が正しかったか確認します。 もし仮説が誤っていた場合は、未知を発見できたということになるので、システムのどの部分が停止したかを確認して改善していきます。 カオスエンジニアリングの目的 前述のとおり、カオスエンジニアリングは未知を発見することが目的ですが、それだけではありません。定常状態を破壊できる事象を考えることや検証することは、開発者のシステムへの理解を高めることにもなります。 例えば、定常状態を破壊できる事象を考えるためにシステムのアーキテクチャ図を眺めることや、仮説が誤っていた場合に実際にはどうだったのかを調査する必要があります。これらを繰り返すことでチームはシステムに対する理解を深め、システムはより堅牢なものになっていきます。 チームで実施したこと チーム内にカオスエンジニアリングの実施経験のあるメンバーがいなかったため、まずはカオスエンジニアリングの目的や期待できる効果について、前述した内容をチーム内で会話して共通認識にしました。その後、仮説をもとに実験と検証を実施したのでそれぞれお話しします。 仮説を立てる 仮説を立てるにあたり、まず発生し得る事象を洗い出しました。下記は洗い出した事象の一例です。 ECSのタスクが落ちる 特定AZで障害が発生してAZ内のリソースが利用不可になる 次に、事象発生の際にどのような影響があるかを考えました。例えば、ECSのタスクが落ちた場合、サービススケジューラで設定してある必要タスク数まで自動復旧する等です。 このような仮説を立てた後はサービスへの影響度を考えます。その仮説が間違っていて問題が発生してしまった場合の影響度を考えることで、カオスエンジニアリングによる実験の優先順位を決定します。 最終的には、すべての仮説を検証して、システムの安全性に自信をもてる状態を目指しますが、まずは優先順位の高いものから手を付けていきます。仮説は上記の例以外にも挙げられましたが、今回は影響度が高いAZ障害への耐障害性を実験することに決定しました。 実験する AZ障害を発生させるにあたり、障害を注入するツールとしてAWS Fault Injection Service(以下、FIS)を利用しました。FISの詳細は AWSのドキュメント をご確認ください。 FISでは、VPCサブネットを出入りする通信を妨害する障害を注入できます。特定AZのサブネットのみ通信を妨害することで、各AWSサービス間の通信を失敗させ、AZ障害を再現しました。 店舗在庫連携では、主にALB、ECS、RDSを用いてシステムを構築しているため、この3つのサービスでAZ障害を起こし、耐障害性を検証します。 下図に各リソースがどのように配置されているかの簡単な図を示します。画像内の赤い枠線で囲まれたサブネットが今回障害を発生させる対象になります。 下の画像のようにFISは実施したいアクションと、そのターゲットを組み合わせることで障害を発生させます。画像では、サブネット通信を妨害するアクションを disrupt-connectivity に、特定AZのサブネットを選択したグループのターゲットを Subnets-Target-1 に設定しています。このアクションとターゲットを紐づけることで、障害を発生させることが出来ます。 FISでは、複数のアクションやターゲットを組み合わせることも可能です。その他、適切なIAMロールを設定することで、不要なリソースへのアクセスを拒否することや、CloudWatch Alarmを用いてメトリクスをもとに実験を停止させる等、安全面も考慮されています。 今回、店舗在庫連携のAPIでは、下記内容で実験しました。 前提 実験環境:検証環境 AWSの各リソースは3AZに配置 10req/sで店舗在庫連携のAPIを7分間実行する 実験 APIを実行中に特定AZのサブネット通信を妨害し、AZ障害を発生させる 仮説 各リソースでマルチAZの冗長構成にしているためサービス提供は止まらない 下図はAPIの実行結果になります。15:59:20から16:01:50の2分30秒間で504エラーが発生していることがわかりました。 実験結果:15:59:20から16:01:50の2分30秒間で504エラーが発生していることがわかる 検証する 実験結果が仮説通りだったか、もし仮説通りでなければ実際には何がおこっていたのかを確認していきます。 今回の実験では「各リソースでマルチAZの冗長構成にしているためサービス提供は止まらない」でした。これ自体は間違いではありませんでしたが、詳しく見ると一部のリクエストが数分間エラーになる状態でした。 カオスエンジニアリングは実験してシステムの安全性を確認して終わりではなく、何が起こっていたのかを確認して開発者の知識を増やすことも目的の1つです。 今回の実験で2分30秒間エラーが発生した理由を調査すると、ALBのターゲットグループに登録してあるECSタスクのうち、障害が発生中のECSタスクにもリクエストが割り振られているためでした。これは考えれば分かる事だと思いますが、ではなぜ2分30秒なのかということも確認してみます。 これは、ターゲットグループのヘルスチェックで異常を検知するための設定に依存していました。店舗在庫連携では、その設定が2分30秒だったためで、2分30秒という時間の元となる設定内容は、ヘルスチェックの回数や間隔です。 カオスエンジニアリングを実施して良かったこと 今回の実験で分かったことは、AZ障害が発生してもサービス提供は停止しないが、2分30秒の間で一部エラーが発生してその後自動復旧するということでした。実験前は「マルチAZでやっているから大丈夫」という軽い認識でしたが、「2分30秒間は一部エラーが発生するものの、その後自動復旧するから大丈夫」という根拠を持った自信になりました。 また、インフラ構築の経験者や知識がある人は最初から結果の想像が付いていましたが、普段インフラを触る機会が少ない開発メンバーにとっては、新しい知識が増える機会の多い良い学びの場になりました。カオスエンジニアリングを継続することで、システムと開発者が共に強くなっていくということも実感しました。 まとめ 本記事では、弊チームのカオスエンジニアリングへの取り組みを紹介しました。カオスエンジニアリングを実施することで、システムの安全性の確認と開発者のシステムへの理解を深めることができました。カオスエンジニアリングの実施を検討している方がいれば、ぜひ参考にしてみてください。今後は洗い出した仮説をすべて検証することと、本番環境で実験していきたいと考えています。 ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com
アバター
はじめに こんにちは、ZOZOMO部FBZブロックの山村です。普段は Fulfillment by ZOZO (以下、FBZ)が提供するAPIシステムを開発・運用しています。 FBZは、 AWS Lambda (以下、Lambda)を中心にAWSが提供するフルマネージドサービスを活用したサーバーレスアーキテクチャを採用しています。 以下の記事にてサービス構成やアーキテクチャ戦略の詳細を説明しています。 techblog.zozo.com 今回は、イベント駆動型のアーキテクチャを開発するうえで直面した課題と、その開発経験をもとにどのようにアプローチしたかをご紹介します。 目次 はじめに 目次 背景・課題 インフラリソース定義のための知識が必要であり、プロジェクトの参入難度を上げていた イベント駆動ではアプリケーションコードに合わせて関連リソース定義も必要になる 記述時点では正しく記述できているか気づくことができず、試行錯誤の回数が多くなっていた 構成管理ツールの見直し アプリケーション側と同様の言語で記述できる 静的型付け言語での記述 コードの再利用性 さいごに 背景・課題 FBZの新機能を開発するため、新たにデータ連携用のマイクロサービスを作ることになりました。 今回の開発では、初期ローンチ時のインフラコストをなるべく抑え、かつサービスのスケールにも対応できるよう、FBZと同じくLambdaを中心としたイベント駆動型のアーキテクチャを採用しました。 しかし、新規開発にあたっては、FBZを開発、運用する上で存在していた以下の課題と向き合う必要がありました。 インフラリソース定義のための知識が必要であり、プロジェクトの参入難度を上げていた FBZでは、構成管理ツールとして Serverless Framework を使用しています。 アプリケーションの構成やインフラリソースは、YAML形式の設定ファイルに記述しています。 コード管理できることはメリットですが、500以上のLambda関数やそれに関連するリソースの多いFBZの開発では、この設定ファイルへの記述を煩雑に感じることが多々ありました。 アプリケーション側のコードとは異なる言語での設定を必要とするため、これに慣れていない開発者にとっては新たな技術スタックに馴染むまでの一定の時間と労力が必要です。 特にリソースの定義においては、正確なパラメータや設定を把握しなければならないため、プロジェクトへの新規参入が一定の学習コストを伴っている状態でした。 イベント駆動ではアプリケーションコードに合わせて関連リソース定義も必要になる 例えば、API GatewayへのリクエストをトリガーにLambdaが起動し、DynamoDBへアクセスする処理があるとします。 下記はLambdaとDynamoDBをそれぞれ2つずつ定義していますが、同じような記述が必要になり、設定ファイル自体が肥大化していきます。 service : my-serverless-app provider : name : aws runtime : python3.11 # Lambda関数の定義 functions : writeDataToDynamoDB : handler : src/write_data_to_dynamodb.main events : - http : path : writeData method : post environment : DYNAMODB_TABLE : MyDynamoDBTable getDataFromDynamoDB : handler : src/get_data_from_dynamodb.main events : - http : path : getData method : get environment : DYNAMODB_TABLE : MyDynamoDBTableWithGSI # DynamoDBの定義 resources : Resources : MyDynamoDBTable : Type : AWS::DynamoDB::Table Properties : TableName : myDataTable AttributeDefinitions : - AttributeName : id AttributeType : N KeySchema : - AttributeName : id KeyType : HASH BillingMode : PAY_PER_REQUEST MyDynamoDBTableWithGSI : Type : AWS::DynamoDB::Table Properties : TableName : myGSITable AttributeDefinitions : - AttributeName : id AttributeType : N KeySchema : - AttributeName : id KeyType : HASH - AttributeName : id KeyType : HASH BillingMode : PAY_PER_REQUEST # セカンダリインデックスの設定 GlobalSecondaryIndexes : - IndexName : MyGlobalSecondaryIndex KeySchema : - AttributeName : name KeyType : HASH Projection : ProjectionType : ALL 上記は簡単な例ですが、実際には関連リソース間のIAMロールやポリシーを紐づける記述も必要になってきます。そして数多くのリソースが存在する場合、冗長な記述の設定ファイルであることが想像できます。 リソースや開発者が増えていくにつれ、記述の際にはリソースの命名規則にも揺らぎが生じることもありました。 記述時点では正しく記述できているか気づくことができず、試行錯誤の回数が多くなっていた また、記述の不足や間違いがあると、期待されるリソースを作成できません。記述時点では気づくことができず、デプロイ時のエラーにより発覚するケースも発生しました。 例えば、API Gatewayを使用してLambda関数をトリガーする場合、YAMLファイル内でのAPI Gatewayの設定が不十分だと、リソースは正しく構築されません。 functions : writeDataToDynamoDB : handler : src/write_data_to_dynamodb.main events : # メソッドの定義が不足している - http : path : writeData environment : DYNAMODB_TABLE : MyDynamoDBTable また、リソース間の依存関係が適切に定義されていない場合、デプロイは成功してもデプロイ後のLambda関数は期待通りに動作しません。 下記の例は、Lambda関数がDynamoDBテーブルに依存している場合、その依存関係がYAMLファイルに正しく記述されていないため、実行時にエラーとなります。 functions : writeDataToDynamoDB : handler : src/write_data_to_dynamodb.main events : - http : path : writeData method : post # 存在しないテーブルに依存している environment : DYNAMODB_TABLE : myTable このような状態に気づかないままデプロイをしてしまうと、下記のような手順が必要になります。 デプロイ時もしくはLamda関数実行時にエラー発生 切り戻しが必要な状態であれば変更前の正常な状態のコードを再デプロイ 原因調査、修正 変更後のコードを再度デプロイ リソースが増えていくとともにデプロイにも時間がかかり、原因調査なども含めると多くの時間を要してしまいます。 デプロイ過程で発生するこのような手戻りが、効率的に開発するうえでのネックになっていました。 構成管理ツールの見直し 上記の課題を受け、新規開発するマイクロサービスでは構成管理ツールの見直しを行いました。 煩雑であったYAMLへの記述を回避するため、プログラミング言語で記述できる AWS Cloud Development Kit (以下、AWS CDK)を利用することにしました。 実際にAWS CDKを利用して感じたメリットは大きく以下の3つが挙げられます。 アプリケーションで利用している言語をCDKがサポートしていれば、同様の言語で記述できる 静的型付け言語を選択することで、型安全性が高まり信頼性と効率が向上した プログラマブルに書けるため、コードの再利用でリソースを追加できるようになった アプリケーション側と同様の言語で記述できる 今回開発するマイクロサービスでは、アプリケーションの実装にGoを採用しています。 Goを使用してリソースを定義できるAWS CDKでは、通常のコードと同様にインフラストラクチャを記述でき、煩雑に感じていたYAMLファイルへの記述を回避できます。 静的型付け言語での記述 静的型付け言語であるGoで記述することで、型安全性が高まり信頼性と効率の向上を実感しました。AWS CDKのコードが正確に記述されているかをデプロイ前に確認できるため、デプロイ時に発覚する設定ミス等の問題を最小限に抑えられます。 以下は、AWS CDKをGoで記述してAPI GatewayとLambda関数をデプロイする例です。 package main import ( "github.com/aws/aws-cdk-go/awscdk" "github.com/aws/aws-cdk-go/awscdk/awsapigateway" "github.com/aws/aws-cdk-go/awscdk/awslambda" ) func main() { app := awscdk.NewApp( nil ) stack := awscdk.NewStack(app, "MyStack" , nil ) // Lambda関数 lambdaFn := awslambda.NewFunction(stack, "MyLambda" , &awslambda.FunctionProps{ Runtime: awslambda.Runtime_GO_1_X(), Handler: "main" , Code: awslambda.AssetCode_FromAsset( "path/to/your/code" ), }) // API Gateway api := awsapigateway.NewRestApi(stack, "MyApi" , &awsapigateway.RestApiProps{ RestApiName: "MyApi" , Description: "My API" , }) apiResource := api.Root().AddResource( "myresource" ) apiResource.AddMethod( "GET" , &awsapigateway.LambdaIntegrationProps{ Handler: lambdaFn, }) app.Synth( nil ) } このように、設定の記述はよりプログラマブルであることがわかります。 さらに静的型付け言語であるGoで記述することで、開発者はコンパイル時に型エラーを検出し、リソースの構築や変更において信頼性と効率を向上させることができるようになりました。 また、アプリケーション側の言語とインフラ設定の言語が統一されることで、開発時のコンテキストスイッチの切り替えが不要となり、抱えていたストレスから開放されました。 コードの再利用性 プログラマブルに記述できるため、コードの再利用でリソースを追加できるようになりました。 GoでLambda関数等のリソースを作成する際、基本となる処理をメソッド化することで、リソースの一貫性を確保できます。 package main import ( "fmt" "os" "github.com/aws/aws-cdk-go/awscdk" "github.com/aws/aws-cdk-go/awscdk/awslambda" // リソース名を定義しているパッケージ name "github.com/st-tech/example-resource-name" ) func main() { app := awscdk.NewApp( nil ) stack := awscdk.NewStack(app, "MyLambdaStack" , nil ) // Lambda関数1 // ハンドラ名はnameパッケージを参照 createLambdaFunction(stack, name.Handler1, "path/to/code1" ) // Lambda関数2 createLambdaFunction(stack, name.Handler2, "path/to/code2" ) app.Synth( nil ) } // Lambda関数を作成するメソッド func createLambdaFunction(stack awscdk.Stack, handler, codePath string ) { // リソース名の生成 resourceName := fmt.Sprintf( "%s%s" , os.Getenv( "stageName" ), handler) awslambda.NewFunction(&stack, resourceName, &awslambda.FunctionProps{ Runtime: awslambda.Runtime_GO_1_X(), Handler: handler, Code: awslambda.AssetCode_FromAsset(codePath), }) } 上記の例では、createLambdaFunctionメソッドにLambda関数の作成ロジックを切り出すことで、同じ構造のLambda関数を作成する際にコードを再利用できます。新しいLambda関数2が追加された場合も、メソッドの引数としてLambda関数の異なるパラメータを指定できるため、簡単に作成できます。 また、リソース名は明示的に指定しなければ自動で生成されますが、この例ではメソッド内でhandler引数(ハンドラの名前)を元にリソース名を生成しています。生成されたリソース名は一貫性を持ち、Lambda関数が異なるハンドラに対しても名前が適切に付与されます。このようにして、関数名とリソース名を結びつけることで一貫性を確保できます。 さらに、ハンドラ名は別のパッケージに定義したものを参照しています。アプリケーション側のコードからも同様のパッケージを参照できるので、インフラ側で定義している名前との揺らぎが無くなります。 このように、プログラミング言語で記述することの強みを活かし、より柔軟で効率的なインフラストラクチャの構築が可能になりました。 さいごに 本記事では、イベント駆動型アーキテクチャの構成管理において生じていた課題と、AWS CDKを利用したよりプログラマブルな課題解決の例を紹介しました。 構成管理ツールの利用を検討している方がいれば、ぜひ参考にしてみてください。 ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 hrmos.co
アバター
はじめに こんにちは、ML・データ部推薦基盤ブロックの栁澤( @i_125 )です。私はZOZOのデータ基盤におけるデータガバナンス強化を実現するために、Analytics Engineerとして複数の部門を跨ぐプロジェクトチームに参加しています。本記事ではZOZOにおけるデータガバナンス上の課題と、その課題の解決策の1つとしてdbtを導入した話をご紹介します。 目次 はじめに 目次 背景 課題 データマートの乱立 集計定義のばらつき 依存関係の洗い出しが困難 データモデリングツールの比較検討 データ変換に関する要件 データモデリングツールの選定 レイヤリングによる責務の分離 実装方針 今後の展望 dbtモデルを開発する上で工夫したこと 環境の分離 背景 工夫したこと ダミーデータセットの生成 背景 工夫したこと SQLFluffを使ったフォーマット統一 依存モデルを含むテスト dbt Docsを使ったドキュメント生成 工夫したこと 今後の展望 dbt導入に期待する効果 データマートの乱立が抑制される バラついていた集計定義の統一 影響範囲の特定がスムーズになる 後続クエリの記述量が減る 品質担保されたデータマートを参照した開発が可能 まとめ 背景 弊社のデータ基盤はGoogle CloudのBigQuery上に構築されています。データマート開発者はSQLファイルを用意しGitHubリポジトリにPull Requestを送ります。これによってCI/CDおよびCloud Composerのワークフローを通じてBigQuery上にテーブルを作成できます。このワークフローを実現しているCloud Composerの導入事例については以下の記事で紹介しているのでぜひご覧ください。 techblog.zozo.com このような仕組みによってデータ利活用が促進され、アドホックな分析目的だけではなく、推薦システムやMarketing Automationなど様々な後続システムで利用されるようになりました。また各部署でダッシュボードによる可視化も積極的に行われ、現在は様々なBIツールから参照されています。主に Looker 、 Looker Studio 、 Power BI を利用しており、Looker(LookML)では一部データモデリングも行われています。 課題 データ利活用が進む中で、データ基盤におけるデータガバナンス上の課題が徐々に顕在化してきました。 データマートの乱立 前述の仕組みによってテーブル単体では一定のレビュープロセスを経たものがデータ基盤上に作成される一方で、以下の理由から徐々に似たような目的のデータマートが作成されるようになりました。 後続システム利用を想定した上で、体系的にモデリングされたデータが存在していない テーブルの数が多く似たようなテーブルの存在に気づかない 可視化・分析での利用を想定したデータマートとその利用方法を示したドキュメントは存在していましたが、後続システム利用を想定したデータマートは存在していませんでした。そのため後続システムの開発者は必要なデータマートをそれぞれ作成していました。 集計定義のばらつき データマートの数に比例してクエリがDRYにならず、同じ指標のはずなのにテーブルによって集計定義が微妙に異なったり、集計定義を変更する際に変更漏れが発生したりしていました。 依存関係の洗い出しが困難 データマートの数の多さに加えて、各後続システム側で必要なワークフローを開発したことでより複雑な依存関係が生まれていました。それによってデータ連携やデータマート生成で障害が発生したり、データマート生成用のクエリを変更したりした場合に以下のような影響が発生していました。 データマートを利用している後続システムが正常に稼働しない 正しい依存関係を全て洗い出すのに多大な工数を割いている データモデリングツールの比較検討 これらの課題を解決するためには体系的なモデリングに則り品質担保されたデータの提供が必要です。この開発をスムーズに行うために、必要な要件を明らかにした上でツールを比較検討しました。 データ変換に関する要件 プロジェクトチームで議論した結果、データ変換に関する要件は以下となりました。 集計定義の集約 集計定義の変更履歴の追跡 実際に実行するクエリが分かる 依存関係や待機を考慮したワークフロー実行 ワークフローのエラー検知 ワークフローの部分的な自動リトライ 「集計定義の集約」「集計定義の変更履歴の追跡」という要件はデータモデリングを実践する上で一般的なものかと思います。一方、それ以外の要件はZOZOの事情に依るものです。 「実際に実行するクエリが分かる」については、データマートのモデリングを行うチームがクエリに書き慣れていた事もあり、実際に実行するクエリをイメージできる方がモデリングしやすかったためです。 残り3つの要件はデータマートの数の多さに関係しています。弊社のデータ基盤には20以上のソースシステムと1000以上のデータマートが存在しています。さらに後続システムでそのデータマートを参照しているので、データマート更新が遅延すると後続システムに影響します。そのため複雑な依存関係を解決しつつ効率的に更新するために、以下のような制御が必要です。 複数のソースシステムからの連携が完了次第、データマート更新を開始する データマートが依存しているソースシステムの連携が完了次第、データマート更新を開始する 失敗したデータマート更新のみ自動で指定回数リトライする 失敗したデータマートに依存しないデータマートは更新を続ける ワークフローの実行イメージは以下のようになります。 データリネージュの可視化、データ品質監視、データ仕様のカタログ化など一見重要そうな要件が入っていないのは、既に Dataplex の導入を検討していたためそちらでもカバーが可能と判断したためです。 データモデリングツールの選定 まずは集計定義の集約、集計定義の変更履歴の追跡という要件にフォーカスして以下の候補からデータモデリングツールを選定しました。 Looker(LookML) Dataform dbt Cloud dbt Core Dataformとdbtは一般的な選択肢です。また弊社ではこれまでも一部でLooker(LookML)を使ってディメンショナルモデリングを行っていたので候補に含めました。その際の導入事例については以下の記事で紹介しているのでぜひご覧ください。 techblog.zozo.com 要件を元に各ツールを比較すると以下のようになります。 要件 Looker(LookML) Dataform dbt Cloud dbt Core 集計定義の集約 ○ ○ ○ ○ 集計定義の変更履歴の追跡 ○ ○ ○ ○ 実際に実行するクエリが分かる △ ※ ○ ○ ○ 依存関係や待機を考慮したワークフロー実行 △ △ △ △ ワークフローのエラー検知 △ △ △ △ ワークフローの部分的な自動リトライ △ △ △ △ ※ ジョブ履歴から辿る必要がある。 既に利用していたLooker(LookML)を継続して利用することも考えられます。しかしLooker(LookML)は候補ツールの中で唯一、記法がYAMLベースとなります。そのため実際に実行するクエリが分かるかという点では他ツールに劣ります。 SQLブロック を使えば部分的にSQLベースの記法を実現できますが、ExploreやViewなどパーツ同士をJOINした結果をBigQueryに投げることもできます。そのため最終的にBigQuery上にどのようなクエリが投げられているのかは、その上層の設計に依ります。また、BigQueryのジョブ履歴を参照すれば実際に実行したクエリは分かります。しかしLooker(LookML)のUI上から簡単に遷移できなかったり、ジョブ履歴のクエリも改行等が挿入されていなかったりと人間にとって読みづらいなどの問題があります。 Looker(LookML)自体は、使いこなせばガバナンスを効かせやすいツールです。しかし今後データマートのモデリングを行うチームにとっては実際に実行するクエリをイメージできる記法が良いという意見が多く、他ツールが有力候補となりました。 問題は最後の3つの要件です。これらはどのデータモデリングツールでも 単体では 満たすことはできませんでした。そこで既に運用中のCloud Composerとの組み合わせで実現することにしました。 それではどのツールが最適でしょうか。実行制御を担うのはCloud Composer、集計定義や依存関係を管理するのはデータモデリングツールといった形で責務の分離をしたいです。しかしDataformには 集計定義と依存関係だけをファイル出力する機能が存在しません でした。またdbt Cloudはメリットとして実行環境や実行制御とセットになっている、IDEが付属している点が挙げられますが、今回はそれらを必要としなかったのと、デメリットとして利用料金がかかります。 そこでシンプルに 集計定義と依存関係をファイル出力できるdbt Core を採用しました。 レイヤリングによる責務の分離 実装方針 集計定義をDRYに集約するためレイヤリングによって責務を分離しました。メンテナンスができるだけ属人化しないように、dbt公式ドキュメントの best-practices をベースに以下の構成を取ることにしました。 Sources:ソースシステムから連携された一次テーブル Staging:退避テーブル等同じスキーマ同士のUNIONおよび重複排除を実施 Marts Core Marts:同じエンティティで、1対1または多対1の多重度のテーブル同士をJOINしたワイドテーブルを定義 Intermediate:Core MartsとDomain Martsの中間層として複数のエンティティにまたがる集計ロジックを定義 Domain Marts:各後続システムで必要なデータマートを定義 ベストプラクティスに則ると型変換やTimeZoneの統一などシンプルな変換処理はStagingで行うべきです。しかし弊社のデータ基盤ではStagingより前段のBigQuery格納時にこれらの処理を行っているため、Stagingでの実装は不要でした。また、今回はまず必要と思われる最低限のレイヤーを実装し、必要に応じて後からレイヤーを追加する等のリファクタリングをすることにしました。 今後の展望 データマートの数が非常に多いため、レイヤーによってはディメンショナルモデルを採用した方がよいかもしれません。またMartsの下層に位置する セマンティックレイヤー を導入するべきか、どのツールでどのように管理するのかについても決めていく必要があります。このあたりは今後の整理や運用する中で検討すべき事項としています。 dbtモデルを開発する上で工夫したこと dbtモデルの開発効率化や品質担保のために以下の工夫をしました。 環境の分離 背景 Cloud Composerを使ったワークフローを開発するため、弊社には元々Dev/Stg/Prdの3環境が存在し、それぞれGoogle Cloudプロジェクトを分けていました。またdbtのモデル開発者とCloud Composerのワークフロー開発者は異なるロールなので、それぞれの環境やGitHubリポジトリを分けることで責務を明確にしたいと考えました。 工夫したこと 環境の分離については以下の方針としました。 dbt開発者が自由にデプロイできるSandbox環境として、Google Cloudプロジェクトを新規作成 dbt開発用のGitHubリポジトリを新規作成 dbt開発用のリポジトリでPull Requestをmain branchにマージすると、Cloud Composer開発用のリポジトリに対して自動でPull Requestが作成されます。Cloud Composer開発者は作成されたPull Requestをmain branchにマージすることで、dbtで定義した集計ロジックや依存関係をCloud Composerに適用します。 ダミーデータセットの生成 背景 dbtモデルを dbt run で実体化したり、 dbt test でテストしたりする際に、Sources層として参照元のデータセットやテーブルが必要です。dbtの Source定義 では本番環境のプロジェクトを直接参照できます。しかし弊社のデータ基盤ではガイドライン上、本番環境のデータを直接参照できません。そこで開発環境(Sandbox環境とDev環境)内に何らかの方法でデータセットやテーブルを生成する必要があります。 さらに弊社のデータ基盤の一部のテーブルでは会員の住所やメールアドレスといった秘密情報にあたるカラムに ポリシータグ を付与した上で保有しており、これをダミーデータに置き換える必要があります。ポリシータグの導入事例については以下の記事で紹介しているのでぜひご覧ください。 techblog.zozo.com 工夫したこと 開発環境(Sandbox環境とDev環境)内にビューを生成することで本番環境を間接的に参照するようにしました。その際にポリシータグ付きカラムはダミーデータに置換しました。また環境内で開発者同士の競合が起きないように、GitHubのbranchごとにデータセット単位でダミーデータを生成する dbt macros を用意しました。 このdbt macrosはGitHub上でbranchを新規作成後、GitHub Actionsの workflow_dispatch イベントで手動実行します。BigQueryではビューの生成にはコストがかかりませんが、ダミーデータ生成が不要な対応の場合はCIの実行リソースが勿体無いので自動実行は避けました。 開発環境(Sandbox環境とDev環境)では参照するデータセットとデプロイ先となるデータセットをbranchごと・レイヤーごとに分岐させる必要があります。具体的には以下のように実装しました。 dbt_project.yml ファイルでbranch名をプロジェクト変数として宣言します。 vars : branch_name : "development" 参照するデータセットは sources ファイルで Jinja を用いて動的に定義しました。 version : 2 sources : - name : source_name database : | { %- if target.name == 'sandbox' -% } sandbox-project-name { %- elif target.name == 'dev' -% } dev-project-name { %- elif target.name == 'stg' -% } stg-project-name { %- elif target.name == 'prd' -% } prd-project-name { %- endif -% } schema : | { %- if target.name == 'sandbox' or target.name == 'dev' -% } dataset_name_{{ var('branch_name') }} { %- else -% } dataset_name { %- endif -% } デプロイ先となるデータセットはdbt公式ドキュメントの Advanced custom schema configuration を参考にハンドリングを実装しました。dbtで提供されているdbt macros generate_schema_name をオーバーライドしています。 {% macro generate_schema_name(custom_schema_name, node) -%} {%- set default_schema = target.schema -%} {%- if custom_schema_name is none -%} {{ default_schema }} {%- else -%} {%- if target.name == ' sandbox ' or target.name == ' dev ' -%} {{ default_schema }} _ {{ custom_schema_name | trim }} _ {{ var( " branch_name " ) | trim }} {%- else -%} {{ default_schema }} _ {{ custom_schema_name | trim }} {%- endif -%} {%- endif -%} {%- endmacro %} さらにデプロイ先となるデータセットにレイヤー名を付与するため、 dbt_project.yml ファイルでディレクトリ構造に従ってカスタムスキーマを定義しました。この値は前述のdbt macros generate_schema_name に対して custom_schema_name として渡されます。 models : dbt_project_name : staging : +schema : stg marts : core : +schema : core domain : intermediate : +schema : int dbtコマンドを実行する時は以下のように引数としてbranch名を渡します。 dbt run --vars " {'branch_name': 'issue_95'} " --target sandbox これによってbranchごと・レイヤーごとにデータセットが生成されます。 SQLFluffを使ったフォーマット統一 可読性向上のために SQLFluff を使ってフォーマットを統一するようにしました。main branchに対するPull Requestを作成すると、CIでSQLFluffを実行し、フォーマットに問題がある場合はエラーとなります。ちなみにSQLFluffを選択した理由はルールの細かなカスタマイズが可能なのと、もし将来的にdbt Cloudと併用する場合も同じルールの適用が可能なためです。 依存モデルを含むテスト データマートを変更する際には、依存関係のあるダウンストリームのデータマートのテストも実行するようにしました。main branchに対するPull Request作成により、CIにて dbt test を実行します。Warningレベルのログを含めたテスト結果がPull Request内にコメントで通知されます。 dbt Docsを使ったドキュメント生成 工夫したこと 各dbtモデルにはYAMLファイルにテーブルレベル、カラムレベルのdescriptionを記述できます。さらに dbt docs コマンドによって、dbtモデルのdescriptionだけでなく、依存関係も可視化された ドキュメントサイト を自動生成できます。 今回Core Marts層およびIntermediate層の実装にあたって、バラついていた集計定義を統一するため利用者にヒアリングの上、調整をしたところもありました。そこで利用者が簡単に集計定義を確認できる様に以下の工夫をしました。 persist_docs オプションを有効化することで、YAMLファイルで定義した内容をBigQueryのdescrptionにも反映 main branchに対するPull Requestをマージすると、GitHub Pagesでホスティングしているdbt Docsサイトを更新 今後の展望 集計定義そのものはdescriptionに記述できても、その経緯や背景の詳細までは記述できません。現在そういった情報は社内ドキュメント管理ツールで管理しています。今後はdbtのドキュメント生成機能を拡張していくのか、それとも全く別のデータカタログツールを導入するのか、検討していきたいと考えています。 dbt導入に期待する効果 dbt導入はこれから実運用に入る段階のため期待する効果を以下に示します。 データマートの乱立が抑制される 後続システムでの利用も想定した上で体系的にモデリングしたデータと、dbt Docsによるドキュメントを提供することで、データマートの乱立が抑制されることを期待しています。ただし、もし今後データマートの整理を進める中でデータマート数をさらに抑制した方がよい場合は、レイヤーの追加や異なるモデリング手法などを検討します。 バラついていた集計定義の統一 Core Marts層またはIntermediate層で重要な指標や共通した指標を定義することで、下層ではそれらを参照すればよいため、集計定義を統一しやすい環境が整いました。今後は定期的に集計定義の見直しを行い、必要に応じてCore Marts層やIntermediate層を拡充していく予定です。 影響範囲の特定がスムーズになる dbt Docsによって依存関係が可視化されるため、どのデータマートがどのデータマートを参照しているのかが明らかになります。これによって、データ連携やデータマート生成で障害が発生した場合やクエリを変更した場合の影響範囲の特定がスムーズになることを期待しています。 後続クエリの記述量が減る これまで各データマートで似たようなJOINを繰り返し記述していましたが、Core Marts層またはIntermediate層を参照することで後続クエリの記述量が減ります。また後続クエリ側はJOIN数が減るのでクエリパフォーマンスの改善、ひいては実行時間や消費スロット削減等のコストメリットも見込めます。 品質担保されたデータマートを参照した開発が可能 各dbtモデルのテスト実装およびCI上での自動テストによって、後続のデータマート開発者は品質が担保されたデータマートを参照して開発できるようになります。 まとめ 本記事ではZOZOのデータ基盤でdbtを導入した話を紹介しました。ただdbtを導入しただけで全てが解決するわけではありませんが、dbtはサードパーティツールが充実していることもあり、ベストプラクティスを適用しやすくなったと感じます。既にデータ基盤を運用している組織でdbt導入を検討している方の手助けになれば幸いです。今後は社内のさまざまなプロダクトに対して品質が担保されたデータを提供できるよう、Marts層の拡充やモニタリングをはじめとする運用の最適化・自動化を進めていきたいと考えています。 ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com
アバター
こんにちは。検索基盤部の山﨑です。検索基盤部では、ZOZOTOWNの検索機能の改善を目的とした施策の有効性をA/Bテストで検証しています。 A/Bテストは、新たな施策の有効性を評価する手法として信頼性の高い手法ではあるものの、下記のような制約があります。 統計的に有意な差が出るためには、多くのユーザーからのフィードバックが必要である 比較手法を実際のユーザーに提示するため、ユーザー体験に悪影響を与えるリスクがある これらの制約から、実験したい全ての施策をA/Bテストで検証することは困難なため、事前に有効な可能性が高い施策に絞った上でA/Bテストに臨むことが大切です。 事前に有効な可能性が高いことを示すためには、オフラインでの評価結果を活用します。しかし、オフライン評価とA/Bテストの結果は必ずしも一致しないことが知られており、ZOZOTOWNにおいても同様の問題が発生しています。 このような課題に対しては、反実仮想機械学習の分野で研究されているOff-Policy Evaluation(OPE: オフ方策評価)が有効であると考えられます。OPEについての詳細は、下記の記事を参照してください。 techblog.zozo.com 本記事ではOPEの詳細を説明しませんが、OPEの考え方を踏まえながら、オフライン評価とA/Bテストの結果が一致しない問題に対しての実践的な対応策について紹介します。 まず、ZOZOTOWN検索においてオフライン評価とA/Bテストの結果が一致しない問題と、その実践的な対応策を解説します。そしてデータの選択バイアスを中心に、この問題の解決策をいくつか紹介します。 目次 目次 オフライン評価とA/Bテストの結果が一致しない問題 オフライン評価でのnDCGの弱点とそれを補う評価指標 ヘルスチェックとしてのオフライン評価指標の活用 データの選択バイアスを考慮したオフライン評価手法 一般的な情報検索の評価指標 選択バイアスを考慮した情報検索の評価手法 インターリービングを活用した情報検索の評価手法 まとめ おわりに 参考文献 オフライン評価とA/Bテストの結果が一致しない問題 オフライン評価結果とKPI(ここでは、A/Bテストの結果とKPIを同義に扱います)が一致しない問題は、多くの研究で報告されています。 例えば、 Bernardi et al, KDD 2019 1 ではオフライン評価の改善率とA/Bテストでのコンバージョン率の改善率を比較したところ、それらの結果が相関しないことを報告しています。 相関しない理由として、KPIの代理変数を過剰最適してしまうことやA/Bテストという短期間でビジネスインパクトに影響のある変更の検知が難しいことなど、幾つかの要因が挙げられています。 同様にZOZOTOWNにおける検索改善の施策でのランキングベースのオフライン評価とA/Bテストを比較した結果を下図に示します。x軸がオフライン実験での購入ベースのnDCG(nDCGの詳細は次章の説明を参照してください)の改善率、y軸がA/Bテストでの商品購入率の改善率です。 この図の示す通り、1つのオフライン評価とA/Bテスト結果が必ずしも相関しない問題はZOZOTOWNの検索改善においても発生しています。 この問題を解決するための研究は今も盛んに研究されていますが、ユーザーの行動ログの様々なバイアスの問題ゆえに、根本解決が難しい問題です。そのため、まずはオフライン評価とA/Bテストの結果は一致しないという前提で、それぞれの評価手法の特性を活用する方針で進めました。 オフライン評価でのnDCGの弱点とそれを補う評価指標 nDCGは情報検索の評価指標として広く使われている指標ですが、過去に一度でもクリック/購入されている商品や検索クエリしか評価できないという弱点があります。この問題は D. Turnbull, BSM 2023 2 でも取り上げられています。 実際にZOZOTOWNでも数%程度の検索クエリにしか購入のイベントが発生していないため、前章で紹介した「購入ベースのnDCG」では、この数%の検索クエリしか評価できていないことが分かりました。 この弱点を補うためには、購入イベントが発生していない検索クエリに対しても評価できるような評価指標が必要です。 我々はこの評価指標として、nDCGのような順序を評価するランクベースの指標の他に、検索結果を集合として扱う集合ベースの評価指標を採用しました。詳細は ZOZOTOWN検索の精度評価への取り組み の記事で紹介していますが、概括として下記2つの指標を使用します。 購入数カバー率 : 購入イベントが発生している商品をどれだけカバーできているか(通常の再現率を購入数に重み付けした指標) 商品一覧の類似度 : 旧ロジックと新ロジックが表示する商品の一覧がどれだけ類似しているか 購入数カバー率が高く商品一覧の類似度が低い場合、新ロジックが旧ロジックではカバーできていない、かつ購入確率の高い商品をカバーできる可能性が高いことを示しています。 商品の絞り込みロジックを改善した場合に、これらの指標を活用することで有効な可能性が高い施策を絞れます。 ヘルスチェックとしてのオフライン評価指標の活用 前節で紹介した各オフライン評価指標の考え方を整理すると、下記のようになります。 評価指標 悪化した場合 nDCG 機械学習モデルが適切に学習できていない可能性がある 購入数カバー率 検索結果が悪化する可能性がある 商品一覧の類似度(類似していないほど良い) 検索結果がほぼ同じ結果となる可能性がある ZOZOTOWNの検索改善では、これらの評価結果をA/Bテストに進むかの判断軸として活用しています。 ただし、オフライン評価はあくまでヘルスチェックとして活用し、実際のビジネスインパクトはA/Bテストで検証する方針を採用しています。 Bernardi et al, KDD 2019 や D. Turnbull, BSM 2023 と同様の考え方を採用しています。 つまり、これらの指標はあくまで大幅な悪化が無いことを確認することで、想定外の施策内容をA/Bテストに進めることを防ぐことを目的としています。 本章では、オフライン評価とA/Bテストの結果が必ずしも一致しない問題に対しての実践的な対応策について紹介しました。次章では、結果の乖離が発生するのかを分析する目的で、特にデータの選択バイアスに着目した評価手法と実験結果について紹介します。 データの選択バイアスを考慮したオフライン評価手法 ランキング学習の機械学習モデルの学習方法は、人手でラベルづけしたデータを用いる方法と、ユーザーの行動ログを用いる方法があります。 人手でラベルづけしたデータを用いて学習する場合、データの選択バイアスを考慮する必要がありませんが、データの収集にコストがかかります。 ユーザーの行動ログを使用することでデータ収集コストを下げられますが、そのデータは観測・未観測の傾向が異なる、いわゆるMNAR(Missing Not At Random)と呼ばれるデータです。 MNARなデータでは、例えばポジションバイアスと呼ばれる、検索結果の上位に表示された商品の方が観測されやすいという選択バイアスが存在します。 本章では、このようなユーザーの行動ログに含まれるデータの選択バイアスを考慮したオフライン評価手法について紹介します。 一般的な情報検索の評価指標 まず、情報検索の評価指標で広く使われているnDCG(normalized Discounted Cumulative Gain)と MRR(Mean Reciprocal Rank)について説明します。 検索クエリ に対して商品の一覧が返されるとき、 を に対する 番目の商品の関連性スコアと定義します。 関連性スコアは任意に定義でき、例えばユーザーがその商品を購入したら1で、クリックしなかったら0などを指定できます。 このとき、 に対する 番目までの検索結果を評価するための指標としてDCG (Discounted Cumulative Gain) を求めます。DCGは C. Burges et al, ICML 2005 3 の定義に従うと、以下のように定義されます。 さらに、関連性スコアが高い商品から順番に並べた理想的なランキングにおけるDCGをmax DCGとします。このとき、nDCGは全てのクエリ におけるmax DCGで正規化済みのDCGを合算することで、下記のように定義されます。 次に、検索クエリ に対して、最初に関連性スコアが1となる商品のランクを とすると、MRRは以下のように定義されます。 MRRのような逆数の平均値を計算する手法は、順序尺度の平均値を計算しているため、情報検索の評価指標として適切な結果とならないケースがあることに注意してください。 直感的な理由として、ランキングの順位が1番目と2番目の差は である一方で、2番目と3番目の差は となり、MRRは上位のランクを過剰に評価してしまうためです。 平均値を計算するためには、順序尺度ではなく間隔尺度などの数量データを使用する必要があります。 例えば、 M. Ferrante et al, IEEE 2021 4 では、MRRを適切な間隔尺度にマッピングした上で評価した場合と比較して、30%以上の結果が変わることを報告しています。nDCGも同様の問題があるもののMRRほど深刻ではないことが報告されています。 本記事ではこの問題についての詳細を割愛しますが、興味のある方は上記の論文や N. Fuhr, SIGIR 2020 5 を参照してください。 選択バイアスを考慮した情報検索の評価手法 近年、前節で紹介したnDCGやMRRに対して選択バイアスを考慮するように拡張した評価手法が提案されています。 X. Wang et al, SIGIR 2016 6 では、選択バイアスを考慮したMRRの拡張手法を提案しています。また、 L. Yan et al, SIGIR 2022 7 や Y. Zhang, KDD 2023 8 では、選択バイアスを考慮したnDCGの拡張手法を提案しています。 どちらも、Inverse Propensity Score(IPS: 逆傾向スコア)と呼ばれる推定量を用いて、バイアスを軽減した評価指標を定義しています。IPSは傾向スコア(Propensity Score)と呼ばれる推定量を活用します。 検索の文脈において傾向スコアは、例えば、ポジションごとの商品の見られ易さと定義できます。この傾向スコアの逆数を使用することで、ポジションによる見られ易さ、すなわちポジションバイアスを軽減できます。 ここで、 を の観測頻度、傾向スコア を が実際に表示されたときのクリック率と定義します。つまり、 のポジションが上位なほど、高い値を取る傾向があります。 傾向スコアを用いると、IPS推定量は と表せます。 このIPS推定量 を用いて、選択バイアスを考慮したIPSMRRは以下のように拡張されます。 また、ポジション ごとのIPS推定量を と定義すると、選択バイアスを考慮したIPSDCGは以下のように拡張されます。 IPSDCGをnDCGのDCGに適用することで、選択バイアスを考慮したIPSnDCGを定義できます。 実際に過去にZOZOTOWN上でA/Bテストを行ったモデルの評価をIPSMRRで再評価しました。特にポジションバイアスを軽減したモデルとそうでないモデルの比較に対して、通常のnDCGよりもA/Bテストの結果に相関する傾向を観測できました。 インターリービングを活用した情報検索の評価手法 最後に、オフライン評価とA/Bテストの結果が一致しない問題に対して、インターリービングを活用している事例を紹介します。 インターリービングは、2つのランキング結果を混ぜ合わせて1つのランキング結果を生成する手法です。インターリービングについての詳細は Effective Online Evaluation for Web Search 9 で詳しく解説されています。 F. Radlinski and N. Craswell, SIGIR 2010 10 では、nDCGとインターリービングでのモデルの良し悪しの傾向が強く相関することを示しています。 また、 Wang et al, SIGIR 2023 11 でも、nDCGとインターリービングの結果がほぼ一致していることを示しています。 このことから、インターリービングを活用することで、オフライン評価とA/Bテストの結果が一致しない問題に対して、より信頼性の高い評価手法を構築できる可能性があります。 まとめ 本記事では、オフライン評価とA/Bテストの結果が一致しない問題に対しての実践的な対応策と、特にデータの選択バイアスを考慮することでこの問題を軽減する方法について紹介しました。 データの選択バイアス削減や反実仮想な機械学習を導入することで、より性能の高いモデルや事前のオフライン検証を進めることを期待できます。 おわりに ZOZOでは検索エンジニア・MLエンジニアのメンバーを募集しています。今回紹介した検索技術に興味ある方はもちろん、幅広い分野で一緒に研究や開発を進めていけるメンバーも募集しています。 ご興味のある方は、以下のリンクからぜひご応募ください! corp.zozo.com 参考文献 Bernardi, L., Mavridis, T., & Estevez, P. (2019, July). 150 successful machine learning models: 6 lessons learned at booking. com. In Proceedings of the 25th ACM SIGKDD international conference on knowledge discovery & data mining (pp. 1743-1751). ↩ Doug Turnbull. (2023, October). NDCG IS OVERRATED. Berlin Search Meetup. ↩ Burges, C., Shaked, T., Renshaw, E., Lazier, A., Deeds, M., Hamilton, N., & Hullender, G. (2005, August). Learning to rank using gradient descent. In Proceedings of the 22nd international conference on Machine learning (pp. 89-96). ↩ Ferrante, M., Ferro, N., & Fuhr, N. (2021). Towards meaningful statements in IR evaluation: Mapping evaluation measures to interval scales. IEEE Access, 9, 136182-136216. ↩ Breuer, T., Ferro, N., Fuhr, N., Maistro, M., Sakai, T., Schaer, P., & Soboroff, I. (2020, July). How to measure the reproducibility of system-oriented IR experiments. In Proceedings of the 43rd International ACM SIGIR Conference on Research and Development in Information Retrieval (pp. 349-358). ↩ Wang, X., Bendersky, M., Metzler, D., & Najork, M. (2016, July). Learning to rank with selection bias in personal search. In Proceedings of the 39th International ACM SIGIR conference on Research and Development in Information Retrieval (pp. 115-124). ↩ Yan, L., Qin, Z., Zhuang, H., Wang, X., Bendersky, M., & Najork, M. (2022, July). Revisiting two-tower models for unbiased learning to rank. In Proceedings of the 45th International ACM SIGIR Conference on Research and Development in Information Retrieval (pp. 2410-2414). ↩ Zhang, Y., Yan, L., Qin, Z., Zhuang, H., Shen, J., Wang, X., ... & Najork, M. (2023, August). Towards Disentangling Relevance and Bias in Unbiased Learning to Rank. In Proceedings of the 29th ACM SIGKDD Conference on Knowledge Discovery and Data Mining (pp. 5618-5627). ↩ Drutsa, A., Gusev, G., Kharitonov, E., Kulemyakin, D., Serdyukov, P., & Yashkov, I. (2019, July). Effective online evaluation for web search. In Proceedings of the 42nd International ACM SIGIR Conference on Research and Development in Information Retrieval (pp. 1399-1400). ↩ Radlinski, F., & Craswell, N. (2010, July). Comparing the sensitivity of information retrieval metrics. In Proceedings of the 33rd international ACM SIGIR conference on Research and development in information retrieval (pp. 667-674). ↩ Xiaojie Wang, Ruoyuan Gao, Anoop Jain, Graham Edge, and Sachin Ahuja. 2023. How well do offline metrics predict online performance of product ranking models? In Proceedings of the 46th International ACM SIGIR conference on Research and Development in Information Retrieval. ↩
アバター
はじめに こんにちは、ZOZOMO部OMOバックエンドブロックの中島です。普段は ZOZOMO店舗在庫取り置き というサービスの開発を担当しています。 2024年1月14日から16日の3日間にかけてニューヨークで開催された「NRF 2024: Retail's Big Show」に初めて現地参加してきました。 前半はNRF Retail's Big Showの概要と関連する情報、後半はFashion TechやRetail Techを中心にお伝えします。NRF 2024全体の概要については、 NRF 2024 Event Recap などをご覧ください。 目次 はじめに 目次 NRF Retail's Big Showとは 会場の概要 セッション Expo Fashion Tech サイズ計測 ARミラー ロレアル社のパーソナライズリップスティック Retail Tech スマートカート RFID AmazonのJust Walk OutとAmazon One おわりに NRF Retail's Big Showとは NRF Retail's Big Show は、毎年1月頃にNRF(National Retail Federation)が主催する世界最大級の小売産業向け展示会です。 2024年1月14日から16日までニューヨークで開催され、190以上のセッションが行われ、1,000以上の企業が出展しました。 NRF Retail's Big Showは毎年テーマが掲げられており、NRF 2024では「Make it Matter」というテーマでした。 NRF 2024のサイト上では、おすすめホテルの紹介もされます。初参加で土地勘もなかったので、会場近くのホテル情報を得られて助かりました(ここから直接予約もできます)。 カンファレンスが近づくと、NRF 2024のアプリも公開されました。アプリを使うと、見たいセッションのチェックができたので、カンファレンス中のスケジュールを管理するのに便利でした。 NRF 2024アプリ 会場の概要 NRF 2024は Jacob K. Javits Convention Center で開催されました。 Jacob K. Javits Convention Centerの外観 2階がメインエントランスになっており、カンファレンスパス購入時に発行されるQRコードを使って受付をします。 NRF 2024に参加するためのカンファレンスパスは、All-Access PassとExpo Passの2種類があります。Expo PassだとExpo会場のみ参加可能で、Keynoteセッションなどが開催される会場に入るには、All-Access Passが必要になります。 All-Access Passは8月頃から販売がはじまり、早ければ早いほど安く購入できます。来年参加する予定の方は、夏くらいから動き始めることをおすすめします。 受付の近くには、Amazon Goの店舗もありました。クレジットカードがあれば利用できる仕組みだったので、実際に商品を手に取り購入してみました。ゲートを通る前に、レシート送付先のメールアドレスを登録すると、ゲート通過後にメールで明細の確認もできます。ゲートを通るだけで商品購入ができるという体験は少し不思議な感じでした。 Amazon Goの店舗 早朝はDonuts Dunkというものがあり、ドーナツなど軽食を食べながらネットワーキングできる時間が設けられています。 Donuts Dunk、ドーナツ以外にヨーグルトやコーヒーもありました 世界的なイベントということもあり、日本企業の方も多く参加されていたので、そこでコミュニケーションを取ることができました(アメリカで名刺交換するとは思いませんでしたが)。 セッション Javits Center内の4階5階が主なセッション会場になっていました。その中で最も大きなステージが、Keynoteセッションが行われるSAP Theatreステージになります(ステージごとにスポンサーの名前がついていました)。 Keynoteセッションが行われるSAP Theatre オープニングセッションでは、NRFのCEOとLevi'sのPresidentのセッションでした。Levi'sが2023年に実施した取り組みの紹介などを対談形式で進行していました。 NRFの各種セッションは、基本的に複数人での対談形式で進行します。技術系のカンファレンスだとプレゼン主体で技術内容の説明をする形式が多いので、このようなセッションは個人的に新鮮でした。 いくつかセッションを聞いたり、Expoをまわったりする中で、やはりAIに関連する話題が非常に多かったです。その中でもWalmart CEOとSalesforce CEOが対談するKeynoteの中で、GUCCIがカスタマーサポートに生成AIを導入したことで一夜にして収益が30%増加した話は象徴的でした。 Expo Javits Center内の1階3階が主なExpo会場になっていました。 主に大手企業が展示されている3階のExpo会場 Expo会場内は、各企業のブースがありますが、その中で、4つ特徴的なエリアがあります。 今回、Fashion TechやRetail Techで先進的な取り組みの内容を知りたかったので、ExpoではInnovation LabとStartup Hubを中心に回りました。 ZOZOTOWNは食品の取り扱いがないため、Foodservice Innovation Zoneは軽く回った程度ですが、ドライブスルーのエリアがあったところにアフターコロナを感じました。 また、Exhibitor Big IdeasはExpo Passでも参加できるExpo出展企業のセッション会場です。Exhibitor Big Ideasは NRF 2024のAgenda からセッション動画にアクセスできるので、気になる人はそちらから確認してください。 Fashion Tech Fashion Tech関連の展示では、サイズ計測のサービスやAR関連のサービスを見ることができました。 サイズ計測 3D Foot Scanの機器(左がAetrex社、右がVolumental社) Aetrex社の機器でScanした結果 足を3Dスキャンすることで、サイズがフィットするかを判別するソリューションがいくつかありました。ただ、その計測機器が高価だったので、一般化していくにはまだ時間がかかりそうな印象を受けました。 ARミラー ZERO10社のARミラー AR関連でバーチャル試着系のサービスもありました。実際に試して見ましたが、人の周りにエフェクトがかかったり、雨が降ったりなど、エンタメ要素が強いものでした。そのため、いろいろな服をデジタルで試着するサービスというよりは、人の足を止める集客ツールという印象でした。試着用の洋服データを準備することも必要になるので、精度の高いバーチャル試着の仕組みを作るのはまだ難しいのかもしれません。 ロレアル社のパーソナライズリップスティック AR関連では、ロレアル社の展示が目を惹きました。 ロレアル社のパーソナライズリップスティック 左側のアプリで口紅の色を決め、右側のデバイスにその色の配合になる口紅が出てきます。決まった口紅をお店で買うのではなく、その日にあった口紅を作るという発想に驚きました。また、このプロダクトはOEMなどではなく、ロレアル社の自社開発プロダクトという点も驚きでした。 サービスの詳細は以下の動画を見てもらうのがわかりやすいと思います。 www.youtube.com このとき初めて知りましたがすでに販売開始しており、 2022年に日本上陸もしています 。国内では 表参道フラッグシップ ブティック で体験できるので、興味のある方は確認してみてください。 Retail Tech Retail Techでは、スマートカートとRFIDに関する展示が多かったように感じました。 スマートカート 展示されていたスマートカートの例 展示されていたスマートカートは、カートとPOSレジが合体したようなものを多く見ました。カートで商品の場所を教えてくれたり、カートに商品をいれると商品判別と価格の自動計算をしてくれたりします。日本でもスーパーなどで、スマホでバーコードスキャンするようなものがありますが、それがショッピングカートと合体してより高精度になったイメージです。数年後は日本でもこのようなカートが当たり前にある世界が来るのかもしれません。 Amazon Dash Cart、レコメンドを表示したり、商品を自動判別したりする ECでの買い物の場合、カートに商品を入れたりすると、おすすめ商品のようにパーソナライズされた情報提供があります。今回展示されていたようなショッピングカートが一般化すれば、リアルの世界でも、同じようにパーソナライズされた情報提供ができるようになっていくと思います。 RFID RFIDは、国内アパレルだと棚卸しなどで使われるイメージでしたが、生鮮食品の生産から店頭に並ぶまで、すべてトラッキングしてデータ化されているものを見ることができました。どこでいつ作られたものかだけではなく、どこからどこに移動したかがわかるため、CO2排出量の管理ができるようになっていました。サステナビリティに対する取り組みにはこういう技術的なアプローチがあることを知ることができました。 RFIDリーダーで商品を特定する様子 RFIDタグが付いた商品のトラッキングができる アパレル関連で、RFID関連を使ったテクノロジーでNexite社のサービスも目に留まりました。商品の位置情報を把握して、商品の移動情報がわかるものでした。例えば商品が何回試着室に持っていかれたかがわかるので、商品の購買前のデータで分析が可能になります。 Nexite社のブース AmazonのJust Walk OutとAmazon One 実店舗での売上の最大化をしたいと考えたときに、どうしてもレジがボトルネックになります。レジの効率化のために、最近だとセルフレジやスマホをレジ代わりにするサービスがよく使われるようになってきています。そんな中で、AWSのブースにあったJust Walk OutとAmazon Oneの組み合わせは新しい解決策に見えました。 AmazonのJust Walk OutとAmazon One 簡単に説明すると、以下のようなもので、すでにアメリカのスタジアムなどで導入されているそうです。 商品判別はRFIDで実施 = レジ打ちが不要 手のひら認証で決済 = 財布・スマホが不要 最近だと、財布がなくてもスマホがあれば買い物に困らないことが増えましたが、スマホすらなくても商品購入できる世界を作ろうとしているのには驚きました。日本でもどこかで使えるようになるのを楽しみにしています。 また、AWSのブースでは、日本語でアテンドをしていただけたことで、Expoをより楽しむことができました。この場を借りて御礼申し上げます。 おわりに ホテルから歩いていけたタイムズスクエア OMO関連の新サービスが出てきているか気になっていましたが、まったく新しいサービスというものはなかったように思います。ただ、日本との違いとして、すでにBOPISやBORISなどは普通にできる環境が整っていて、その先でよりオンラインとオフラインをシームレスにつなげることを考えているように感じました。 今回のNRF視察は開発部門の福利厚生である「 セミナー・カンファレンス参加支援制度 」を利用しての参加となります。 NRF 2024は技術カンファレンスではないですが、小売の中でテクノロジーが介在しない事はありえないので、技術者がこのようなイベントに参加することで得られるものも確実にあります。また、Expoを回る中で、ZOZOFITやZOZO CHAMPIONSHIPでZOZOを知っている企業がいくつかあったのは嬉しかったです。 NRF Retail's Big Showはこれまでニューヨークでのみ開催されていましたが、 2024年6月にはじめてシンガポールで開催されます 。アジア系の企業が多く出店されるようですので、日本からだとこちらのほうが参加しやすいかもしれません。 ZOZOでは、各種エンジニアを採用中です。ご興味のある方は以下のリンクからご応募ください。 corp.zozo.com 最後までご覧いただきありがとうございました!
アバター
はじめに こんにちは、計測プロデュース部の歌代です。 私たちはZOZOFITやZOZOMATといった計測系プロダクトの開発PM、データ収集、精度検証などサービス構築から、UI/UXの分析・評価など幅広く業務を行っております。 今回は私たちのチームが抱えていた課題と、対応策として行った工夫についてご紹介します。 目次 はじめに 目次 直面した課題 目的の重要性 概要とデータを分ける 概要をまとめた効果 文書フォーマットの効果 最後に 直面した課題 私たちのチームが直面した課題は「QAスタッフの入れ替えが発生する度に、引き継ぎがうまくできず、検証のクオリティが下がる」というものでした。私たちの業務は、計測に関する独自のノウハウや技術が多くあります。また過去に実施した検証の内容を定期的に振り返り、過去の検証手法や検証結果を参考にして、現在の検証を決定する場面が多く発生しています。 スプレッドシートでまとめたメモには検証結果や分析データが精緻に記載されており、検証当時の環境や条件・結果を確認できるようになっていました。 その後、新しいスタッフを迎え、プロジェクトを振り返るタイミングで今回の課題が発覚しました。スプレッドシートを開き、当時の検証の内容を説明しようとすると難しい点に気がつきます。一つ一つの検証データは詳細に記載されていますが、検証の目的や概要などの記載はなく、日付とデータを見て、当時の状況を思い出しながら説明することになりました。結果として、説明をする側、説明を受ける側双方にとって、多くの時間を費やすことになりました。 目的の重要性 後任のスタッフに過去の検証実績をスムーズに引き継ぎできないのはなぜでしょうか。 当時の細かなデータは確かにスプレッドシートに記載されており、結果についてもしっかりまとめられており、検証結果を示すものとしては十分な資料になっていました。 実際、自分で資料を見直してみたところ、あることに気がつきました。それはそれぞれの検証の目的が具体的に記載されていないことでした。 それぞれの検証で「何のために」「何を調べたかったのか」などの情報が記載されていませんでした。その結果、細かなデータの記載はあるものの、そもそもこの検証を行った背景を理解しないまま、検証結果だけを共有されていました。後任のスタッフはゴールが見えないまま、データだけを読み解いていきながら、検証目的をぼんやりと理解しているのではないかと仮説を立てました。 概要とデータを分ける そこで私たちは、検証の概要はドキュメントWebサービス(Confluence)、データはスプレッドシートで運用することにしました。 改善前までは検証に関する概要などのメモをフリーフォーマットでスプレッドシート上にまとめていました。しかしその状態では、ドキュメントごとに記載されている項目にばらつきがあり、必要な情報を探すのにも時間がかかり、ストレスがかかるものでした。 検証の「概要」と「データ」をそれぞれ得意とするドキュメントにまとめる運用を始めるにあたって、私たちは以下のポイントを重視しました。 概要はできるだけシンプルなフォーマットにすること 概要はできるだけ情報量を少なくし、初めて読む人がうんざりしない量に留めること データは今まで通り、細かなデータを記載すること 基本的に過去の検証結果を振り返る際、まず概要の情報だけで「当時の検証が何を目的としており、結果どうだったのかサマリーで理解できること」を「概要」の役割としました。そして「概要」を読んで、もう少し詳しく知りたいと思う人が、さらに精緻な情報として「データ」を閲覧します。これにより、検証の目的を念頭においた状態で、より詳しく検証の理解を深めていくという運用を想定しました。 概要をまとめた効果 検証概要を情報共有ドキュメントWebサービスにまとめるにあたって、以下の項目に絞り込んで記載することにしました。 目的(検証の背景やゴール) 概要(具体的な環境・端末の情報) 結果(検証結果のサマリー:結論から書く) 課題(発覚した課題や残課題) 関連リンク(関連する過去検証の情報) 「後任スタッフへの引き継ぎがうまくいかなかった」私たちは特に「検証目的を明示すること」を重視しました。 目的を資料に書き起こすメリットは大きく3つあります。 資料を作成する人の頭を整理できる 資料を読む人の情報量を処理する負担が減る 明文化することで、目的を客観視・共有できる 特に「目的を客観視・共有できる」ことは、私たちのチームではとても効果的でした。「過去検証の振り返り」の対応策として実施した運用でしたが、現在進行形で検証を行う時にも非常に効果的でした。作業を依頼する人、作業を引き受ける人が口頭のみで共有するよりも、まず資料に文章で目的を明文化します。その後、文書と口頭で説明を補足することで、認識の齟齬を減らす事ができます。検証時に本来の目的以外で発見されたバグなどについては、スケジュールや発生頻度を考慮して、バグ修正を次回アップデートに回すといった柔軟な対応が取れるようになりました。 文書フォーマットの効果 私たちのチームでは検証の概要を、基本的には5つの項目でまとめました。ここで決められた項目(目的・概要・結果・課題・関連リンク)というフォーマットととても相性が良かったです。 自由なフォーマットに落とし込んで書いた場合、資料作成者が「伝えたいことを伝えたいだけ、細かく記載してしまう」ことが発生します。必要項目に記入していく形式にしたことで、資料を読む人やこのあと引き継ぎする人が、知りたい目的や概要、結果などをまとまった状態で読む事ができるようになりました。さらに文書共有ドキュメントWebサービスを使うことで誰が記載してもフォーマットさえ決めておけば、資料のクオリティを下げる事なく、情報をまとめる事ができます。書式なども決められたものを使うので、資料としてのまとまりがよく読みやすいという利点があります。このようなWebサービスでは資料をツリー管理しているため、前後の検証の資料と紐づいた形で資料を閲覧・管理できます。 私たちのチームでは概要ページのタイトルを「yyyymmdd_検証概要」という命名規則で、管理しています。単純に過去資料の振り返りをしやすくなったという効果の他に、半年に一度、自分が行ってきたタスクや業務を振り返る時にも役に立ちました。まとめた資料は見やすく、リンクを一覧化することで、同僚や上司にそのまま報告ができるといった効果がありました。また自身の実績を整理する際に、さまざまなファイルやドライブを探しまわる必要がなくなりました。ドキュメントWebサービスにこれまでの検証内容が概要としてまとまっていることで、作業効率が上がり、管理しやすいという効果を感じる事ができました。 最後に 私たちのチームではドキュメントWebサービスとスプレッドシートのそれぞれの得意分野に「概要」と「データ」に分けて管理することで、とても運用しやすくなりました。 「概要」に「目的」を記載することで、引き継ぎの際、当時何を目指したのかわかりやすくなった 「目的」を理解して、データを読むと引き継ぎの理解度が上がった 「目的の明文化」は、現在進行形の検証でも有効だった ドキュメントWebサービスはシステム的に文書を管理するため、検索や振り返りが容易になった もし過去実績の引き継ぎがうまくいかないなどでお悩みでしたら、「概要」と「データ」を分けるところから始めてみるのはいかがでしょうか。 ありがとうございました。 ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com
アバター
こんにちは、ZOZO NEXTでFashion Tech Newsの開発を担当している 木下 です。先日、弊社が運営するオウンドメディアのFashion Tech Newsにおいて英語版が公開されました。本記事では、機械翻訳サービスの比較検討、翻訳精度を向上するための調整、スムーズな翻訳を実現する仕組みについてご紹介します。比較検討の結果GPT-4を採用したため、GPT-4の本番運用を検討されている方の参考にもなるかと思います。 fashiontechnews.zozo.com 背景 翻訳の全体像 機械翻訳サービスの比較検討 翻訳精度を向上するための調整 プロンプトの調整 ルールベースでの前後処理 人によるチェック スムーズな翻訳を実現する仕組みの導入 日本語記事の公開をトリガーに翻訳 ネイティブチェックの完了をトリガーにチェック内容を可視化、Slackに通知 まとめ 背景 「 Fashion Tech News 」とは、2018年に運用を開始したZOZO NEXTのオウンドメディアです。ファッションテック領域へ挑戦を続けるZOZO NEXTが、独自の視点でファッション × テクノロジーのニュースを提供しています。 Fashion Tech Newsは記事本数の増加やPV数の増加で、メディアとしての価値が向上していました。そのような状況の中、より多くの方々に読んでいただけるように、また日本のファッションテックの情報を世界に発信することを目的に、記事を英語翻訳して公開することとなりました。限られたリソースの中で実現するために、機械翻訳を行なった上で人によるチェックを行う体制をとることとしました。 本番運用までに以下のステップで進めました。 機械翻訳サービスの比較検討 翻訳精度を向上するための調整 スムーズな翻訳を実現する仕組みの導入 翻訳の全体像 結論としては、以下のような流れで翻訳が行われます。記事は microCMS と呼ばれるCMSで管理されています。 日本語の記事の公開をトリガーに翻訳し、英語の記事を作成 CMS上でネイティブチェックを行う 編集者による最終チェックを行い公開する 機械翻訳サービスの比較検討 技術選定に当たっては一般的な機械翻訳サービスに加えて、大規模言語モデル(LLM: Large Language Model)も話題になっていたため比較に加えました。機械翻訳サービスの選定に当たって比較したポイントは以下です。 文意が正しいか 翻訳抜けがないか 固有名詞が正しいか 自然な英語表現か(面白い文章か) 料金 以下に比較表を示します。実際に翻訳結果を比較し、候補を絞り込んだ上で、ネイティブの方々からの評価をまとめたものです。表以外のサービスとして、GoogleのCloud Translation, Amazon Translate, Microsoft Tranlator, GPT-3.5も試しました。 比較項目 DeepL GPT-4 DeepLで翻訳し、GPT-4で編集 文意が正しいか △ ○ × 翻訳抜けがないか △ ○ △ 固有名詞が正しいか ○ △ ○ 自然な英語表現か(面白い文章か) △ ○ ○ 1年分(400万文字)の料金 (¥150/$1換算) ¥18,000 ¥41,000 ¥51,000 その他の特徴 - 用語集 - 語調切り替え - 専門用語等の翻訳が正確 - 漢字の読みが比較的正確 総合評価 △ ○ △ ※翻訳抜けとは、文章の一部や一文が翻訳されず、すっぽりと抜けてしまうことを指します。一度に長い文章を翻訳すると翻訳抜けが発生しやすいため、短い文章に分割して翻訳する工夫が必要です。ただ一方で短い文章ごとの翻訳では、表記揺れが発生しやすいという問題があります。 メディアとして正しい情報を伝える必要があるため、文意が正しく翻訳されなければいけません。その上で、読み物として文章が面白いことも必要になってきます。ネイティブチェックでは文章の面白さに重点が置かれており、ネイティブではない自分にとって印象的な視点でした。ネイティブチェックの結果、GPT-4が高く評価されました。 DeepLの強みである専門用語の正確な翻訳と、GPT-4の強みである自然な英語表現という2つの長所を活かし、DeepLで翻訳した文章をGPT-4で編集する方法も試しました。しかしDeepLで正しく翻訳できなかった箇所を、GPT-4でより異なる意味に変換してしまう事象が発生しました。そのため、GPT-4のみを採用しました。 カジュアルな内容から学術的な内容までを広く含むメディアであったこと、またネイティブチェックを行う体制をとることを踏まえてGPT-4を採用しました。今回は採用を見送りましたが、DeepLは専門用語の翻訳に定評があり、語調の切り替えなど便利な機能が存在します。翻訳の目的、チェック体制などを考慮して選定するのが良いと感じました。参考にした記事は以下です。 4大自動翻訳サービスの実力比較と活用ポイント DeepLの弱点が明らかに AI翻訳を比較してみました。Google, DeepL, OpenAI DeepL の翻訳精度は? ビジネスメールでの Google、Microsoft との比較結果 翻訳精度を向上するための調整 GPT-4を採用した上で、翻訳精度を向上するために以下の調整をしました。 プロンプトの調整 ルールベースでの前後処理 人によるチェック プロンプトの調整 最終的なプロンプトの抜粋を以下に示し、それぞれの意味を解説します。プロンプトの調整は、OpenAIのPlaygroundを利用しました。 記事IDには取り上げるテーマや人名の英語が入っていることがあり、それが翻訳の参考となるためプロンプトに含めました。 This article's id is ${id}. 書籍名は、日本語のみのものも多いため、日本語のまま表記するというルールがチーム内で決められました。プロンプトでは指示文のみの場合、意図した通りに動作しなかったため、以下のように具体例を含める必要がありました。 Translate the text into English, but all titles, including academic papers, reports, and literature should be left in Japanese. (ex. 『書籍のタイトル』(2023年、xx出版)-> "書籍のタイトル" (2023, xx Publishing)) メディアとしての文体で、読みやすくするための指示を追加します。こうすることで文体もある程度統一されます。 To enhance the web media tone of the text, replace any words. ルールベースでの前後処理 その他の必要な処理は、ルールベースで行いました。具体的には、英語の敬称(Mr., Ms.など)や「〜さん」といった表現は翻訳に含めないことになったため、削除する処理を入れました。 人によるチェック 上記のような調整を行なっても、正しい翻訳結果が得られないこともあります。具体的には以下のような場合です。 珍しい、もしくは特殊な読み方をする漢字 直訳ではない、会社名や人名の英語表記 専門用語の訳 文脈内で特殊な使い方をされている用語 これらはここまでの工程では対応できないため、人によるチェックが必要です。そのためチェックする方と認識を揃えるために、チェックリストを作成しました。 この部分はまだ改善の余地はあり、GPT-4を用いるなどして最初に文章から固有名詞を抜き出し、固有名詞の辞書を用いる、もしくは検索を利用して予め翻訳しておくなどの方法が考えられます。辞書としては、 ENAMDICT/JMnedict などが挙げられます。 スムーズな翻訳を実現する仕組みの導入 ここまで説明した内容は翻訳の質を担保するものでした。それに加えて、スムーズな翻訳を実現する仕組みを導入しました。 日本語記事の公開をトリガーに翻訳 ネイティブチェックの完了をトリガーにチェック内容を可視化、Slackに通知 冒頭でも紹介したように、記事の管理はmicroCMSを用いています。英語記事の公開に当たり、日本語とは別の環境(API)を作成しました。日本語記事をmicroCMSから取得し、翻訳結果を英語の環境に保存します。その後のチェックではmicroCMSを直接編集することになります。 日本語記事の公開をトリガーに翻訳 日本語記事が公開されると、microCMSの Webhookの機能 を利用し、それをトリガーに翻訳が開始されます。翻訳結果はmicroCMSの英語の環境に保存されます。またネイティブチェックの差分を可視化する際に必要となるため、AWS S3にも保存されます。 ネイティブチェックの完了をトリガーにチェック内容を可視化、Slackに通知 その後ネイティブチェックが行われます。ネイティブチェックが完了したら、チェックを担当した人はmicroCMSの カスタムステータス を変更します。これをトリガーに、現状のmicroCMSのデータと、S3に保存されている当初の翻訳との差分を可視化するコードが実行されます。このコードの実装にはGoogleの Diff Match Patch を利用し、Googleドキュメントに結果を保存しました。 続いて、ネイティブチェックとその差分の可視化が完了したことをSlackに通知し、編集者による最終チェック、公開へと進みます。2段階のチェックとすることで、英語と情報、両方の正しさを担保しています。 まとめ Fashion Tech Newsの翻訳に当たり、様々な機械翻訳サービスを比較しGPT-4を採用しました。採用理由として挙げられるのは、様々なジャンルの記事に対応できたこと、チェック体制との親和性が高かったことです。そして翻訳精度の向上を目指し、プロンプトの調整、ルールベースでの前後処理、人によるチェックを導入しました。さらに翻訳をスムーズに進めるため、microCMSの機能やチェックの差分を可視化する仕組みを活用しました。よりチェックの負担を減らすには、改善の余地はまだありますが、今回の手法により効率的で安定した翻訳体制を実現できました。 ZOZO NEXTでは、最先端を含む様々な技術を取り入れプロダクト開発に取り組んでいます。絶賛仲間を募集しておりますので、興味を持ってくださった方は以下をご確認ください。 カジュアル面談はこちらからご応募ください。 hrmos.co 募集している職種はこちらからご確認ください。 https://hrmos.co/pages/zozo/jobs/0000209 hrmos.co https://hrmos.co/pages/zozo/jobs/1809846973241688076 hrmos.co
アバター
はじめに こんにちは、ZOZOTOWN開発1部Android2ブロックの井上晃平( @ねも )です。普段はZOZOTOWN Androidアプリの開発を担当しています。ZOZOTOWN Androidチームでは、以前から商品に対して口コミや評価を投稿・閲覧できる、アイテムレビュー機能を開発していました。そして、2023年11月29日に晴れてアイテムレビュー機能がリリースされました。 アイテムレビュー機能を設計・開発していく中で見えてきた課題を、解決策とともにご紹介します。 そもそもアイテムレビュー機能のことを知りたいという方は、 プレスリリース で機能紹介をしているので、あわせてご覧ください。 目次 はじめに 目次 課題 解決策 マイルストーン方式 モジュール構成の工夫 FeatureFlag まとめ 課題 アイテムレビュー機能の開発における最大の課題はビッグバンリリース問題です。アイテムレビュー機能は開発に長い時間をかけていて、弊社の中ではかなりの大型案件でした。弊社の案件では基本的に1回のリリースで全コード差分を本番コードに取り込みます。しかし、今回のアイテムレビュー機能では機能要件が他と比べて複雑かつ巨大なため、それを実現するコードの量も多いです。そのため普段の機能開発と同じリリース方式を採ってしまうと、とても大きなコンフリクトが発生してしまったり、万が一リリース後に不具合が発生した際は、原因究明が難しくなったりするなどの問題がありました。 解決策 マイルストーン方式 この問題の解決策として、弊チームはマイルストーン方式のリリースを採用しました。開発期間を6分割し、マイルストーン毎にリリースする要件やタスクを決めます。そして、マイルストーンが終わるタイミングでQAを行い実際にプロダクションコードに差分を取り込んでいくというリリース手法です。このマイルストーン方式によって主に3つの成果を得ることができました。 大規模なコンフリクトの防止 アイテムレビュー機能の開発を6分割した小さい粒度でプロダクションコードに差分を取り込むので、大規模なコンフリクトが起きにくいです。 アプリ品質の向上 マイルストーン毎にQAを実施するので、不具合を早期発見できて原因も特定しやすいです。そのため結果的にほとんどの不具合が解消された状態で、アイテムレビュー機能の本番リリース日を迎えることができました。 チームメンバーモチベーションの向上 マイルストーン毎にリリース時の達成感を味わえるため、長い開発期間の中でチームの士気が落ちてしまうことを防げました。 マイルストーン方式について1つ考えるべきことがあります。それは開発中のアイテムレビュー機能を、リリースビルド上でいかに隠すかという点です。リリースしていない機能のプログラムがリリースビルドで動いてしまうのは問題があります。マイルストーン毎にコードをリリースしますが、実際にアイテムレビュー機能を世に出すのは6回目のマイルストーンが終わったタイミングです。それまでの間はコード差分をプロダクションコードに取り込むものの、実際のリリースビルド上ではそのコードが含まれないもしくは実行されない状態になっている必要がありました。 この問題についてZOZOTOWN Androidチームではモジュール構成の工夫とFeatureFlagを解決策として採用しました。 アイテムレビュー機能周りのコードは以下のように2つに分けることができます。 アイテムレビュー機能のモジュールの内側で閉じているコード アイテムレビュー機能のモジュールの外側に出ているコード モジュール構成の工夫 まず1のコードをリリースビルドに含めない方法を紹介します。これを実現するためにモジュール構成を工夫しました。ZOZOTOWN Androidアプリではマルチモジュール構成を採用しています。アイテムレビュー機能の実装においては、このモジュール同士の依存関係を工夫しました。具体的には以下の画像の通りです。 これらのモジュールの役割を1つずつ解説していきます。 まずinterfaceモジュールは、その名の通り interface を保持するモジュールです。具体的に言うと以下の内容を interface として抽象化して保持しています。 アイテムレビュー機能のUI モーダルを表示させるための処理群 アイテムレビュー機能のCRUD処理 次にcoreとnoopモジュールについてです。これらは両方ともinterfaceモジュールの処理の実装を保持しています。interfaceモジュールにおいて依存関係を逆転しているので、それらの処理の実装をcoreとnoopの2パターン用意できます。具体的なコード例を次に示します。 まずinterfaceモジュールに以下のような interface を定義します。 interface GetTopItemReviewUseCase { suspend operator fun invoke(parameter: Parameter): Result <TopItemReview> data class Parameter( val goodsId: GoodsId, ) } interface TopItemReview { fun findUserReviewById(id: Long ): Serializable? } そしてその実装をcoreとnoopでそれぞれ用意します。 // coreモジュール class GetTopItemReviewUseCaseImpl @Inject constructor ( private val itemReviewRepository: ItemReviewRepository, ) : GetTopItemReviewUseCase { override suspend fun invoke(parameter: GetTopItemReviewUseCase.Parameter): Result <TopItemReview> { return itemReviewRepository.getTopItemReview(parameter.goodsId) } } // noopモジュール class GetTopItemReviewUseCaseImpl : GetTopItemReviewUseCase { override suspend fun invoke(parameter: GetTopItemReviewUseCase.Parameter): Result <TopItemReview> { return Result .success( object : TopItemReview { override fun findUserReviewById(id: Long ): Serializable? { return null } }, ) } } noopの invoke メソッドの処理で return している TopItemReview#findUserReviewById は、 null を返すだけの空の実装になっています。逆にcoreモジュールの方は ItemReviewRepository#getTopItemReview の処理を呼び出しています。 最後のintergrationモジュールはcoreとnoopモジュールをスイッチするためのモジュールです。プロジェクト内の他のモジュールがアイテムレビュー機能にアクセスしたいときは、このintegrationモジュールに依存させます。integrationモジュールに対して、noopは releaseImplementation として、coreは debugImplementation として依存させます。そうすることでビルドバリアントを変更するだけで実際のビルドさせるモジュールが切り替わるようになっています。 以上のようにモジュール構成を工夫することによって、リリースビルドでは空の実装が使用され、開発中のコードがリリースビルドに含まれてしまうことを防いでいます。 FeatureFlag 次に2のアイテムレビュー機能のモジュールの外側に出ているコードをリリースビルド上で動作させないための仕組みを解説します。そもそもモジュールの外側に出ているコードとは以下のようなものがあります。 アイテムレビュー機能のUI表示処理 既存画面に追加されるアイテムレビュー機能の初期データ取得処理 Google Analyticsなどのログ送信処理 これらのコードがリリースビルド上で動かないようにするため、FeatureFlagを導入しました。今回の案件ではサーバーサイドのAPIやFirebase RemoteConfigでフラグを管理する方法ではなく、ローカルで管理する方法を採用しました。実運用されているコードを例に解説します。 まずFeatureFlagのモジュールはfeatureFlag:flag、featureFlag:core、featureFlag:core-noopの3種類存在します。featureFlag:core-noopをリリースビルドで、featureFlag:coreをデバッグビルドでそれぞれ使用します。featureFlag:flagモジュールはビルドバリアント関係なく使用します。 featureFlag:flagモジュールにFeatureFlagを以下のような形で宣言します。 title と description を保持することで、そのFeatureFlagがどのような意味を持ち、どのような働きをするのかという内容を明確にします。 // featureFlag:flag sealed class FeatureFlag( val title: String , val description: String , ) { object ItemReview : FeatureFlag( "ItemReview有効化" , "ItemReviewの有効/無効を切り替える" ) // 他のFeatureFlag } そしてfeatureFlag:core-noopとfeatureFlag:coreのそれぞれに必要なコードを追加します。 // featureFlag:core-noop // in FeatureFlagEx.kt fun FeatureFlag.isEnable(): Boolean = false // in FeatureFlagContainer.kt object FeatureFlagContainer { @JvmStatic fun init (context: Context) { // no-op } } // featureFlag:core // in FeatureFlagEx.kt fun FeatureFlag.isEnable(): Boolean = FeatureFlagContainer.isEnable( this ) // in FeatureFlagContainer.kt object FeatureFlagContainer { private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "feature_flag" ) private var dataStore: DataStore<Preferences>? = null private fun FeatureFlag.toPreferencesKey() = booleanPreferencesKey(javaClass.name) private fun store(): DataStore<Preferences> = checkNotNull(dataStore) @JvmStatic fun init (context: Context) { if (dataStore != null ) return dataStore = context.dataStore } internal fun isEnable(feature: FeatureFlag): Boolean = runBlocking { store().data.first()[feature.toPreferencesKey()] ?: false } suspend fun set (feature: FeatureFlag, enable: Boolean ) { store().edit { setting -> setting[feature.toPreferencesKey()] = enable } } } リリースビルドでは isEnable が常に false です。デバッグビルドではJetpackのDataStoreを利用してFeatureFlagを保存しています。そのため必要に応じて isEnable の true/false を切り替えることができます。 また、 isEnable の処理だけをFeatureFlagの拡張関数として別ファイルに切り出している理由は以下です。 FeatureFlag. と入力し、sealed classで定義したFeatureFlagをIDEの一覧サジェストから選択して、 .isEnable とスムーズに続けることができるため。 isEnable の true/false のチェックのみが必要な箇所に対して、不必要に保存処理等までを公開しないようにするため。 FeatureFlagを利用する側では isEnable を呼び出しその戻り値によって実行する処理を分岐します。今回はアイテム詳細画面において、レビューの平均値を表示する星を出し分けるコードを例に紹介します。下記のように onViewCreated 内部でFeatureFlagを確認し、 ItemReview#isEnable が true になっていれば星を表示して、 false であれば何もしません。結果的にアイテムレビュー機能のON/OFFを切り替えることができるようになります。 override fun onViewCreated(view: View, savedInstanceState: Bundle) { super .onViewCreated(view, savedInstanceState) if (FeatureFlag.ItemReview.isEnable())) { // 星を表示させる処理 } } レビュー機能ON レビュー機能OFF 以上のようなFeatureFlagを用いて、アイテムレビュー機能のモジュールの外側に出ているコードがリリースビルド上で動かないようにできました。 まとめ 本記事ではZOZOTOWN Androidチームのアイテムレビュー機能実装における取り組みを紹介しました。比較的大きな案件でビッグバンリリースを未然に防いだ実績を作ることができました。今回学んだことを活かして次回以降の案件の遂行をより円滑にしようと思います。 ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 hrmos.co
アバター
はじめに こんにちは、ZOZO研究所AppliedMLチームの古澤です。私たちは商品画像の検索の基礎として、深層距離学習という技術を研究しています。本記事では、本研究所からICLR2024に採択された「Mean Field Theory in Deep Metric Learning」という研究について紹介します。対象読者としては、機械学習系のエンジニアや学生を想定しています。 目次 はじめに 目次 Notation 深層距離学習 磁性体と平均場理論 磁性体 平均場理論 磁性体と深層距離学習のアナロジー 深層学習への応用 Contrastive Loss ClassWiseMultiSimilarity Loss 実験 評価指標 データセット 実装の詳細 比較手法 定量評価 まとめ 参考文献・注釈 Notation この記事では以下のnotationを使用します。 訓練データ: 画像などのデータ: クラスラベル: クラス に属する訓練データ: データ を長さ1のd次元ベクトルに写す特徴抽出器: 特徴空間の距離: 深層距離学習 深層距離学習は、データ間の距離や類似度を学習するタスクであり、画像認識、顔認証、推薦システムなど、多くのアプリケーションで利用されています。 深層距離学習では、画像やその他の複雑なデータから、データの類似度を反映した特徴ベクトルを抽出することを目指しています。これを実現するためには、ベクトル空間内で類似するデータ点が近く、異なるデータ点が遠くなるように損失関数を設計することが重要です。 深層距離学習の損失関数の典型例として、Contrastive Loss 1 が知られています。 ここで、 です。第一項は同じクラス内のデータ点(正例ペア)の相互作用を、第二項は異なるクラスのデータ点(負例ペア)の相互作用を表しています。 ( )は正例(負例)ペア間の距離を制御するハイパーパラメータです。正例(負例)ペアの距離が ( )よりも小さく(大きく)なるように学習が進むことを意味しています。 Contrastive Lossのようなペアに基づく損失関数の場合、可能なペアの組み合わせはデータ数が増えるに従って多項式的に増加します。このペアのうちの多くは学習に寄与しない簡単なペアであるため、学習が遅くなってしまうという課題があります。 一方で、深層距離学習の損失関数としてはペアを使用せず、分類問題と同じように訓練できる損失関数も存在します。 今回の研究では、統計物理学における解析手法である平均場理論を使用することで、ペアに基づく損失関数から、分類問題と同じように訓練できる損失関数を導出する方法を確立することを目的としています。 磁性体と平均場理論 今回の研究は、統計物理学における磁性体のモデルと深層距離学習とのアナロジーに基づいています。磁性体、特に強磁性体は、その物質の微視的な磁気モーメント(スピン)が互いに相互作用し、同じ方向を向くことで巨視的な磁化を示します。磁性体の相転移現象は、統計力学の典型的な問題として知られています。 2 磁性体 磁性体の簡単なモデルとして、以下のような無限レンジモデルを考えてみます。無限レンジモデルのエネルギー関数は次のように表されます。 ここで、Nはスピンの総数で、 とします。 は半径1の球面上に値をとるベクトルで、i番目のスピンを表します。この は、スピンが同じ方向を指している状態がエネルギー的に好まれることを示しています。 統計力学によると、温度Tでのスピン配位の確率分布はギブス分布に従います。 このモデルの熱力学的に重要な特性は、分配関数 から計算できます。 しかし、スピン間の相互作用のため、分配関数 の積分の解析的、数値的な計算は困難に見えます。 3 平均場理論 次に平均場理論について説明します。平均場理論の基本的なアイデアは、各スピンが他のスピンと相互作用する際に、他のスピンをその「平均場」で近似し、揺らぎを無視することです。これにより、各スピンが他のスピンと直接相互作用するのではなく、平均場と相互作用するようにハミルトニアンを近似できます。 具体的には、 という恒等式を使用して を の揺らぎに関してTaylor展開し、スピン間の揺らぎの交差項を無視します。この操作によりエネルギー関数と分配関数は次のようになります。 ここで、平均場 は を最小化することで決定されます。この条件は次の自己整合方程式に帰着します。 この式は、平均場 が実際に他のスピンの平均を表しており、実際に平均値周りでの揺らぎに対して展開が行われていたことを示しています。 平均場理論により、新しく最適化パラメータが導入され、スピン間の相互作用が単純化されます。これにより、分配関数に含まれる積分は独立に実行可能となります。 磁性体と深層距離学習のアナロジー 次に、分配関数のT=0の極限を考えてみましょう。この極限では、分配関数に最も寄与するスピン配位はエネルギー関数 を最小化する配位となります。これは、T=0の極限で、元の問題が を最小化するスピン配位を見つける問題と同等であることを意味します。 平均場理論を適用すると、この問題はハミルトニアン を最小化する問題に変換されます。そして、この極限では、 は に対して最小化された に比例します。したがって、 を と について最小化する問題となることがわかります。すなわち、以下のように問題が変換されたことになります。 一方で、深層距離学習では、損失関数を最小化する最適な学習パラメータ を見つけることが目標であり、次のように書くことができます。 この形式は磁性体の問題と同様であり、どちらもペア間の相互作用の関数として記述されています。このアナロジーは、平均場理論を深層距離学習に適用することで、ペア間の相互作用を平均場との相互作用に置き換えることが可能であることを示唆しています。 深層学習への応用 次に、深層距離学習の損失関数に平均場理論を適用してみます。簡単のため、Contrastive Lossに平均場理論を適用し、その後、より複雑な損失関数への適用について議論します。 Contrastive Loss 平均場理論を適用するために、各クラスの平均場 を導入し、 をこれらの平均場周りの揺らぎに関して展開します。この際、平均場間の相対距離を制約する条件を課します。 この条件は、0次の展開で を最小化する平均場のみを探索することを意味します。 磁性体の場合と同様、各特徴ベクトル について、同じクラスの平均場 の周りで展開します。 展開の際にはやはり交差項は無視しますが、揺らぎの自己相互作用に関しては残しつつ、resummationを行います。結果として、以下のMeanFieldContrastive(MFCont)Lossが得られます。 ここで、平均場に対する制約条件は を用いてソフトに取り入れられています。以上の議論から、深層距離学習の場合でも、平均場を導入することでペアの相互作用を取り除き、通常の分類問題の形に帰着できることがわかります。 ClassWiseMultiSimilarity Loss より発展的な損失関数として、MultiSimilarity Loss 4 のようなミニバッチ内のペア間の相互作用を考慮した損失関数の平均場理論について考察します。 Taylor展開を簡単にするためには、 と に関して対称な損失関数が望ましいです。次のような新しい損失関数を導入しましょう。 ここで、 、 、 はハイパーパラメータです。この損失関数はMultiSimilarity Lossに似ていますが、クラス別の負のサンプル間の相互作用を含むため、ClassWiseMultiSimilarity(CWMS)Lossと呼びます。 次に、この損失関数に平均場理論を適用してみましょう。最初と2番目の項のlogの中身は、Contrastive損失の正と負の相互作用に似た形をとります。このため、Contrastive Lossの議論を繰り返すことで、MeanFieldClassWiseMultiSimilarity(MFCWMS)Lossを導くことができます。 ここで、平均場の制約条件もソフトに導入しています。この損失関数は、各サンプル間の相互作用も取り入れており、より複雑な関係性まで取り入れられていることがわかります。 実験 今回は従来のMetric learningの評価手法と A Metric Learning Reality Check (MLRC) 5 で導入されたより公平なベンチマーク手法の両方を使用して評価しました。 評価指標 深層距離学習の単純な評価指標としてはPrecision@K(P@K)やRecall@K(R@K)などが考えられます。しかし、今回はMLRCに従い、Mean Average Precision at R(MAP@R)と呼ばれる指標を使用します。 各クエリに対し、 はk番目に近いデータ点が同じクラスの場合はP@k、そうでなければ0となる関数です。また、Rはクエリと同じクラスに属するデータ点の数を表します。 MAP@Rは最近傍のデータ点が同じクラスかという情報だけではなく、順位の情報も反映しています。このため、データ点がよりよくクラスタリングされているかを評価できます。 評価指標の比較 (figure from A Metric Learning Reality Check ) データセット 実験では、以下の4つの画像検索用データセットを使用しました。 CUB-200-2011 (CUB):200クラス・11788枚の鳥の画像データセット 6 Cars-196 (Cars):196クラス・16185枚の車の画像データセット 7 Stanford Online Products (SOP):22634クラス・120053枚の商品画像データセット 8 InShop:7982クラス・52712枚のファッション商品画像データセット 9 CUBとCarsと比較するとSOPとInShopは規模が大きいデータセットです。 MLRCのベンチマークではCUB、Cars、SOPを、通常の評価手法ではすべてのデータセットを使用しました。 実装の詳細 ベースとなる埋め込みモデル には、ImageNetで事前学習されたBN-Inceptionネットワークを使用し、最後の線形層を所望の次元の特徴ベクトルを得られるように置き換えました。 MLRCの評価手法では、損失関数のハイパーパラメータ最適化のため、ベイズ最適化を50回繰り返します。データセットはtrain-valid(最初の半分のクラス)とtestデータセット(残り)に分割しました。さらに、train-validセットをクラスが重複しないように4分割し、4-foldのクロスバリデーションを実施しました。クロスバリデーションにおけるMAP@Rの平均をベイズ最適化の目的関数として使用しました。また、特徴ベクトルの次元は128次元に、バッチサイズは32に設定しました。 テスト段階では、最適なハイパーパラメータで同様にクロスバリデーションを実行し、4つの埋め込みモデルを得ました。それぞれの特徴ベクトルを独立に使用した場合のMAP@Rの平均と、4つの特徴ベクトルを結合して新しい512次元の特徴ベクトルを作成し、これをもとに計算したMAP@Rを評価に使用しました。テストでは上記の施行を10回繰り返し、各指標の平均値と95%信頼区間を報告しました。 一方、通常の評価手法では、データセットをクラス間が重複しないようにtrainとevaluationの2つに分割しました。各エポックでevaluationスコアを計算し、その最大値を最終的なevaluationスコアとして採用しました。また、特徴ベクトルの次元は512次元に、バッチサイズは128に設定しました。こちらも同様に10回繰り返し、その平均値と95%信頼区間を報告しました。 比較手法 比較手法としては、以下の損失関数を採用しました。 Contrastive (Cont.) ClassWiseMultiSimilarity (CWMS) MultiSimilarity (MS) MultiSimilarity + Miner (MS+Miner) ArcFace 10 CosFace 11 ProxyNCA 12 ProxyAnchor (ProxyAnch.) 13 最初の4つはペアに基づく損失関数で、残りの4つは分類問題と同様に訓練できる損失関数です。特にProxyAnchor Lossは非常に性能の良い損失関数と考えられています。 定量評価 MLRCベンチマークでは、MFContとMFCWMSは、ほとんどの場合で元の損失関数よりも優れたスコアを示しました。これは、平均場理論を適用することで、学習を単純化するだけでなく、より汎化性能が高い特徴空間を学習できることを意味しています。 CarsデータセットではProxyAnchorおよびCWMSがMFContとMFCWMSよりも優れたパフォーマンスを示しています。しかし、CUBおよびSOPデータセットでは、分離されたMAP@Rおよび連結されたMAP@Rの両方で他のベースライン手法を一貫して上回りました。 ベンチマーク結果 また、従来の評価方法では、Carsデータセットを除く全てのデータセットにおいて、MFContとMFCWMSはProxyAnchor Lossおよび元の損失関数の性能を上回りました。これはMLRCベンチマークとも一致する結果です。精度の向上は特に大きなデータセットで顕著でした。さらに、全てのデータセットにおいて、MFContとMFCWMSは他の損失関数よりも早く収束することが確認されました。 評価結果 まとめ この記事では、統計物理学における解析手法である平均場理論を深層距離学習に適用した研究を紹介しました。特に、平均場理論を用いることで、学習が難しいペアに基づく損失関数を、分類問題のような損失関数に帰着させることができることを示しました。さらに、平均場理論をContrastive LossとCWMS Lossに適用し、新しい損失関数としてMFCont LossとMFCWM Lossを提案しました。 導出された損失関数の評価した結果、これらは比較手法と比較して、多くのデータセットで優れたパフォーマンスを示すことが確認されました。この結果は、深層距離学習における平均場理論の有効性を示唆しています。 ZOZO研究所では、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 hrmos.co hrmos.co 参考文献・注釈 Raia Hadsell, Sumit Chopra, and Yann LeCun, "Dimensionality reduction by learning an invariant mapping", In 2006 IEEE Computer Society Conference on Computer Vision and Pattern Recognition (CVPR’06), volume 2, pages 1735–1742. IEEE, 2006. ↩ Nishimori, Hidetoshi, and Gerardo Ortiz, "Elements of Phase Transitions and Critical Phenomena", (Oxford, 2010; online edn, Oxford Academic, 1 Jan. 2011), ↩ 無限レンジモデルの場合は、実は解析的に分配関数を計算可能です。しかし、より現実的なスピンが格子上にある場合などについては、通常、解析的な計算は困難です。 ↩ Xun Wang, Xintong Han, Weilin Huang, Dengke Dong, and Matthew R Scott, "Multi-similarity loss with general pair weighting for deep metric learning", In Proceedings of the IEEE/CVF conference on computer vision and pattern recognition, pages 5022–5030, 2019. ↩ Kevin Musgrave, Serge Belongie, and Ser-Nam Lim, "A metric learning reality check", In Computer Vision–ECCV 2020: 16th European Conference, Glasgow, UK, August 23–28, 2020, Proceedings, Part XXV 16, pages 681–699. Springer, 2020. ↩ Catherine Wah, Steve Branson, Peter Welinder, Pietro Perona, and Serge Belongie, "The caltech-ucsd birds-200-2011 dataset", 2011. ↩ Jonathan Krause, Michael Stark, Jia Deng, and Li Fei-Fei, "3d object representations for finegrained categorization", In Proceedings of the IEEE international conference on computer vision workshops, pages 554–561, 2013. ↩ Hyun Oh Song, Yu Xiang, Stefanie Jegelka, and Silvio Savarese, "Deep metric learning via lifted structured feature embedding", In Proceedings of the IEEE conference on computer vision and pattern recognition, pp. 4004–4012, 2016. ↩ Ziwei Liu, Ping Luo, Shi Qiu, Xiaogang Wang, and Xiaoou Tang, "Deepfashion: Powering robust clothes recognition and retrieval with rich annotations", In Proceedings of the IEEE conference on computer vision and pattern recognition, pp. 1096–1104, 2016. ↩ Jiankang Deng, Jia Guo, Niannan Xue, and Stefanos Zafeiriou, "Arcface: Additive angular margin loss for deep face recognition", In Proceedings of the IEEE/CVF conference on computer vision and pattern recognition, pp. 4690–4699, 2019. ↩ Feng Wang, Jian Cheng, Weiyang Liu, and Haijun Liu, "Additive margin softmax for face verification", IEEE Signal Processing Letters, 25(7):926–930, 2018a. ↩ Yair Movshovitz-Attias, Alexander Toshev, Thomas K Leung, Sergey Ioffe, and Saurabh Singh, "No fuss distance metric learning using proxies", In Proceedings of the IEEE international conference on computer vision, pp. 360–368, 2017. ↩ Sungyeon Kim, Dongwon Kim, Minsu Cho, and Suha Kwak, "Proxy anchor loss for deep metric learning", In Proceedings of the IEEE/CVF Conference on Computer Vision and Pattern Recognition, pp. 3238–3247, 2020. ↩
アバター
はじめに こんにちは。ブランドソリューション開発本部WEARバックエンド部SREブロックの山岡( @ymktmk )です。 2024年1月25日にFindy社によるオンラインイベント「Kubernetes活用の手引き 私たちの基盤構築・運用事例 Lunch LT」が開催されました。このイベントでは、株式会社メルカリさん、株式会社MIXIさん、LINEヤフー株式会社さんから一人ずつ、弊社からも私がLTをしてきましたので、こちらのブログでも報告いたします。 findy.connpass.com 登壇内容 今回のイベントでは、以下のような方をターゲットとして、各社からKubernetesに関する取り組みを紹介しました。 他社のKubernetes活用事例を知りたい方 Kubernetesを運用するための工夫点や検討すべき点を知りたい方 私からは、「Kubernetesを活用した開発者体験向上の取り組み」というタイトルで発表いたしました。 speakerdeck.com 発表内では、弊チームがこれまで取り組んできた開発者体験の向上施策のうち、「負荷試験基盤」と「Pull Request毎のPreview環境」の2つの事例を取り上げてお話ししました。以下は今回の発表の要約です。 負荷試験基盤 弊チームの負荷試験では各々が異なる負荷試験ツールを使用しており、統一されていませんでした。そのため、チームにおける負荷試験のノウハウの蓄積が難しく、特に新規メンバーは負荷試験の実施が容易ではありませんでした。 そこで、使用するツールをK6に統一し、 k6-operator とGitHub Actionsを活用した負荷試験基盤を作成しました。これにより、負荷試験の実施者は、テストシナリオをGitHubにPushし、実施したいタイミングでGitHub Actionsのワークフローを手動実行するだけで負荷試験が可能になりました。 導入後の効果としては、負荷試験を手軽に実施できるようになり、負荷試験のハードルが下がりました。また、+αの効果として、GitHubでテストシナリオを管理するようになったため、過去のシナリオを再利用できるようになり、負荷試験の準備やレビューが容易になりました。それにより、負荷試験の妥当性が向上し、心理的な負担も軽減しました。 Pull Request毎のPreview環境 私たちが担当する WEAR はローンチから10周年を迎えており、10年間の技術負債を解消すべく、Webフロントエンドをリプレイス中です。リプレイスにおいて、旧環境のUIと比較しながらレビューすることが多いため、負担が大きいことや、デザイナーを巻き込む場合にはStaging環境を使うため、他の機能のリリースを妨げていました。また、VercelやHerokuなどのモダンなPaaSに備わっているPreview Deployment機能を使った開発者体験を求める声が多く寄せられていました。 そこで、 Argo CD Pull Request Generator を活用し、Pull Request毎に Virtual Service 、Service、DeploymentのセットをKubernetesクラスターに適用することでPreview環境を実現しました。 導入後の効果としては、「Pull Requestのレビューがしやすくなりました」「開発スピードが爆上がりしました」などの嬉しいフィードバックをいただきました。現在では、フロントエンドチームが作成するPull Requestの半分以上はPreview環境が活用されています。 まとめ Kubernetesは高い拡張性を活かして様々な機能開発ができます。弊チームでは、k6-operatorを使って負荷試験基盤、Argo CD Pull Request Generatorを使ってPull Request毎のPreview環境を実現しています。Kubernetesと開発者体験の向上はとても相性が良いです。運用において発生した課題を解決するためOperatorの導入や開発に取り組んでみてはいかがでしょうか。 10分という短い時間でしたので詳細なことは話せませんでしたが、これらの事例の技術面にフォーカスした話は、弊社のTechBlogやイベントで発表していますので、ぜひご覧ください。 techblog.zozo.com techblog.zozo.com 登壇後の所感 登壇後のフィードバックやX( #k8s_findy )の実況を拝見すると、「k6-operator」や「Argo CD Pull Request Generator」といったKubernetes Operatorに対する関心が高いようでした。弊社での活用事例が参考になったとの声もいただき、大変嬉しく思います。 普段、社外からチームの活動に対する評価をいただく機会が少ない中、このような登壇の機会を頂けたことをとても感謝しています。今後も機会がありましたら、新たな情報発信ができればと思います。 謝辞 本イベントはFindy社の主催でしたので、Findyの方々には様々なサポートをしていただきました。そして、登壇に至るまでサポートしてくれた社内のDevRelチームや弊チームにもこの場を借りて感謝申し上げます。また、ご視聴いただいた皆様ありがとうございました。 おわりに ZOZOでは、プロダクト開発以外にも、今回のようなイベントの開催や登壇など、外部への発信も積極的に取り組んでいます。 一緒にサービスを作り上げてくれる方はもちろん、エンジニアの技術力向上や外部発信にも興味のある方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください! corp.zozo.com corp.zozo.com
アバター
初めまして。MLデータ部データ基盤ブロックの小泉です。 本記事ではGartner社から提唱されたActive Metadata Managementに着目し、BigQueryのCompute費用を削減した方法についてご紹介します。 目次 目次 Active Metadata Managementとは 結局どんなことを行なったのか、ざっくりまとめ Compute費用のpricing modelとReservationについて pricing model Reservation Metadataを使用して最安値のpricing modelを求める方法 マート集計クエリ実行時、pricing modelを切り替える方法 まとめ Active Metadata Managementとは Active Metadata Managementとは、Gartner社が提唱するメタデータ管理の新しい考え方です。 簡単に一言で説明すると、 システムが自らメタデータを収集、分析、洞察し、具体的なアクションを起こすこと です。この 一連のプロセスが継続的に行われること もポイントです。これにより、データの品質管理、セキュリティ、コストなどの最適化が期待される考え方になります。 詳細な解説がされているGartner社の記事はこちらです。 towardsdatascience.com 上記記事を要約すると、Active Metadata Managementには以下4つの特徴があります。 Active metadata platforms are always on. 人間が手作業でメタデータを入力することなく、常にあらゆるメタデータを自動収集する。 Active metadata platforms don’t just collect metadata. They create intelligence from metadata. メタデータを収集するだけでなく、収集したメタデータを分析して洞察する。加えて時間と共に洞察の精度を向上させる。 (例)クエリログからSQLコードを解析して自動的にカラムレベルのリネージを作成し、どのテーブルが最もクエリされているかを推察する。 Active metadata platforms don’t just stop at intelligence. They drive action. 収集したメタデータを分析して洞察した結果を、自ら活用する。 (例)過去のログを使用して、どのデータセットが最も利用されているかを分析。その分析結果をデータパイプラインシステムに送り、データパイプラインの実行スケジュールを自動的に最適化する。 Active metadata platforms are API-driven, enabling embedded collaboration. 外部ツールとAPI連携が可能。 (例)あるデータへのアクセスをリクエストされた際、データ所有者はSlack上でリクエストを受け取り、承認または拒否ができる。 今回私たちは、3つ目の特徴(Active metadata platforms don’t just stop at intelligence. They drive action.)に着目しCompute費用削減を行ないました。 3つ目の特徴についてもう少し詳しく説明します。 この特徴を言い換えると「メタデータを使用し、システムに対して自動的に何かをする」ということです。この「自動的に何かをする」というところが重要になります。先ほど挙げた例においては、「データパイプラインの実行スケジュールを自動的に最適化する」という部分が「自動的に何かをする」を指しています。また、データパイプラインの実行スケジュールが最適化されるまでに、人間が手を加える必要がないことも重要です。 つまり、 人間が介入することなく、システムが自動的にメタデータを分析し、アクションを起こすこと が3つ目の特徴となります。 以上がActive Metadata Managementの特徴です。本記事がActive Metadata Management活用のきっかけとなれば幸いです。 では、本題に入ります。 結局どんなことを行なったのか、ざっくりまとめ 費用削減までの工程をざっくりまとめると以下の通りです。 各工程における詳細な情報は後述します 。 各種pricing modelを設定したReservationを用意し、各々専用のprojectを割り当てる INFORMATION_SCHEMA.JOBS_BY_PROJECTから取得した過去の集計実績を元に、データマート毎に最安値のpricing modelを求める マート集計クエリ実行時、クエリを実行するprojectを最安値のpricing modelを設定しているprojectに切り替えて集計する 改めまして、ここからCompute費用の削減方法について詳しくご紹介していきます。 Compute費用のpricing modelとReservationについて 今回のCompute費用削減においては、Compute費用のpricing modelとReservationへの理解が重要になります。特に後述するReservationへprojectを割り当てる「Assignment」は、Compute費用削減に利用する機能です。それでは、Compute費用のpricing modelとReservationについて説明していきます。 pricing model まず、Compute費用のpricing modelについて簡単に説明します。 ※私たちはUSリージョンでBigQueryを使用しているので、USリージョン版単価での説明となります。 Compute費用のpricing modelを簡単な図にまとめました。 cloud.google.com 順を追って説明していきます。 まず、Compute費用のpricing modelは On-demand compute pricing と Capacity compute pricing の2種類に分類されます。違いは以下の通りです。 On-demand compute pricing:クエリ実行時にスキャンされたデータ量に対して課金される Capacity compute pricing:クエリ実行時に使用された計算リソース(Slot)に対して課金される それぞれのpricing modelについてもう少し詳しく解説していきます。 On-demand compute pricingは クエリ実行時にスキャンされたデータ量に対して1TBあたり6.25USD が課金されます。従って、クエリ実行時にスキャンされるデータ量が多いほど費用が高くなります。そしてCompute費用削減の観点からは、クエリ実行時にスキャンされるデータ量を少なくすることが重要になります。以下がOn-demand compute pricingを適用した方がお得になるクエリのイメージです。 (例)On-demand compute pricingを適用した方がお得になるクエリのイメージ SELECT Window関数等の複雑な計算 FROM 小さいテーブル_1 CROSS JOIN 小さいテーブル_2 CROSS JOIN 小さいテーブル_3 上記クエリは、複雑な計算を行なっているのでSlot使用量は多くなりますが、小さいテーブル_1,2,3のデータ量が少ないため、クエリ実行時にスキャンされるデータ量が少なくなります。従ってOn-demand compute pricingを適用した方がお得になります。 一方、Capacity compute pricingは クエリ実行時に使用された計算リソース、いわゆるSlot に対して課金されます。つまり、クエリ実行時に使用されるSlot数が多いほど費用が高くなります。Compute費用削減の観点からは、クエリ実行時に使用されるSlot数を少なくすることが重要になります。以下がCapacity compute pricingを適用した方がお得になるクエリのイメージです。 (例)Capacity compute pricingを適用した方がお得になるクエリのイメージ SELECT * FROM 巨大なテーブル LIMIT 10 上記クエリは、処理がシンプルなのでSlot使用量は少なくなりますが、巨大なテーブルのデータ量が多いため、クエリ実行時にスキャンされるデータ量が多くなります。従ってCapacity compute pricingを適用した方がお得になります。 また、Capacity compute pricingは、 BigQuery editions という、利用可能な機能・単価に違いがある3種類のplanから選択をする必要があります。以下がBigQuery editionsの一覧です。 pricing model 課金単位 単価 Standard Edition Slot(hour) 0.04USD Enterprise Edition Slot(hour) 0.06USD Enterprise Plus Edition Slot(hour) 0.1USD 料金の傾向として、Standard Editionsが最安値のpricing modelになります。 各Editionの違いに関する詳細は以下をご確認下さい。 cloud.google.com 各Editionで使用できる詳細な機能の違いについては上記ドキュメントをご確認いただければと思いますが、ここでは今回のCompute費用削減に関係する2点をご紹介します。 まず1点目は、3つのEditionでは費用の計算方法が異なることです。詳細は Reservation でご紹介しますが、以下が各Editionでの課金方法です。 Standard Edition:Autoscale Slot Enterprise Edition:Autoscale Slot + Baseline Slot Enterprise Plus Edition:Autoscale Slot + Baseline Slot 2点目は、 Fine-grained security controls の使用可否です。Fine-grained security controlsとは、BigQueryテーブルのカラムに対して、以下のようなことができる機能(他にもいくつかあります)です。 policy_tag というタグを付与することで特定カラムのデータへのアクセス権限を制限すること 特定カラムのデータをマスキングすること この機能についても3つのEditionで使用できるかどうかが異なります。今回、このFine-grained security controlsの使用可否は重要なポイントになりますので、念頭に置いておいていただけると幸いです。 Standard Edition:使用不可 Enterprise Edition:使用可能 Enterprise Plus Edition:使用可能 Fine-grained security controlsの詳細は以下をご確認下さい。 cloud.google.com ここからは私たちが採用している3つのpricing modelについてご紹介します。 Compute費用削減前の私たちは、全てのデータマートをEnterprise Editionのみで集計していましたが、今回新たにOn-demandとStandard Editionを導入しました。現在はこの3つのpricing modelからデータマート毎に最安値のpricing modelを求め、Compute費用を削減しています。 On-demand Standard Edition Enterprise Edition 以上がCompute費用のpricing modelについての説明です。続いてReservationについて説明します。 Reservation まず、Compute費用削減においてなぜReservationが関係するのか説明します。 現状(2024年2月時点)では クエリ実行時に使用するpricing modelをダイレクトに切り替える機能がありません 。そのため、今回のCompute費用削減ではReservationのAssignment機能を利用してpricing modelを切り替えることにしました。では、最初にReservationの解説に入ります。 Reservationとはクエリ実行に使用するSlot数をあらかじめ予約できるBigQueryの機能です。作成時は以下の設定をします。On-demand用のReservationに関しては、 Explicit On Demand Resources というReservationがデフォルトで用意されていますのでこちらを使用しています。 Reservation:Reservationの名前 Location:リージョン Editions:Reservationへ適用するBigQuery editions Max Reservation size:クエリ実行中、必要に応じて自動的に追加されるSlot(Autoscale Slot)の上限 Baseline Slot:常時Reservationへ割り当てるSlot数 以下がOn-demand用のReservation、Explicit On Demand Resourcesです。 詳細なReservationの設定方法は以下をご確認下さい。 cloud.google.com また pricing model でも触れましたが、Reservation作成時に選択した BigQuery editionsによって課金されるCompute費用の計算方法が異なります のでご注意ください。 Standard Editionを選択した場合は、以下の計算方法で課金されます。加えて設定できるMax Reservation sizeの上限は 1600Slot です。 クエリ実行中、自動的に追加されるSlot数(Autoscale Slot)のみ Enterprise EditionとEnterprise Plus Editionは以下の計算方法で課金されます。こちらはMax Reservation sizeの上限がありません。 Baseline Slot + クエリ実行中、自動的に追加されるSlot数(Autoscale Slot) 加えてこの2つのEditionは クエリ実行をしていない場合でも、設定したBaseline Slot分の料金が常に課金される ので注意してください。 以上がBigQuery editionsにおける費用の計算方法の違いです。Baseline SlotやAutoscale Slotの詳細については以下をご確認下さい。 cloud.google.com 続いてAssignmentについて説明します。AssignmentとはReservation作成後、設定したSlotを使用するため、project, folder, organizationのいずれかを割り当てる機能のことです。今回のCompute費用削減においては、projectをAssignmentするパターンを採用しているので、projectをAssignmentする方法について説明します。 まず、ReservationへprojectをAssignmentする効果は 設定したprojectで実行されるクエリがReservationのSlotを確保できる ということです。 そして最大のポイントは、クエリ実行する際にAssignmentしたprojectを選択すると、 Reservationで設定したpricing modelを使用して費用課金される ということです。今回はこのポイントを利用してCompute費用削減を行なっています。 具体的なAssignmentの方法ですが、BigQuery editionsを採用しているReservationの場合は、コンソールから操作が可能です。 注意点は、On-demand用Reservation(Explicit On Demand Resources)にはコンソールからprojectのAssignmentができないことです。On-demand用Reservation(Explicit On Demand Resources)へprojectをAssignmentする方法は2種類あります。 bqコマンドを実行する CREATE ASSIGNMENT DDLステートメントをコンソールから実行する 今回私たちは、bqコマンドを実行する方法を採用しました。 bq mk \ --location = LOCATION \ --reservation_assignment \ --reservation_id = none \ --job_type = QUERY \ --assignee_id = PROJECT_ID \ --assignee_type = PROJECT reservation_id=noneに設定する ことで、指定したprojectはOn-demand用Reservationを使用したクエリ実行が可能になります。加えて、On-demandはQUERYジョブ(job_type=QUERY)のみのサポートとなりますのでご注意下さい。 CREATE ASSIGNMENT DDLステートメントをコンソールから実行する方法を含む詳細なAssignment設定方法は以下をご確認下さい。 cloud.google.com ここからは私たちのReservation設定についてご紹介します。 1つのReservationに複数のprojectを割り当てることも可能ですが、今回私たちは各price modelを設定したReservationに対し、1つずつ専用projectを割り当てました。 以上がReservationについての説明です。続いて、Compute費用削減における各工程について説明します。 Metadataを使用して最安値のpricing modelを求める方法 まず、私たちが採用している3つのpricing modelを使用する際の注意点について説明します。 pricing model でご紹介した通り、今回のCompute費用の削減に伴い新たにOn-demandとStandard Editionを導入しました。3つのpricing modelからデータマート毎に最安値のpricing modelを求め、Compute費用削減をしています。 On-demand Standard Edition Enterprise Edition しかし、 pricing model でも触れた通り、Standard Editionは Fine-grained security controlsが使用できません 。つまり、policy_tagが付与されているテーブルを使用したクエリには適用不可能ということになります。従って、Standard Editionを適用するには以下の注意点を考慮する必要があります。 実行クエリ内でpolicy_tagが付与されているテーブルを参照していないこと この条件を判定するには policy_tagが付与されているテーブル情報 が必要になりますが、現状(2024年2月時点)では INFORMATION_SCHEMA からこの情報を取得できません。そのため、私たちはpolicy_tagが付与されているテーブルの一覧情報を管理するテーブルを自作しています。こちらの管理テーブルからpolicy_tagが付与されているテーブルの一覧を取得することで Standard Edition を適用する条件を満たしているかどうかを判定できます。 以下が最安値のpricing modelを求めるクエリです。このクエリを実行していただければ、全データマートの最安値pricing model情報を一括で求めることが可能です。 WITH -- policy_tagが付与されているテーブル一覧を全て取得 GetSensitive AS ( SELECT DISTINCT TableName FROM `プロジェクトID.データセットID.policy_tag管理テーブル` ), -- 集計されているデータマート情報を各3種類の料金プランが設定されているプロジェクトから取得 --(On-demand, Standard Edition, Enterprise Edition) JOBS_BY_PROJECT AS ( SELECT destination_table, total_bytes_processed, total_slot_ms, referenced_tables, creation_time, user_email, job_type FROM ` On -demand設定のプロジェクトID.region-us.INFORMATION_SCHEMA.JOBS_BY_PROJECT` UNION ALL SELECT destination_table, total_bytes_processed, total_slot_ms, referenced_tables, creation_time, user_email, job_type FROM `Standard Edition設定のプロジェクトID.region-us.INFORMATION_SCHEMA.JOBS_BY_PROJECT` UNION ALL SELECT destination_table, total_bytes_processed, total_slot_ms, referenced_tables, creation_time, user_email, job_type FROM `Enterprise Edition設定のプロジェクトID.region-us.INFORMATION_SCHEMA.JOBS_BY_PROJECT` ) --データマート毎に過去7間の平均価格を各3種類の料金プランで算出(On-demand, Standard Edition, Enterprise Edition) SELECT destination, ondemand_avg_price, enterprise_avg_price, standard_avg_price, policy_tag_flag, IF (policy_tag_flag , CASE WHEN enterprise_avg_price > ondemand_avg_price THEN ' On-demand設定のプロジェクトID ' ELSE ' Enterprise Edition設定のプロジェクトID ' END , CASE WHEN standard_avg_price > ondemand_avg_price THEN ' On-demand設定のプロジェクトID ' ELSE ' Standard Edition設定のプロジェクトID ' END ) AS lowest_project FROM ( SELECT CONCAT (destination_table.project_id, ' . ' ,destination_table.dataset_id, ' . ' ,destination_table.table_id) AS destination, SUM ((total_bytes_processed / 1024 / 1024 / 1024 / 1024 ) * 6 . 25 ) / 7 AS ondemand_avg_price, SUM ((total_slot_ms / 1000 / 60 / 60 ) * 0 . 06 ) / 7 AS enterprise_avg_price, SUM ((total_slot_ms / 1000 / 60 / 60 ) * 0 . 04 ) / 7 AS standard_avg_price, -- policy_tagが付与されているテーブルを参照しているかどうかを判定 LOGICAL_OR(ARRAY_LENGTH(ARRAY( SELECT referenced_table.table_id FROM UNNEST(referenced_tables) AS referenced_table INNER JOIN GetSensitive ON referenced_table.table_id = TableName)) > 0 ) AS policy_tag_flag FROM JOBS_BY_PROJECT WHERE DATE (creation_time, " Asia/Tokyo " ) BETWEEN DATE_SUB( CURRENT_DATE ( " Asia/Tokyo " ), INTERVAL 8 DAY) AND DATE_SUB( CURRENT_DATE ( " Asia/Tokyo " ), INTERVAL 1 DAY) AND user_email = ' データマート集計に使用するサービスアカウント ' AND job_type = ' QUERY ' GROUP BY destination ) クエリをご覧いただければわかる通り、データマート毎に過去7間の平均価格を各3種類の料金プランで算出及び比較をして最安値のpricing model( lowest_project カラム)を求めています。特に lowest_project カラムは、pricing modelを切り替える際に使用する情報ですのでご注目ください。 INFORMATION_SCHEMA.JOBS_BY_PROJECTの詳細については以下をご確認下さい。 cloud.google.com 以上がMetadataを使用して最安値のpricing modelを求める方法の説明です。 マート集計クエリ実行時、pricing modelを切り替える方法 続いて、マート集計クエリ実行時にpricing modelを切り替える方法について説明します。流れとしては、データマート集計クエリ実行時、取得した最安値のpricing modelへ切り替えをしてから、集計・更新するというものなっています。該当部分のコードを抜粋します。 from google.cloud import bigquery client = bigquery.Client(project= 'Enterprise Edition設定のプロジェクトID' ) query_job = client.query(project= '最安値のプロジェクトID' , query= 'データマート集計クエリ' , job_config= '集計結果を格納するテーブル設定など' ) 一番下の query_job に注目して下さい。ここで、 bigquery.Client のprojectパラメーターに先程の lowest_project カラム情報を指定しています。この設定をすることで、最安値のpricing modelを使用して集計ができます。つまり、以下3つのprojectの内、最安値のReservationにAssignmentされているprojectがパラメーターに設定されます。現状(2024年2月時点)では bigquery.Client にpricing modelを切り替えるパラメータが存在しないため、このような手法を採用しています。 また、 client にて bigquery.Client を定義する際、projectパラメータへ初期値として Enterprise Edition専用project を設定しています。これにより、最安値のpricing model情報が取得できなかったデータマートは、初期値に設定したproject(Enterprise Edition)で集計が行われます。 以上がマート集計クエリ実行時、pricing modelを切り替える方法です。 まとめ 本記事では、Active Metadata Managementに着目し、BigQueryのCompute費用を削減する方法についてご紹介しました。Compute費用の削減前は、全てEnterprise Editionでデータマート集計しておりましたが、この方法を採用した結果、以下のような割合でデータマート集計がされています。 On-demand:約20% Enterprise Edition:約30% Standard Edition:約50% Compute費用削減後、約50%のデータマートがStandard Editionを使用しています。Standard EditionはMax Reservation sizeの上限が1600Slotとなっています。そのため、集計遅延が発生する懸念をしておりましたが、Compute費用削減後も集計遅延は起きておらず削減前と変わらない速度で集計が完了しています。 そして本記事のタイトルにもなっていますが、トータルのCompute費用は約40%削減ができました。 比較的お手軽にCompute費用削減ができる方法ですので、是非お試しください。 ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com
アバター
こんにちは。カート決済部カート決済基盤ブロックの斉藤とSRE部カート決済SREブロックの飯島です。普段はZOZOTOWNカート決済サービスのリプレイスに携わっています。 弊社はモノリスからマイクロサービスへのリプレイスを進めており、カート決済サービスもその一環としてリプレイスを進めています。 よろしければ、カートリプレイスPhase1としてカート投入リクエストのキャパシティコントロールを実現させた事例もご覧ください。 techblog.zozo.com 本記事ではリプレイスの中でも、カートリプレイスPhase2としてZOZOTOWNで扱う在庫データをクラウドリフトした事例を紹介します。 はじめに 背景・課題 解決へのアプローチ アプリケーションの説明 DynamoDB API その他、AWSリソース 在庫の同期・補正を行うバッチ リリースまでの道のり 負荷試験 リリース 1. 初期データの移行 DynamoDBのキャパシティ 整合性の担保 2. リリース 効果 他の施策との相乗効果 さいごに はじめに 本章ではまず、リプレイス前のZOZOTOWNの在庫データの概要を説明します。 ZOZOTOWNの在庫データはオンプレミス環境で稼働しているSQL ServerのDB・テーブルで管理されています。CartDBとFrontDBという2つのDBで管理しており、CartDBはユーザのカート操作に関わる在庫データ、FrontDBはユーザの注文確定に関わる在庫データを管理しています。CartDBとFrontDBの在庫データの概要を以下に示します。 CartDBの在庫テーブル 販売可能数を管理している メタデータとして販売種別や販売開始日、販売価格等も管理している カート投入操作で更新される CartDBのカートテーブル ユーザがカートに確保している販売可能数を管理している カート投入・カート削除操作で更新される FrontDBの在庫テーブル 販売可能数を管理している メタデータとして販売種別や販売開始日、販売価格等も管理している 注文確定で更新される 上記テーブルのデータの流れを以下の図に示します。 CartDBとFrontDBの在庫テーブルは過去1つのDBで管理されていました。しかしZOZOTOWNの成長に合わせてカート投入と注文確定という在庫データへの主要なユーザアクションによるDBへの負荷を分散させるために分離されました。在庫テーブル・カートテーブルはSQL Serverのトランザクションで更新することによって販売可能数の整合性を担保していました。 背景・課題 昨今のZOZOTOWNの成長やカートリプレイスPhase1でも触れていた特定商品へのカート投入リクエストの集中によって在庫データへのアクセスが増加しており、負荷が上昇していました。また、上述した通り在庫データの管理にオンプレミス環境で稼働しているSQL Serverを使用しているため、耐障害性やスケーラビリティに課題がありました。このような課題から現在の在庫データの構成だと数年後には限界を迎えると考えられます。 具体的な負荷上昇につながっているボトルネックについて説明します。 CartDBで管理している在庫テーブルはカート投入時に以下のようなクエリが実行されます。 update 在庫テーブル set 販売可能数 = 販売可能数 - 1 where PK = *** 上記のクエリが同一の特定商品へ大量に実行されると、在庫テーブルへの読み取り要求・更新要求の競合が発生し、クエリの実行時間の大幅な遅延やタイムアウトが発生します。最悪のケースでは、クエリの遅延によりワーカースレッドが枯渇しCartDB全体のスループットが著しく下がるという障害が発生することもあります。SQL Serverの読み取り要求・更新要求の競合については以下に詳しく記載されていますので気になる方はご覧ください。 techblog.zozo.com 解決へのアプローチ この問題を解決するためにCartDBで管理していた販売可能数をAmazon DynamoDB(以下、DynamoDB)へ移行することにしました。 DynamoDBはオンプレミス環境で稼働しているSQL Serverと比較して以下のようなメリットがあります。 耐障害性が高い スケーラビリティが高い 自動でキャパシティがスケールイン・スケールアウトされる また、カートリプレイスPhase1でも採用されているため、既に運用ノウハウがある点も採用に至った理由です。弊社の半澤がカートリプレイスプロジェクトでDynamoDBを採用した背景について説明している記事がAmazon Web Serviceブログに掲載されていますので、そちらもぜひご覧ください。 aws.amazon.com アプリケーションの説明 ここからカートリプレイスPhase2のシステム構成について説明します。 システム構成図は以下のようになっています。 DynamoDB SQL Serverから移行された在庫データを持ちます。テーブル定義の例を以下に示します。 AttributeName Type KeySchema Description stock_id N Hash 商品のサイズ・カラーを特定できるID goods_id N 商品を特定できるID stock_quantity N 販売可能数 created_at S 作成日時 updated_at S 更新日時 上記のようにZOZOTOWN上で更新頻度が高い販売可能数をDynamoDBへ移行しました。これによって同一の特定商品へカート投入リクエストが集中した場合でも負荷に耐えられることが可能となりました。 上記のテーブルが生まれたことにより、ZOZOTOWNの在庫データの流れは以下のようになりました。 在庫同期については後述します。 今回、読み取り/書き込みスループットの課金方法と管理方法についてはオンデマンドモードを採用しています。その理由としては、ZOZOTOWNはカート投入リクエスト数がイベントや時間帯によって大きく異なり、また人気商品の発売による突発的なリクエスト増加もあるためキャパシティを予測することが困難なためです。リクエストが少ない時はキャパシティが縮小されてコストが抑えられ、逆にリクエストが増加した際には自動で拡張されるため柔軟性と拡張性を担保できます。 バックアップについてはPoint-in-time recovery(以下、PITR)を採用しています。PITRとは自動で継続的な増分バックアップを作成し、直近5分より以前の特定の状態に復元できるDynamoDBの機能です。バックアップの保持は35日間であるためそれ以前の状態には復元できません。他にも定期的にフルバックアップを作成するオンデマンドバックアップ機能もありますが、今回は以下の理由からPITRを採用しました。 在庫データは常に変動するため、何日前の何時いつの状態にリカバリするといった要件がない 運用ミスでテーブルを削除してしまった時の念のためのバックアッププランがほしい 復旧時間を短縮するためにできるだけ直近のデータにリカバリしたい API 在庫データをDynamoDBに移行するとともに、在庫データを操作する機能をCartAPIという名称のAPIとして切り出しています。元々はオンプレミスのWebサーバで稼働していたClassic ASPをJavaにリプレイスし、Amazon EKS上(以下、EKS)でマイクロサービス化しました。 CartAPIが持つエンドポイントは以下のものがあります。 販売可能数を取得する参照系 販売可能数を減少、増加する更新系 またSQL ServerとDynamoDBの在庫同期や不整合を防ぐためのバッチから呼び出されるCartBatchAPIも誕生しています。CartAPIと同様にClassic ASPからJavaにリプレイスし、EKS上でマイクロサービス化しました。バッチについては後述します。 CartBatchAPIが持つエンドポイントは以下のものがあります。 販売可能数を取得する参照系 販売可能数を同期、補正する更新系 その他、AWSリソース DynamoDBの更新内容はAmazon Kinesis Data Streams(以下、KDS)を用いてキャプチャし、Amazon Aurora MySQL(以下、MySQL)に保存しています。KDSにキャプチャされたデータはKinesis Client Library (以下、KCL)アプリケーションによって取り出し、MySQLに書き込みます。このMySQLのデータは後述する在庫を補正するバッチで参照されます。KCLアプリケーションの仕様の詳細についてはカートリプレイスPhase1のテックブログで触れているのでご参照ください。 techblog.zozo.com 在庫の同期・補正を行うバッチ 今まではFrontDBの在庫データをCartDBに同期していましたが、リプレイス後はFrontDBの在庫データ内の販売可能数をDynamoDBに同期するように変更しました。 同期のフローは以下のとおりです。 上述のバッチが常に稼働しているためニアリアルタイムでSQL Serverの販売可能数の変更がDynamoDBに反映されます。 しかし、DynamoDBとSQL Serverの更新処理は別トランザクションであるため、ユーザのカート操作によって以下のような不整合が考えられます。 カート投入時 DynamoDBの更新が成功したがCartDB(カートテーブル)の更新に失敗する カート削除時 CartDB(カートテーブル)の更新が成功したがDynamoDBの更新に失敗する 上記のような不整合を防ぐために不整合を検知して補正するバッチを作成しました。 バッチの仕組みとしては以下の2つの履歴から在庫変動があったデータをターゲットとします。ターゲットとなる在庫に対して、オンプレミスの販売可能数とユーザのカートに確保されている販売可能数を用いてDynamoDBの販売可能数を補正するというものです。 ユーザのカート操作の履歴(SQL Server) DynamoDBの操作履歴(MySQL) 上記2つのバッチ処理によって常にDynamoDBの販売可能数はSQL Serverの販売可能数と一致するようになります。 リリースまでの道のり 負荷試験 在庫を同期するバッチや在庫を補正するバッチの負荷試験を行いました。在庫データの更新される件数をもとに同期の件数を処理し切れるか、また同期の処理時間がどの程度かを検証しました。検証結果に応じてチューニングを行い、バッチの並列度・処理間隔を決定しました。 リリース 本番環境におけるSQL ServerからDynamoDBへの在庫データの移行はN%リリースで行いました。以下がN%リリースの手順です。 初期データの移行 ZOZOTOWN内で扱う販売可能数のN%がDynamoDBの販売可能数を参照するように変更するリリース 各ステップの詳細を以下に説明します。 1. 初期データの移行 最初のステップとして、本番環境のSQL Serverの在庫データをDynamoDBに同期しました。SQL Serverの在庫データをCSV形式で出力し、AWSのブログで紹介されているソリューションを参考にしたスクリプトを使用してDynamoDBにインポートしました。 aws.amazon.com スクリプトではBoto3(AWS SDK for Python)というPythonからAWSリソースを操作できるライブラリを使用しています。DynamoDBへの書き込みは batch_writer を使用し、1回の書き込みリクエストの上限を気にすることなく、大量のデータを効率良く処理させています。 以下はスクリプトから一部抜粋、加工したもので、動作は保証しませんのでご了承ください。 import boto3 import csv s3 = boto3.resource( 's3' ) dynamodb = boto3.resource( 'dynamodb' ) def main (s3_bucket=s3_bucket, csv_file=csv_file, dynamodb_table=dynamodb_table): obj = s3.Object(s3_bucket, csv_file).get()[ 'Body' ] table = dynamodb.Table(dynamodb_table) batch_size = 100 batch = [] for row in csv.DictReader(codecs.getreader( 'utf-8' )(obj)): if len (batch) >= batch_size: write_to_dynamo(batch, table) batch.clear() batch.append(row) if batch: write_to_dynamo(batch, table) def write_to_dynamo (rows, table): with table.batch_writer() as batch: for i in range ( len (rows)): batch.put_item( Item=rows[i] ) 今回Lambdaには以下の制約があるためスクリプトの実行環境はEC2を選択しました。 Lambdaのタイムアウトが15分 15分で処理できる件数は約100万件 対象の在庫データの件数は1500万件を超えており、また日々増え続けていました。Lambdaのタイムアウト値や並列数の計算せずにシンプルに考えるためそのような制約がないEC2を選択しました。以下が手順と実行環境の全体図です。 CartDBから在庫データをCSV形式でエクスポート CSVをS3にアップロード EC2にS3のCSVをダウンロード EC2上でスクリプトを実行してCSVの中身をDynamoDBにインポート 次に、インポート時に発生した問題とその解決方法、またインポート後に整合性を確認するために行なった作業をご紹介します。 DynamoDBのキャパシティ 対象の在庫データは1500万件以上で、SQL Serverとの差分を小さくするためできる限り短時間での同期が望まれました。そのため複数台のEC2上でスクリプトを並列実行したところ、オンデマンドモードでの書き込みキャパシティの拡張が間に合わず、スロットリングエラーが多発しました。この問題に対して以下のように解決しました。 同期前にオンデマンドモードからプロビジョニングモードに切り替える 事前にキャパシティを手動で拡張する DynamoDBはオンデマンドモードからプロビジョニングモードに切り替えた場合、24時間経過しないと再度オンデマンドモードに戻せませんのでご注意ください。 整合性の担保 同期完了後に、SQL Serverから出力したCSVファイルとDynamoDBのデータにズレが生じていないか確認しました。スクリプトでログ出力やリトライ設定などは行っているので異常時には検知やリカバリができますが、より厳密に整合性を担保するためです。まずはデータの件数が一致しているかAWS CLIで確認しました。 $ aws dynamodb scan --table-name < table-name > --select COUNT --return-consumed-capacity TOTAL 補足ですが、上記コマンドでDynamoDBの読み込みキャパシティがある程度必要になります。そのため同期前に書き込みだけでなく読み込みのキャパシティも拡張しています。 次にSQL Serverから出力したCSVファイルを正として、データ1件1件をDynamoDBのデータと照らし合わせる作業を実施しました。先述の在庫同期と同じ環境下で、こちらもスクリプトを使用しました。同期完了後はすぐにDynamoDBのテーブルとSQL Serverとの差分を補正するためのバッチ処理を実行する必要がありました。そのため照合作業は以下のように非同期で行いました。 同期した直後のDynamoDBをPITRで復元する 復元元DynamoDBはバッチ処理の対象となりデータが更新されていくためバッチ処理開始前に復元する 復元先のDynamoDBに対してCSVと差分がないか確認する 同期後のデータの件数の一致と、データ内容に差分がないことを確認し、在庫同期において整合性を担保できました。 2. リリース 当然、在庫データはZOZOTOWNでも重要なデータなので影響範囲を限定するためにN%リリースを実施しました。その際、ZOZOTOWNで扱っている全在庫をN%に区切りながらリリースすることが難しかったため、以下のように決めました。 ZOZOTOWNで扱っている特定の1ショップだけクラウド対象にする ZOZOTOWNで扱っている特定の10ショップだけクラウド対象にする ZOZOTOWNで扱っているショップのN%をクラウド対象にする ZOZOTOWNで扱っているショップの100%をクラウド対象にする というように、段階的にクラウド対象を増やしていきました。最終的にはZOZOTOWNで扱っているすべてのショップをクラウド対象にしました。 効果 本リプレイスの効果について説明します。 リプレイス前は同一の特定商品へ大量のカート投入リクエストが実行されると、在庫テーブルにおいて読み取り要求・更新要求の競合が発生していました。その結果CartDBの負荷が上昇していました。しかしリプレイス後では競合の発生を防ぎ、CartDBの負荷を抑えられました。 下図はリプレイス前後で、同一の特定商品へ大量のカート投入リクエストが行われた某日を比較しています。リプレイス後はリプレイス前より5倍近いカート投入リクエストが実行されているにもかかわらず、競合は発生せずにCartDBの負荷も上昇していないことがわかります。 他の施策との相乗効果 カート決済チームではリプレイス以外にもカート機能の改善に取り組んでいます。本リプレイスと同時期にCartDBの負荷を下げる施策として、Istio Rate Limitを用いた商品単位でのカート投入リクエスト制限を導入しています。先述した本リプレイス後の結果はこの施策との相乗効果といえます。 Istio Rate Limitについては注文リクエストの制限に導入した事例が公開されているのでご覧ください。 techblog.zozo.com さいごに ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com
アバター
はじめに ML・データ部推薦基盤ブロックの佐藤( @rayuron )です。私たちはZOZOTOWNのパーソナライズを実現するために、機械学習モデルとシステムを開発・運用しています。本記事ではクーポン推薦のための機械学習モデルとシステム改善に取り組んだ話を紹介します。 はじめに 背景 課題 1. 古い基盤でシステムが運用されている 2. KPIに改善の余地がある 3. 機械学習モデルの評価体制がない 課題解決のために 1. Vertex AI Pipelinesへの移行 2. Two-Stage Recommenderの導入 プロジェクトへの導入 Candidate Generation 1. 過去の実績 2. 人気ブランド 3. 興味を持っているブランドの類似ブランド 評価方法 Reranking 学習データの作成 アンダーサンプリング 特徴量エンジニアリング 学習 バリデーション 推論 3. 機械学習モデルの評価と体制整備 オフライン評価 メトリクスのオフライン評価 過去の施策を活用したオフライン評価 定性評価 実験管理の導入 結果 運用業務の簡易化 KPIの改善 リリースサイクルの改善 今後の展望 バイアスの考慮 評価体制の改善 評価指標の探求 定性評価の体制整備 おわりに 背景 ZOZOTOWNでは一部の提携するショップで使用可能なクーポンが発行されます。クーポンが発行されていることをユーザーに知らせるため、メールやPush通知のチャネルを通してクーポン対象のショップとアイテムの訴求を行なっています。以下はメール配信のイメージ画像です。 クーポン対象となるショップおよびアイテムが決まっている条件下において、メール配信の対象者と推薦ショップ、推薦アイテムを決定するために機械学習モデルとシステムを運用しています。以降、この機械学習モデルをクーポン推薦モデルと表現して紹介します。 課題 既存のクーポン推薦モデルとシステムには以下の課題がありました。 1. 古い基盤でシステムが運用されている 弊社の機械学習パイプラインはVertex AI Pipelinesを標準としているのですが、既存のクーポン推薦のパイプラインは標準化前のCloud Composerで運用されていました。Vertex AI Pipelinesは運用を簡易化するために内製の整備が進んでいる一方、Cloud Composerは整備されていないため運用業務が煩雑で時間を要します。さらにCloud Composerで機械学習パイプラインを作成している社員がほぼいないので属人性が高いという課題がありました。 2. KPIに改善の余地がある 前述の様にクーポン推薦モデルでは配信対象者の選定、ショップ推薦、アイテム推薦という3つの観点を考えます。 各観点に対する既存のアプローチは以下の通り大部分がルールベースのロジックで構成されているため、機械学習モデルに置き換えることでKPIを改善できると考えられます。 推薦の観点 既存のアプローチ 配信対象者の選定 Matrix Factorizationとルールベース ショップ推薦 ルールベース アイテム推薦 ルールベース 3. 機械学習モデルの評価体制がない 既存システムでも配信対象者の選定で機械学習モデルが利用されていますが、その当時は現在ほどMLOpsが整備されていなかったため、機械学習モデルを評価する体制がありませんでした。モデルやデータに変更がある際に精度の変化を確認できないためモデルの改善サイクルを回しにくく、意図せず低品質なモデルをデプロイしてしまう危険性がありました。 課題解決のために 上記の課題解決のために以下の3つに取り組みました。 Vertex AI Pipelinesへの移行 Two-Stage Recommenderの導入 機械学習モデルの評価と体制整備 1. Vertex AI Pipelinesへの移行 既存のシステムはCloud Composer上で機械学習パイプラインを構築していたため、現在弊社で機械学習基盤として利用されているVertex AI Pipelinesへ移行しました。移行に際し、 2021年に公開した「Kubeflow PipelinesからVertex Pipelinesへの移行による運用コスト削減」という記事 で今後の展望として挙げられていたパイプラインテンプレートを利用しています。パイプラインテンプレートは GitHubのテンプレートリポジトリ機能 を利用し、Vertex AI Pipelinesの実行・スケジュール登録・CI/CD・実行監視等をテンプレート化したものです。以下のテックブログでもVertex AI Pipelinesの導入事例についてご紹介しているので是非ご覧ください。 techblog.zozo.com techblog.zozo.com 2. Two-Stage Recommenderの導入 KPIを改善するために、推薦分野で広く利用されている Two-Stage Recommender という推薦手法を導入します。このモデルアーキテクチャは推薦に関するコンペティションの上位解法でよく使用されており、最近だとKaggleの H&M Personalized Fashion Recommendations などで使用されていました。その名の通りCandidate GenerationとRerankingという2つのステージで構成されています。ユーザーとアイテムの全ての組み合わせに対するスコア計算が困難な場合に、ユーザーとアイテムに対するスコアの計算を一部の組み合わせに限定できます。 Two-Stage Recommenderは以下の工程で推薦を実現します。 Candidate Generationステージで、ユーザーと関連度の高いアイテムの集合を取得する Rerankingステージで、取得したアイテムをユーザーの関心度に基づいて並べ替える プロジェクトへの導入 本プロジェクトでは前述の3つの推薦の観点を以下の様に変更します。 推薦の観点 変更前のアプローチ 変更後のアプローチ 配信対象者の選定 Matrix Factorizationとルールベース Two-Stage Recommender ショップ推薦 ルールベース Two-Stage Recommender アイテム推薦 ルールベース ショップ推薦を考慮したルールベース 配信対象者の選定とショップ推薦では同一のTwo-Stage Recommenderを使用しています。配信対象者の選定ではユーザー毎にTwo-Stage Recommenderのスコアの最大値を集計し、集計値が高い上位Nユーザーを配信対象とします。ショップ推薦ではTwo-Stage Recommenderのスコアが大きい順にショップを掲載します。アイテム推薦ではショップ推薦を考慮したルールベースのロジックを使用しますが、本記事では取り上げません。 ZOZOTOWNに出店しているショップは複数のブランドを取り扱うことがあるため、同じショップ内でもブランドによってユーザーの興味は異なるという仮説をモデルに反映します。以上を踏まえて、以下では本プロジェクトのTwo-Stage Recommenderの詳細について説明します。 Candidate Generation Candidate Generationステージではユーザー、クーポン対象ショップ、取り扱いブランド、日付の組み合わせを取得します。 以下の3つの軸を考慮してCandidate Generationを行いました。 過去の実績 人気ブランド 興味を持っているブランドの類似ブランド 1. 過去の実績 リターゲティング施策はコンバージョンにつながりやすいことが分かっているため、ユーザーのお気に入りブランドと過去N日間で閲覧したアイテムのブランドを推薦候補とします。 2. 人気ブランド トレンドを考慮した推薦をするため、性年代別の人気ブランドの上位N件を取得しユーザーに紐付けます。 3. 興味を持っているブランドの類似ブランド 多様性のある推薦をするため、ユーザーが閲覧したブランドを元に以下の手順で類似ブランドを取得します。 ユーザーのブランドに対する行動を学習データとしたMatrix Factorizationモデルを学習 学習の中間生成物であるブランドのEmbeddingを取得 Embeddingから全ブランド同士のコサイン類似度を計算 ライブラリは LightFM を使用しています。指定可能なLogistic、BPR、WARPのロスを変えた時の精度を比較し、最終的には次項で説明する評価指標が最も良かった WARPロス を使用しました。 評価方法 Candidate Generationの評価は後述する 全レコード数に対するRerankingモデルの正例の割合 を使用します。使用データの日数や人気ブランドの取得件数などのパラメータは正例の割合と推薦に必要な最低ユーザー数を考慮して決定します。 Reranking RerankingステージではCandidate Generationステージで取得した組み合わせを使用して、ユーザーが興味を持つ順番でクーポン対象ショップ、取り扱いブランドを並び替えます。 学習データの作成 ユーザーのブランドへの興味を数値化しランク付けをしたいのですが、興味を直接数値化することは困難です。そこで、ユーザーのブランドへの興味を アイテム閲覧データを用いて表現 します。閲覧アイテムに紐づくブランドを取得し、アイテム閲覧データをブランド閲覧データに変換します。これをCandidate Generationで取得したデータに紐付けることでZOZOTOWN会員が次の日にあるブランドを閲覧することを正例とした学習データを作成します。ブランド推薦をメール配信以外でも展開したいという要件があったため、クーポン施策に関わるユーザー行動を正例とするのではなくZOZOTOWN全体のユーザー行動を正例としています。 アンダーサンプリング 前述の通りに学習データを作成するとCandidate Generationで取得したレコードでは正例の割合が1〜2%程度となります。 学習時間の短縮と不均衡データへの対処を目的 とし、学習と検証データに負例のアンダーサンプリングを行いました。後述するオフライン評価指標が低下しないことを確認し、最終的に正例:負例 = 1:1を採用します。 特徴量エンジニアリング 特徴量としてユーザーとアイテムのマスタデータに加えてユーザーの行動データを用います。行動データとはZOZOTOWN上での注文、検索、カートイン、お気に入り、閲覧などのデータです。特徴量は全てBigQueryを使用して作成します。 ユーザー軸 デモグラフィック属性 価格に関わる指標 ブランド軸 ユーザー行動を集計した指標 価格に関わる指標 クーポン付与ポイント Candidate Generationで使用したモデルのEmbeddingを主成分分析した第N主成分 ユーザーとブランド軸 ユーザー行動を集計した指標 価格に関わる指標 ブランドロイヤルティを表す指標 特に特徴重要度が高かった特徴量は以下です。 価格に関わる特徴量 Embeddingを主成分分析した特徴量 学習 推薦モデルの学習には LightGBM を使用し、スコアが0〜1の範囲に収まる様に二値分類をします。これはスコアが閾値以上の時に配信対象者とすることで将来的にはクーポンに興味があるユーザーのみに配信を送りたいという要件のためです。 バリデーション Time Series Hold outによって検証データを作成します。最新のN日間のデータを取得し、約20〜30%が検証データとなります。アーリーストッピングを設定し検証データに対するロスが最善の時点で学習を終了します。 推論 Dataflow を使用して並列推論を行います。実験中に推論の実行時間に課題感を感じていましたが、導入後はテストデータと推薦対象データを対象に100GB超のデータに対して約20分で推論が完了します。 3. 機械学習モデルの評価と体制整備 上記の機械学習モデルを評価するためにオフライン評価の体制を整備します。具体的にはオフライン評価と実験管理の導入について紹介します。 オフライン評価 オフライン評価は定量評価と定性評価の2軸で評価をし、特に定量評価ではメトリクスの評価と過去の施策を活用したオフライン評価をします。 メトリクスのオフライン評価 テストデータに対するモデルメトリクスが向上していることを確認すると同時にvalidation lossやユーザーユニーク数などのメトリクスが異常ではないかを確認します。具体的には以下のメトリクスを確認します。 テストデータに対するprecision@k テストデータに対するrecall@k train loss validation loss 学習/推薦ユーザーのユニーク数 学習/推薦ブランドのユニーク数 過去の施策を活用したオフライン評価 過去の施策によって変更前、変更後のモデルに関わらないメール配信者やメール経由サイト流入者のデータが手に入っていました。このデータを活用することで以下の指標を擬似的に再現し、変更前のモデルと変更後のモデルを評価します。 N万位以内のユーザーの配信流入率 メール経由流入者がN万位以内である割合 定性評価 定量評価に加えて、定性評価をチームメンバーに行なってもらいます。具体的には各自のZOZOTOWNアカウントを用いて推薦結果に対するフィードバックをしてもらいます。以下の様なフィードバックをもらい、モデル改善の方向性を決定する際の参考にしました。 変更前の推薦は人気ブランドに偏っているが変更後の推薦はブランドのバリエーションが増えていて良い ライトユーザーは変更前の推薦を好むが、ミドル/ヘビーユーザーは変更後の推薦を好みそう 変更後の推薦は未知のブランドが多いが、その中に興味を持っているブランドがある時はかなり嬉しい 実験管理の導入 変更前の機械学習モデルは評価指標を保存する場所がなく他のモデルとの比較ができませんでした。対策として、オフライン評価指標の保存と参照を簡易化するために実験管理ツールである MLflow を導入します。パラメータはパイプラインの実行時に保存され、メトリクスは学習や評価の際にMLflowへ保存します。保存したデータは以下の画像のように確認できます。メトリクスにはダミーの値を入れています。 ▼ パラメーター ▼ メトリクス ▼ メトリクスの推移 結果 運用業務の簡易化 今回のシステム移行によってVertex AI Pipelinesを利用した他システムと同様の方法で 簡単に運用業務が行える ようになりました。また、Cloud Composerを使用した機械学習モデルの運用が全社的に終了し Vertex AI Pipelinesへと完全移行 できました。 KPIの改善 配信対象者の変更とショップ推薦の変更に対して実施したA/Bテストではcontrolと比較して KPIが以下の様に改善 しました。絶対数を表す指標は比率を、割合を表す指標は差を示しています。 KPI 配信対象者の変更 ショップ推薦変更 売上 124.69 % 104.02 % 1配信あたりの売上 123.81 % 104.40 % 注文者数 120.08 % 103.76 % 配信流入率 + 0.12 pt + 0.01 pt 配信注文率 + 0.004 pt + 0.001 pt リリースサイクルの改善 MLflowの導入によってモデルの精度比較が簡単にできるようになったため、 モデル改善のサイクルが早まりました 。また、モデルに関わらない変更をリリースする際も精度が低下していないことを確認できるため リリースの安全性が向上しました 。 今後の展望 バイアスの考慮 現在はアイテムを閲覧したブランドを学習データとしてショップ推薦をしています。このデータは推薦経由であっても記録されるため推薦モデルが自己の学習データに影響を与えることになります。推薦したブランドは再び推薦されやすくなるというバイアスを生み出しているため、このバイアスを考慮した設計をする必要があります。 評価体制の改善 評価指標の探求 現在の評価指標は推薦の精度に着目したものが多いです。一方で推薦には多様性、新規性、意外性等の精度以外にも考慮すべき指標が提案されています。弊社でも長期的な推薦の価値を捉えた指標を明らかにし、モニタリングする必要があると考えています。 定性評価の体制整備 今回のプロジェクトでは定量評価の体制を整備しましたが、定性評価がモデル改善の方針の参考になることが多かったため、定性評価の方法も標準化することでモデル改善を加速させたいと考えています。 おわりに 本記事ではクーポン推薦モデルとシステムの改善について紹介しました。現在ZOZOでは 「ワクワクできる『似合う』を届ける」 という経営戦略のもとパーソナライズを強化している最中です。推薦を含め機械学習に関わるエンジニアを募集しているので、ご興味がある方は是非以下のリンクからご応募ください。 hrmos.co hrmos.co
アバター