TECH PLAY

株式会社ZOZO

株式会社ZOZO の技術ブログ

974

はじめに こんにちは、データシステム部MLOpsブロックの 木村 です。MLOpsブロックでは、継続的にGoogle Cloudのコスト削減に取り組んでいます。その一環として、夜間や休日といった利用されていない時間帯にも稼働し続けることで発生していた、開発・検証・テスト環境の余分なコストに着目しました。 この課題を解決するために、MLOpsブロックでは Kubernetes Event-driven Autoscaling (以下KEDA)を導入しました。KEDAは、Kubernetes環境でイベントドリブンによるオートスケールを実現するオープンソースのツールです。KEDAにより利用されていない時間帯のPodを停止させ余分なコストを削減しました。 本記事ではKEDAを導入したモチベーションや効果、導入する際に直面した課題や、加えて事故なく本番環境へ適用するために工夫した点をご紹介します。KEDAの導入でさらなるコスト削減を目指したい方々の助けになれば幸いです。 目次 はじめに 目次 背景 KEDAを導入した経緯 1. Podの台数を0台にできない 2. スケジュールベースのオートスケールができない 解決策 KEDAの導入 KEDAの概要 KEDAのアーキテクチャ KEDAのコンポーネント KEDAの導入によるコスト削減の試算 KEDAの導入方法 インストール方法 マニフェストを直接適用する方法 kustomizeを使用する方法 Podのリソース設定と冗長化 Podのリソース設定 冗長化の設定 スケジュールベースのトリガー設定 ゼロスケール時に注意すべきPDBの設定 CPU使用率に応じたトリガー設定 スケジュール・CPU使用率を組み合わせたトリガー設定 KEDA導入時の注意点 既存のHPA無効化時のPod調整と導入タイミング KEDAの導入効果・メリット 導入によるコスト削減 削減額の算出 年間のコスト削減効果 Compute Engine のコスト削減 Datadog のコスト削減 KEDA導入後の課題と解決策 外形監視の課題と解決策 Argo RolloutsでKEDAを適用するための設定変更 今後の展望 まとめ 背景 前提として、MLOpsチームではパブリッククラウドにGoogle Cloudを使用し、 Google Kubernetes Engine (以下、GKE)上にサービスを構築しています。また、1Pod1Nodeの構成をとっているため、Podの数に対応するNodeが存在します。GKEのNodeはCompute EngineのVM上で稼働し、稼働時間と台数・マシンタイプに応じてコストが発生します。加えて、バッチ処理のような単発実行のPodとは異なり、API Podは常時稼働が必要です。保守対象のAPIも52個と多く、GKEのNodeを実行しているCompute EngineのコストがGoogle Cloud全体のコストの中で大きな割合を占めていることが課題となっていました。 この課題を解決するために、MLOpsブロックではPod台数を見直すことでCompute Engineのコストを削減できないか検討しました。 本番環境ではユーザー影響を出さないように、可用性・パフォーマンスの観点から十分なPodの台数を維持する必要があります。そのため、Podの台数を減らす際は慎重に対応する必要があります。一方、開発・検証・テスト環境では、開発者が開発・検証・テスト目的でのみ利用されるため、営業時間外は稼働する必要がありません。 そこで、開発・検証・テスト環境のPodを営業時間外に停止した場合、どの程度のコスト削減が可能か試算したところ、Compute Engineのコストを約30%削減できると見込まれました。 これらを踏まえ、開発・検証・テスト環境では営業日以外にPodを停止し、本番環境では時間帯ごとのトラフィックを分析した上でPodをスケールインすることで、余分なVMコストを削減する方針に決定しました。 KEDAを導入した経緯 これまでMLOpsブロックではKubernetes標準の Horizontal Pod Autoscaler (以下、HPA)を使用して、Podのオートスケールを行っていました。しかし、Compute Engineのコスト削減を進める中で、標準のHPAだけでは対応が難しい課題に直面しました。 標準のHPAでは対応できなかった課題は、以下の2点です。 1. Podの台数を0台にできない 標準のHPAでは、最小でも1台までしかスケールインできないため、深夜や週末などの時間帯にもPodが維持され、GKEで余分なコストが発生していました。 2. スケジュールベースのオートスケールができない 標準のHPAではトラフィックベースのスケールが可能です。しかし、負荷の増加を検知してからPodのスケジューリングや起動が完了するまで時間がかかるため、急激な負荷変動には対応できず、スケールアウトが負荷の変動に間に合わないことがあります。そのため、本番環境ではトラフィックの多い週末を基準にPod数を維持する必要があり、ピーク時と比較して負荷の少ない時間帯でも過剰なリソースが維持され、余分なコストが発生していました。 解決策 標準のHPAの課題を解決するために、MLOpsブロックではKEDAを導入しました。KEDAを活用することで柔軟なスケール要件に対応できます。具体的には、「開発・検証・テスト環境のPodを夜間や週末の業務時間外に完全停止すること」や「本番環境のPodを負荷の少ない時間帯に合わせてスケールインすること」を実現できます。また事前にスケジュールを設定できる点はコスト面以外にも利点があります。従来の運用では、負荷試験時に検証環境のPod数を本番環境と同じ台数まで増加させたい場合など、一時的にPodをスケールアウトし、作業完了後にスケールインしていました。このとき完了後のスケールインを忘れるミスが度々ありました。スケールアウト・スケールインを事前にスケジュール設定することで、作業漏れの防止にもつながります。 KEDAの導入 KEDAの概要 KEDAの主要な機能と構成コンポーネントをご紹介します。 KEDAは標準のHPAの仕組みを活用しながら、次の拡張機能を提供し、柔軟なスケーリングを可能にします。 ゼロスケール Podを完全に停止する1→0や0→1のスケールが可能。 イベントドリブンなオートスケール CPUやメモリに加えて、メッセージキューの長さやAPIリクエスト数などの外部メトリクスをトリガーにスケーリングを制御できる。 スケジュールベースのオートスケール KEDAのCustom Resourceである ScaledObject にあらかじめスケジュールを設定することで、特定の時間帯や曜日にPod数をオートスケールできる。 KEDAのアーキテクチャ 次図はKEDAのアーキテクチャを示す概念図です。 KEDAの公式ドキュメント より引用します。 KEDAはKubernetes標準のHPAと連携し、Podの台数を0⇄1にスケールできる拡張機能を提供します。 ScaledObject というカスタムリソースに、スケール対象やトリガー(CPU使用率、スケジュールなど)を設定できます。KEDAはその設定に基づき外部イベントを定期的に監視し、必要に応じてHPAの作成とスケールを行います。 HPAが1⇄nのスケールを担い、KEDAが0⇄1のスケールを補完することで、より柔軟なオートスケールが可能になります。 KEDAのコンポーネント KEDAには3つの主要なコンポーネントが存在します。 実装コンポーネント 機能 役割 keda-operator KubernetesのDeploymentを0⇄1にスケールし、イベントがないときはPodをスケールインする Agent keda-operator-metrics-apiserver 外部メトリクスなどのイベントをHPAに提供し、スケールアウトを支援する Metrics keda-admission ScaledObject などのリソース変更が適用される前に、自動的にバリデーションを行い、誤った設定の適用を防止する Admission Webhooks KEDAの導入によるコスト削減の試算 KEDAを利用するとゼロスケールとスケジュールベースのオートスケールにより、Compute Engineのコスト削減を実現できます。一方でKEDAのコンポーネントを稼働するための追加コストも発生します。小規模なクラスタやPod停止時間が短い場合、KEDAの追加コストがPod停止によるコスト削減を上回るおそれがあります。そのため、導入前に期待されるコスト削減効果を見積もることが重要です。 KEDA導入による週あたりのCompute Engineの削減コストは、次の式で見積もりました。 KEDA導入によるCompute Engineの削減コスト = Compute Engineのコスト ×(週あたりの停止時間 / 168h)- KEDAの運用コスト MLOpsブロックでは、保守している52個のAPIのうち、開発・検証・テスト環境にある38個のAPIに対応するPod(38台)を対象としました。 これらのPodは、平日夜間(1日8時間 × 平日5日間)と、土日終日(1日24時間 × 休日2日間)の稼働が不要なため、週あたり合計88時間分を停止対象としています。38台のPodを対象に停止することで、週あたり3,344Pod時間(38台 × 88時間)の削減効果が見込めます。 一方で、KEDAの運用には、開発・検証・テストでPodが15台必要です。そのため、週あたりのKEDAの運用コストは2,520Pod時間(15台 × 24時間 × 7日)となります。 以上より週あたり824Pod時間の削減となります。 加えてAPIが稼働するNodeは、KEDAが稼働するNodeよりも高スペックなマシンタイプを要求するNodeが多くあります。そのため実際の削減コストはPod時間以上の効果が見込めます。 さらに、MLOpsブロックではモニタリングに Datadog を使用しています。Datadog AgentはDaemonSetとして各Node上で常に動作しているため、起動中のNodeごとにモニタリングコストが発生します。KEDAによって不要な時間帯のPodが停止されることで、Datadogのモニタリング対象リソースも削減され、Datadogのコスト削減にもつながります。 これらの結果から、KEDAの導入はCompute EngineおよびDatadogの両面で、十分なコスト削減効果が見込めると判断しました。 KEDAの導入方法 ここからは具体的なKEDAの導入方法を説明します。 次の項目について実際のコードを交えて解説します。 インストール方法 Podのリソース設定と冗長化 ScaledObject の設定 スケジュールベースのトリガー設定 CPUトリガーの設定 スケジュール・CPU使用率を組み合わせたトリガー設定 インストール方法 KEDAを使用するには、KEDAのCustom OperatorをKubernetesクラスタに追加する必要があります。 インストール方法については、 公式ドキュメント を参考にしました。また、KEDAのバージョンとKubernetesの互換性については、 KEDA公式ドキュメント(GitHub) を事前に参照してください。MLOpsブロックではKubernetes v1.30 系を使用しており、KEDA v2.16.0 との互換性を確認した上で導入しました。 KEDAのインストール方法には、マニフェストを直接適用する方法と kustomize を利用して環境ごとに管理する方法の2つがあります。 マニフェストを直接適用する方法 次のコマンドでKEDA v2.16.0 をKubernetesクラスタにインストールできます。この操作によりKEDAのCustom Resource Definition(CRD)やCustom Controller、関連リソースが作成され keda 名前空間にデプロイされます。また ScaledJob のCRDはサイズが大きく、クライアント側で処理しきれないため、以下のように --server-side を指定してKubernetes APIサーバー側で適用する必要があります。 > kubectl apply --server-side -f https://github.com/kedacore/keda/releases/download/v2. 16 . 0 /keda-2. 16 . 0 .yaml namespace/keda serverside-applied customresourcedefinition.apiextensions.k8s.io/cloudeventsources.eventing.keda.sh serverside-applied customresourcedefinition.apiextensions.k8s.io/clustercloudeventsources.eventing.keda.sh serverside-applied customresourcedefinition.apiextensions.k8s.io/clustertriggerauthentications.keda.sh serverside-applied customresourcedefinition.apiextensions.k8s.io/scaledjobs.keda.sh serverside-applied customresourcedefinition.apiextensions.k8s.io/scaledobjects.keda.sh serverside-applied customresourcedefinition.apiextensions.k8s.io/triggerauthentications.keda.sh serverside-applied serviceaccount/keda-operator serverside-applied role.rbac.authorization.k8s.io/keda-operator serverside-applied clusterrole.rbac.authorization.k8s.io/keda-external-metrics-reader serverside-applied clusterrole.rbac.authorization.k8s.io/keda-operator serverside-applied rolebinding.rbac.authorization.k8s.io/keda-operator serverside-applied rolebinding.rbac.authorization.k8s.io/keda-auth-reader serverside-applied clusterrolebinding.rbac.authorization.k8s.io/keda-hpa-controller-external-metrics serverside-applied clusterrolebinding.rbac.authorization.k8s.io/keda-operator serverside-applied clusterrolebinding.rbac.authorization.k8s.io/keda-system-auth-delegator serverside-applied service/keda-admission-webhooks serverside-applied service/keda-metrics-apiserver serverside-applied service/keda-operator serverside-applied deployment.apps/keda-admission serverside-applied deployment.apps/keda-metrics-apiserver serverside-applied deployment.apps/keda-operator serverside-applied apiservice.apiregistration.k8s.io/v1beta1.external.metrics.k8s.io serverside-applied validatingwebhookconfiguration.admissionregistration.k8s.io/keda-admission serverside-applied kustomize を使用する方法 MLOpsチームでは、KEDAのマニフェストに対して環境ごとのパッチを適用できるようにするため、 kustomize を活用し、KEDAをインストールしました。 kustomize は、ベースとなるマニフェストと環境ごとの差分を管理するツールです。環境差分のパッチをベースマニフェストに適用することで、各環境に対応したマニフェストを生成・管理します。 kustomization.yaml に以下のマニュフェストを記載し、 kustomize build で生成したマニフェストを kubectl apply することでKEDAをインストールできます。 apiVersion : kustomize.config.k8s.io/v1beta1 kind : Kustomization namespace : keda resources : - https://github.com/kedacore/keda/releases/download/v2.16.0/keda-2.16.0.yaml Podのリソース設定と冗長化 KEDAの各コンポーネントに対して、コンテナに割り当てるCPU・メモリの配分と水平スケールによる冗長化の構成値を適切に設定する必要があります。 MLOpsブロックでは、 KEDAの公式ドキュメント に記載されている設定値を基準にしつつ、本番環境に適したリソース配分と冗長化構成を採用しました。 Podのリソース設定 CPU・メモリの使用率には余裕があるためデフォルトの設定値を適用しました。CPU・メモリの使用状況を確認し、必要に応じてResource RequestsとResource Limitsを調整してください。 pod CPU (Limits/Request) Memory (Limits/Request) keda-admission 1 core / 1 core 1000Mi / 1000Mi keda-metrics-apiserver 1 core / 1 core 1000Mi / 1000Mi keda-operator 1 core / 1 core 1000Mi / 1000Mi 冗長化の設定 一方、KEDAの冗長化設定は開発・検証・テストの各環境、および本番環境で異なる値を設定しました。 KEDAの公式ドキュメント では、高可用性を考慮した構成として keda-metrics-apiserver は1台、 keda-operator は2台が推奨されています。開発・検証・テスト環境はこの値を使用し、本番環境ではさらなる可用性向上のため、どちらも3台構成を採用しました。 pod 開発環境 検証環境 テスト環境 本番環境 keda-admission 1台構成 1台構成 keda-metrics-apiserver 2台、AZ分散 3台構成、AZ分散 keda-operator 2台、AZ分散 3台構成、AZ分散 以下は開発・検証・テストの各環境におけるKEDAコンポーネントのDeployment構成です。 apiVersion : apps/v1 kind : Deployment metadata : name : keda-admission namespace : keda spec : replicas : 1 template : spec : containers : - name : keda-admission-webhooks resources : requests : cpu : 1000m memory : 1Gi limits : cpu : 1000m memory : 1Gi affinity : nodeAffinity : requiredDuringSchedulingIgnoredDuringExecution : nodeSelectorTerms : - matchExpressions : - key : cloud.google.com/gke-nodepool operator : In values : - <nodepool名> tolerations : - key : "dedicated" operator : "Equal" value : <nodepool名> effect : "NoSchedule" apiVersion : apps/v1 kind : Deployment metadata : name : keda-metrics-apiserver namespace : keda spec : replicas : 2 template : spec : containers : - name : keda-metrics-apiserver resources : requests : cpu : 1000m memory : 1Gi limits : cpu : 1000m memory : 1Gi affinity : nodeAffinity : requiredDuringSchedulingIgnoredDuringExecution : nodeSelectorTerms : - matchExpressions : - key : cloud.google.com/gke-nodepool operator : In values : - <nodepool名> tolerations : - key : "dedicated" operator : "Equal" value : <nodepool名> effect : "NoSchedule" apiVersion : apps/v1 kind : Deployment metadata : name : keda-operator namespace : keda spec : replicas : 2 template : spec : containers : - name : keda-operator resources : requests : cpu : 1000m memory : 1Gi limits : cpu : 1000m memory : 1Gi affinity : nodeAffinity : requiredDuringSchedulingIgnoredDuringExecution : nodeSelectorTerms : - matchExpressions : - key : cloud.google.com/gke-nodepool operator : In values : - <nodepool名> tolerations : - key : "dedicated" operator : "Equal" value : <nodepool名> effect : "NoSchedule" 本番環境では、上記のマニフェストに対して kustomize を使用してパッチを当てることで環境ごとに異なる設定値を上書きして適用しています。 次の記述は kustomize を使用して本番環境に適用するパッチ内容です。 apiVersion : apps/v1 kind : Deployment metadata : name : keda-metrics-apiserver namespace : keda spec : replicas : 3 apiVersion : apps/v1 kind : Deployment metadata : name : keda-operator namespace : keda spec : replicas : 3 上記マニフェストを適用することで keda-metrics-apiserver と keda-operator を3台で構成でき、冗長化できます。 スケジュールベースのトリガー設定 ScaledObject に次のマニフェストを適用すると、特定の時間にPodをオートスケールできます。この節では主要な設定のみを抜粋しています。詳細な設定方法は、 公式ドキュメント を参照してください。 spec : scaleTargetRef : name : <deployment-name> # スケール対象のDeployment名 minReplicaCount : 0 # 最小Replica数を0に設定(ゼロスケールを有効化) maxReplicaCount : 3 # 最大Replica数を3に設定 triggers : - type : cron metadata : timezone : Asia/Tokyo # タイムゾーンを指定 start : 0 7 * * 1-5 # 平日の午前7時にスケーリングを開始 end : 0 23 * * 1-5 # 平日の午後23時にスケーリングを終了 desiredReplicas : "1" # スケールアウト時のPod数 ゼロスケール時に注意すべきPDBの設定 KEDAでPodをゼロスケールさせる場合、PodDisruptionBudget(PDB)の設定を見直す必要があります。PDBとは、Kubernetesにおいて常に最低限稼働させておくべきPod数を保証するための仕組みです。 PDBの minAvailable に1以上が設定されていると、「最低でも1つのPodを稼働させる」という制約が働くため、Podのゼロスケールがブロックされます。 そのため、開発・検証・テスト環境などでゼロスケールを実現したい場合は、PDBの minAvailable を0に設定しておく必要があります。 CPU使用率に応じたトリガー設定 HPAと同様にCPU使用率をトリガーとするスケールも可能です。 負荷に応じてPodをスケールするには、次のマニフェストのようにCPUトリガーを設定します。この設定により、CPU使用率が50%を超えるとPodが自動的にスケールアウトします。 triggers : - type : cpu metricType : Utilization metadata : value : "50" # CPU使用率が50%を超えた場合にスケールアウト スケジュール・CPU使用率を組み合わせたトリガー設定 CronトリガーとCPUトリガーを組み合わせた最終的な ScaledObject の構成は次のマニフェストになります。 この構成により、次のオートスケールを実現できます。 夜間(23:00〜07:00) Podをゼロスケールし、余分なリソース消費を抑える。 日中(07:00〜23:00) 最低1つのPodを維持し、CPU使用率が50%以上になるとスケールアウトして負荷に対応する。 apiVersion : keda.sh/v1alpha1 kind : ScaledObject metadata : name : <scaledobject-name> # ScaledObjectの名前を指定 namespace : keda # 対象のNamespaceを指定 spec : scaleTargetRef : name : <deployment-name> # スケール対象のDeployment名 minReplicaCount : 0 # 最小Replica数を0に設定(ゼロスケールを有効化) maxReplicaCount : 3 # 最大Replica数を3に設定 triggers : - type : cpu metricType : Utilization metadata : value : "50" # CPU使用率が50%を超えた場合にスケールアウト - type : cron metadata : timezone : Asia/Tokyo # タイムゾーンを指定 start : 0 7 * * 1-5 # 平日の午前7時にスケーリングを開始 end : 0 23 * * 1-5 # 平日の午後23時にスケーリングを終了 desiredReplicas : "1" # スケールアウト時のPod数 KEDA導入時の注意点 KEDAを導入する際、MLOpsブロックが特に注意したポイントをご紹介します。 既存のHPA無効化時のPod調整と導入タイミング KEDAを導入する際、すでにHPAが適用されている環境では競合が発生します。これはKEDAが内部的にHPAを生成・管理する仕組みを持っているためです。そのため、KEDAを導入する前に既存のHPAを無効化する必要があります。 ただし、HPAを無効化するとKEDAを導入するまでの間Pod数が固定され負荷変動に対応できなくなります。特に本番環境では可用性・パフォーマンスの観点から慎重な対応が求められます。そのためHPAを無効化する前に minReplicas を適切に設定しスケールアウトしておくことで、急激なリクエスト増加に備える必要があります。 これらの点を考慮し、HPA無効化前に、 minReplicas の見直しを行いました。トラフィックのピーク時にも対応できるように、現在の構成で許容可能なリクエスト数に対して、直近1週間の最大リクエスト数が上回るか接近している場合に minReplicas を増加させました。 さらに、移行時のエラーを最小限に抑えるため、HPAの無効化からKEDAの適用までの一連の作業を、トラフィックが少ない午前中に適用を実施しました。 KEDAの導入効果・メリット 導入によるコスト削減 MLOpsブロックでは開発・検証・テスト環境において合計で38個のAPIに対してKEDAを活用し、夜間および土日終日にPod停止を実施しました。この施策によりCompute Engineのコストを大幅に削減できました。 削減額の算出 以下の算出額はゼロスケールによるCompute Engineの削減分のみを対象としており、モニタリングツールのコスト削減や本番環境のリソース最適化は含まれていません。 KEDA導入前後の比較では、平日のCompute Engineコストは約27%削減されました。また、休日(土日)においては約45%の削減効果が見られました。 1 年間のコスト削減効果 Compute Engine のコスト削減 区分 削減率 対象日数(年間) 平日 約27% 261日 休日(土日) 約45% 104日 KEDAの導入により0台へのスケールインができるようになりました。これにより稼働していない時間帯にかかっていたCompute Engineのコストを大幅に削減できました。 Datadog のコスト削減 モニタリング対象リソースの削減により、Datadogのコストも約40%削減される効果がありました。 KEDA導入後の課題と解決策 KEDA導入後にいくつかの課題が発生しました。これらの課題と解決策を紹介します。 外形監視の課題と解決策 MLOpsチームでは、サービスの可用性を担保するため、外部から定期的にリクエストしてAPIの応答状況を確認する外形監視を導入しています。KEDAによってPod数を0にスケールダウンしている間に外形監視を継続するとリクエストが失敗し、不要なアラートが発生します。 この問題を回避するために、APIの外形監視を ScaledObject のトリガーと同じスケジュールで停止するように設定していました。しかし、Pod数が0台の状態から起動する際、Podのスケジューリングやコンテナの立ち上げに時間を要するため外形監視のリクエストに間に合いませんでした。例えば、7:00に外形監視を開始する設定の場合、同じタイミングでAPIをスケールアウトしても、コンテナがReady状態になりAPIがレスポンスを返せるのは7:10頃になります。その結果、外形監視がPodの起動前に開始され、アラートの誤報が発生しました。 この問題を解決するため、APIごとに異なる起動時間を考慮し、外形監視が開始されるまでに確実にPodがReadyになるように ScaledObject の設定をより早い時間に調整しました。これによりPodの起動が完了してから外形監視を行えるようになり、不要なアラートの発生を防ぐことができました。 外形監視に加え、自チームが管理する以外のサービスから送信されるリクエストも考慮する必要がありました。KEDAの導入前は営業時間外にはAPIを使用しないため、Podを停止しても問題ないと想定していました。しかし、実際には自チームが管理する以外の他のAPIからのリクエストを見落としておりPod停止中にリクエストがタイムアウトすることでアラートを発生させてしまいました。 これを解決するために、リクエストが発生する時間帯に合わせて、Podの停止および起動のタイミングを調整しました。さらに、他チームのAPIリクエストがこの時間帯に重ならないよう、リクエストの送信タイミングも調整してもらうことで対応しました。 Argo RolloutsでKEDAを適用するための設定変更 MLOpsブロックでは、Kubernetes上でプログレッシブデリバリーを実現するために、カナリアリリースをサポートする Argo Rollouts を使用しています。Argo Rolloutsについては、計測SREチームの記事でも触れられていますのでぜひこちらもご参照ください。 techblog.zozo.com 通常、KEDAでは Deployment をスケール対象とする場合は scaleTargetRef に name のみを指定すればよく、 apiVersion や kind を省略できます。一方で、 Rollout のようなカスタムリソースを対象とするには scaleTargetRef に apiVersion と kind を明示的に指定する必要があります。 そのため、Argo RolloutsのCustomResourceである、 Rollout リソースを適用するには、スケール対象の指定方法を変更する必要がありました。 設定例は以下の通りです。 spec : scaleTargetRef : apiVersion : argoproj.io/v1alpha1 kind : Rollout name : <rollout-name> # スケール対象のRollout名 今後の展望 今回は開発・検証・テスト環境を対象にKEDAの導入による夜間・休日のPod数の削減を実施し、コスト削減しました。一方で本番環境では標準のHPAからKEDAの ScaledObject に置き換えたのみで、スケジュールベースでのオートスケールによるコスト削減は未着手です。 現在の本番環境では時間帯ごとのトラフィックに応じたPod台数調整ができていないため、余剰なリソースが残っています。具体的には平日朝方などのトラフィックが少ない時間帯でも、必要以上のPod台数が維持されている状況です。今後は時間帯ごとのトラフィックを分析し、実際の負荷に応じたスケーリングルールを適用することで余分なリソースの削減と最適なスケーリングを実現していきます。 まとめ 最後までお読みいただきありがとうございました。本記事ではKEDAを導入した目的や効果に加え、導入に際しての課題や安全に本番環境へ適用するための工夫した点をご紹介しました。KEDAの導入により、MLOpsブロックではGKEにおける大幅なコスト削減に成功しました。またKEDAのスケジュールベースのオートスケールにより運用負荷の低減にも繋がりました。本記事が皆様のお役に立てば幸いです。 最後になりますが、ZOZOでは一緒にサービスを作り上げてくれる方を募集中です。MLOpsブロックでも絶賛採用を行っているため、ご興味ある方は以下のリンクからぜひご応募ください。 hrmos.co 本試算はゼロスケール対象のdev/stg/qa環境のみを分母として計算しています ↩
アバター
はじめに こんにちは! WEARバックエンド部バックエンドブロックの小島( @KojimaNaoyuki )です。普段は弊社サービスであるWEARのバックエンド開発・保守を担当しています。 WEARのバックエンドはRubyで動作しており、Ruby 3.3.6にアップデートしたことを機にYJITを有効化しました。本記事ではWEARにYJITを導入した際の効果とその考察をご紹介します。 目次 はじめに 目次 YJITとは パフォーマンス計測条件 事前準備 本番環境の計測結果 認可サーバー レスポンスタイム(99th percentile) メモリ使用量 リソースサーバー レスポンスタイム(99th percentile) メモリ使用量 計測結果についての考察 リソースサーバーのメモリ使用量の増加が大きいことについて さらにYJITを効果的に使うには まとめ 参考文献 お知らせ YJITとは YJITはJITコンパイラの一種で、Rubyの処理速度の向上を図るものです。多くの企業でパフォーマンスの向上が報告されています。一方でマシンコードやそのメタデータをメモリに保管するため、メモリ使用量の増加には注意が必要です。詳しくは YJIT README をご覧ください。 パフォーマンス計測条件 Ruby 3.3.6 YJIT無効化状態とRuby 3.3.6 YJIT有効化状態でAPIのパフォーマンス計測を実施してそれらを比較しました。 --yjit-exec-mem-size などのパフォーマンスに影響するオプションはデフォルト設定のまま実施しました。 WEARを構成するシステムの中でもRailsで構築している認可サーバーとリソースサーバーが存在しており、今回はその2つで計測しました。また、それぞれの規模を示すため、コード行数の概算を以下に示します。 認可サーバー: 1,000 ~ 5,000行程度 リソースサーバー: 300,000行程度 事前準備 事前準備として、本番環境へ適用する前にステージング環境でYJITを有効化し、パフォーマンス計測を実施しました。 結果、レスポンスタイムが改善し、メモリ使用量なども過度に悪化することはありませんでした。しかし、YJITは頻繁に利用されるコードをコンパイルしてメモリに保存するため、本番環境に比べて極端にリクエスト数が少ないステージング環境では正確な検証ができていませんでした。 実際に、リソースサーバーにおいてステージング環境ではメモリ使用量が13%増加に留まっていましたが、本番環境では31%もの増加(詳しい結果は後述)をしてしまいました。これを回避するためには、負荷試験ツールなどを利用して本番同様に様々なAPIをリクエストすることでより正確な検証が可能と思います。 本番環境の計測結果 実際に本番環境でYJITを有効化した際の効果をご紹介します。 一週間の平均値をYJIT無効化状態とYJIT有効化状態に分け、それぞれレスポンスタイムとメモリ使用量を計測しました。 結果は、認可サーバーにおいて全体のレスポンスタイムは約11%改善し、メモリ使用量は約20%増加しました。リソースサーバーにおいて全体のレスポンスタイムは約19%改善し、メモリ使用量は約31%増加しました。 以降に計測結果の詳細を記載します。 認可サーバー レスポンスタイム(99th percentile) YJIT無効状態(紫) YJIT有効状態(青) 47ms 42ms 約11%の改善が見られました。 メモリ使用量 YJIT無効状態(紫) YJIT有効状態(青) 541MiB 650MiB メモリ使用量は稼働しているpodsの合計値です。 約20%の増加が見られました。 リソースサーバー レスポンスタイム(99th percentile) YJIT無効状態(紫) YJIT有効状態(青) 549ms 445ms 約19%の改善が見られました。 メモリ使用量 YJIT無効状態(紫) YJIT有効状態(青) 64.3GiB 84.2GiB メモリ使用量は稼働しているpodsの合計値です。 約31%の増加が見られました。 計測結果についての考察 結果は概ね期待通りのものでした。レスポンスタイムの改善が見られ、メモリ使用量は増加しましたが許容値以内の増加となりました。 リソースサーバーに関してはメモリ使用量が31%と大きく上昇してしまったのですが、メモリ使用率は元から余裕があったため、許容値の範囲に収まりました。 以下に今回の結果で気になった点で調査したことなどを掲載します。 リソースサーバーのメモリ使用量の増加が大きいことについて リソースサーバーはYJIT有効化状態では約31%もメモリ使用量が増加しており、認可サーバーの増加率よりも高いことが気になりました。それは、リソースサーバーでは提供しているAPIの数が多く、使用されているRubyコードの種類も多いことが原因と考えられます。 YJITは一定の回数以上呼び出されたコードがYJITのコンパイルに対応していたらコンパイルしメモリに保存します。そのため、サービスで使われているコードの種類が多いほどメモリ使用量が増加すると考えられます。 そこで、YJITがコンパイルして生成したコードサイズを示すcode_region_sizeを確認しました。 上記グラフの通り、認可サーバー(緑)では平均7.7MiBだったのに対してリソースサーバー(赤)では平均42.9MiBとなっていました。code_region_sizeの他にそれと比例してメタデータも生成されメモリに保管されるなど、実際はより多くのメモリを使用していることになります。 このことから、認可サーバーよりもリソースサーバーにおいてメモリ使用量が増加してしまっているのは、使用されているコードの種類が多いことに原因があると考えられます。 さらにYJITを効果的に使うには YJITによるメモリ使用量を抑えるならば、 --yjit-exec-mem-size や --yjit-call-threshold の値調整で可能です。 --yjit-exec-mem-size はYJITが生成するコードサイズの上限を指定できるのでメモリ使用量を抑えることができます。 --yjit-call-threshold はYJITが関数のコンパイルを開始するまでの呼び出し回数を指定できます。大きくすることであまり呼び出されない関数はコンパイルしなくなるためメモリ使用量を抑えられます。詳細は YJIT README Command-Line Options をご覧ください。 しかし、これらの値の調節はYJITの効果が薄れる可能性もあります。そのため、メモリ使用量とパフォーマンスのバランスを考慮して設定する必要があります。 WEARでは現在のメモリ使用量でも問題ないため実施していませんが、今後改善が必要になったら検討しようと思っています。 また、Ruby 3.4にアップデートすることでもさらなるパフォーマンス改善が期待できます。 Ruby 3.4.0 リリース より、「x86-64とarm64の両方のプラットフォームにおいて、ほとんどのベンチマークのパフォーマンスが向上しました」や「メタデータの圧縮と統一的なメモリ使用量制限によりメモリ使用量を削減しました」と記載されています。そのため、Ruby 3.4でもYJITの改善が期待できます。 まとめ 本記事ではWEARでのYJITの効果と考察についてご紹介しました。今回改めて、既存コードの修正が不要で全体のパフォーマンスを改善できるYJITは素晴らしいと感じました。今後のYJITの更なる発展に期待したいと思います。 参考文献 本記事を作成するにあたり、以下の文献を参考にさせていただきました。貴重な情報を公開してくださった著者の皆様に感謝申し上げます。 YJIT README Ruby 3.3 YJITのメモリ管理とRJIT 〜すべてが新しくなった2つのJITを使いこなす YJITの性能を最大限引き出す方法 - k0kubun's blog Ruby 3.4.0 リリース お知らせ ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 hrmos.co
アバター
はじめに はじめまして。2025年4月に株式会社ZOZOへ入社予定の坂元菜摘( @skysky0208 )です。チームの皆さんにはもっちゃんと呼ばれています。 この記事では、約半年間WEARバックエンドチームにて参加した内定者アルバイトについての体験談をお話ししたいと思います。ZOZOに興味がある人はもちろん、内定者アルバイトに興味がある人、また入社に対して不安を抱いている人など、様々な方々の参考になれば幸いです! 目次 はじめに 目次 内定者アルバイトとは 私が内定者アルバイトに参加した目的 内定者アルバイトの概要 WEARバックエンドチームはどんなチーム? 内定者アルバイトの1日 実際に行なったタスク タスクの概要 企画共有と仕様検討 仕様検討MTGへの参加 ロジック検討のためのモック作成 実装方法の選定・検証 複数の実装案の比較検討 本番DBにおけるパフォーマンス検証 実装・リリース 学びと振り返り 1. 質問は質よりスピードを大切にすべし 2. 実務で学べるテストコードの重要性 3. 誰に何を伝えたいのか? という視点での言語化 4. 積極的に発言できなかったことへの反省 まとめ 最後に 内定者アルバイトとは 内定者アルバイトとは内定承諾から入社までの期間にアルバイトの雇用契約を結び、内定先で就業できるものです。 毎月さまざまな部署や職種のアルバイトの募集があり、内定をいただいた職種はもちろん、他の職種にも応募できます。頻度や勤務時間など柔軟に対応可能なため、学業や他のアルバイトとの両立もしやすいのが嬉しいポイントです! 私が内定者アルバイトに参加した目的 私は入社に向けて、以下のような不安や疑問を抱えていました。 業務内容や仕事の進め方のイメージがついておらず、入社後が不安 部署配属にあたり、自身のやりたいことや興味があることを明確にしたい これから一緒に働く方々の雰囲気を知りたい このような不安や疑問を抱えている方は、多いのではないでしょうか?そのとき、内定者アルバイトという制度があることを知り、不安を解消しつつ、初めての実務に挑戦したいと考え応募しました。 内定者アルバイトの概要 私は、2024年8月から入社までの約半年間、WEARのバックエンドチームで内定者アルバイトに参加しました。勤務体系は固定シフト制で、10時から19時までの1日8時間勤務を週に2日のペースで、フルリモートで働かせていただきました。 WEARバックエンドチームはどんなチーム? WEARとは、株式会社ZOZOが運営する日本最大級のファッションコーディネートアプリです。2024年の5月に「WEAR by ZOZO」としてリニューアルされ、新たなファッションジャンル診断機能やコンテンツが追加されました。 corp.zozo.com WEARバックエンドチームは、WEARのバックエンド開発を担当しているチームです。チームはおよそ10名ほどで構成されており、新機能開発や既存機能の改善、リプレイス、障害対応など幅広い業務を行っています。言語は主にRubyを使用しており、コミュニケーションツールとしてSlackやDiscordなどを活用しています(執筆時点)。 内定者アルバイトの1日 ここで、実際の私の1日の流れをご紹介したいと思います。 10:00 - 出勤・レビュータイム 出勤打刻をした後、まずは今日の予定を確認します。その後、自身のPullRequestについたコメントを確認し、他のメンバーのPullRequestレビューをします。WEARバックエンドチームは、PullRequestを提出したら少なくとも2人にレビューをしてもらう文化があります。レビューを受けるだけでなく、先輩方のPullRequestやコードを見ることで新たな知識や視点を学べるため、予め時間を確保して取り組んでいました。 11:00 - 朝会 WEARバックエンドチームでは、毎朝11時に朝会を行っています。朝会の始めには小話があり、近況やハマっていること、週末の出来事などを話し、とても和やかな雰囲気でスタートします。個人的に、一緒に仕事をする皆さんの趣味や日常を聞くことができるので、とても楽しみにしていました! その後、自分の昨日の進捗や今日の予定、課題などを共有し、チーム全体で進捗を確認します。また、朝会では、実装・設計上の課題や悩んでいることなど、気軽に先輩方に相談できる場があります。自身だけでは解決できない問題も一緒に考えていただけるので、とても心強かったです。 12:00 - デイリースクラム WEARでは、スクラムを採用しており、バックエンドチームだけでなくマトリックス型のチームにも所属しています。マトリックス型のチームには、PdM、デザイナー、エンジニア(iOS・ Android・バックエンド)、QAのメンバーが数名ずつ所属し、それぞれの専門分野を活かしてチームの担当領域を改善していきます。 そのため、朝会後、毎日12時に所属チーム内でデイリースクラムを行っています。ここでは、チームで進めている企画の進捗や課題を共有し、他職種の方々との連携を図ります。内定者アルバイトとして、実際のスクラムに参加しながらプロダクトの改善に取り組むことは、なかなかない経験だと思います。さらに、専門の領域以外の話も展開されるため、より幅広い視点や知識を身につけることができました。 12:30 - 開発 朝会やデイリースクラムで今日のタスクを確認した後は、実際に開発作業に入っていきます。私は、Slackに作業用スレッドを立てて、そこに今日のTODOや進捗、課題などを書き込んでいました。また、先輩方がDiscordにいてくださるので、作業中に困ったことがあれば、すぐ相談し解決できました。 14:00 - メンターとの1on1 内定者アルバイトでは、メンターがついてくださり、1日から2日に1回程度、進捗や困りごとなどを相談する時間をいただいていました。アルバイトを開始してすぐは、他の先輩方とも1on1を日替わりで組んでいただき、楽しく雑談しながら皆さんのことを知ることができました。また、1on1では技術的なことはもちろん、最近の出来事や入社に向けた不安なども相談できます。最初のうちは緊張することもありましたが、回数を重ねるたびになんでも話せるようになり、だんだんと先輩方との距離が縮まったと感じています。 15:00 - 休憩 16:00 - 開発・MTGの参加 ここから、引き続き開発作業を進めていきます。担当するタスクによっては、他のチームの方々とやり取りをしながら進めることもあります。その際は、MTGに参加し、進捗や仕様のすり合わせを行いました。 19:00 - 退勤 最後に、今日の進捗や感想を日報にまとめ、退勤となります。 内定者アルバイトの1日はいかがでしたでしょうか?フルリモートでの勤務に不安もありましたが、皆さんの手厚いサポートのおかげで、一歩ずつ前に進むことができました。特に、技術的な質問をした際には、忙しい中でも時間を割いて丁寧にアドバイスをくださり、安心して取り組むことができました。 実際に行なったタスク 約半年間の内定者アルバイトを通して、さまざまなタスクに挑戦させていただきました。今回は、その中の1つをご紹介します。このタスクでは、単にコードを書くだけではなく、仕様の検討や技術選定にも関わることができ、とても貴重な経験となりました。 ここでは、企画が起票されてからリリースされるまでのプロセスを、内定者アルバイトの視点でご紹介します! タスクの概要 今回、私が担当したのは「メイク投稿画面で用いられる人気タグランキングの作成」という企画でした。WEARでは、コーディネートやメイク、ノウハウ動画などのコンテンツを投稿できる機能があります。その際に、「ナチュラルメイク」や「春メイク」など投稿に関連するタグを選択できます。この機能において、ユーザーが選択しやすいよう、人気のタグを表示するような機能が求められていました。 企画共有と仕様検討 まず、スクラム開発では、PdM(プロダクトマネージャー)から企画の説明があります。ここでは、この機能がなぜ必要なのか、どのようにユーザー体験を向上させるのかといった背景を理解することが重要です。 しかし、この時点では、どのようなロジックでランキングを決定するかという点について、まだ明確に定まっていませんでした。ここで、スクラムチームにおけるディスカッションが重要となります。 仕様検討MTGへの参加 私自身も仕様検討のMTGに参加し、どのような仕様が適切かをチームの皆さんと一緒に考えました。初めてこのようなMTGに参加し、「どのようなタグが出たら投稿しやすいか」というユーザー視点と、「どのようなタグが出たら投稿の質が向上するか」というプロダクト視点を両立させることの難しさを感じました。チームの皆さんが一人ひとり、プロダクトとユーザー体験に真剣に向き合っている姿がとても印象的で、WEARがこうして生み出されているのだと肌で感じました。 ロジック検討のためのモック作成 また、仕様を決定するにあたり、さまざまなランキングロジックパターンのモック作成を担当させていただきました。モックを使うことで、ランキング集計ロジックを変更した際に、どのタグが上位に表示されるかを事前に確認でき、ロジックの調整がどのような影響を与えるかをシミュレーションできます。 ロジックを検討する際、ユーザーの体験価値を重要視することは前提ですが、バックエンドとしての実装可能性(フィジビリティ)やパフォーマンスも考慮する必要があります。そこで、それぞれのバランスを考えつつ複数のロジックを作成し、さらに実際のデータを用いたモックを作成することで、PdMやデザイナー、他のエンジニアの方々にもわかりやすく提案できるよう工夫しました。 一般的に「モックを作るだけ」と思われがちですが、異なるモックを比較することで新たな視点や発見が生まれ、議論をより活性化させることができます。特に、ロジックのわずかな違いでランキング結果が大きく変わることもあり、その変化を視覚的に確認できるモックは非常に有用だと実感しました。 実装方法の選定・検証 仕様が固まったら、次は「どのような手法で実装するか?」を考えていきます。 複数の実装案の比較検討 今回、タグランキングを集計し取得するAPIの実装を担当しました。実装方法を検討する際には、以下のような案を挙げ、それぞれのメリット・デメリットを比較しました。 リクエスト毎にクエリベースで集計 バッチ処理でランキングを定期更新 キャッシュ(Redis)を使用する方法 この時点では、どの方式が最適なのか確信が持てなかったため、朝会でチームの方々に相談し、最終的に1の方法を採用することにしました。理由としては、実装コストが低く、初回のミニマムな実装として適していること、クエリからバッチ処理へと実装を移行する際にそこまで手戻りが発生しないこと、などが挙げられます。検討する中で、実装コスト・長期的な運用コスト・負荷のバランスを見ながら、最適な方法を選択することがとても難しいと感じました。 本番DBにおけるパフォーマンス検証 今回クエリでの実装となったため、事前に本番環境のDBを用いてパフォーマンス検証も行いました。クエリの実行計画を確認し、インデックスの追加を検討したり、本番環境でクエリを実行して実行時間を計測し、運用上問題がないかを確認したりといった作業をしました。 実装・リリース 仕様と実装方法が決まったらいよいよ実装です。今回は、事前にさまざまなロジックでモックを作成し、実際のデータで検証していたため、実装自体はスムーズに進めることができました。 最後に、実装した機能のQAを行い、問題がなければリリースとなります。このリリースによって、トレンドに合った人気タグが表示されるようになりました。これにより、ユーザーはそれらのタグを選択して投稿することで、自身の投稿がより多くの人に見てもらいやすくなる、という形で体験価値を向上させることができるようになりました。自身の書いたコードがプロダクトの一部として動いているのを見るのは、とても嬉しい瞬間でした! 学びと振り返り ここからは、内定者アルバイトを通して得た学びや、振り返りをピックアップしてお話ししたいと思います。 1. 質問は質よりスピードを大切にすべし 最初のうちは、「もっとちゃんと調べてから質問した方がいいのでは?」と考えすぎて、作業が止まってしまうことが多々ありました。そのとき先輩方から、「調べることももちろん大切だけれど、聞いた方が早く解決できることも多い」というアドバイスをいただきました。それ以降、15分調べても解決しない場合はすぐに質問するようにし、スピードを意識することで、徐々に作業を早く進められるようになっていきました。 ついつい、先輩方の時間を取らないように、と考えてしまいがちですが、経験豊富な先輩方の側で学べるというこんな恵まれた環境はありません。これからも、たくさん質問しながら、たくさん吸収していきたいと思います。 2. 実務で学べるテストコードの重要性 学生のうちは、動けば問題ないという考えで、軽く動作確認をして「さあデプロイしよう!」と、えいやと進めてしまうことが多々ありました。そのため、恥ずかしながら、今までしっかりとテストコードを書いたことがありませんでした。しかし、実際のサービスでは、デプロイ後に大きなバグや障害を引き起こしてしまう可能性があり、大変危険です。 そこで、先輩方とのペアプロやコードレビューを通して、どのようにテストを書くことで品質を担保できるのかという観点について実践しながら学ぶことができました。最初のうちは、なかなかうまく書けず時間がかかってしまいましたが、その分自身の実装に自信を持つことができ、今では書かないと不安になるほどです(たくさん書けば良いというわけではないことも難しいですね)。この学びは、実際の現場に入って働けるからこそ、身をもって学べたことだと感じました。 3. 誰に何を伝えたいのか? という視点での言語化 エンジニアはコードを書くだけでなく、ドキュメントを作成して共有したり、他のチームの方々すり合わせしたりと、コミュニケーションが欠かせません。その中で、内定者アルバイトとして現場に入った際、チームの皆さんの話がとてもわかりやすく、あまり詳しくない私でもすっと理解できることに感動したことを今でも覚えています。色々な先輩方に意識している点を聞いてみると、「誰に何を伝えたいのか?」という視点での言語化が共通していると感じました。今でも模索しながらではありますが、自分なりに「誰に何を伝えたいのか?」を常に考えながら会話するよう心がけるようにしています。 4. 積極的に発言できなかったことへの反省 はじめのうちは、業務の全体像が見えていない、かつ技術的にわからないことが多く、MTGなどで積極的に発言できないことがありました。 しかし、振り返ってみれば、右も左も分からない状態だからこそ、素朴な疑問や新しい視点での発言ができるチャンスだったと感じています。 実際、少しずつ質問や意見を出せるようになってきたことで、さらに理解が深まるきっかけになりました。今後、さらに積極的に発言できるよう、意識していきたいと思います。 まとめ 振り返ると、内定者アルバイトを通して、たくさんの学びを得ながら、冒頭の目的を達成できたと実感しています。特に、チームの一員として、ユーザー体験の向上を本気で考え抜く体験を通じて、「自分がやりたいことはまさしくこれだ!」と再確認できました。さらに、内定者アルバイトを通じて他の部署の方々ともお話しする機会が多くあり、会社全体の雰囲気を知れたことで、入社への楽しみがより一層高まりました。 ここまでやってこれたのは、配属先のチームの皆さんのサポートがあったからこそだと感じています。入社後、よりチームに貢献できるように、引き続き努力していきたいと思います。 ぜひ、同じような不安を抱えている方々がいれば、少しでも不安が解消されるきっかけに、そしてZOZOや内定者アルバイトに興味がある方々にとって参考になれば幸いです! 最後に ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 hrmos.co corp.zozo.com
アバター
はじめに こんにちは、計測プロデュース部の井上です。私たちはZOZOFITやZOZOMATといった計測系プロダクトの開発PM、データ収集、精度検証などサービス構築から、UI/UXの分析・評価などの幅広い業務を行っております。 あなたの「似合う」が探せるファッションコーディネートアプリ「 WEAR by ZOZO 」では、2024年5月に「 WEARお試しメイク 」機能をリリースしました。ARを活用し、WEARのユーザーが投稿したメイクを自分の顔で試すことができます。 この機能の開発にあたり、明るさチェックの閾値調整が大きな課題となりました。本記事では、その最適化プロセスとUX向上の取り組みについて解説します。 WEARお試しメイクとは? WEARお試しメイクは、アプリ内のボタンを押すだけで、以下の体験が可能な機能です。 投稿されたフルメイクを自分の顔に適用できる メイクの濃さを調整できる モデルやインフルエンサーのメイクに切り替えられる AR技術を活用し、リアルなメイクシミュレーションを提供します。 wear.jp WEARお試しメイクの登録フロー ユーザーがメイクを登録する際の流れは以下の通りです。 メイク投稿用の画像を選択 編集画面でメイクデータを登録 インカメラ起動後、環境チェック(顔の距離・明るさ) 8方向から顔の計測 生成された計測データをプレビュー メイク登録完了 この中で、明るさチェックがUXと計測品質の両面で重要なポイントでした。 明るさチェックの課題 明るさチェックの閾値がUXとメイクデータ品質に大きく影響することがわかりました。 UXの問題 :明るさチェックに時間がかかると、ユーザーのストレスになり離脱率が増加する 品質の問題 :明るさが適切でないと、メイクデータが白飛びし、実際の肌色と異なってしまう 明るさチェックの閾値を適切に調整することで、UXと品質のバランスを最適化する必要がありました。 明るさ閾値の影響調査 調査環境 端末:iPhone 14 照明条件:室内照明下 指標: 明るさチェックにかかる平均時間 白飛び発生率(肌の色と著しく差があるケースを白飛びと定義) 調査結果と最適な閾値の決定 閾値 平均時間(秒) 白飛び発生率 高 18.8 0% 中 6.5 33% 低 2.4 83% 結果から、 「閾値を中程度」にすることで、UXと品質のバランスが最も良い と判断しました。 閾値が高すぎると、白飛びは防げるがUXが悪化 閾値が低すぎると、UXは良いがメイクの品質が低下 閾値「中」が最適解:UXとメイク品質のバランスを両立 まとめ 明るさチェックの閾値は、単純に「高ければ良い」「低ければ速い」といったものではなく、UXと品質のトレードオフを考慮する必要があると考えています。私たちは、データをもとに最適な閾値を導き出し、ユーザーが快適に使える「WEARお試しメイク」機能を提供できるよう改善を続けています。今後も、計測系プロダクトの品質向上とUX最適化を両立する取り組みを進めていきます。「WEARお試しメイク」機能の体験が向上したことを、ぜひ実際に試してみてください! ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com
アバター
はじめに こんにちは。データシステム部推薦基盤ブロックの佐藤( @rayuron )と住安( @kosuke_sumiyasu )です。私たちはZOZOTOWNのパーソナライズを実現する推薦システムを開発・運用しています。 ZOZOTOWNでは、様々な改善施策の効果を検証するためにA/Bテストを実施していますが、そのプロセスには多くの工程があり、効率化の余地がありました。本記事では、A/Bテストの工程を自動化・標準化し、効率化を図った取り組みについてご紹介します。 はじめに 背景 課題 1. A/Bテスト設計書のテンプレートがない 2. A/Bテストの開催期間が決め打ち 3. 有意差検定をするために手動運用が必要 4. チーム間で集計定義とダッシュボードが複数存在している 5. 施策ごとに新しいダッシュボードを作り直す必要がある 6. システム改修漏れが発生する 解決策 1. A/Bテスト設計書のテンプレートを作成 2. A/Bテストの開催期間の自動見積もり 3. 有意差検定の自動化 4. 集計定義とダッシュボードの統一 5. ダッシュボードの再利用 6. システム改修の手順書の作成 課題と解決策のまとめ 効果 1. A/Bテスト設計開始から意思決定までの時間の削減 2. ヒューマンエラーの低減 今後の展望 A/Bテスト体制の評価と改善 おわりに 背景 推薦基盤ブロックでは、ZOZOTOWNのトップページやメール配信などにおいて、ユーザーに最適なアイテムを推薦するシステムを開発・運用しています。これらのシステムを改善する際には、A/Bテストを通じて効果を検証します。 課題 まず、推薦基盤チームで運用されているA/Bテストの開始から終了までのプロセスは以下のようになっています。 上記のプロセスで運用を続けていましたが、以下の課題に直面していました。 A/Bテスト設計書のテンプレートがない A/Bテストの開催期間が決め打ち 有意差検定をするために手動運用が必要 チーム間で集計定義とダッシュボードが複数存在している 施策ごとに新しいダッシュボードを作り直す必要がある システム改修漏れが発生する 1. A/Bテスト設計書のテンプレートがない A/Bテスト設計を行う際には、A/Bテストの目的や、テストの内容、テスト期間、モニタリング指標などを記載したA/Bテスト設計書を作成します。しかし、明確なフォーマットが定められておらず、その結果、以下のような問題が生じていました。 必要項目が抜け落ちる 設計時の留意点がチーム全体で共有されず、共通認識が不足する フォーマットや記載文言にばらつきが生じ、読み手の認知コストが高くなる 2. A/Bテストの開催期間が決め打ち 従来のA/Bテストでは、テスト期間を固定し、その期間のデータをもとに任意の指標に関して有意差を判定していました。しかし、A/Bテスト期間を決め打ちする方法には2つの問題点があります。 1つ目は、「余分なリードタイムによる機会損失」です。これまでのA/Bテストはテスト期間を4週間に固定して実施していました。もし、A/Bテスト期間を短縮できれば、売上向上が期待できる施策は早期にリリースし、迅速に利益を得ることができます。 2つ目は、「十分なサンプルサイズが確保できない可能性」です。A/Bテストにおいてリリース判断を行うために、観察された差が統計的に偶然に起こる確率が十分に低いことを示す必要があります。しかし、A/Bテスト期間を単に短縮すると、サンプルサイズが減少し、指標にポジティブな効果があった場合でも統計的に有意差を確認できないリスクがあります。回避するためには、事前にMDE(Minimal Detectable Effect)を算出し、その効果の有意差を示すのに必要なサンプルサイズを見積もった上で、十分なテスト期間を確保する必要があります。 3. 有意差検定をするために手動運用が必要 A/Bテストの意思決定の段階では、有意差検定にJupyter Notebookを手動実行しており、分析作業が自動化されていませんでした。これにより、意思決定プロセスに余計な時間がかかっていました。 4. チーム間で集計定義とダッシュボードが複数存在している 指標をモニタリングするチームが複数あり、それぞれが集計定義とダッシュボードを個別管理していました。あるチームではスプレッドシートを使って指標の集計と可視化をしています。一方で別のチームでは、BigQueryのスケジュールクエリ機能を使って指標を集計しLooker Studioを使って可視化しているという状態でした。そのため以下の問題が生じていました。 クエリとダッシュボードの管理が属人化している クエリをGit管理できていないので、変更履歴を追跡できない レビュープロセスが十分でないため、品質が担保されていない 同じ意味を持つ指標でも集計定義が異なり混乱する どのダッシュボードを参考にすべきか混乱する 5. 施策ごとに新しいダッシュボードを作り直す必要がある 従来の運用では、A/Bテストごとにモニタリング指標に関するテーブルを作成し、そのテーブルを用いてLooker Studioで指標の紐付けやグラフ化を行い、ダッシュボードを新規作成していました。しかし、テーブルとLooker Studioを連携させる際、施策ごとにアドホックなクエリを記述していたため、バグ混入のリスクがありました。さらに、A/Bテストの件数増加に伴い、作成されるテーブルやダッシュボードの数も増加し、管理が非常に煩雑になるという課題も生じていました。 6. システム改修漏れが発生する A/Bテストに関するシステム改修には、ユーザー振り分けシステムの改修やテスト集計用の改修およびダッシュボード作成、結果を保存するための改修などの様々な作業が必要になります。しかし、これらの作業手順の標準化をしていなかったため、設定漏れや改修漏れの発生により、結果として意図したA/Bテストを実施できないケースが確認されていました。 解決策 これらの課題を解決するために、以下の解決策を採用しました。 1. A/Bテスト設計書のテンプレートを作成 A/Bテスト設計書のテンプレートとして、表形式で必要な項目を記入するだけで設計が完了する資料を作成しました。このテンプレートは、A/Bテストのシステム改修内容を除いた、テスト実施に必要なすべての情報を網羅しているため、設計時に必要な情報を一目で確認できます。具体的には、以下のような項目が含まれています。 - 概要 - 目的 - テスト内容 - UXの改善の仮説 - A/Bテスト設定 - A/Bテスト期間 - 対象の出面 - 対象デバイス - テスト対象ユーザーの割合 - 対象ユーザーの振り分け方法 - 分析対象のユーザー - 改善案 - テストパターン - モニタリング指標 - 意思決定方法 - A/Bテスト関係者へのアナウンス(設計後のアナウンス用) また本資料では項目ごとに詳細な記入例を記載しており、テンプレート利用者の解釈の差異を無くし、記入内容の統一化を図っています。例えばモニタリング指標の項目ではあえて55個もの指標を網羅的に記載しており、実際の記載時には必要な指標のみ残すようにしています。こうすることで、必要な指標が抜け落ちるリスクを確実に回避できるようにしています。 以下に、テンプレートの内容をイメージしていただくための一部を示します。 2. A/Bテストの開催期間の自動見積もり A/Bテストの期間見積もりプロセスを整備・自動化しました。このプロセスは、有意差を検出したい各指標について、以下の3つのステップに基づいてテスト期間を算出することで、A/Bテスト期間を見積もります。 1週間あたりのサンプルサイズの計測 有意差検定に必要なサンプルサイズの算出 A/Bテスト期間の見積もりと調整 1. 1週間あたりのサンプルサイズの計測 まず、過去のユーザーの行動ログをもとに、テスト期間中のユニークユーザー数を大まかに見積もります。これにより、1週間あたりのユニークユーザー数が把握でき、必要なサンプルサイズが何週間分に相当するかの概算が可能となります。私たちのブロックでは、他のA/Bテストとのスケジュール調整が円滑にできる4週間をテスト期間の上限として運用しています。そのため、4週間分のユニークユーザー数の合計を4で割って1週間あたりの数値を算出します。ただし、同じユーザーの複数回の訪問が多いため、初週や2週目の予測値は実測値よりも低くなる傾向があります。その結果、必要なサンプルサイズを過小に見積もってしまい、有意差を検出するのに十分なサンプルサイズを確保できないリスクが生じます。これを回避するため、予測値が実測値より低い状況を前提に見積もりを算出します。 2. 有意差検定に必要なサンプルサイズの算出 私たちのチームでは、A/Bテストでリリース判断を行うには、実際の施策が単なる偶然の結果ではなく、統計的に有意な効果をもたらしているかどうかの確認をしています。MDEは、リリースに必要な最低限の効果量を示すものです。このMDEが統計的に有意であるかどうかが、最終的なリリース判断の根拠となります。しかし、施策ごとに影響を及ぼす範囲や期待される効果量は異なります。そのため、あらかじめ正確なMDEを設定するのは難しいです。そこで、現状では過去に実施したA/Bテストの中から、施策内容や規模が類似している事例を参考にして、適切なMDEを見積もる方法を採用しています。これにより、施策ごとに「どれくらいの効果があればリリースすべきか」という判断基準を示すことができます。ここで見積もったMDEをもとに、統計的に有意な効果を検出するために必要なサンプルサイズを算出します。 必要サンプルサイズ は以下のように計算できます。 :有意水準 :検出力(Power) :検出したい差(実際は、MDE * 平均値で計算している) :分散 任意の期間におけるユニークユーザー数が、このサンプルサイズ より大きい場合、その期間内でMDEを検出するために必要なサンプルサイズが十分に確保されていると言えます。 3. A/Bテスト期間の見積もりと調整 手順1で求めた1週間あたりのユニークユーザー数と、手順2で計算されたサンプルサイズを比較して、テスト期間を見積もります。具体的には、まず有意差を検出したい全ての指標について手順2を実施し、その中で最も大きなサンプルサイズを採用します。ここで最も大きなサンプルサイズを選択する理由は、全ての指標に対して有意差を検出するために必要なサンプルサイズを確保するためです。次に、手順1で算出された1週間あたりのユニークユーザー数を使用して、最大サンプルサイズをまかなうために必要な週数を計算します。これにより、各指標で必要なサンプルサイズがテスト期間内に十分集まるかどうかを判断します。 ただし、実際のユニークユーザー数に対し、手順2で求められる必要サンプルサイズが非常に大きくなる場合があります。特に、MDEが小さい施策では、有意差検定に必要なサンプルサイズが急激に増加する傾向があります。このような状況を踏まえ、運用上はA/Bテスト期間にあらかじめ下限と上限の日数を設定しています。推薦基盤では、下限を2週間、上限は「1. 1週間あたりのサンプルサイズの計測」で言及した4週間としています。下限が2週間である理由は、曜日ごとのユーザーの行動パターンの違いを捉えたり、プロモーションやイベントがA/Bテスト期間と重なる可能性を考慮したりするためです。もし手順2で求めたサンプルサイズが、手順1で見積もった期間内に集まらない場合、その旨をリリース判定者へ伝え、指標を相対的な基準に基づいた判断を促す運用としています。 自動的なA/Bテスト期間の見積もり ここまでに示したA/Bテスト期間見積もりのプロセスをコード化しました。 その結果、施策者はMDEや各指標の分析対象ユーザー数を求めるクエリの記載のみ行えば、自動的にテスト期間の見積もりが得られる運用体制になりました。これにより、施策者の負担が低い状態で統計的に有意な効果を検出するためのサンプルサイズを算出でき、効率的なA/Bテスト期間見積もりとA/Bテスト期間の短縮を実現しました。 from scipy.stats import norm import numpy as np import pandas as pd # 有意差検定に必要なサンプルサイズの算出 def sample_size_for_mde ( s: pd.Series, mde: float , alpha: float = 0.05 , beta: float = 0.2 ) -> float : variance = np.var(s, ddof= 1 ) z_alpha = norm.ppf( 1 - alpha / 2 ) z_beta = norm.ppf( 1 - beta) average_value = np.mean(s) diff_value = average_value * mde sample_n = 2 * variance * (z_alpha + z_beta) ** 2 / diff_value** 2 return sample_n 3. 有意差検定の自動化 過去に以下の記事でも取り上げましたが、KPIをモニタリングする文脈で私たちのチームではKPIの集計にVertex AI Pipelinesを活用したパイプラインを構築している最中でした。このパイプラインは、BigQueryからデータを取得し、データの前処理、特徴量エンジニアリング、モデルの学習、評価、デプロイまでの一連の処理を自動化しています。 techblog.zozo.com 今回の改修では上記のパイプラインに対し自動的に有意差検定をし、結果をBigQueryに保存する処理を追加しました。 これにより、ノートブックの手動実行の必要がなくなり有意差検定の自動化が実現しました。 4. 集計定義とダッシュボードの統一 これまでGit管理されていなかったクエリをGitHubリポジトリで一元管理しました。さらに、上記で説明した通りに、KPIの集計をVertex AI Pipelinesで統一し、ダッシュボードはLooker Studioに統一しました。 上記の取り組みによって、以下のような効果が得られました。 クエリとダッシュボード管理の属人化が解消された Git管理によって変更履歴の追跡が可能になった コードレビュープロセスを通じて品質を担保できるようになった 複数チームで指標の定義を一覧化できるようになった どのダッシュボードを参照すべきかが明確になった 5. ダッシュボードの再利用 まず、テーブル構造を標準化し、すべてのA/Bテストの指標データをひとつの統合テーブルに保存するように変更しました。この統合テーブルには、各A/Bテストの識別子としてA/Bテストのバケット名も保存されており、これがダッシュボード上での識別キーとして使用します。 この統合テーブルを用いてLooker Studioでダッシュボードを作成することで、各A/Bテストの結果をひとつの画面上で一元管理・表示できるようになりました。具体的には、Looker Studioのフィルタ機能を活用し、プルダウンメニューからA/Bテストバケット名を選択するだけで対象データが自動的にフィルタリングされ、指標の数値が切り替わります。 この変更により、新しい施策のためにダッシュボードを再作成する必要がなくなり、過去に実施されたテストについても、簡単に結果を確認できるようになりました。実際に作成されたダッシュボードは、以下の画像に示されている通り、直感的な操作でA/Bテストの成果を確認できる仕組みとなっています。 6. システム改修の手順書の作成 A/Bテストシステム改修内容のテンプレートとして、改修項目、改修内容、担当者、ステータス、対応内容、という5つの項目で表形式に管理できる資料を作成しました。 資料には、改修の条件の記述から、具体的な改修内容、動作確認の方法、そして改修作業時に特に注意すべき点が詳細に記載されています。これにより、担当者は改修作業の背景や目的、手順、検証方法を明確に把握でき、漏れなく作業を実施することが可能となります。 このテンプレートを活用することで、システム改修漏れのリスクを低減し、担当者、進捗、対応状況の情報を一元的に記入・管理できる環境が整いました。その結果、事前調査や過去施策の検索にかかる工数が削減され、誰でも迅速かつ正確に改修作業を進められるようになりました。また、レビュアーや関係者は、1つの資料で全ての改修作業とその進捗状況を包括的に把握できるため、進捗の透明性が向上しました。 課題と解決策のまとめ 以下の表に、今回の取り組みで対処した課題と解決策をまとめます。 課題 解決策 1 A/Bテスト設計書のテンプレートがない A/Bテスト設計書のテンプレートを作成 2 A/Bテストの開催期間が決め打ち A/Bテストの開催期間の自動見積もり 3 有意差検定をするために手動運用が必要 有意差検定の自動化 4 チーム間で集計定義とダッシュボードが複数存在している 集計定義とダッシュボードの統一 5 施策ごとに新しいダッシュボードを作り直す必要がある ダッシュボードの再利用 6 システム改修漏れが発生する システム改修の手順書の作成 効果 これらの取り組みにより、以下の効果が得られました。 1. A/Bテスト設計開始から意思決定までの時間の削減 A/Bテストの設計から意思決定までの工程を標準化・自動化したことで、全体的な工数が大幅に削減されました。標準化後、はじめてのA/Bテストで工数を計測したところ、具体的には以下の改善が見られました。 工程 改善前 改善後 A/Bテスト設計 24h 8h ダッシュボード作成 16h 0h 有意差検定の実行と分析 2h 0h これらを合計すると、A/Bテスト1回あたり34hの工数削減を実現したことになります。 2. ヒューマンエラーの低減 標準化・自動化の取り組みにより、ヒューマンエラーのリスクが大幅に低減しました。 解決策 効果 A/Bテスト設計書のテンプレートを作成 必要項目の抜け漏れ防止 クエリの集計定義を一元管理 コードレビュープロセスを通した品質の向上 ダッシュボードの統一 指標解釈時の混乱の低減 システム改修の手順書の作成 システム改修漏れの防止 上記の取り組みによって、以前は年に数回発生していた集計ミスやシステム改修漏れの発生リスクを大幅に低減しました。 今後の展望 今回の取り組みにより、A/Bテストの効率化と標準化が進みましたが、まだ以下のような課題や今後の展望があります。 A/Bテスト体制の評価と改善 現在、A/Bテストの各工程にかかる工数や、問題が発生した工程を記録する仕組みがありません。ボトルネックを特定し、A/Bテストのサイクルを高速化するために、今後は各工程の工数、問題の発生率、発生原因を記録・分析できる仕組みを導入したいと考えています。 おわりに 本記事では、A/Bテストの設計から意思決定までの工程を標準化し、効率化を図った取り組みについてご紹介しました。標準化されたテンプレートの導入、有意差検定の自動化、ダッシュボードの統一など、様々な改善により、A/Bテストの実施工数の削減とヒューマンエラーのリスク低減を実現しました。 今後も継続的に改善し、より効率的かつ効果的なA/Bテストの実施体制を整えていきたいと考えています。 ZOZOでは一緒にサービスを作り上げてくれる方を募集しています。ご興味がある方は以下のリンクからぜひご応募ください! corp.zozo.com
アバター
ZOZOTOWN開発本部でAndroidのテックリードをやっている いわたん です。最近はでっかいモンスターをハントするゲームにハマっており、夜な夜な一狩りしてます。 今回は、私たちのチームで行っている業務効率化の一例を紹介します。 背景・課題 解決方法 スクリーンショットをトリガーにしたフィードバック 必要な情報を自動的に収集する ログの収集 アプリ内情報の収集 課題を作成する機能 別アプリとして実装する フィードバック内容の取得 アクセストークンの管理 Activity破棄対応 実際に運用した結果 まとめ 背景・課題 私たちのチームでは、デザイナーやプロジェクトマネージャーによる動作確認のためにDeployGateとGoogle Play Consoleの内部テストを使用しています。また、QAチームによるテスト時の不具合や日々のタスク、プロジェクトマネージャーやデザイナーからのフィードバックの修正管理にJiraを利用しています。 しかし、現状のフィードバック収集プロセスには以下の課題がありました。 QAチームからの不具合チケットの起票 開発中のデバッグ情報やログイン状態などのアプリ内情報や、実行時のログ情報を添付するのに手間がかかる。 デザイナーやプロジェクトマネージャーからのフィードバック Slackでフィードバックを受けることが多く、情報が散逸しやすい。 Jiraに慣れていないメンバーにとって、課題起票や情報添付のハードルが高い。 これらの課題により、フィードバックの収集に時間がかかり、効率が低下していました。 解決方法 フィードバックの収集を効率化するために次の仕組みを構築しました。 フィードバックを手軽に送信できるようにする 必要な情報を自動的に収集する DeployGateからインストールしたときのみフィードバックを送信できるように、別アプリからフィードバックをする ZOZOTOWNアプリのスクリーンショットを撮影するとDeployGateのキャプチャ機能が起動する DeployGateのキャプチャ機能が完了するとDeployGateからZOZOTOWNアプリへコールバックが呼び出される ZOZOTOWNアプリがコールバック内でアプリの情報を自動で収集する ZOZOTOWNアプリとは別アプリとして実装されているフィードバック用アプリにIntent経由で収集した情報を渡して起動する フィードバック用アプリがIntentの内容をもとにJiraのIssueを作成する 次の章から具体的な実装方法を紹介します。 スクリーンショットをトリガーにしたフィードバック フィードバックを簡単に送信できるよう、アプリ内で明確なトリガーを設ける必要があります。本記事ではDeployGateの キャプチャ機能 を利用します。 キャプチャ機能はスクリーンショット撮影時に端末情報やLogcatを自動で収集し、DeployGate上に登録します。また、DeployGate SDKを利用することでキャプチャ完了時にアプリ内でコールバックを受け取ることができます。このコールバックを利用してフィードバックの送信処理を行います。 次に実装方法を紹介します。 まずはDeployGateのキャプチャ機能のコールバックを利用するためにバージョン4.9.0以降のDeployGate SDKをlibs.versions.tomlに追記します。 deploygate = "4.9.0" [ libraries ] deploygate = { group = "com.deploygate" , name = "sdk" , version.ref = "deploygate" } 続いて、app/build.gradleのdependenciesにDeployGateを追加します。 dependencies { implementation libs.deploygate } 続いてDeployGateのキャプチャ機能が完了した際にフィードバックを送信するため、キャプチャ機能が完了した際に呼び出されるコールバックをApplicationを継承しているクラスに追加します。 まずはコールバックの登録だけを行い、具体的なフィードバックの送信処理は後述します。 class App : Application() { override fun onCreate() { DeployGate.registerCaptureCreateCallback { url -> // TODO ここでフィードバックを送信する } } } 必要な情報を自動的に収集する 次に、フィードバックを送信する際に以下の情報を自動で収集する機能を実装します。これらの情報は、開発チームが問題を特定し修正するために必要な情報と、QAチームが課題を管理するために必要な情報を含んでいます。 アプリに関する情報 ログ アプリのバージョン 接続先の環境 ユーザー情報 ログイン済みかどうか ユーザー名 UUID 端末情報 端末名 OSバージョン ログの収集 まずは操作ログです。ストアに公開するReleaseビルドでは、パフォーマンス向上やセキュリティ対策のため、 Timber などのライブラリを使用してログの出力を抑制することが一般的です。しかし、QAにおいては詳細なログ情報が問題の原因究明に役立ちます。そこで、DeployGate経由でインストールされたReleaseビルドでのみログを出力できるように変更を加えます。 具体的には、DeployGate SDK 4.9.0で導入された DeployGate.registerStatusChangeCallback メソッドを利用します。このメソッドで登録したコールバックの第一引数は、アプリがDeployGateで管理されているかどうかを示すフラグです。このフラグを参照し、DeployGate経由でインストールされた場合にのみLogcatを有効化します。 class App : Application() { override fun onCreate() { DeployGate.registerStatusChangeCallback { isManaged, _, _, _ -> if (isManaged) { // ここでlogcatを有効にする Timber.plant(DebugTree()) } } } } アプリ内情報の収集 続いてアプリ内の情報を収集します。これらの必要な情報は開発しているアプリによって異なります。 ReportBuilder は、フィードバック情報を構築するためのインタフェースであり、build()メソッドによって情報を収集し、文字列形式で返します。このインタフェースを実装するクラスを複数利用することで、必要な情報を組み合わせたフィードバック内容を生成できます。 /** * フィードバック情報を作成するためのインターフェース */ fun interface ReportBuilder { /** * 必要な情報を収集してフィードバック情報として文字列を返す */ fun build(): String } 本記事では ReportBuilder を実装した、 BasicReportBuilder クラスと DebugReportBuilder クラスを用意しました。 BasicReportBuilder クラスは全てのBuild Variantで共通して必要なユーザー情報、アプリのバージョン、端末情報などを提供します。サンプルではイメージしやすいように UserRepository を仮で用意しています。 build()メソッドでこれらの情報をフォーマットし、フィードバック情報の文字列として返します。 /** * すべてのBuild Variantで収集したい情報を返す */ class BasicReportBuilder @Inject constructor ( private val userRepository: UserRepository, ) : ReportBuilder { override fun build(): String { val user = userRepository. get () val loginName = user.loginName val uid = user.uid val versionName = BuildConfig.VERSION_NAME val versionCode = BuildConfig.VERSION_CODE val buildVariant = BuildConfig.BUILD_TYPE return """ 【概要】 【再現手順】 【補足】 【確認アカウント】 loginName: $loginName uid: $uid 【アプリ情報】 Version: ${ versionName } ( ${ versionCode } ) Variant: $buildVariant 【端末情報】 Build.VERSION.SDK_INT: ${ Build.VERSION.SDK_INT } Build.VERSION.BASE_OS: ${ Build.VERSION.BASE_OS } Build.VERSION.RELEASE: ${ Build.VERSION.RELEASE } Build.BRAND: ${ Build.BRAND } Build.DEVICE: ${ Build.DEVICE } Build.MODEL: ${ Build.MODEL } Build.PRODUCT: ${ Build.PRODUCT } """ .trimIndent() } } DebugReportBuilder クラスはDebugビルド時にのみ必要な情報、たとえばデバッグメニューの状態や設定を提供します。これらはアプリによって異なりますのでサンプルは仮のクラスを用意しています。 これら以外にもBuild Variantによって収集する情報が変わる場合はそれぞれ必要な情報を集める ReportBuilder クラスを実装します。 /** * Debugビルド時に収集したい情報を返す * @param DebugOption デバッグ時の情報を保持するオブジェクト */ class DebugReportBuilder( private val debugOption: DebugOption ) : ReportBuilder { override fun build(): String { return """ 【デバッグ情報】 ${ debugOption } """ .trimIndent() } } Reporter クラスは ReportBuilder を使って複数のフィードバック情報を結合し、それをIntentのエクストラとして追加する役割を担います。builderリストには複数の ReportBuilder が含まれ、それぞれのbuildメソッドの結果を結合し、1つのフィードバック情報として出力します。 /** * [other]で指定されたReportBuilderを後に続けて結合する */ fun ReportBuilder.compose(other: ReportBuilder): ReportBuilder { return ReportBuilder { build() + other.build() } } class Reporter( private val builder: List <ReportBuilder> ) { /** * フィードバック用アプリを起動するIntentを作成する */ fun createReportIntent( additionalBuilder: ReportBuilder, ): Intent { return Intent( "com.example.report" , ).apply { flags = Intent.FLAG_ACTIVITY_NEW_TASK // すべてのReportBuilderを結合 val composedReportBuilder = builder.fold(additionalBuilder) { acc, reportBuilder -> acc.compose(reportBuilder) } // レポートをIntentに追加 val description = composedReportBuilder.build() putExtra(Intent.EXTRA_TEXT, description) } } } これらのクラスはHiltによる依存性注入でBuild Variantごとに異なるReporterのインスタンスを提供しています。 Debugビルド時のHiltのModuleは次のようにDebug関連の情報を集める RepoterBuilder を使用します。 @InstallIn (SingletonComponent :: class ) @Module object ReporterModule { @Provides fun provideReporter( userRepository: UserRepository, debugMenu: DebugMenu, ): Reporter { return Reporter( versionName = BuildConfig.VERSION_NAME, builder = listOf( BasicReportBuilder(userRepository), DebugReportBuilder(debugMenu), ), ) } } Releaseビルド時のHiltのModuleは次のように最低限の情報を集める RepoterBuilder を使用します。 @InstallIn (SingletonComponent :: class ) @Module object ReporterModule { @Provides fun provideReporter( userRepository: UserRepository, ): Reporter { return Reporter( versionName = BuildConfig.VERSION_NAME, builder = listOf( BasicReportBuilder(userRepository), ), ) } } DeployGateのキャプチャ機能が完了した際のコールバックで、作成したIntentを利用してフィードバック送信用アプリを起動します。 class App : Application() { @Inject lateinit var reporter: Reporter override fun onCreate() { // DeployGate経由でインストール時にログを有効化する DeployGate.registerStatusChangeCallback { isManaged, _, _, _ -> if (isManaged) { // ここでlogcatを有効にする Timber.plant(DebugTree()) } } // DeployGateにスクリーンショットを保存した際にフィードバックを作成する DeployGate.registerCaptureCreateCallback { captureUrl, _ -> val intent = reporter.createReportIntent { """ 【スクリーンショット】 $captureUrl """ .trimIndent() } startActivity(intent) } } } 課題を作成する機能 私たちのチームではJiraで課題管理していますが、課題を作成する実装を変更することで別の課題管理ツールを使用している場合でも応用が効くと思います。 課題を作成する機能の実装部分はチームごとで必要な実装が異なるため、本記事は他チームでも参考になりそうな部分を紹介します。 別アプリとして実装する フィードバック機能は、JiraのAPIトークンや開発用の課題管理ツールのURL等の機密情報や、ログ取得・内部状態の出力等のデバッグ機能を含みます。これらの情報は、セキュリティやアプリのパフォーマンス、ユーザーエクスペリエンスの観点から、ストアに公開するReleaseビルドには含めるべきではありません。 そのため、Jiraへの課題作成は開発中のアプリとは別アプリとして実装します。 フィードバック内容の取得 Reporter で作成したIntentで ReportActivity を起動するようにAndroidManifest.xmlを設定します。 <? xml version = "1.0" encoding = "utf-8" ?> <manifest xmlns : android = "http://schemas.android.com/apk/res/android" > <application android : label = "フィードバック送信" > <activity android : name = "com.example.ReportActivity" android : windowSoftInputMode = "adjustResize" android : theme = "@style/Theme.Design.NoActionBar" android : exported = "true" > <intent-filter android : label = "@string/report_create" > <action android : name = "com.example.report" /> <category android : name = "android.intent.category.DEFAULT" /> </intent-filter> </activity> </application> </manifest> Intentからフィードバック内容を取得し、descriptionに格納しています。このdescriptionを各チームに合わせてAPI等を呼び出してフィードバックの課題を作成します。 class ReportActivity : ComponentActivity() { // フィードバック内容 val description = intent.getStringExtra(Intent.EXTRA_TEXT) ?: "" } アクセストークンの管理 JiraのアクセストークンやHostの情報はGit上で直接管理するのはセキュリティ上の懸念があります。そこで、環境変数からBuildConfigへ登録できるよう、次のようにbuild.gradle.ktsに設定を追加します。 // 必要な部分のみ抜粋 android { defaultConfig { buildConfigField( "String" , "JIRA_TOKEN" , " \" ${ System.getenv( "JIRA_TOKEN" ) } \" " ) buildConfigField( "String" , "JIRA_HOST" , " \" ${ System.getenv( "JIRA_HOST" ) } \" " ) } } そして、フィードバックを入力して送信するためのActivityでBuildConfigから情報を取得します。 class ReportActivity : ComponentActivity() { private val jiraHost: String get () { // build.gradleの設定経由でJIRA_HOSTで定義された環境変数を取得している val env = BuildConfig.JIRA_HOST require(env.isNotEmpty()) { "環境変数JIRA_HOSTが空文字でビルドされています" } require(env != "null" ) { "環境変数JIRA_HOSTが未定義でビルドされています" } return env } private val jiraToken: String get () { // build.gradleの設定経由でJIRA_TOKENで定義された環境変数を取得している val env = BuildConfig.JIRA_TOKEN require(env.isNotEmpty()) { "環境変数JIRA_TOKENが空文字でビルドされています" } require(env != "null" ) { "環境変数JIRA_TOKENが未定義でビルドされています" } return env } } Activity破棄対応 QA期間中などに開発者向けオプションで、「アクティビティを保持しない」を有効にしているケースがあるかと思います。このオプションを有効にした状態で、フィードバック送信用アプリからQA対象のアプリを再度表示した等のタイミングでフィードバック内容が消えてしまう可能性があります。そのため、先述のReportActivityでは簡略化していましたが、rememberSaveableやonSaveInstanceState等でActivityの破棄に対応した実装が必要になります。 実際に運用した結果 実際にシステムの運用を開始し、現場での実証テストを通じて、QAチームから様々な意見が寄せられました。全体的に概ね好評であり、現場で感じた使いやすさや業務効率の向上についての具体的なフィードバックは以下の通りです。 ログをスクリーンショットと同時に用意できる点 QAチームはこれまで手動でログやスクリーンショット画像を用意し、必要に応じて添付していました。DeployGateのキャプチャ機能によって自動で同時に用意できるようになり、負担削減が好評でした。 各種情報の自動記録 ログイン状態、UID、アプリのバージョン、接続先環境、端末情報といった重要な情報が自動的に記録されるため、従来必要であった手作業が省略でき、入力ミスも防止できる点が便利だと好評でした。 Jiraの必須項目の自動記入 各種アプリ関連情報に加え、Jira上で必須となる担当者や影響を受けるバージョンなどの定型的な入力項目についても、自動で記入される仕組みが実装されています。これにより、手動での入力作業が省け、全体の作業負担が大幅に軽減される点が非常に有用であるとの評価を受けました。 特定画面での準必須情報の自動記録への期待 一方で、商品詳細画面における問い合わせ番号や検索結果画面での絞り込み条件など、頻繁に記載が求められる情報については機能拡張が欲しいという意見もありました。これらの情報はReporterの実装を工夫することで実現が可能と考えられるので今後追加する予定です。 まとめ 本記事ではDeployGateのキャプチャ機能を利用して、フィードバック用アプリ、そしてJiraとの連携を活用することで、従来プロセスの手作業や情報散逸の問題を解消しました。主なポイントは以下の通りです。 自動化による効率化 スクリーンショット撮影をトリガーに、ログやアプリ・ユーザー、端末情報などの必要情報を自動で収集し、手動での情報添付やPCへのデータ転送の手間を削減しました。 安全かつ柔軟な設計 機密性の高いJiraのAPIトークンやデバッグ情報は別のフィードバック用アプリで管理し、環境変数を利用することでセキュリティ面も考慮しています。 現場からの好評と今後の展開 QAチームや関係者からは、ログとスクリーンショットの同時送信による効率向上や、Jiraの必須項目の自動入力による入力ミス防止が高く評価されています。さらに、特定画面で必須情報の自動記録など、さらなる機能拡張が期待されています。 このシステムにより、開発・テスト・運用の各フェーズでのフィードバック対応がスムーズになり、結果として業務全体の効率化と品質向上が実現されました。今後は、さらなる改善と機能追加を通じて、より充実したフィードバック環境の構築を目指していきます。 ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com
アバター
はじめに こんにちは。計測プラットフォーム開発本部で研究開発をしている 皆川 です。2024年の10月にスイスで2日間に渡って開催された3DBODY.TECHに、同部署でプロジェクトマネジメントをしている嶺村と二人で参加しました。カンファレンスの開催から少し時間が経ってしまいましたが、参加レポートをお届けします。 目次 はじめに 目次 3DBODY.TECHとは? 日本からルガーノへの行き方 ルガーノという街 ZOZOとの関わり カンファレンスの特色 新しいレビュー方式 特に印象に残った発表 “Advancements in Body Composition Assessment using Mobile Devices” プレゼンテーション概要 精度検証のプロセス 検証結果 感想 “3D Bodyscan in Professional Sports: Practical Use Cases in Prevention, Bodytracking and Rehabilitation” プレゼンテーション概要 ユースケース1:脚長さの左右差のデータを活用したケガの予防 ユースケース2:計測結果のトラッキング ユースケース3:ケガのリハビリへの活用 感想 ”Learning to Predict Anthropometric Landmarks via Feature Refinement” by Yibo Jiao, University of British Columbia 背景 方法 結果 感想 ”From Smartphone 3D Scanning to Body Measurements Extraction” by Olivier SAURER, Astrivis Technologies 汎用性の高いSDK 顔のスキャンとその精度 足のスキャンとその精度 感想 ”Fast and Accurate 3D Foot Reconstruction from a Single Image” by Joaquin SANCHIZ, IBV 概要 方法 結果 感想 さいごに 3DBODY.TECHとは? 毎年10月に開催される人体計測に関する国際カンファレンスです。最近はほぼ毎年スイスのルガーノで開催されていますが、2017年にはカナダのモントリオールでも開催されています。正式名称は 「3DBODY.TECH Conference & Expo」 で、今回は15回目の開催でした。 公式発表 によると、参加者は200名(うちオンラインが20%)、発表は80件、さらに20の企業が展示したとのことです。会場はティチーノ州ルガーノにある「ルガーノ国際会議場(Palazzo dei Congressi Lugano)」です。 ルガーノ国際会議場の外観。 日本からルガーノへの行き方 日本からスイスへ行く場合、成田空港からチューリッヒ空港への直行便がスイス航空から出ています(執筆時点)。チューリッヒからルガーノへはスイス鉄道に乗って約2時間。2016年にできた世界最長のトンネルであるゴッタルド基底トンネルのおかげで、スイスの他の地域で乗る山間を縫うような山岳鉄道と比べて乗り心地がとても良いです。成田からチューリッヒまでの約14時間にわたる空路は大変ですが、チューリッヒからは快適な旅が期待できます。 チューリッヒ駅のホームの様子。 チューリッヒからルガーノへ向かう途中の車窓からの風景。 ルガーノ駅の様子。 ルガーノという街 ルガーノはイタリア語圏であるティチーノ州に位置し、その街並みもどこかイタリアっぽさが感じられます。住民はみなイタリア語で会話し、図書館や本屋はイタリア語の本であふれ、街はジェラート屋やイタリア料理屋で溢れています。住民の雰囲気もドイツ語圏のチューリッヒやダボスとは大きく異なります。英語はときどき通じない方がいるので、簡単な日常会話はイタリア語でできるようにしておいた方が良いかもしれません。この点は英語だけで安心して歩けるチューリッヒと比べると大きな違いです。 ルガーノ駅付近から観た市街地の様子。 ルガーノ市街の中心地。手前に見えるのはジェラート屋。 ルガーノ湖周辺エリア。ルガーノ駅から徒歩十分ほど。 ZOZOとの関わり ZOZOはコロナ禍を含めこのカンファレンスに頻繁に参加、発表してきました。2020年にはZOZO New Zealand Ltd.(以降ZOZO NZ)のCTOであるBoがZOZOSUITやZOZOMATの技術的側面について 発表 しています。2022年には同じくZOZO NZのEugeneがZOZOMATのサイズ推奨アルゴリズム開発時の技術的課題について 発表 しています。また同年に計測プラットフォーム開発本部の本部長(執筆時点)の山田がZOZOの計測事業の歴史と今後について 発表 しています。同年には展示ブースでZOZOSUIT、ZOZOMAT、ZOZOGLASSの展示も行っています( その様子 )。 今回は2020年から数えて5年連続での参加となりました。そのためかZOZOを認知している他の参加者から様々な場面で話しかけられることが多かったように感じます。 カンファレンスの特色 3DBODY.TECHは、全発表の3割以上がアパレル分野での計測技術の応用をテーマにしており、その点で他に類を見ないカンファレンスとなっています。 アパレル分野では、いかに正確に身体を3Dスキャンして計測するかというテーマ以上に計測結果をどう活かすかというテーマが多かったように感じます。具体的に以下がテーマとして多かったです。 身体にフィットしたアパレルのサイズ推奨やアパレルのデザイン 着用した際の見た目を再現できる、実際のサイズを反映した(size awareな)アパレルのバーチャル試着(Virtual Try-On) アパレルのサイズ推奨やデザインは事例が多くサービス化が着実に進んでいる一方で、実際の服のサイズを反映したバーチャル試着はまだ事例が少なかったです。 メイン会場。ここで全体の半分にあたる発表が行われる。 Bodidata社のサイズ推奨デモ画面。主催者の許可を得て発表スライドより抜粋。なお、発表の動画は2025年の7~9月頃にカンファレンスの 抄録集 にアップロードされるとのことです(スライド自体へのアクセスは参加者のみ可能です)。 Alter Ego AI社のバーチャル試着デモ画面。画像では身長183センチ、体重75kgの男性がSサイズのTシャツを着用した様子を再現している。 公式HP より抜粋。 新しいレビュー方式 これまでは要旨のレビューをもって採択が決められ、受理された発表は全て抄録集(Proceedings。なお抄録とは学会発表をするときに提出する発表内容の要旨のこと)として発行、という形をとっていました。今回からは受理された発表のうち、内容のレビューを受けたものを新しく発行を開始する学会誌「 3DBODY.TECH Journal 」に載せる、という形をとっています。また、全体の発表の約3割にあたる27の発表が、学会誌に収録されています。 逆に言うと残りの約7割の発表は内容のレビューを受けておらず、その中には普通の研究発表のフォーマットとは大きく異なる発表もあり、最初は少し戸惑いました。例えば、発表の冒頭でその会社のプロダクトがいかにECアパレルにおけるサイズのずれという問題を解決するか雄弁に語っていながら、それを客観的に支持するデータがひとつも出てこない。あるいは彼らのプロダクトが身体計測の新しい、革新的な方法として紹介されたものの、具体的な手法の詳細や精度などの情報が発表中に出てこないなどです。 特に印象に残った発表 計測プロデュース部データ開発ブロックの嶺村です。 ここからはカンファレンス参加時に見た発表の中から、特に印象に残ったものについて紹介します。私から2本、皆川から3本の発表についてご紹介いたします。なお、論文として発行されているものは論文のURLを発表のタイトルに埋め込んでいます。またすべての発表は動画が 抄録集 にて2025年中に公開される予定です。 “Advancements in Body Composition Assessment using Mobile Devices” プレゼンテーション概要 私から紹介する1つ目の発表は、Size Stream社の体脂肪率および体脂肪量の計測精度について詳しく紹介したものです。 www.sizestream.com 同社から過去に販売されていたハードウェアを用いた3Dボディスキャナと、現在提供中のスマホアプリでの計測結果が比較され、後者も前者に匹敵する計測精度が出せていることが強調されていました。 Size Stream社はかつて、体脂肪を含む様々な指標の計測ができるブース型の3Dボディスキャナを販売していました。近年では、ブース型スキャナからモバイル端末での計測に力を入れており、MeThreeSixtyというスマートフォンアプリで身体計測サービスを提供しています。 www.methreesixty.com 精度検証のプロセス MeThreeSixtyアプリの計測精度を検証するために、Size Stream社はテキサス工科大学と共同で研究を行いました。118人の被検者から209のスキャンデータを収集し、同アプリの計測結果をベンチマークと比較した精度検証を実施しました。ベンチマークとしたのは、二重エネルギーX線吸収測定法、空気置換法および生体インピーダンス測定システムを組み合わせて計測した体脂肪の計測結果です。モバイルアプリでの計測結果をインプット、ベンチマークである体脂肪率や体脂肪量をアウトプットにし、回帰分析を行い、モバイルアプリの計測結果とベンチマークの計測結果との相関関係を明らかにしました。 検証結果 体脂肪率と体脂肪量の計測結果をプロットしたところ、モバイルアプリとベンチマークとの間に強い相関関係が見られました。95%区間での最大誤差は、体脂肪率においては±7.5%、体脂肪量においては±5kgに収まっていました。さらに、他社の体脂肪を計測可能な製品やSize Stream社のブース型の3Dボディスキャナとの計測精度の比較も行われました。結果として、Size Streamのモバイルアプリが他社のサービスと同等ないしより高い精度を持ち、自社のボディスキャナに匹敵する精度となる結果だったことが説明されました。 感想 こちらの発表では、検証内容やその結果が詳細に紹介されており、情報量が非常に豊富で密度が濃い印象を受けました。検証のために集めたデータ量の規模や、ベンチマークとの比較誤差についても具体的に明示されていたため、Size Stream社のプロダクトの計測精度を明確に理解できる発表でした。ヒップの周囲長などのシンプルな寸法値とは異なり、体脂肪量・体脂肪率は人体の3Dモデル生成だけでは計測が難しいです。そのため、それを計算するアルゴリズムが必要となりますが、その計算式の中身についても具体的な言及がありました。ZOZOFITでも体脂肪率の計測機能を提供しています。こちらの発表で触れられていた検証方法、アルゴリズムの内容や精度などの情報は、弊社内での開発や検証内容と比較してみたいと考えています。 “3D Bodyscan in Professional Sports: Practical Use Cases in Prevention, Bodytracking and Rehabilitation” プレゼンテーション概要 2本目にご紹介するのはVITRONIC社による発表です。 www.VITRONIC.com VITRONIC社は、ハードウェアを用いた3Dボディスキャナを販売しており、欧州のプロサッカーチームのメディカルチームに同社製品を提供しています。このプレゼンテーションでは、同社の3Dスキャナの計測データがスポーツの現場でどのように活用されているかについて説明されました。具体的な活用事例として、発表の中では3つのユースケースが紹介されました。 ユースケース1:脚長さの左右差のデータを活用したケガの予防 発表の中では、両脚の長さの左右差が9mm以上あると腰椎に影響を及ぼす可能性のあることが説明されました。脚長さに左右差があることで腰の脊椎周辺の姿勢に影響が出てしまい、その結果として腰回りの不調につながることがあります。IFD cologne(ドイツのスポーツ整形外科病院)に通うサッカー選手をVITRONIC社の3Dボディスキャナで計測したところ、計測した選手のうち33%に9mm以上の左右差があったそうです。 ユースケース2:計測結果のトラッキング 姿勢のバランスの悪さは競技のパフォーマンス低下につながります。特に、若い選手の姿勢が身体的成長に伴ってどのように変化しているのかを把握することは重要です。成長やトレーニングに伴う姿勢変化を早期に捉えるため、VITRONIC社の3Dボディスキャナが活用されています。シーズン前に計測した結果をレファレンスとし、シーズンイン後、4〜8週間ごとにスキャンしシーズン前の結果と比較することで姿勢の変化を継続的に確認しています。 ユースケース3:ケガのリハビリへの活用 前十字靭帯の損傷はプレイ中にしばしば発生するケガのひとつです。前十字靭帯のケガのリハビリにおいても計測データが活用されています。具体的には、リハビリを開始できるかどうかの判断をする際に上膝部分の周囲およびボリュームを確認しています。リハビリ開始後は、太ももと膝上部分の周囲長を確認することで、リハビリがどの程度完了しているかを把握しています。 感想 カンファレンス全体では計測の応用分野としてファッション・アパレル領域のものが多くを占めていましたが、こちらの発表は数少ないスポーツ分野での事例として非常に新鮮で勉強になりました。特に、各ユースケースの説明の中で、どの身体部位の計測結果をどのように利用するのかが具体的に解説されており、身体計測の活用の幅広さを改めて実感しました。任意の部位の計測結果から健康状態を把握し改善するという観点では、弊社でも ZOZOSUITを用いた側弯症検知 や リンパ浮腫四肢測定 の研究実績があり、これらの取り組みと類似していると感じました。ZOZOSUITやアプリのみの身体計測データを活用することで、弊社でも同様のサービスを提供できる可能性があると期待しています。 ”Learning to Predict Anthropometric Landmarks via Feature Refinement” by Yibo Jiao, University of British Columbia ここからは再び皆川が、印象に残った発表を3つご紹介します。 背景 基準点(landmark)とは、身体の部位の長さ計測の際に参照する解剖学的な点のことです。一般的には経験を積んだ専門家が1箇所ずつ触診によって検出していくため時間のかかるプロセスです。また、その精度は測定者のスキルに依存することも知られています。著者によると、人体表面のメッシュデータから基準点の検出を行うアルゴリズムは一部の基準点で精度が著しく低下するという問題があったそうです。この論文では、新しい損失関数を導入することによってSOTA、すなわち最高精度を達成しています。 誤差が生じやすい身体の基準点の例として、上前腸骨棘が挙げられています(論文中ではASISと呼ばれている。下記の画像を参照)。 上前腸骨棘(Anterior superior iliac spine、通称ASIS)の位置。緑の丸が触診による正解値。黄色の丸は以前のSOTA手法、赤の丸は提案手法によって推測された頂点。 本論文 の図1より引用。 方法 論文の中では、これまで取られてきたさまざまな方法の紹介とトレードオフについての議論があります。以下に要点をまとめました。 身体の基準点はラベリングのコストが高く、データセットがCAESARなど一部のものに限られていることが現状の教師あり学習の課題。 近年ではDeepShellsに始まる教師なし学習がSOTAを叩き出している。 近年2次元画像のキーポイント検出タスクで、ロバスト性と精度が示されてきたヒートマップベースのアプローチを採用した。 ヒートマップベースアプローチの中でも頻出手法であるガウス分布(ある一点を中心とした2次元のガウス分布)の回帰を採用しようとしたが、3Dメッシュへの回帰が技術的に難しく断念した。 なおヒートマップベースのアプローチとはターゲットの座標を直接予測するのではなく、ターゲットの確率分布を予測する方法のことです。 提示される方法では、この課題があると言われている教師あり学習を用い、SOTAを叩き出しています。以下に方法をまとめました。 学習時のパイプラインの全体図。 本論文 の図2より引用。 上図はモデル学習の全体図です。提案手法のゴールは、入力となるメッシュの各頂点で、その頂点がある特定の基準点である確率を算出することです。そのためにニューラルネットワークを学習させるのですが(上図のDiffusionNetに相当)、その学習のための損失関数として以下の2つの関数を提案しています。 基準点のポテンシャル(P)関数:対象の頂点(vertex)が基準点である確率を示す関数 相似関数(D):対象の頂点が基準点からどれくらい離れているかを示す関数 相似関数Dはニューラルネットワークとは独立に、正解データである基準点と対象の頂点間の距離で決まります。一方でポテンシャル関数は入力であるメッシュの情報とそれを処理するニューラルネットワークの重みで決まります。基準点に近い頂点でポテンシャル関数が大きく反応するように学習したいので、学習時には相似関数とポテンシャル関数の相関係数が最大化するように学習させます。結果、基準点付近ではポテンシャル関数の低くなるような重みをニューラルネットワークが学習します。そして最もポテンシャルの低い点が予測点として出力されます。モデルの学習フェーズが終わると、ニューラルネットワークとポテンシャル関数だけを用い入力のメッシュに対して基準点の予測される頂点が出力されます。なお学習にはFAUSTデータセットから50サンプルを抽出して使用したと記述があります。 学習パイプラインの詳細や、関数PとDの相関最大化問題の緩和(relaxation)のための手法(核ノルムの導入)といった詳細な議論については論文を参照してください。 結果 提示される方法では、従来の方法に比べて大きな精度向上がみられています(下記の画像を参照)。また検証にはFAUSTデータセットおよびCAESARデータセットからそれぞれ50サンプルずつ使用したとあります(FAUSTは学習時に使われたデータは除外してあります)。 正解値(緑色)、従来のSOTAモデルであるDeep Shellsの出力(黄色)、提案されたモデルの出力(赤色)。なお、青いエリアは、頂点が基準点である確率を表すポテンシャル。 本論文 の図3より引用。 また、提案モデルは学習データに含まれていないポーズや3Dメッシュの欠損に対してもロバスト性を示しています(下図参照)。より細かい結果については論文を参照してください。 さまざまなポーズや部分的なメッシュの欠損があるときの提案モデルの出力。 本論文 の図4より引用。 感想 提案されたモデルは、 SMPL のようなパラメトリックメッシュではなく、生の3Dメッシュを入力として想定しているモデルです。本カンファレンスではブース型の3Dスキャナーも多数展示されており、それらから得られる高精度な生のメッシュを元に計測値を抽出する方法のニーズが高まっていることが伺えます。本研究はそのニーズを反映していると考えることもできそうです。 方法に関しては基準点検出でSOTAを達成した一方で、基準点ごとに異なるニューラルネットワークを学習させる必要がある点には注意が必要です。このために演算コストや実装コストが高くなりそうだと思いました。 身体の高精度なスキャンだけでは、必ずしもサイズ推奨やアパレルデザインといった用途に活かすことができません。場合によってはそこから腹囲や肩幅といった計測値の抽出が必要になることがあります。この研究は生のメッシュからの基準点の検出というタスクを、新しい学習機構を導入することで一歩進めています。また発表を聞いた感想としては、学習用のデータセットをスケールさせることでどこまでモデルのパフォーマンスが増加するか気になりました(もちろんそれには大きなコストがかかります)。 また現在、スマホを用いた身体計測アプリの主流は先述のパラメトリックメッシュを用いた方法のため、この研究のように生のメッシュを処理するタイプの研究はあまり関連性がないかもしれません。ただし今後、ブース型の3Dスキャナーを検討する際は押さえておきたい技術だと思いました。 ”From Smartphone 3D Scanning to Body Measurements Extraction” by Olivier SAURER, Astrivis Technologies 3Dスキャンを専門とするチューリッヒに本籍のあるAstrivis TechnologiesのSDKについて、この分野で20年以上の経験があるというCTOのOliver Saurer氏が発表しています。 汎用性の高いSDK Astrivis Technologiesが提供するSDKは3DスキャンのソフトウェアにフォーカスしたSDKであり、LiDARやFaceIDといった特殊なハードウェアを必須としません。またiOSやAndroidだけでなく、Windows等のPC上でも動作するとのことです。 顔のスキャンとその精度 計測方法は一般的なスマホの3Dスキャンアプリと同様です。計測対象が満遍なく映るようにスマホを動かして顔をスキャンしていきます。スキャンはおよそ10秒くらいあれば完了するそうです。 計測の様子。発表中のスライドより抜粋(カンファレンス主催者の了承を得て掲載。以降のスライドも同様)。なお、発表の動画は2025年の7~9月頃にカンファレンスの 抄録集 にアップロードされるとのことです(スライド自体へのアクセスは参加者のみ可能です)。 スキャン完了後、処理時間として10秒ほど待つと顔の3D再構成が完了します。得られた3Dメッシュは専用の高額な3Dスキャナーと比較した場合ほぼ1mm以下の誤差に収まっています。 3DハンドスキャナーArtec Eva HDと比較した誤差(メッシュ間の距離をヒートマップとして表示)。 足のスキャンとその精度 足も顔と同様のプロセスでスキャンが可能です。 フットスキャンの様子。 スキャン後、計測値を自動で抽出する機能もあります。発表中に明言はされていませんでしたが、3Dスキャンで得られた生のメッシュから不要なメッシュを除去し、足のメッシュを取得し、そこに計測抽出アルゴリズムを適用しているようでした。テンプレートメッシュを使用したかどうかや、計測抽出アルゴリズムがルールベースのものかAIを使用しているか等、技術的な詳細はほとんど触れられていませんでした。なお計測箇所は下図の画像の通りです。 スキャンされた3Dメッシュから自動で計測値を抽出する機能。 以下の画像は、上記の足の計測値(足の長さやかかとの幅)についての結果です。計156回の計測の結果のうち、ほとんどの試行で誤差がほぼ2mm以下に収まっています。ただし、結果は3Dプリントされた足型を用いているとスライド上部に記載があり、注意が必要です。 156回の試行における計測値の誤差の分布。 また以下の画像は、計測値の信頼性(繰り返し同じものを計測した際にどれくらい試行間で誤差があるか)についての結果です。37回の試行に関して、誤差が3mmくらいの範囲内に収まっています。なお、先ほどの結果は3Dプリントされた足に対してのデータでしたが、こちらは実際の足に対しての計測結果のようです。発表中はこの点が強調されておらず、ここは聴衆に誤解を与えかねないと思いました。 繰り返し計測した際の計測値の分布。 感想 筆者は発表を聞いて、長さ参照物なしで誤差2mm以下の精度が達成されていると解釈しました。この2mm以下という誤差は、長さ参照物なしの計測精度だとすれば驚くべき精度です(注:2mmは3D身体計測の国際規格であるISO 20685における、足計測の互換性に関わる許容誤差です。詳しくは ”Anthropometry, Apparel Sizing and Design” edited by Norsaadah Zakaria, Deepti Gupta” のp.41を参照してください)。しかし本発表では、計測方法についての詳細は省かれているため、必ずしも長さ参照物なしとは断言できないことに注意が必要です。この発表はJournal用の発表枠ではなかったため、技術詳細や精度計測方法について曖昧な点がいくつか残る発表でした。今後同社から更なる情報が公開されることを期待します。 現状、スマートフォンを用いた計測では物理参照物を使わない場合、スケール推定がボトルネックになり計測精度が落ちてしまいます。この精度の低下が足のサイズ推定にはクリティカルになるらしく、スマホを用いた足の計測アプリは現状なんらかの物理参照物を要しています(A4の紙やZOZOMATなど)。この物理参照物が必要なくなれば計測に対するユーザーの敷居は大きく下がると思うので、今後も本発表のような関連ある研究を注視していきたいと思います。 ”Fast and Accurate 3D Foot Reconstruction from a Single Image” by Joaquin SANCHIZ, IBV 概要 従来の足の3Dスキャンは画像が複数枚必要だったり、演算に時間のかかったりするものが多いですが、この手法では1枚の画像からほぼリアルタイムに高精度な形状の推論が可能になっています。従来のスマホを使った足の3Dスキャンの簡易化や短時間化が狙えるほか、靴のバーチャル試着にも応用が可能です。 方法 モデルの詳細や学習方法について論文中に詳細な記載はなく、概要のみ説明されています(下図参照)。モデルはエンコーダー・デコーダー型です。エンコーダーの2つの出力ヘッドでセグメンテーション情報(バイナリーマスク)と9つの基準点の位置情報を出力します。またデコーダーは足のパラメトリックモデルの形状パラメーターと位置、角度情報を出力します。 モデル学習の全体図。 本論文 の図1より引用。 また学習に用いたデータセットに関しても学習方法と同様、概要のみ記載されています。IBVの所有するAvatar 3D Feetデータセットを増分処理をしたものが50万セット(増分処理前のサンプル数に関しては記載なし)。IBV所有の足のパラメトリックモデルを用いて合成したデータセットが100万セット。どちらのデータセットも付加情報として、カメラパラメーター、基準点の3次元位置情報、セグメンテーション情報、PCAの形状パラメーター等がそれぞれの画像とセットになっています。 Avatar 3D Feetデータセットの一例。画像中に写っているA4紙はカメラのキャリブレーションに用いる。 本論文 の図2より引用。 結果 評価には先述のAvatar 3D Feetデータセットから674サンプルを抽出して、正解データとモデルの出力のメッシュ間距離を算出し、評価しています。下図は誤差の平均を可視化したものです。平均の誤差が0.9mmであり、高い精度で足の形状が推定できている事がわかります。論文中には明記されていませんが、3D Avatar FeetデータセットはA4紙という長さの参照物が含まれていることに注意が必要です。 評価データ(n=674)における平均誤差をヒートマップで表現したもの。単位はmm。 本論文 の図4より引用。 下図は3つの実装パターンにおける推論時間です。組み合わせが包括的でないため特定の結論を導くことは難しいですが、およそリアルタイムに動かせるくらいの速度で推論できている事がわかります。 演算装置、およびモデルタイプごとの推論時間の変化。 本論文 の表2より引用。 感想 論文は全体として明瞭に書かれていて学ぶことが多かった一方で、この研究の要となる3D Avatar Feetデータセットが公開されていないのは少し残念でした。また未公開のデータセットを使用していることの欠点として客観的な性能評価や他のモデルとの性能比較の難しい点があります。 ただし足の計測がリアルタイムでかつ高精度に行えている点は目を見張るものがあります。従来、計測目的でテンプレートメッシュを使う際は、時間がかかるが高精度の出やすい最適化手法を使うのが通例だと思います。しかしこの研究は、最適化手法より一般に高速と言われている回帰手法でも、高精度な足の計測が可能であることを示唆しています。今後、ZOZOMATやZOZOSUIT等の計測プロセスの簡易化にも活かせそうな研究だと感じました。また本研究は、靴のバーチャル試着に関しても今後の進化が期待できるような内容でした。 さいごに 昨年10月にスイスで開催された身体計測のカンファレンス3DBODY.TECHについて参加レポートをお届けしました。このレポートではほんの一部しかご紹介できませんでしたが、計測技術の進化を感じることができる刺激的なカンファレンスでした。また今回で15回目の開催ですが、ジャーナルの発行開始やレビュープロセスの変化など、カンファレンス自体の進化も感じることができました。今後もZOZOの既存の計測プロダクトの進化や新規のサービスにつながるような研究を求め、3DBODY.TECHを注視していこうと思います。 ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com
アバター
はじめに こんにちは、MA部MA基盤ブロックの @turbofish_ です。ZOZOTOWNではプッシュ通知やLINE、メール、サイト内お知らせでのキャンペーン配信を行っており、MA部ではそれらの配信を担うマーケティングオートメーション(MA)のシステムを開発しています。本記事ではその中でも、メールの配信を担当する基盤システムをリアーキテクチャし、バッチでの配信とリアルタイムな配信の両立を実現した取り組みをご紹介します。 目次 はじめに 目次 背景・課題 ZOZOTOWNでのキャンペーン配信 メール配信基盤の機能要件 旧メール配信基盤のシステムアーキテクチャと課題 旧メール基盤のアーキテクチャ 旧メール配信基盤の課題 新メール配信基盤の機能要件とアーキテクチャ 新メール配信基盤の機能要件 新メール配信基盤のアーキテクチャ 新メール配信基盤における配信処理の流れ 配信トリガーくん 配信リストアップロードくん 配信してくれるくん 配信完了作業くん リアーキテクチャの結果 まとめ 背景・課題 ZOZOTOWNでのキャンペーン配信 ZOZOTOWNでの配信の特徴はさまざまですが、ここでは配信タイミングを軸として下図のように分類します。 分類 配信ロジック 配信タイミング バッチ配信 事前に決められた条件に当てはまる会員に、決められた時刻に一斉に送信する。 配信開始から終了まで数十分〜数時間の幅があってもよい リアルタイムイベント配信 在庫残り1点や値下げなど、会員に関係するイベントが発生した時に配信する。 データの変更を検知したらできるだけ早く配信する必要がある(配信が遅れるとユーザへのメリットが薄れる)。 上記の分類はメールのみならず全てのチャネルにおいて共通しており、マーケティングメールもその性質によって配信処理に対して許容できる時間に差があります。 キャンペーン配信の分類とMA部で開発しているマーケティングプラットフォームZMPのアーキテクチャについて詳しく知りたい方は、2024年3月に齋藤が執筆した記事をご参照ください。 techblog.zozo.com メール配信基盤の機能要件 メール配信基盤とは、ZOZOTOWNのマーケティングメールの配信を担うシステムです。MA部が開発するマーケティングプラットフォームZMPをはじめ、事前に登録された他システムから配信リクエストを受け取り、リクエストの内容に沿ってメールを配信します。 メールの配信内容は、メールデザインのテンプレートと、キャンペーンやユーザごとに動的な商品などのパラメータにより決まります。 メール配信基盤を使用するシステムは、メール配信基盤に配信リクエストを送る前に、このメールテンプレートと、メールの配信先とメール本文に挿入するパラメータを保存した配信対象者テーブルを準備します。配信対象者テーブルは、CSV形式に変換し、適切な大きさに分割して、配信リストとして外部サービスにアップロードして配信に使用します。 キャンペーン配信時間に期限が存在する場合は、メール配信基盤はリクエストを受信してから期限までに間に合うよう配信処理を行う必要があります。前提として、配信処理にかかる時間は、メール配信数に比例して増加する傾向があります。ZOZOTOWNのメール配信で最も配信量が多い時は、配信リクエストの生成を開始してから全ての送信先に配信されるまでに数時間かかることもあります。そのため、配信数が多いバッチ配信の場合は、それを見越して配信処理にかかる時間を考慮した上で適切な時間に配信されるようスケジューリングしています。 メール配信基盤では、実際にメールを配信する処理は外部サービスをAPI経由で利用しており、その外部配信サービスのAPIの同時実行数には上限があります。よってメール配信基盤も、その上限数以上の並列処理はできません。 旧メール配信基盤のシステムアーキテクチャと課題 この記事では、リアーキテクチャ前の基盤を旧メール配信基盤、リアーキテクチャ後を新メール配信基盤と呼びます。新旧の記載がないメール配信基盤という表記は、リアーキテクチャの前後に関わらない文脈であることを意味します。MA部ではほとんどのシステムをGCP上に構築しており、メール配信基盤も同様にGCPを使用しています。 旧メール基盤のアーキテクチャ 旧メール配信基盤は、DigdagというOSSのワークフローエンジンを使用して実装されており、ワークフロー起動時に配信待ちのリクエストを取得してバッチで処理していました。DigdagはリトライやSLAに加え、プラグインでエラー発生時のSlack通知などの機能をサポートしており、配信処理のロジックの実装のみに集中できる点が大きな魅力でした。旧メール配信基盤のアーキテクチャは下図の通りです。 旧メール配信基盤は、下記の3つのワークフローで構成されていました。 ワークフロー 役割 実行タイミング 起動 - 配信対象のリクエストの取得 - 配信の有効期限チェック - 配信前処理ワークフローの起動 3分おきに定期実行 配信前処理 - 重複配信の制御 - メールアドレスの取得 - 配信リストを外部サービスにアップロード 起動ワークフローから起動される 配信処理 - 外部の配信サービスへのリクエスト - 配信実績の書き込み 5分おきに定期実行 旧メール配信基盤システムの処理やアーキテクチャの詳細について知りたい方は、2024年3月に松岡が執筆した記事をご参照ください。 techblog.zozo.com 旧メール配信基盤の課題 Digdagは1つのジョブが持つ全てのリクエストの配信処理が終わるまで次の処理を開始できません。これにより、配信スループットを決める最大の要因である外部サービスのAPIへのリクエスト量を分散させることができず、リソース効率があまり良くない状態でした。また、大規模なバッチ配信の直後にリアルタイムイベント配信が発生した場合、バッチ配信を捌き切るまで、リアルタイムイベント配信に長い処理待ちが発生してしまう状態でした。 例として、簡単のために外部サービスへの同時リクエスト数上限を2と仮定し、バッチ配信が4リクエスト、リアルタイムイベント配信が1リクエストほぼ同時に作成された場合の処理について考えてみます。旧メール配信基盤では、処理は下図のようになります。バッチ配信のリクエスト中に外部サービスへのリクエストが1並列になっている時間もあるにも関わらず、バッチ配信が全て完了するまでリアルタイムイベント配信が行われません。 以上の理由から、旧メール配信基盤はリアルタイムイベント配信に対応できず、バッチ配信のリクエストのみを処理していました。リアルタイムイベント配信は、MA部の中でも最も古いシステムの1つであるRTMという別のシステムが担っていました。RTMの詳細について知りたい方は、2020年8月に田島が執筆した記事をご参照ください。 techblog.zozo.com メールを配信するシステムが社内に複数存在することの最も重要な弊害として、複数システムから外部の配信サービスへのリクエストの総量が制御できない状態でした。複数のシステムが同じタイミングで配信すると、外部メール配信サービスのサーバーに過剰な負荷がかかり、全ての配信が遅くなってしまうリスクがありました。 新メール配信基盤の機能要件とアーキテクチャ 新メール配信基盤の機能要件 旧メール配信基盤の処理効率の改善に着手できていなかったなか、リアルタイムイベント配信を担っていたRTMをリプレイスするにあたり、RTMからのメール配信においてもメール配信基盤を使用することになりました。よって、新規の要件として下記の2つが求められました。 外部の配信サービスのリソースを最大限活用できるアーキテクチャ リアルタイムイベント配信を優先的に配信できる仕組み 以前より課題だった外部配信サービスの活用効率を改善し、配信全体の処理を最適化する必要がありました。加えて、配信対象が少なく優先度の高いリアルタイムイベント配信が高頻度でリクエストされた場合にも、配信対象が多いバッチ配信の影響を受けることなく配信できる必要があります。こうして、新たな要件に対応すべく、旧メール配信基盤を抜本的にリアーキテクチャすることにしました。 新メール配信基盤のアーキテクチャ 新メール配信基盤では、処理の性質に適した実装にすべくワークフローの形式をやめ、ジョブキューを使ってワーカー的なサーバーが連携して並列処理を行うようリアーキテクチャしました。新メール配信基盤のアーキテクチャは下図のとおりです。 新メール配信基盤を構成する4つのワーカーはCloud RunサービスもしくはCloud Runジョブで作成しており、それぞれの役割は下記の通りです。サーバーを4つに分割したのは、インスタンスごとに同時処理数を制御することに加え、それぞれのワーカーの処理を非同期に行うことで、処理効率を最大化するためです。 ワーカー名 サービス 処理 配信トリガーくん Cloud Runサービス 処理中のタスク数が減少したら、その時最も優先順位が高い配信リクエストを少量ずつ抽出し、ジョブキュー(Cloud Tasks)に詰める。 配信リストアップロードくん Cloud Runサービス 配信リストを外部サービスにアップロードし、完了したら配信リクエストをジョブキュー(Pub/Sub)に詰める。 配信してくれるくん Cloud Runサービス(常時稼働CPU) Pub/Subからメッセージを受け取り、重複配信の制御と配信処理を行う。 配信完了作業くん Cloud Runジョブ 配信が完了したことを確認し、リクエストのステータスを変更したり、配信ログを記録するなどの後処理を行う。 新メール配信基盤における配信処理の流れ 配信トリガーくん 配信トリガーくんは、その時々で配信待ちリクエストの配信優先度を確認し、次に処理するリクエストを決定してCloud Tasksにタスクを送信します。キューの中にある処理待ちのタスクを最小限に保ち、随時その時最も優先度の高いリクエストを少量ずつ追加することで、柔軟な配信順序を実現しています。キューに入ったリクエストは、配信リストアップロードくんが既存のタスクを終えて新規にリクエストを受け付け可能な状態になったら、配信リストアップロード処理を開始します。 配信リストアップロードくん 配信処理の前半は、配信リストのアップロードです。メール配信のフローの中でも、配信前処理の配信リストのアップロードは、そのほかの処理と比較してかなり時間がかかります。さらに、1つのキャンペーンに対する配信先の会員数が多いほど、配信関連データのアップロードにかかる時間は増加する傾向があります。つまり、配信リストアップロードの処理が処理全体のボトルネックになります。この配信リストアップロード処理の外部APIを同時リクエスト数上限まで活用できるよう、配信リストアップロードくんはインスタンスが処理する同時リクエスト数に応じてオートスケールし、並列で処理を行います。オートスケール時の最大インスタンス数とCloud Runの最大同時実行リクエスト数の設定を掛けた数が、外部サービスAPIの同時リクエスト数上限を超えないよう設定しています。配信リストのアップロードの処理が完了したらPub/Subにメッセージをパブリッシュし、新しいリクエストを受け付けます。 配信してくれるくん 配信してくれるくんは、受信したリクエストが重複配信ではないことを確認してから、メール配信APIを実行します。外部の配信サービスから取得した配信履歴を確認することで、同じ配信先に対して複数回同じメールを配信してしまうことを防いでいます。 配信完了作業くん 配信完了作業くんは、配信してくれるくんがメール配信を行った後の処理を担当します。まず、外部サービスのAPIを叩いて、メール配信ステータスが配信済みに更新されたことを確認します。配信完了を確認したら、データベースに配信実績を記録し、配信リクエストのステータスを更新するなどの後処理を行います。配信処理と配信後の処理がキューを挟んで分かれていることで、配信ステータスの確認を行なっている間にも、配信してくれるくんは後続のリクエストを処理して配信処理を行うことができます。 リアーキテクチャの結果 新メール配信基盤では、配信量が多いバッチ配信と断続的に発生する少量のリアルタイムイベント配信を同時に実行しても、全ての配信においてそれぞれの要件を満たす時間内で配信ができるようになりました。また、配信全体の処理効率が向上していることも確認できました。 旧メール基盤のアーキテクチャの説明と同様の状況を例に説明します。同時実行数の上限が2のとき、バッチ配信4、リアルタイムイベント配信1リクエストが追加された状況で、新旧基盤での処理は下図のように異なります。 処理効率の向上については、想定していた以上の改善が見られました。負荷テストの結果、バッチ配信で新旧基盤を用いて同じ配信数を捌き切るまでの時間を計測したところ、配信処理の開始から配信完了までの時間がおよそ40%短縮されました。 また、メール配信基盤がメール配信処理を一括で担うことにより、社内の他システムがメール配信のロジックを再実装する必要がなくなり、外部の配信サービスへの負荷もメール配信基盤で制御できるようになります。 まとめ 本記事では、メール配信システムのリアーキテクチャの事例を紹介しました。メール配信基盤は、ワークフローエンジンを用いたバッチ処理からワーカーをジョブキューで連携させ並列処理を行う非同期的なアーキテクチャに変更しました。これにより、処理効率を大幅に向上させ、配信の性質に応じて柔軟に処理を進めるシステムへと進化しました。アプリケーションの再実装は工数もかかりましたが、システムの最適化によってマーケティングメール配信が管理しやすくなり、保守面でも大きな恩恵がありました。大量のリクエストを処理する必要があるシステムの開発、スケーラブルで処理効率が高いアプリケーション設計に興味がある方の参考になれば幸いです。 ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com
アバター
はじめに こんにちは。2025年4月に新卒で株式会社ZOZO(以下、ZOZO)に入社予定の清板海斗(せいたかいと)です。2024年8月から入社までの約半年間、「WEAR by ZOZO」(以下、WEAR)のiOSチームで内定者アルバイトに参加しました。この記事では、内定者アルバイトの目的やチームでの取り組み、全体の振り返りについてご紹介します。 目次 はじめに 目次 内定者アルバイトの概要 ZOZOの内定者アルバイトについて 内定者アルバイトでの働き方 WEARとは WEAR iOSチームについて 主な取り組み MVPへのリアーキテクチャ タスク概要 Presenter移行の背景 WEAR iOSにおけるPresenter 実施したこと 結果 学び 1. コミュニケーションと言語化の重要性 2. UIKitの理解 3. 実務レベルで求められる視点 最後に 内定者アルバイトの概要 ZOZOの内定者アルバイトについて 内定者アルバイトとは、内定承諾から入社までの期間にアルバイトとして就業できるものです。また、選考時に志望していた職種以外の求人に応募できます。職種以外にもサービスごとで募集もしているので入社後に希望している配属先のサービス以外の選択肢もあり、様々なサービスの雰囲気を知ることができる良い機会になると思います。 内定者アルバイトでの働き方 私は2024年2月に内定をいただき、同年の8月から翌年の3月の大学卒業までの8か月間、WEARのiOSアプリを開発するチームで内定者アルバイトとして働きました。 WEARとは ZOZOはWEARという日本最大級のファッションコーディネートサイトを運営しています。2024年の5月に「WEAR by ZOZO」として「似合う」が探せるアプリを目指してリニューアルしました。 WEAR iOSチームについて WEAR iOSチームは10名程度で構成されており、チームのコミュニケーションはSlack、Google Meet、Discord等で行われています。案件ごとでチームがさらに細分化されており、改善に向けた取り組みはその中で行われることが多いです。 iOSチーム全体では次のような活動をしています。 朝会 連絡事項の共有 雑談(ご飯、スニーカー、格闘技などの話題が多く、個性的な雰囲気) チーム定例 技術的な知見、調査内容の報告など 登壇の発表練習 Findyの数値を見ながら開発パフォーマンスの確認 Warning数など技術負債のモニタリング タスク、PRの細分化に関する相談、共有 1on1 配属時にチームメンバー全員と1on1 お互いを知るための雑談 年齢層が高めのチームで、相談には若い頃のお話も交えて親身に乗っていただけますし、ミスがあっても優しく指摘していただけます。とにかくアットホームなチームです。 主な取り組み 最初は、UIやバグの修正などの簡単なタスクから始め、徐々にテストの実装やリアーキテクチャといった難しいタスクに取り組みました。 デザイン修正 ボタンの位置の修正などの対応 バグ修正 年月日表記の修正対応 warning解消 Swift 6対応に伴うXcodeのwarning解消 リプレイス 独自実装を UICollectionViewCell の automaticallyUpdatesContentConfiguration を使用するようにリプレイス 独自実装を UICollectionViewCell の標準APIを使用するようにリプレイス リアーキテクチャ Presenter移行 その中でも特に印象的だったタスクについてご紹介します。 MVPへのリアーキテクチャ タスク概要 「ノウハウ動画投稿の着用コーディネートアイテム画面」にMVPアーキテクチャを適用しました。この画面は、ホームや探すタブからアクセスできる「ノウハウ動画」コンテンツの中心的な機能の1つです。 WEARのiOSアプリでは、MVPアーキテクチャを採用していますが、今回の対象のように適用されていない画面もあります。中心的な機能であるため、今後も改修の影響が考えられることもあり、今回のタイミングでリアーキテクチャに取り組みました。 Presenter移行の背景 アーキテクチャが適用されていない従来の画面では、責務の分離ができていないという課題がありました。これにより、以下の問題が発生していました。 画面のコードが肥大化し、可読性が低下 テストがしづらく、品質を担保しにくい 修正時に影響範囲が広がりやすい MVPアーキテクチャが導入されていないため、ViewControllerに全ての処理を実装することで、様々な処理を同時に複数抱えた、いわゆる「FatViewController」になっていました。 そこで、MVPアーキテクチャを導入し、責務を明確に分離することで、これらの課題を解決することを目指しました。 WEAR iOSにおけるPresenter Presenterは、Viewの状態管理とアクション処理を行う責務を持つコンポーネントとして定義しています。各画面の一貫性を保つため、Presenterプロトコルに準拠しています。 Presenterの構成は次の通りです。 ViewState UIを構築するための状態(値のみを持ち、副作用を排除) InputAction Viewからのアクション(ユーザー操作やライフサイクルイベント) OutputAction Viewへの命令(画面更新や遷移指示) Presenter InputActionやOutputActionに対応した処理 ViewStateを変更 実際のコードは次の通りです。 protocol Presenter < ViewState , InputAction , OutputAction >: Sendable { associatedtype ViewState associatedtype InputAction associatedtype OutputAction var state : ViewState { get } var output : (( OutputAction ) -> Void ) ? { get } func apply (outputAction output : @escaping (( OutputAction ) -> Void ) ) func send (_ action : InputAction ) async } ViewStateを追加して状態管理を分離することで、Presenter本体にはInputActionおよびOutputActionに対応した処理だけを実装でき、役割がさらに明確になります。 実施したこと Presenter移行方針と単一責任の原則に基づき、以下の対応をしました。 Presenterの追加(ロジックをViewから分離) ViewState、InputAction、OutputActionの追加(責務の明確化) テストの追加(動作保証とリファクタリングのしやすさ向上) ログ送信をPresenter側に移行 結果 Presenterの導入前後でどのようにコンポーネントが変わったのかを以下に示します。 まず、わかりやすくするためにコンポーネントをUI、仲介役、データ処理の3つの役割に分けます。Stateは状態を保持する補助的なものとします。 次に、実際のコンポーネントをそれぞれの役割で分類し、Presenter導入前後の関係図を示します。 【Before】 データ処理の部分にあるFetcherなどは実際に使用している処理を担当するクラス名です。示したもの以外にも存在し、責務が最小化された複数のクラスに分割しています。 小さい規模のプロジェクトであればこのような構成でも問題ないですが、Viewのイベントの数が多いほどViewControllerの仕事が増えてしまい、「Fat」になってしまいがちです。 【After】 ViewControllerを仲介役から切り離し、Presenterにデータの加工、ログの送信などを任せることで一気に見通しが良くなりました。 学び 内定者アルバイトを通して学んだことは、大きく3点あります。 1. コミュニケーションと言語化の重要性 タスクに慣れていないうちは、問題点や解決方針を明確に言語化できず、メンターやレビュー担当者に負担をかけてしまっていました。 タスクに取り組むスピードも大切ですが、それ以上に チームの一員として、普段から言語化を意識し、相手の負荷を減らす習慣をつけることが重要 だと学びました。 2. UIKitの理解 自分は学生時代の多くの時間をSwiftUIに費やしていたのでUIKitに慣れていませんでした(同じような学生の方はこれから増えていくのではないでしょうか?)。以下のような実装に触れましたが、UIKitをほとんど一から触ることとなり、手探り状態でとても苦労しました。 UICompositionalLayout UICollectionView UIContentConfiguration 宣言型のSwiftUIしか触れていないと手続き型の実装方法はとても複雑に感じ、理解にも時間がかかります。そのため何度も社員の方々にペアプログラミング、コードレビューをお願いしてしまいましたが、本当に親切に対応してくださる方ばかりで手探り状態の自分でもなんとか乗り越えることができ、本当に感謝しています。ZOZOの人々の良さを改めて実感しました。 3. 実務レベルで求められる視点 仕事としてのプログラミングでは、以下の点が特に重要だと感じました。 チームの生産性を意識した開発 大規模サービスならではのアーキテクチャ設計 責務の最小化と適切な分離 コードを書くことだけでなく、 長期的なメンテナンスやチーム開発を意識した設計が求められる ことを実感しました。 最後に 長期インターンを継続している方や内定者期間に他社でエンジニアのアルバイトをしている方もいますが、自分はそうでなく、技術的なことや仕事としての立ち回りなどの不安がありました。そんな中でこのような就業機会をいただけたことで、不安も払拭され、入社後のイメージもはっきりして実現したいことがいっぱいです。もし内定をもらって自分のような境遇の方がいましたらぜひ内定者アルバイトに挑戦してほしいです! ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 hrmos.co corp.zozo.com
アバター
はじめに こんにちは、WEARバックエンド部バックエンドブロックの伊藤です。普段は弊社サービスである WEAR のバックエンド開発・保守を担当しています。 WEARでは、天気予報データを活用してその日の天気に合わせたコーディネートを提案する「コーデ予報」機能を提供しています。リリース当初はコーデ予報の地域を一覧から選んで設定する必要がありましたが、2025年1月にユーザーの位置情報をもとにコーデ予報の地点を自動設定する機能をリリースしました。 本記事では、ユーザーの現在地から最寄りのコーデ予報地点を取得するために使用したアルゴリズムの詳細をご紹介します。 目次 はじめに 目次 コーデ予報とは? 背景・課題 ユーザーの位置情報から最寄りの地点をどのように特定するか? 1.ユーザーの位置情報を基に検索範囲を絞る 2.範囲内の各地点との距離を計算 3.最も近い観測地点を特定 4. 全体のアルゴリズム 動作確認 リリース後 まとめ コーデ予報とは? コーデ予報とは、気温や降水確率などの天気情報をもとに、その日のコーディネートを提案する機能です。WEARでは、ウェザーニューズが提供するWxTechの高解像度かつ高精度な「1kmメッシュ体感予報」を活用し、地域ごとの体感指数を取得しています。 体感指数とは、ウェザーニューズに寄せられたユーザーの体感報告と、天気・気温・湿度・風速などの気象データを分析し、「暑い」「寒い」「ちょうどよい」などの人の体感を10ランクに分類した指数です。この体感指数をもとに、「半袖」「長袖」などの袖丈やアイテムカテゴリーのおすすめを判別し、気温や体感に応じたアイテムやコーディネートを提案しています。 背景・課題 コーデ予報では、どの地域の天気情報を取得し、コーディネートを提案するかを設定します。リリース当初は、WEARアプリ内の地域設定で、地域一覧から手動で選択する必要がありました。設定できる地域は、各都道府県の代表的な都市を中心に103地点あります。 しかし、103地点の中から最寄りの地域を探すのは手間がかかり、直感的に判断するのも難しいという課題がありました。特に、自分の住んでいる地域が候補に含まれていない場合、どの地点を選べばよいのか迷ってしまうこともありました。その結果、地域設定するユーザーが限られ、十分に活用されないケースもありました。 そこで、位置情報を利用して最寄りの地域を自動設定する機能を導入し、よりスムーズにコーデ予報を活用できるようにしました。 ユーザーの位置情報から最寄りの地点をどのように特定するか? 103地点の位置情報はウェザーニューズから取得し、ActiveYamlで管理しています。 class WeatherMunicipality < ActiveYaml :: Base set_filename ' weather_municipalities ' field :id , type : :integer field :name , type : :string field :latitude , type : :float field :longitude , type : :float end # weather_municipalities.yml - id : 1 name : 札幌市中央区 latitude : 43.062638 longitude : 141.353921 - id : 2 name : 函館市 latitude : 41.768702 longitude : 140.728938 ... また、ユーザーの位置情報はデータベースに保持せず、リクエストごとに取得します。 WEARではSQL Serverを使用しているため、各地点の位置情報をGEOGRAPHY型でデータベースに保持することで、 location.STDistance を用いて地点間の距離を計算できます。 しかし、今回はActiveYamlで各地点の位置情報を管理しているため、Rubyのみを使用してユーザーの位置情報と各地点との距離を計算するアルゴリズムを実装しました。 1.ユーザーの位置情報を基に検索範囲を絞る ユーザーの現在地から最寄りの地点を探す際、すべての地点との距離を計算するのは非効率です。そこで、まずはユーザーの端末の位置情報をもとに所在する都道府県を特定し、その都道府県内の地点との距離のみを計算する方法を検討しました。 しかし、この方法では、県境付近のユーザーが隣接する県の地点のほうが近い場合でも、都道府県内の地点が優先されて正確に最寄りの地点を特定できない可能性があります。 次に、緯度経度をもとにエリアを均等に分割する「格子メッシュ」を導入し、各地点をあらかじめ格子メッシュに割り当てておく方法を検討しました。ユーザーの位置情報が属する格子メッシュを判定し、その格子メッシュ内の地点との距離のみを計算する仕組みです。 しかし、この方法でも、格子メッシュの境界付近にいる場合は最寄りの地点を正しく選べない可能性があるため、最適とは言えません。 そこで、バウンディングボックスを導入することにしました。バウンディングボックスとは、対象を囲む最小の矩形(四角形)を指します。通常、最小緯度・最小経度・最大緯度・最大経度の4点で定義されます。 今回は、ユーザーの現在地を中心に、指定した半径(km)の範囲をカバーする緯度経度の範囲をバウンディングボックスとして設定します。 バウンディングボックスの計算方法は以下の通りです。 (1)緯度の変化量 (2)経度の変化量 (3)バウンディングボックス ここで、 :ユーザーの現在地からの半径(km) :地球の平均半径(6371.0km) Rubyで実装すると、次のようになります。 # 地球の平均半径(km) EARTH_RADIUS = 6371.0 # @param latitude [Float] 緯度 # @param longitude [Float] 経度 # @param distance_km [Float] 半径(km) # @return [Hash] 矩形範囲 def bounding_box ( latitude :, longitude :, distance_km :) delta_lat = distance_km / EARTH_RADIUS * ( 180 / Math :: PI ) delta_lon = distance_km / ( EARTH_RADIUS * Math .cos(latitude * Math :: PI / 180 )) * ( 180 / Math :: PI ) { min_lat : latitude - delta_lat, max_lat : latitude + delta_lat, min_lon : longitude - delta_lon, max_lon : longitude + delta_lon } end バウンディングボックス内の地点は、緯度経度の範囲内にある地点を取得することで絞り込めます。以下のように実装できます。 # @param latitude [Float] ユーザーの緯度 # @param longitude [Float] ユーザーの経度 # @param search_radius_km [Float] 検索半径(km) # バウンディングボックスを定義 box = bounding_box( latitude : latitude, longitude : longitude, distance_km : search_radius_km) # バウンディングボックス内の地点を取得 weather_municipalities = all.select do |weather_municipality| weather_municipality.latitude.between?(box[ :min_lat ], box[ :max_lat ]) && weather_municipality.longitude.between?(box[ :min_lon ], box[ :max_lon ]) end この方法により、ユーザーの現在地を中心に指定した半径(km)以内の地点を効率的に絞り込むことができ、県境などに関係なく正確な最寄り地点を特定できます。 また、バウンディングボックス内に地点がない場合は、半径を拡大して再検索することで最寄りの地点を取得できます。地点の数や分布に応じて適切な初期半径を設定し、徐々に拡大することで、効率的に最寄りの地点を見つけることができます。 WEARでは、初期半径を50kmに設定し、50km刻みで半径を広げながら検索し、最大2000kmまで拡張できるようにしました。 2.範囲内の各地点との距離を計算 次に、バウンディングボックス内の各地点とユーザーの位置情報との距離を計算します。 地球上の2点間の距離は、球面上の2点間の最短距離を計算するための数学的公式である「ハーバサインの公式」を使用して求めます。ハーバサインの公式は、以下のように表されます。 (1) (角度差の二乗和) (2) (大円距離の角度) (3) (実際の地球上の距離) ここで、 :地点1と地点2の緯度の差(ラジアン単位) :地点1と地点2の経度の差(ラジアン単位) :地点1の緯度(ラジアン単位) :地点2の緯度(ラジアン単位) :地球の平均半径(6371.0km) 上記をRubyで実装すると、次のようになります。 # 地球の平均半径(km) EARTH_RADIUS = 6371.0 # @param lat1 [Float] 地点1の緯度(ラジアン単位) # @param lon1 [Float] 地点1の経度(ラジアン単位) # @param lat2 [Float] 地点2の緯度(ラジアン単位) # @param lon2 [Float] 地点2の経度(ラジアン単位) # @return [Float] 2点間の距離(km) def haversine_distance ( lat1 :, lon1 :, lat2 :, lon2 :) delta_lat = lat2 - lat1 delta_lon = lon2 - lon1 a = ( Math .sin(delta_lat / 2 )** 2 ) + ( Math .cos(lat1) * Math .cos(lat2) * ( Math .sin(delta_lon / 2 )** 2 )) c = 2 * Math .atan2( Math .sqrt(a), Math .sqrt( 1 - a)) EARTH_RADIUS * c end 3.最も近い観測地点を特定 バウンディングボックス内の各地点とユーザーの位置情報との距離をすべて計算し、最小となる観測地点を特定します。 weather_municipalities.min_by do |weather_municipality| haversine_distance( lat1 : latitude * Math :: PI / 180 , lon1 : longitude * Math :: PI / 180 , lat2 : weather_municipality.latitude * Math :: PI / 180 , lon2 : weather_municipality.longitude * Math :: PI / 180 ) end 4. 全体のアルゴリズム 上記の手順をまとめると、次のようになります。 class WeatherMunicipality < ActiveYaml :: Base set_filename ' weather_municipalities ' field :id , type : :integer field :name , type : :string field :latitude , type : :float field :longitude , type : :float EARTH_RADIUS = 6371.0 class << self # @note 指定した緯度経度に最も近い地点を取得する(検索半径を徐々に広げて地点を探し、max_radius_kmに達した場合はnilを返す) # @param latitude [Float] ユーザーの緯度 # @param longitude [Float] ユーザーの経度 # @param initial_radius_km [Integer] 検索半径の初期値(km) # @param max_radius_km [Integer] 検索半径の最大値(km) # @param radius_step_km [Integer] 検索半径のステップ(km) # @return [WeatherMunicipality, nil] 最も近い地点 def nearest ( latitude :, longitude :, initial_radius_km : 50 , max_radius_km : 2000 , radius_step_km : 50 ) search_radius_km = initial_radius_km loop do # バウンディングボックスを定義 box = bounding_box( latitude : latitude, longitude : longitude, distance_km : search_radius_km) # バウンディングボックス内の地点を取得 weather_municipalities = all.select do |weather_municipality| weather_municipality.latitude.between?(box[ :min_lat ], box[ :max_lat ]) && weather_municipality.longitude.between?(box[ :min_lon ], box[ :max_lon ]) end if weather_municipalities.any? # 地点が1つだけの場合はその地点を返す return weather_municipalities.first if weather_municipalities.size == 1 # 2つ以上の地点がある場合はハーバサインの公式で距離を計算し、最も近い地点を返す return weather_municipalities.min_by do |weather_municipality| haversine_distance( lat1 : latitude * Math :: PI / 180 , lon1 : longitude * Math :: PI / 180 , lat2 : weather_municipality.latitude * Math :: PI / 180 , lon2 : weather_municipality.longitude * Math :: PI / 180 ) end end # バウンディングボックス内に地点がない場合は、検索半径を広げて再度検索 search_radius_km += radius_step_km # 検索半径が最大値に達した場合は終了 break if search_radius_km > max_radius_km end nil end private # @note 緯度経度から指定した距離の矩形範囲を求める # @param latitude [Float] 緯度 # @param longitude [Float] 経度 # @param distance_km [Float] 半径(km) # @return [Hash] 矩形範囲 def bounding_box ( latitude :, longitude :, distance_km :) delta_lat = distance_km / EARTH_RADIUS * ( 180 / Math :: PI ) delta_lon = distance_km / ( EARTH_RADIUS * Math .cos(latitude * Math :: PI / 180 )) * ( 180 / Math :: PI ) { min_lat : latitude - delta_lat, max_lat : latitude + delta_lat, min_lon : longitude - delta_lon, max_lon : longitude + delta_lon } end # @note 2点間の距離をハーバサインの公式で求める # @param lat1 [Float] 地点1の緯度(ラジアン単位) # @param lon1 [Float] 地点1の経度(ラジアン単位) # @param lat2 [Float] 地点2の緯度(ラジアン単位) # @param lon2 [Float] 地点2の経度(ラジアン単位) # @return [Float] 2点間の距離(km) def haversine_distance ( lat1 :, lon1 :, lat2 :, lon2 :) delta_lat = lat2 - lat1 delta_lon = lon2 - lon1 a = ( Math .sin(delta_lat / 2 )** 2 ) + ( Math .cos(lat1) * Math .cos(lat2) * ( Math .sin(delta_lon / 2 )** 2 )) c = 2 * Math .atan2( Math .sqrt(a), Math .sqrt( 1 - a)) EARTH_RADIUS * c end end end 動作確認 上記のアルゴリズムを実装し、バウンディングボックス内に6地点が含まれるケースで、ユーザーの現在地が千葉県・松戸市(緯度:35.750169、経度:139.91816)の場合を検証しました。 この場合、最寄りの地点として東京都・千代田区が返却されることを期待します。 以下が実行結果です。 ❯ rails c [1] pry(main)> require 'benchmark' => false [2] pry(main)> [3] pry(main)> # 千葉県松戸市の緯度経度 [4] pry(main)> lat,lon = 35.750169,139.91816 => [35.750169, 139.91816] [5] pry(main)> nearest = nil => nil [6] pry(main)> [7] pry(main)> time = Benchmark.realtime do [7] pry(main)* nearest = WeatherMunicipality.nearest(lat, lon) [7] pry(main)* end => 0.00025700032711029053 [8] pry(main)> [9] pry(main)> puts "最寄りの地点: #{nearest.region.region_name} #{nearest.name}\t実行時間: #{time} seconds" 最寄りの地点: 東京都 千代田区 実行時間: 0.00025700032711029053 seconds => nil 想定通り、最寄りの地点として東京都・千代田区が返却されました。実行時間は0.000257秒と高速に処理されています。 リリース後 上記のアルゴリズムを導入し、2025年1月にユーザーの位置情報から最寄りのコーデ予報地点を選択できる機能を無事にリリースしました。 地域設定のUX向上により、地域登録数がリリース前の6〜7倍に増加し、大きな成果を得られました。 また、APIのレイテンシは平均20ms程度と高速に処理されており、リソース使用状況も安定しているため、ユーザーがストレスなく利用できる状態を維持しています。 まとめ 本記事では、WEARのコーデ予報機能において、ユーザーの位置情報から最寄りの地点を取得するアルゴリズムについて紹介しました。 まず、地点間の距離を効率よく計算するために、バウンディングボックスを使って候補を絞り、その後、ハーバサインの公式で正確な距離を算出しました。この方法により、高速かつ正確に最寄りの地点を特定できました。 WEARでの実装例を紹介しましたが、データの保持方法や地点数、サービスの特性に応じて最適なアルゴリズムは異なります。WEARの事例が参考になれば幸いです。 ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 hrmos.co
アバター
ZOZO開発組織の2025年2月分の活動を振り返り、ZOZO TECH BLOGで公開した記事や登壇・掲載情報などをまとめたMonthly Tech Reportをお届けします。 ZOZO TECH BLOG 2025年2月は、前月のMonthly Tech Reportを含む計8本の記事を公開しました。昨年に続き、今年も NRF Retail's Big Show のレポート記事を掲載しています。ぜひご覧ください。 techblog.zozo.com ZOZO DEVELOPERS BLOG 計測プラットフォーム開発本部 計測システム部の児島と髙橋が、計測システム部の「現在の取り組み」を紹介する記事を公開しました。海外の開発拠点であるZOZO NEW ZEALANDとの関わり方についても触れています。ぜひご一読ください。 technote.zozo.com 登壇 Developers Summit 2025 2月13日に開催された『 Developers Summit 2025 』で、技術戦略部の諸星( @ikkou )がパネルディスカッション「 「Apple Vision Pro」の登場で何が変わるのか? ITエンジニアとXRの可能性を語ろう 」のモデレーターを務めました。 2/13~14にホテル雅叙園東京で開催されるDevelopers Summit 2025に技術戦略部の諸星 @ikkou がパネルディスカッション『「Apple Vision Pro」の登場で何が変わるのか? ITエンジニアとXRの可能性を語ろう』のモデレーターとして登壇します🎙️ https://t.co/rzcXttn1a7 #devsumi #zozo_engineer — ZOZO Developers (@zozotech) 2025年2月12日 ZOZO Tech Meetup ~データサイエンス~ 2月14日に開催された『 ZOZO Tech Meetup ~データサイエンス~ 』に、3名のZOZOエンジニアが登壇しました。 【ZOZOエンジニア登壇情報】 2/14(金)開催の『ZOZO Tech Meetup ~データサイエンス~』に、ビジネスアナリティクス部に所属するデータサイエンティスト 茅原、橘、佐々木の3名が登壇します🎙️ https://t.co/6uT5E0xUP8 #zozo_ds — ZOZO Developers (@zozotech) 2025年2月13日 茅原: 因果推論が浸透した組織の現状と未来 橘: 難題に挑むデータアナリティクス:意思決定を支える分析の舞台裏 佐々木: 中途入社1年目社員が語る!ZOZOのデータ分析組織の魅力 / 意思決定の"正しさ"を測れるようにした話 イベントレポートには、登壇した3名全員の資料も掲載しています。ぜひご覧ください。 techblog.zozo.com Tokyo dbt Meetup #12 2月21日に開催された『 Tokyo dbt Meetup #12 』で、データシステム部の栁澤( @i_125 )が「 システム・ML活用を広げるdbtのデータモデリング 」というタイトルで登壇しました。 【ZOZOエンジニア登壇情報】 2/21(金)開催の『Tokyo dbt Meetup #12』に、データシステム部のAnalytics Engineerである栁澤 @i_125 が『システム・ML活用を広げるdbtのデータモデリング』というタイトルで登壇します🎙️ https://t.co/Pt9SxcAL06 #dbt_tokyo — ZOZO Developers (@zozotech) 2025年2月20日 speakerdeck.com Elasticsearch Community in Tokyo 2025 2月26日に開催された『 Elasticsearch Community in Tokyo 2025 』で、SRE部の花房が「 Elastic Cloudの特権アカウント共用から脱却! 」というタイトルで登壇しました。 【ZOZOエンジニア登壇情報】 本日2/26(木)開催の『Elasticsearch Community in Tokyo』に、SRE部の花房が「Elastic Cloudの特権アカウント共用から脱却!」というタイトルで登壇します🎙️ Elasticsearchに関心のある方はぜひご参加ください! https://t.co/S8eE0XQp98 #elasticsearchjp #elasticsearch — ZOZO Developers (@zozotech) 2025年2月26日 speakerdeck.com 【SRE特集】3社が語るSREの役割 組織の変遷と直面した課題とは? 2月27日に開催された『 【SRE特集】3社が語るSREの役割 組織の変遷と直面した課題とは? 』で、SRE部基幹プラットフォームSREブロック ブロック長の柴田が「 ZOZOTOWNにおけるSREの変遷と現在 」というタイトルで登壇しました。 【ZOZOエンジニア登壇情報】 本日2/27開催の『【SRE特集】3社が語るSREの役割 組織の変遷と直面した課題とは?』に、SRE部の柴田が「ZOZOTOWNにおけるSREの変遷と今後」というタイトルで登壇します🎙️ ZOZOTOWNのSREに興味をお持ちの方はぜひご参加ください! https://t.co/h4w0uJVNot #JobLT_findy — ZOZO Developers (@zozotech) 2025年2月27日 掲載 日本ネット経済新聞 EC&流通のデジタル化をリードする専門紙『日本ネット経済新聞』のAI特集記事で、AI・アナリティクス本部 本部長の牧野が取材を受け、ZOZOTOWNやWEAR by ZOZOにおけるAIの活用事例が紹介されました。 netkeizai.com 「Girls Meet STEM〜ITのお仕事を体験しよう〜」開催報告 2024年12月に実施した「Girls Meet STEM〜ITのお仕事を体験しよう〜」の開催報告が公開され、ZOZOの取り組みも紹介されています。 prtimes.jp Girls Meet STEMの取り組みについてはイベントレポートも公開していますので、あわせてご覧ください。 techblog.zozo.com 以上、2025年2月のZOZOの活動報告でした! ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com
アバター
はじめに こんにちは、MA部MA開発ブロックの平井です。普段はマーケティングオートメーションシステムの運用、開発を担当しています。現在、開発ブロックではリアルタイムマーケティングシステムのリプレイスプロジェクトに取り組んでいます。リプレイスプロジェクトを進める上で、性能目標を満たすためにアプリケーションのパフォーマンスチューニングが必要でした。今回、Cloud Traceを利用してアプリケーションパフォーマンスを可視化し、パフォーマンスチューニングを行ったためその知見を共有したいと思います。 この記事の内容を読むと、以下の内容について知ることができます。 Cloud Run上の処理をCloud Traceを用いて可視化する方法 Cloud Traceでトレース情報を確認する方法 Cloud Traceをパフォーマンスチューニングに活用した一例 目次 はじめに 目次 背景 課題 Cloud Traceについて Cloud Traceで処理を可視化するために アプリケーションの計装 Google Cloudサービスの各トレース情報紐付け Cloud Run AlloyDB Pub/Sub Cloud Logging Cloud Trace上でのトレース情報の見え方 AlloyDB Pub/Sub Cloud Logging パフォーマンスチューニング データベースへの最大接続数の調整 Pub/Subメッセージパブリッシュ数の削減 クエリ実行回数の削減 まとめ 背景 本記事の本題へ入る前にリプレイス先のシステムについて説明します。私たちMA部ではインフラにGoogle Cloudを利用することが多く、今回開発しているシステムもGoogle Cloudを利用しています。Cloud Run上に構築された複数のアプリケーションがPub/Subで連携し1つのシステムを構成しています。また、メインのデータベースとしてAlloyDBを利用しています。 今回は、システムを構成している個々のアプリケーションのパフォーマンス改善ポイントを特定するためにCloud Traceを利用しました。 課題 パフォーマンスチューニングを行う上で、Cloud Traceを利用するまでは以下のような課題がありました。 アプリケーション内で実際にどのような処理が実行されているかわかりづらい。 アプリケーション内の各処理の実行時間がわからない。 また、「アプリケーション内の各処理の実行時間がわからない」という課題について、具体的に実現したいことは以下です。 アプリケーション上で実行されるクエリ実行時間を知りたい。 Pub/Subメッセージのパブリッシュにかかる時間を知りたい。 Cloud Loggingに連携したログをもとに特定処理の実行時間を知りたい。 Cloud Traceを利用することで以上のような課題を解決できました。 Cloud Traceについて 今回利用した Cloud Trace はGoogle Cloudが提供する分散トレースシステムです。分散トレースシステムを活用することで、マイクロサービスシステムの処理の流れを把握できます。しかし、今回の目的はマイクロサービス全体の処理を把握することではなく、アプリケーションのパフォーマンスチューニングだったため、APM(アプリケーションパフォーマンス監視機能)として利用しました。 Cloud Traceに関する詳細な情報については 公式ドキュメント を確認してください。 APMを提供するサービスは多数ありますが、システムがGoogle Cloud上に構築されていて、利用しているサービスとの親和性が高く簡単に導入できることから、Cloud Traceを選択しました。 Cloud Traceで処理を可視化するために Cloud Traceを利用して適切に処理を可視化するためには、以下を実現する必要がありました。 アプリケーションの計装 Google Cloudサービスの各トレース情報紐付け これから、これらのポイントについて実際のアプリケーションコードを使って説明します。今回実装したシステムではGoを使用しているため、説明に使用するのはGoのソースコードです。 アプリケーションの計装 「計装」とは分散トレーシングの文脈でよく使用される用語です。「アプリケーションを計装」するとは、アプリケーションにトレース機能を組み込み、トレース情報を収集できるようにすることを指します。今回実装したシステムではOpenTelemetryフレームワークを利用して計装を実現しています。GoでOpenTelemetryを構成するために必要な設定については 公式ドキュメント を参考にしてください。 Google Cloudサービスの各トレース情報紐付け Cloud Run Cloud Runではリクエスト受信時に自動でトレース情報を生成します。ただ、アプリケーションにOpenTelemetryを構成したとしても自動生成されたトレース情報がアプリケーションのトレース情報と紐づかないため処理を可視化するには不十分です。 今回実装したシステムではルーティングライブラリに chi を利用しています。以下のようにotelhttpのハンドラをミドルウェアとして登録することで、Cloud Runが自動生成したトレース情報とアプリケーションのトレース情報が紐づきます。 func tracingHandler(next http.Handler) http.Handler { return otelhttp.NewHandler(next, "http-request" ) } func NewRouter(conf *config.Config) (*chi.Mux, error ) { //その他のコードが続く //otelhttpハンドラの登録 router.Use(tracingHandler) //その他のコードが続く return router, nil } AlloyDB AlloyDBで実行されたクエリはトレース情報として自動的にCloud Traceへ送信されます。ただ、何も設定しない状態だとCloud Runが自動生成したトレース情報と紐づいていないため、完全に別の処理として認識されてしまいます。今回実装したシステムではGORMをO/Rマッパーとして利用しているため、GORMが用意している go-gorm/opentelemetry を利用しました。 以下が実際のアプリケーションコードを簡素化したものです。 func NewPostgresHandler(config *config.Config) *PostgresHandler, err { dsn := "host=localhost user=gorm password=gorm dbname=gorm port=9920 sslmode=disable TimeZone=Asia/Tokyo" db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{ Logger: customLogger, }) if err != nil { return nil , err } //その他のコードが続く err = db.Use(tracing.NewPlugin(tracing.WithoutMetrics())) if err != nil { return nil , err } //その他のコードが続く } func (h *PostgresHandler) GetDB(ctx context.Context) *gorm.DB { tx, ok := ctx.Value( "transaction" ).(*gorm.DB) if ok { return tx } return h.db.WithContext(ctx) } NewPostgresHandler 関数ではDB接続設定をしています。 db.Use(tracing.NewPlugin(tracing.WithoutMetrics())) の部分でopentelemetryライブラリを利用することを宣言しています。 GetDB はリポジトリ層から呼び出される *gorm.DB を返すメソッドです。重要なのは最後の return h.db.WithContext(ctx) でContextを渡している部分です。Contextを利用してトレース情報を伝播するため *gorm.DB に渡す必要があります。 Pub/Sub Pub/Subに関してもトレース情報は自動生成されます。そのため、アプリケーションのトレース情報と自動生成されるトレース情報を紐づける必要があります。 func PublishMessage(ctx context.Context, projectID string , message *pubsub.Message, isLoggingBody bool ) error { client, err := pubsub.NewClientWithConfig(ctx, projectID, &pubsub.ClientConfig{ EnableOpenTelemetryTracing: true , }) if err != nil { return err } defer client.Close() t := client.Topic(topicID) defer t.Stop() result := t.Publish(ctx, message) //その他のコードが続く } PublishMessage はPub/Subメッセージをパブリッシュする関数です。 NewClientWithConfig 関数でクライアントを初期化する際に EnableOpenTelemetryTracing を true に指定する必要があります。この設定をすることでメッセージAttributesの googclient_traceparent フィールドに親トレースIDが入り、このフィールドの値をもとにトレース情報を伝播できます。 Cloud Logging Cloud Loggingに連携したログデータをトレース情報として紐づけるためには、構造化ログの 特定フィールド に適切な値を設定する必要があります。 type TraceHandler struct { slog.Handler project string } func (h *TraceHandler) Handle(ctx context.Context, record slog.Record) error { path := "projects/" + h.project + "/traces/" if s := trace.SpanContextFromContext(ctx); s.IsValid() { record.AddAttrs( slog.String( "logging.googleapis.com/trace" , path+s.TraceID().String()), slog.String( "logging.googleapis.com/spanId" , s.SpanID().String()), slog.Bool( "logging.googleapis.com/trace_sampled" , s.TraceFlags().IsSampled()), ) } return h.Handler.Handle(ctx, record) } func NewTraceHandler(baseHandler slog.Handler, project string ) *TraceHandler { return &TraceHandler{Handler: baseHandler, project: project} } func InitLogger(config *config.Config) error { // logger設定のコードが続く traceHandler := NewTraceHandler(baseHandler, config.GcpProject) logger := slog.New(traceHandler) // logger設定のコードが続く } こちらはloggerを設定しているコードです。今回実装したシステムでは構造化ロギングパッケージとして log/slog を利用しています。 Handle メソッドはslog.HandlerのHandlerメソッドをオーバーライドしています。 その実装でログレコードに必ず logging.googleapis.com/trace 、 logging.googleapis.com/spanId 、 logging.googleapis.com/trace_sampled が設定されるようになっています。 SpanContextFromContext でctxから現在のトレース情報を取得して各フィールドに適切なフォーマットで値を指定しています。こうすることで、現在のトレース情報とログを紐づけることができ、Cloud Traceの画面上で可視化できるようになります。 Cloud Trace上でのトレース情報の見え方 以上の設定をするとCloud Traceに処理を可視化できます。 ここで、用語の説明をすると、画面上に表示される1つのプロセス全体を「トレース」と呼び、その中の各処理を「スパン」と呼びます。 一番上の /sample_service と記載されているスパンがCloud Runのロードバランサーが生成したスパンです。 http-request がCloud Run内の処理の起点となるスパンでその下にアプリケーションで実行された各処理が可視化されています。 このトレース情報を見るとCloud Runがリクエストを受け取った後に数回のクエリを実行して、Pub/Subメッセージをパブリッシュしていることがわかります。また、各スパンをクリックするとスパンの詳細情報を確認できます。 補足ですが、Cloud Runは一定レートでサンプリングを行いトレース情報を生成しているため、全てのリクエストのトレース情報を見ることは出来ません。 これから、アプリケーション上で利用されている各サービス毎の情報について説明したいと思います。 AlloyDB gorm.XXX と記載されているのがアプリケーション上から実行されたAlloyDBへのクエリを表すスパンです。 スパン詳細情報を確認すると、トレース情報からわかるクエリ実行時間の他に rows_affected 、クエリステートメントなど処理を理解するために有用な情報を確認できます。これらの情報は go-gorm/opentelemetry が自動で設定してくれるものです。 また、AlloyDBにはQuery Insightsという機能があります。この機能では、AlloyDBクラスター毎に遅いクエリの検索や、クエリの実行計画の確認などができます。Query Insightsから確認できるクエリ情報とトレース情報が紐づいていて、Cloud Trace画面へとリンクされています。そのため、Query Insightsで特定できた遅いクエリがどのアプリケーションで実行されているのかを簡単に把握できます。 Pub/Sub こちらがPub/Subパブリッシュ処理を表すスパンです。 sample-topic create のsample-topicがトピック名になるため、どのトピックに対してパブリッシュしたかを確認できます。スパン詳細情報を確認するとメッセージのBodyは確認できないもののBodyサイズなどは確認できます。トレース詳細画面からPub/Subのflow controlが行われ、その後にbatchパブリッシュが行われていることがわかります。 Cloud Logging Cloud Loggingのログ情報はトレース詳細情報に白丸として確認できます。対象のログイベントがスパン上にプロットされているため処理中のログの実行タイミングを把握できます。 クリックすると発行されたログを一覧で確認できます。 パフォーマンスチューニング 以上のようにCloud Traceにトレース情報を可視化できました。次にCloud Traceを利用して実際に実施したパフォーマンスチューニングをいくつか説明したいと思います。 データベースへの最大接続数の調整 アプリケーションに本番相当のデータを流したところ想定よりも性能が出ませんでした。 そこでトレース情報を確認したところ、以下の画像のように各クエリの実行間隔が空いていました。 各クエリ実行の間に重い処理が無かったため、DBへのコネクション待ちが発生していることがわかりました。Cloud Runの設定を確認したところ、Cloud Runの最大同時実行数よりもデータベースへの最大接続数が少なく設定されていました。つまり、アプリケーション側から作成できる接続数をクエリ発行数が上回ってしまい、コネクション待ちが発生していたという状況でした。そのため、データベースへの最大接続数を上げることでコネクション待ちを解消できました。 以下は修正後のトレース情報です。クエリが間隔を空けずに実行されていることがわかります。 クエリ実行間隔については処理を可視化しなければ気づけなかったため、Cloud Traceを利用したメリットがありました。 Pub/Subメッセージパブリッシュ数の削減 トレース情報を確認したところPub/Subメッセージのパブリッシュに想定よりも時間がかかっていることがわかりました。かつデータを1件毎パブリッシュしていて、1回のプロセスにおけるパブリッシュ回数が多く、全体の処理時間が増加していました。 そこで、今まで1つずつパブリッシュしていたデータを配列で1つにまとめ、パブリッシュ数を減らしました。メッセージ1件のサイズは大きくなったものの、パブリッシュの実行時間はほぼ変わらなかったため全体の実行時間を大きく改善できました。 クエリ実行回数の削減 トレース情報を確認したところ、以下のように単独のクエリ実行時間は短かったものの、その実行回数が多く全体の処理時間を増加させていました。 そこで、アプリケーションコードとクエリを修正することでクエリ実行回数を減らしました。以下の画像からわかるようにクエリ自体の実行時間は増えましたが、実行回数を減らしたメリットが大きく、全体の実行時間を改善できました。 「Pub/Subメッセージパブリッシュ数の削減」「クエリ実行回数の削減」に関してはある程度予想できましたが、トレース情報を確認することでより確信を持つことができました。 まとめ 本記事では、Google CloudのCloud Traceを活用してアプリケーションパフォーマンスを可視化し、パフォーマンスチューニングに取り組んだ事例をご紹介しました。Cloud Traceでデータを可視化することで、対象アプリケーションの実際の処理を理解でき、改善ポイントをより素早く特定できました。 ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com
アバター
はじめに こんにちは、ブランドソリューション開発本部FAANS部でAndroidアプリを担当している田中です。本記事ではバグ件数削減の施策の1つとしてFAANS Androidで実施したJetpack ComposeのUIテストの自動化についてご紹介します。 目次 はじめに 目次 背景 Firebase Test Labについて 料金について UIテストを記載する build.gradleの設定 UIテストで使用するテストファイル 1. 特定の文字列が表示されているかのテスト 2. 特定のコンポーネントが表示されているかのテスト 3. アイコン押下で意図したダイアログが表示されているかのテスト GitHub ActionsでFirebase Test Labを実行する 1.Google CloudのAPIの有効化 2.サービスアカウントとCloud Storageバケットの作成 Cloud Storageバケットの種類の選定 サービスアカウントの作成 3.GitHub ActionsからGoogle Cloudへの認証 マルチモジュールプロジェクトでFirebase Test Labを実行する (1)差分モジュールを検出 (2)変更のあったモジュールごとにテストAPK作成 (3)GitHub Actionsで利用できるように一時的にアップロードする (4)Google Cloud SDKのセットアップ (5)テストAPKごとのFirebase Test Lab実行 UIテストを導入してみて テストケースの作成状況 導入後のQAにおける不具合の検出件数について 終わりに 背景 FAANSでは、リリースまでに下記のようなフローを踏んでいます。 昨年、変更行数は約6万行で開発期間が約半年の比較的大きなリリースを伴う案件がありました。品質保証を担当するQAチームによるテスト実施において、Androidアプリは166件という非常に多くの不具合の指摘を受けてしまいました。 FAANSでは、各案件のリリース後、QAチームが品質管理の観点から開発プロセスの改善点を見出す品質報告会を実施しています。その品質報告会で当案件について、Androidで検知された不具合のうち約8割がQA実施フェーズに入る前の段階で検知可能であると共有されました。 Androidチームでも不具合を分析してみると、たしかにその多くが仕様に対する認識漏れや実装漏れといった開発フェーズで対処できる軽微な内容でした。 このような軽微なバグがQA実施フェーズまで流れ込むのを防ぐため、Androidチームでは不具合を検知・防止するアプローチの1つとしてUIテストを導入することにしました。 本記事では、Firebase Test Labを活用してGitHub Actions上でUIテストを自動化した方法と、その運用状況について紹介します。 Firebase Test Labについて 以前弊社が公開した Firebase Test Labを使ったAndroidアプリのテスト に詳しい内容が記載されているのでご参照ください。 Firebase Test Labは、GoogleのFirebaseが提供するクラウドベースのアプリテストサービスです。多様な実機や仮想デバイス上で自動化されたテストを実行し、アプリの動作やパフォーマンスを検証できます。 本記事では、UIテストを実行するためにFirebase Test Labで実行できる Instrumentation Test について触れます。Firebase Test Labを活用することで、GitHub Actionsを用いた自動テストにUIテストを組み込むことが可能になります。 料金について Test Labの利用料金は、1日あたりのテスト実行数や実行時間で算出されます。 FAANS AndroidはFirebaseのBlazeプランを利用しており、また、Firebase Test Labでは仮想デバイスを使用するテストを選択しました。その場合の料金体系は以下のとおりです。 無料枠: 1日あたり60分のテスト時間 超過時: 各仮想デバイスにつき1時間あたり$1 料金の詳細はTest Lab公式ドキュメントの 割り当て をご覧ください。 UIテストを記載する 本記事ではFirebase Test Labの運用についての内容を主軸としているためUIテストについての説明は比重を低くしています。 UIテストを記載、実行するために以下を設定します。 build.gradleの設定 dependencies { androidTestImplementation "androidx.test:runner: $androidXTestVersion " androidTestImplementation "androidx.test:rules: $androidXTestVersion " androidTestImplementation "androidx.compose.ui:ui-test-junit4: $compose_version " debugImplementation( "androidx.compose.ui:ui-test-manifest: $compose_version " ) } android { defaultConfig { testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } } 詳細な設定方法については、 Instrumented Testsの設定 および Compose レイアウトをテストする をご参照ください。 UIテストで使用するテストファイル デフォルトで作成される ExampleInstrumentedTest.kt があれば、最低限テストを実行できます。このファイルは module-name/src/androidTest/java/ に配置されます。 /** * Instrumented test, which will execute on an Android device. * * See [testing documentation](http://d.android.com/tools/testing). */ @RunWith (AndroidJUnit4 :: class ) class ExampleInstrumentedTest { @Test fun useAppContext() { // Context of the app under test. val appContext = InstrumentationRegistry.getInstrumentation().targetContext assertEquals( "jp.android.faans.base.test" , appContext.packageName) } } FAANS AndroidではFragmentごとにテストを作成しています。実際に記載しているテストケースを3つ紹介します。 1. 特定の文字列が表示されているかのテスト このテストでは、投稿された動画一覧の画面で投稿が0件の場合に、特定の文言が表示されるかを確認します。 onNodeWithText は特定の文字列が画面に表示されている場合に検知でき、 assertIsDisplayed() で表示されているかを検証できます。 @Test fun when_video_items_is_empty_then_display_enroll_video_nothing_text() { composeTestRule.setContent { FaansTheme { RegisterRelatedVideoScreen( onNavigationIconPressed = {}, dispatchAction = {}, state = fakeSuccessState.copy(videoItems = emptyList()), initialSelectedVideoItems = emptyList(), onEnrollButtonClicked = {}, ) } } composeTestRule.onNodeWithText( "ノウハウ動画は \n まだ投稿されていません" ).assertIsDisplayed() } 2. 特定のコンポーネントが表示されているかのテスト こちらのテストではUIを構成するStateがLoadingの状態の時にローディングのコンポーネントが表示されているかをテストしています。 onNodeWithTag は、 Modifier.testTag で設定したタグ名のコンポーネントを検知でき、1と同様に assertIsDisplayed() で表示されているかを検証できます。 @Test fun when_state_is_loading_then_ui_is_loading() { composeTestRule.setContent { FaansTheme { RegisterCoordinateTagScreen( onNavigationIconPressed = {}, dispatchAction = {}, state = RegisterCoordinateTagState.Loading, onEnrollButtonClicked = {}, ) } } composeTestRule.onNodeWithTag( "progress" ).assertIsDisplayed() } 3. アイコン押下で意図したダイアログが表示されているかのテスト このテストでは、特定のアイコンを押下した後に表示されるボタンを選択した際、意図したダイアログが正しく表示されるかを確認します。 assertIsDisplayed() を使った検証は上記2つと同様で、 performClick() を利用することで特定のコンポーネントを押下できます。 @Test fun when_video_delete_menu_tap_then_is_display_video_delete_dialog() { composeTestRule.setContent { FaansTheme { CoordinateDetailScreen( page = 0 , state = fakeCoordinateState.copy( isTransitionFromRanking = false , wayDetail = fakeWayDetail, ), actionDispatcher = { }, onVolumeChanged = { }, onPopBackStack = { }, onTransitionToReviewComments = { _, _, _ -> }, onTransitionToEditPage = { _, _ -> }, onTransitionToBreakDownDetail = { _, _ -> }, onNavigateWearPreview = {}, onNavigateVideoRegistration = {}, ) } } composeTestRule.onNodeWithTag( "know_how_menu_icon" ).performClick() composeTestRule.onNodeWithText( "投稿を削除する" ).performClick() composeTestRule.onNodeWithText( "削除の確認" ).assertIsDisplayed() composeTestRule.onNodeWithText( "ノウハウ動画を削除しますがよろしいですか?" ).assertIsDisplayed() composeTestRule.onNodeWithText( "削除する" ).assertIsDisplayed() composeTestRule.onNodeWithText( "キャンセル" ).assertIsDisplayed() } 他にも performScrollTo() を利用した画面のスクロール操作や performTextInput() を利用した文字入力によるUIのテストなども作成しています。 Jetpack ComposeのUIテストで可能なことは、公式ドキュメントの テスト早見表 にまとめられているのでご参照ください。 GitHub ActionsでFirebase Test Labを実行する Firebase Test Labを使ったUIテストを自動化させるにあたり、 CIシステムでテストする ではJenkins CIでの実行について記載があります。 FAANS AndroidではCIシステムをGitHub Actionsで行なっているため、 要件 に記載がある手順を自チームの環境に適応させて以下の流れで導入しました。 Google CloudのAPIの有効化 サービスアカウントとCloud Storageバケットの作成 GitHub ActionsからGoogle Cloudへの認証 こちらの手順について、FAANS Androidで実施した内容を説明します。 1.Google CloudのAPIの有効化 要件 に記載がある通り、 Google Cloud Testing API と Cloud Tool Results API を有効にします。 2.サービスアカウントとCloud Storageバケットの作成 Cloud Storageバケットの種類の選定 要件 には、Test Labで使用するサービスアカウントに関して次のような記載があります。 Create a service account with an Editor role in the Google Cloud console and then activate it. 「Editor role」とは、Google CloudのIAMの基本ロールの1つである編集者ロール( roles/editor )を指しています。 このロールはGoogle Cloudプロジェクト内の全てのプロダクトに跨る大量のIAM権限を有した強い権限を持つため、サービスアカウントへの使用はセキュリティの観点から一般的には推奨されません。 そのため、編集者ロールを付与したサービスアカウントの使用はできれば避けたいと考えました。 調査したところ、Test Labで生成されたテスト結果保存先のCloud Storageバケットとして、以下の2種類から選択できることが分かりました。 構築・管理不要なFirebaseが自動管理するデフォルトバケット 自前で構築・管理する独自バケット 独自バケットを利用すると、編集者ロールよりもずっと少ないIAM権限(※後述)で済みます。ただし、デフォルトバケットはテスト結果の保持期間に90日という制約がある一方、ストレージにかかるコストが無料である点にメリットがあります。 FAANSでは、Test Lab上で想定されるテストの実行頻度や保存期間を基に、独自バケットのストレージコストを見積もりました。その結果、独自バケットの場合に上乗せされるCloud Storageのコストとセキュリティ強度のトレードオフを踏まえ、独自バケットを使用することに決めました。以降は独自バケットを使用する前提の説明になります。 以下は、独自バケットを作成するTerraform定義の例です。 resource "google_storage_bucket" "firebase_test_lab" { name = "<バケット名>" location = "us-central1" uniform_bucket_level_access = true # Firebase Test Labのテスト結果を過去7日分保持する場合のライクサイクル設定 lifecycle_rule { action { type = "Delete" } condition { age = 7 } } } gcloud CLIでTest Lab実行時、独自バケットへテスト保存は「 gcloud firebase test android run 」の --results-bucket で指定可能です。 サービスアカウントの作成 独自バケットを利用する場合、Firebase Test Lab用サービスアカウントに以下2つの事前定義ロールを付与する必要があります。 Firebase Test Lab Admin( roles/cloudtestservice.testAdmin ) Firebase Analytics Viewer( roles/firebase.analyticsViewer ) 詳細な権限設定については公式ドキュメントの IAM 権限リファレンス ガイド を参照してください。 サービスアカウントと、必要なIAMロールを付与するためのTerraform定義例は以下の通りです。 resource "google_service_account" "firebase_test_lab" { account_id = "firebase-test-lab" } resource "google_project_iam_member" "firebase_test_lab" { for_each = toset ( [ "roles/firebase.analyticsViewer" , "roles/cloudtestservice.testAdmin" , ] ) project = <Google CloudのプロジェクトID> role = each.key member = "serviceAccount:$ { google_service_account.firebase_test_lab.email } " } 次の手順で行う「GitHub ActionsからGoogle Cloudへの認証」にて上記で作成したサービスアカウントを利用します。 3.GitHub ActionsからGoogle Cloudへの認証 gcloud CLIでテストを開始する ではGoogle Cloud SDKをダウンロードしてgcloud CLIにログインするといった手順が記載されています。GitHub Actionsの場合は以下のyaml定義で、Google Cloudへの認証とCloud SDKのインストールをCIの処理で実行できます。 - name : Authenticate to Google Cloud uses : google-github-actions/auth@v0.4.0 with : # 上記で作成したサービスアカウントと連携済みのWorkload Identityプロバイダーを指定。 workload_identity_provider : 'projects/123456789/locations/global/workloadIdentityPools/my-pool/providers/my-provider' # 上記で作成したサービスアカウントを指定。 service_account : 'my-service-account@my-project.iam.gserviceaccount.com' - name : Set up Google Cloud SDK uses : google-github-actions/setup-gcloud@v2 Google Cloudへの認証をGitHub Actionsで行う際は以下のアクションを利用できます。 google-github-actions/auth@v0.4.0 google-github-actions/setup-gcloud@v2 以上でFirebase Test Labを使用する前準備が完了します。 マルチモジュールプロジェクトでFirebase Test Labを実行する FAANS Androidは複数のGradleモジュールが存在するマルチモジュールプロジェクトです。 Firebase Test LabのInstrumentation Testは、モジュールごとに実行されます。 マルチモジュールプロジェクトの場合、全体をテストするには1度のCIの実行で全モジュールのテストを実施する必要があります。 しかし、毎回全モジュールをテストすると膨大なテストが走ってしまうため、テスト実行回数を抑制することにしました。 そのため、FAANS Androidでは差分のあるモジュールのみを対象にテストを実施しています。 以下の流れで実際にテスト実行回数を制御しています。 (1)差分モジュールを検出 次のコードを1つのステップで実施します。 1.ベースブランチのフェッチ # Fetch the base branch echo "Fetching the base branch: refs/heads/$BASE_REF" git fetch origin refs/heads/$BASE_REF || git fetch --all 2.プロジェクトの全モジュール名を格納 # Get the list of modules echo "Getting the list of modules..." modules=$(./gradlew projects --quiet | grep -oP "^.*--- Project ':\K[^\']+" ) modules_list=$(echo "$modules" | sed 's/:/\//g' | tr '\n' ' ' | sed 's/[[:space:]]*$//' ) ここでは feature/mypage といったモジュールがあったときに、 featureとfeature/mypage が取得されます。 親の階層の feature 自体は不要なので後続のステップで取り除く形としています。 3.ベースブランチと作成中のブランチの差分ファイルを検出 # Determine changed files echo "Getting the list of changed files..." changed_files=$(git diff --name-only origin/$BASE_REF $GITHUB_SHA) echo "Changed files:" echo "$changed_files" | tr ' ' '\n' 4.差分ファイルがあるモジュールの割り出し # Identify changed modules echo "Identifying changed modules..." changed_modules="" for module in $modules_list; do if echo "$changed_files" | grep -q "^$module/" ; then changed_modules="$changed_modules $module" fi done 5.余分な空白の削除 # Clean up leading/trailing spaces changed_modules=$(echo $changed_modules | sed 's/^ *//' ) echo "Changed modules detected: $changed_modules" 6.差分モジュールが存在するかの確認 # Set environment variable or skip tests if [ -z "$changed_modules" ] ; then echo "No changed modules detected. Skipping instrumentation_test." echo "SKIP_TESTS=true" >> $GITHUB_ENV else echo "Tests are required for changed modules: $changed_modules" echo "changed_modules=$changed_modules" >> $GITHUB_ENV echo "SKIP_TESTS=false" >> $GITHUB_ENV fi SKIP_TESTS フラグでテスト実施不要な場合の制御を行なっています。 changed modules には、 base domain domain/domain のようなモジュール名が格納されます。 (2)変更のあったモジュールごとにテストAPK作成 変更があったモジュール毎にassembleDebugAndroidTestを実行してテストAPKをそれぞれ作成します。 - name : Build test APK if : env.SKIP_TESTS == 'false' run : | for module in ${{ env.changed_modules }}; do echo "Building test APK for module $module" if [[ $module == */* ]] ; then ./gradlew :${module//\//:}:assembleDebugAndroidTest || true else ./gradlew :$module:assembleDebugAndroidTest || true fi done ${module//\//:} は、モジュール名中のすべての / を : に置き換える処理 :app:module:assembleDebugAndroidTest の形でテストを行う必要があるため、 app/module を app:module の形に変換する || true を設定しているのはモジュールの階層の親でテストが存在しないモジュールも含まれてしまっているため、そのような場合のテストが失敗した時にテストが止まらないようにするため (3)GitHub Actionsで利用できるように一時的にアップロードする 各モジュールのテストAPKは以下に格納されます。 /$module/build/outputs/apk/androidTest/debug/*-debug-androidTest.apk それぞれのモジュールに配置されたテストAPKを利用しやすい形にするため、GitHub Actions上の ./test-apks というパスに作成されたテストAPKを配置するようにします。 - name : Archive assembled test APKs if : env.SKIP_TESTS == 'false' run : | mkdir -p ./test-apks for module in ${{ env.changed_modules }}; do apk_path=$(find . -type f -path "*/$module/build/outputs/apk/androidTest/debug/*-debug-androidTest.apk" ) if [ -n "$apk_path" ] ; then echo "Found test APK for module $module: $apk_path" cp "$apk_path" ./test-apks/ else echo "No test APK found for module $module" fi done - name : Check if test-apks directory has files if : env.SKIP_TESTS == 'false' run : | if [ -z "$(ls -A ./test-apks)" ] ; then echo "No test APKs found. Setting SKIP_TESTS=true." echo "SKIP_TESTS=true" >> $GITHUB_ENV fi - name : Upload test APKs if : env.SKIP_TESTS == 'false' uses : actions/upload-artifact@v4 with : name : assembled-test-apks path : ./test-apks (4)Google Cloud SDKのセットアップ GitHub ActionsでFirebase Test Labを実行する セクションの「3.GitHub ActionsからGoogle Cloudへの認証」に記載した設定方法を実行します。 (5)テストAPKごとのFirebase Test Lab実行 ./test-apks に格納されているテストAPKごとにInstrumentation Testを実行することで、差分があるモジュールの数だけFirebase Test Labが実行されます。 - name : Run Firebase Test Lab if : env.SKIP_TESTS == 'false' run : | for test_apk in ./test-apks/*.apk; do gcloud firebase test android run \ --type instrumentation \ --app app/build/outputs/apk/debug/app-debug.apk \ --test "$test_apk" \ --results-bucket=<作成したCloud Storageの独自バケットの名前> \ --device model=lynx,version=33,locale=ja_JP,orientation=portrait done 以上の設定を含むGitHub Actionsのワークフローファイルをリポジトリに配置することで、GitHub ActionsからFirebase Test Labを利用した自動テストを実行できます。 UIテストを導入してみて テストケースの作成状況 現在のテストケース作成状況は以下のとおりです。 テストが存在しているモジュールは18個 プロジェクト全体で作成済みのテストケースは154個 FAANS Androidで最もテストケースを作成しているモジュールには現状79個のテストケースが存在します。 こちらのモジュールでテストを実施した際、Firebase Test Labの実行時間は40秒ほどでした。 差分を検出したモジュールが1つの場合一連のテスト実行時間は大体20分程度になっています。FAANS Androidで別途、並列実施しているCIと実行時間があまり変わらないため、現状は開発の生産性を落とすようなテストとはなっていません。 また、CI上でGoogle Cloudへの認証から、APKのビルド、そして全モジュールに対するInstrumentation Testの実施完了までにかかる合計時間は約40分となります。 (なお、この時間はFirebase Test Labの従量課金で計算される実行時間とは異なります) 1つのプルリクエストで全てのモジュールにコード差分が発生することは基本的にはないので目安程度の数字です。 導入後のQAにおける不具合の検出件数について UIテスト導入前の開発案件(変更行数は約6万行、開発期間は約半年)では、166件の不具合検出がありました。 一方、UIテストを実装した案件(変更行数は約6万行、開発期間は約4か月)では、不具合検出が23件に減少しました。 また、QAチームからの品質報告会でもQA実施時に検出された不具合の内、QA実施前に検出されるべき不具合の件数が非常に下がったと共有を受けることができました。 UIテスト導入は、実装者とレビュアーが満たすべき仕様を正確かつ効率的に把握しやすくなった点やリグレッションを検知しやすくなった点で、品質向上に一定の効果が得られたと感じています。 終わりに 本記事ではFAANS Androidチームにおける、UIテストの自動化と現状のテスト運用について紹介しました。 UIテストの導入により、実装者とレビュアーは仕様誤認や実装漏れを防ぎやすくなり、開発プロセスの品質向上に大きく繋がりました。 しかし、現状は差分があるモジュールであれば実行対象になってしまうので明らかにUIに影響がないような差分でもテストが実行されてしまうなど、まだまだ改善の余地があります。 また、UIテストについても重要度が高いテスト内容とは何かなど、テストの質についても日々チームで議論を重ねています。 今後もチームとしてさらに品質向上に努めていければと思います。 Firebase Test Labの導入を検討している方がいれば、ぜひ参考にしてみてください。 ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com
アバター
はじめに こんにちは、ZOZOMO部の中島です。普段は Fulfillment by ZOZO や ZOZOMO店舗在庫取り置き というサービスの開発を担当しています。 2025年1月12日から14日の3日間にかけてニューヨークで開催された「NRF 2025: Retail's Big Show」に現地参加してきました。私個人としては、昨年に引き続き2回目の参加になります。 前半はNRF Retail's Big Showの概要と関連する情報、後半はセッションの内容やExpoで気になったものを中心にお伝えします。NRF 2025全体の概要については、 NRF 2025 Event Recap などをご覧ください。 目次 はじめに 目次 NRF Retail's Big Showとは 会場の概要 セッション オープニングセッション NRF 2025イベントの紹介 小売業におけるAIの活用事例 Physical AIとロボット訓練 キャリアに関するアドバイスと業界の展望 オープニングセッションまとめ Expo 生成AI 気になったデバイス ORA Sphere iRomaScents あなたはどのGame Changer? おわりに NRF Retail's Big Showとは NRF会場のエントランス NRF Retail's Big Show は、毎年1月にNRF(National Retail Federation)が主催する、世界最大級の小売業界向け展示会です。2025年は1月12日から14日までニューヨークで開催され、175のセッションが行われ、1,000社以上の企業が出展しました。 毎年、NRF Retail's Big Showにはテーマが設定されており、2025年のテーマは「GAME CHANGER」でした。 今年のテーマ:GAME CHANGER 今回も、公式サイトではおすすめホテルの紹介や、イベントをより快適に楽しむための「NRF 2025アプリ」が提供されました。私自身もアプリを活用し、事前に見たいセッションを登録しました。当日はスケジュール管理やセッション開始時間のチェックに役立てました。 NRF 2025アプリ 会場の概要 NRF 2025は、2024年と同様に Jacob K. Javits Convention Center で開催されました。 Jacob K. Javits Convention Centerの外観 2階がメインエントランスになっており、カンファレンスパス購入時に発行される2次元コードを使って受付をします。 NRF 2025の受付 NRF 2025に参加するためのカンファレンスパスは、All-Access PassとExpo Passの2種類があります。Expo PassだとExpo会場のみ参加可能で、Keynoteセッションなどが開催される会場に入るには、All-Access Passが必要になります。 前回は、All-Access Passの値段が時期によっては結構差がありましたが、今回はあまり時期による変動が大きくなかったです。ただ、早ければ早いほど安く購入できますので、次回参加する予定の方は、夏くらいから動き始めると最安で購入できると思います。ちなみに、受付後にバッジをもらいますが、そのバッジを紛失すると再発行にカンファレンスパス購入時と同じ金額が必要になります。そのため、受付のときに絶対なくさないように念押しされてバッジを渡されました。 早朝はDonuts Dunkというものがあり、ドーナツなど軽食を食べながらネットワーキングできる時間が設けられています。今年も数社、日本企業の方とコミュニケーションを取ることができました。 Donuts Dunkで提供されるドーナツや果物 セッション NRFでのセッションは、Javits Centerの1階、3階、4階、5階の各会場で実施されます。参加パスの種類によってアクセスできるセッションが異なります。All-Access Passを持っていればすべてのセッションを視聴できますが、Expo Passの場合はExpoが行われる1階と3階のセッションのみに限られます。 5階フロア全体を使用するSAP Theatreステージは、最も大きな会場となっており、ここでKeynoteセッションが開催されます。各ステージにはスポンサー名が付いており、前回と名称が変わらなかったため、毎年同じスポンサーがついているのかもしれないです。 オープニングセッション オープニングセッションでは、Walmart U.S.のPresident and CEOであるJohn Furner氏と、NVIDIAのVice President and General Manager, Retail & CPGであるAzita Martin氏の対談が行われました。NRFは小売業向けの展示会ですので、例年だと小売企業の幹部がオープニングセッションに登壇していました。今回はNVIDIAが登壇したことからも、AIに対する業界の関心が非常に高まっていることがうかがえます。 このセッションでは、以下のようなテーマが取り上げられました。 NRF 2025イベントの紹介 小売業におけるAIの活用事例 Physical AIとロボット訓練 キャリアに関するアドバイスと業界の展望 それぞれの内容について、以下で詳しく紹介します。 NRF 2025イベントの紹介 セッションの冒頭では、NRFイベントの紹介が行われました。NRFは1911年に始まり、114年の歴史を誇るイベントであり、現在では40,000人の参加者、6,200のブランド、450人のスピーカー、175のセッションを擁する大規模イベントへと成長しています。また、ニューヨーク市には約7,500万ドルの経済的影響をもたらしているとのことです。さらに、小売業界の成長率についても言及され、2024年の成長率は2.5%から3.5%の範囲で予測されています。 小売業におけるAIの活用事例 小売業界におけるAIの導入事例として、以下の企業の取り組みが紹介されました。 L'Oreal:マーケティングの分野で生成AIを活用し、コンテンツ制作や顧客対応を強化。 Walmart:膨大なデータを処理し、SKUと店舗の何億もの組み合わせを毎週予測。需要予測の精度向上に貢献。 Lowe's:1,700店舗でデジタルツインを導入し、店舗レイアウトの最適化と業務効率化を推進。 これらの事例からも、小売業界におけるAI活用の広がりと、データ分析やシミュレーション技術の進化がうかがえます。 Keynoteで紹介されたAI活用の動画はこちらになります。 www.youtube.com Physical AIとロボット訓練 次に、Physical AIの概念とその応用について紹介がありました。Physical AIについて初めて聞いたため調べたところ、 NVIDIAの公式サイト に説明がありました。要約すると、「現実世界の空間や物理法則を認識・理解し、適切な判断を下しながら自律的に行動できるAI」と定義されています。 Keynoteの紹介では、倉庫や配送センターのデジタルツインを作成し、Physical AIを活用することで、現実世界では想定できない数多くのシナリオをシミュレーションできるようになるとのことでした。物理法則を考慮したシミュレーションデータを利用することで、ロボットの学習が効率的になり、より高度なAIロボットの開発が期待されます。 この技術によって、物流・倉庫業務におけるAIの応用範囲が拡大し、業務効率の向上とコスト削減の可能性があります。特に、デジタルツイン技術と組み合わせることで、よりリアルなシミュレーションが可能になり、試行錯誤のコストを削減しながらロボットの最適化が実現できます。 キャリアに関するアドバイスと業界の展望 セッションの最後には、キャリア開発に関するアドバイスが語られました。特に以下の点が強調されました。 AI技術の活用が今後ますます重要になること 継続的な学習の必要性 キャリア開発におけるリスクを計画的に取ること これからの小売業界では、AI技術が不可欠な要素となるため、企業だけでなく個人のスキルアップも求められる時代になっていくでしょう。 オープニングセッションまとめ NRF 2025のオープニングセッションでは、小売業界におけるAIの活用がいかに重要であるかが強調されました。WalmartやL'Oreal、Lowe'sなどの企業がすでにAIを活用して成果を上げていることからも、この分野の進化が加速していることが分かります。また、Physical AIとロボット訓練の進展により、物流業界における自動化がさらに進んでいくかもしれません。 AIが小売業界に与える影響は今後も大きくなると予想されます。これに伴い、業界関係者はAI技術への理解を深め、積極的に活用していくことが求められそうです。 Expo Javits Center内の1階と3階が主なExpo会場になっていました。Expo会場は、多くの企業がブースを出展し、最新のテクノロジーやサービスを紹介しています。また、各ブースではデモンストレーションやプレゼンテーションが行われており、訪れた参加者に直接情報を提供していました。 1階、3階、RIVER PAVILIONのフロアマップ ブースの中でも、RIVER PAVILION内の「NRF 2025 Innovators Showcase」エリアや、1階の「Startup Hub」エリアに多くのスタートアップ企業が集まっています。ブースの回り方として、私はまず最新の技術やトレンドを確認するためにこれらのエリアを巡り、その後、3階にある大手企業のブースを訪れる流れでExpoを回りました。 生成AI 前回NRFに参加した際は、生成AIについて取り組み始めているという温度感だったものが、今回のNRFでは生成AIに関連しないものを探すほうが難しいくらいAIに溢れていました。また、生成AIに関してはスタートアップより、大手企業の方が網羅的にカバーしている印象が強かったです。 AWS展示、左からDiscover、Find、Purchase、Post-Purchaseが表現されている 上記はAWSのブース展示です。Discover、Find、Purchase、Post-Purchaseというカスタマージャーニーがモデルで表現されています。このカスタマージャーニーに沿ったパーソナライゼーションをAWSのサービスを使って提供する仕組みについて紹介されていました。 最初に、対象の製品とブランドやキャンペーンのテーマを決めると、自動的に、各フェーズで顧客を引き付けるための推奨事項を提供できるとのことでした。 具体的には、ユーザーごとにパーソナライズされた画像やメールのコピー文の作成や、購入商品に合わせたレコメンデーション、バーチャルアシスタントでの購入後のユーザーサポートなどができるとのことです。 左:3Dホログラムでの商品表示、右:AIアシスタントが商品説明 他にも、以前見たことがあった Proto という製品も進化していました。大きな3Dホログラムのディスプレイというだけではなく、生成AIを活用して商品の説明を音声で提供する機能が追加されていました。さらに、英語だけでなく日本語にも対応しており、言語の壁を超えて情報を伝えられる点は、生成AIならではの進化だと感じました。 気になったデバイス Startup Hubのブースを回っている中で、特に印象に残ったデバイスを2つ紹介します。 ORA Sphere まず1つ目は、球体のタッチ操作が可能なディスプレイ ORA Sphere です。 このデバイスの特徴は、360度に映像を投影できるだけでなく、タッチ操作も可能な点です。球体ディスプレイ自体が珍しいですが、触れて操作できるのは新しい体験でした。主な用途としては、地球や惑星の表示などが想定されているようです。球体×ディスプレイ×タッチ操作という組み合わせがユニークで、今後の活用シーンに期待したいです。 iRomaScents 次に紹介するのは、 iRomaScents という香りを提供するデバイスです。 このデバイスには45種類の香りをセット可能で、ユーザーが選択した香りをサンプルとして提供できます。さらに、ウィザード形式で質問に答えることで、ユーザーに最適な香水をレコメンドする機能も備わっていました。 香りのパーソナライズが進化することで、店舗やECでの香水選びの体験が変わる可能性を感じました。 あなたはどのGame Changer? イベント会場を回っていると、American Expressのブースで「あなたはどのGame Changer?」という診断ゲームが実施されていたので、試してみました。性格診断のような質問に答えていくと、結果はTech Expertでした。 ちなみに、診断では以下のようなタイプに分類されるようです。 Operations Ace Tech Expert Marketing Genius Digital Professional Gen AI Specialist 「Gen AI Specialist」が診断結果に含まれることが、今回のNRFの特徴だと感じました。 American ExpressのGame Changer診断 おわりに ホテルの近くにあったエンパイア・ステート・ビル 今年のテーマが「GAME CHANGER」だったため、昨年と比べてどのような変化があるのかを意識しながら参加しました。 2024年、2025年と2年連続で参加しましたが、昨年は一部の企業が生成AIを使い始めた段階だったのに対し、今年はほぼすべての企業が生成AIを活用し、その成果も見え始めている印象を受けました。ただし、生成AIによる劇的な変化はまだ先の段階と感じており、来年以降の進化に期待しています。 今回のNRF視察は、開発部門の福利厚生である「 セミナー・カンファレンス参加支援制度 」を利用しての参加となります。 NRFは技術カンファレンスではありませんが、小売業におけるテクノロジー活用の最新事例を学べる貴重な機会です。技術者としてこうしたイベントに参加することで得られるものは大きいと感じています。また、NRF Retail’s Big Showはこれまでのニューヨークとシンガポールに加え、新たにパリでの開催も発表されていましたので、ご興味のある方はぜひ参加してみてください。 ZOZOでは、各種エンジニアを採用中です。ご興味のある方は以下のリンクからご応募ください。 corp.zozo.com 最後までご覧いただき、ありがとうございました!
アバター
はじめに こんにちは。Developer Engagementブロックの @wiroha です。2月14日に「 ZOZO Tech Meetup ~データサイエンス~ 」を開催しました。ZOZOTOWNを支える開発において「データサイエンス」にフォーカスして、弊社データサイエンティストが具体的な事例を交えながら紹介するオフラインイベントです。 登壇内容まとめ ビジネスアナリティクス部から次の3名が登壇しました。 発表タイトル 登壇者 因果推論が浸透した組織の現状と未来 マーケティングサイエンスブロック 茅原 難題に挑むデータアナリティクス:意思決定を支える分析の舞台裏 データサイエンスブロック 橘 中途入社1年目社員が語る!ZOZOのデータ分析組織の魅力 / 意思決定の"正しさ"を測れるようにした話 マーケティングサイエンスブロック 佐々木 因果推論が浸透した組織の現状と未来 マーケティングサイエンスブロック 茅原による発表 speakerdeck.com 茅原からは因果推論(因果関係を統計的に推論すること)が浸透しているZOZOにおいてどのような課題が生じているか、またその対策について発表しました。部門内外でのナレッジシェアが活発に行われているのを感じました。 難題に挑むデータアナリティクス:意思決定を支える分析の舞台裏 データサイエンスブロック 橘による発表 speakerdeck.com 橘の発表では、難題を「技術的新規性×事業的複雑性」と定義し、これまでの事例を分類して解説しました。難易度の高い案件の解決方法は参加者の参考になっていたと感じます。 中途入社1年目社員が語る!ZOZOのデータ分析組織の魅力 / 意思決定の"正しさ"を測れるようにした話 マーケティングサイエンスブロック 佐々木による発表 speakerdeck.com 佐々木からの発表で、分析部門が相対する事業部の社員はSQLを習得済みであると語ったところ、参加したみなさまは驚いていたようです。発表後のAsk the Speakerの時間でも「どうしたらこういう組織を作れるのか」といった話題がよく出ていました。 (2025/03/13追記)質問への回答 開催後のアンケートにて多数の質問をいただきました。この場にて回答いたします。 Q. 事業部門とのコミュニケーションをとるときに意識していること(特に分析のリテラシーが高くない場合)、各事例のプロジェクト運用についても詳細を知りたいです。大きなプロジェクトは運用も大変なのかなと予想しています。 A. 事業部門とのコミュニケーションでは特に「寄り添い」と「染み出し」を意識しています。まずビジネス部門に寄り添い、相手の真意を汲み取った上で使いやすいアウトプットを設計します。その上で、分析側がビジネス部門側へ染み出し、ビジネス部門側の課題設計や方針検討など、分析の根本の部分から並走して進めることを意識しています。また、各事例でのプロジェクト運用について、大きなプロジェクトでは積極的に小さなタスク粒度にブレイクダウンした上でのゴール設計を行っています。こうすることで、短いスパンで方針の再検討でき、大きなプロジェクトにありがちな「完了してからゴールがズレていることに気づいた」といった事例を防ぐことができます。 Q. 難題がどうかを分けるメリットについて。スケジュールやアサイン調整に利用するとか浮かんだのですが、他に案件難度を定量化する先のメリットがあれば、お伺いしたいです。 A. おっしゃる通り、スケジュールやアサイン調整に利用します。他の用途としては、半期に1度の評価の際に自分の成果を定量的に示すために使ったりします。 Q. ゆっくり配送のような難題案件を対応する際はアナリスト何名くらいで行なっているのでしょうか。また、ビジネス側なども含めてどのような業務分担で実施しているのでしょうか。 A. アナリスト側はシニア1名、レビュアー(マネージャー)1名の体制が基本です。シニアがビジネス側のMTGに参加し、困ったときはレビュアーに相談という形ですが、案件の難易度によってはレビュアーも全MTGに参加することがあります。ビジネス側は事業や施策の担当者が1~2名、開発が伴う場合は要件に合わせて各チームから担当者が付きます。大規模になると、全体を取りまとめるPMがアサインされる場合もあります。 Q. 施策の内容的に長期的影響(購買頻度の変化)とかも重要なのではと思ったりしたのですがどうでしょうか? A. おっしゃる通り長期的な影響もあるかもしれないのですが、送料無料CPについては月単位の予算を達成するためのギャップフィル施策として月に複数回実施しているため、現状は長期的な影響を気にしていません。 Q. 三つ目の発表に関して、意思決定後にKPIをフォローし上手く行かなかった際の組織的な振り返りについてどのようにされているかお伺いしたいです。 A. 「なぜうまくいかなかったのか?」について事業部とディスカッションを行っています。 想定通りの事象が起きていたがKPIは下がっていた 想定通りの事象が起きておらずKPIは下がっていた の切り分けを行ったうえで、各要因の仮説出し・検証を行います。 Q. 中途入社1年目社員が語る!ZOZOのデータ分析組織の魅力 / 意思決定の"正しさ"を測れるようにした話について、事業部の人がクエリを書けるとそれで簡単な検証ができてしまい、専門チームのバリューが少なくなりそうとおもいました。特に因果推論みたいな保守的な手法はかなりクリティカルかなと思うのでもっと聞いてみたかったです。 A. ご記載いただいている通り難度の低い検証は事業部の方でも可能ではありますが、他の部分で専門チームのバリューを発揮できると考えています。例えば、次のような部分です。 簡単な検証が可能となるような施策及びABテスト設計 検証後の「ではどうしたら良いのか?」という部分に対する示唆だし 発表でも記載している通り、「数値抽出」ではなく「意思決定への寄与」が分析チームに求められる部分のため、「専門チームのバリューが少なくなる」とはとらえていません。むしろ逆に、事業部が単独で簡単な検証を間違えずに行える環境・ルールを作り上げていくのも専門チームの役割と捉えています。全社の意思決定の加速を考えた時、分析チームがボトルネックにならない体制を作り上げていきたいです。 最後に 登壇したみなさん 本イベントでは発表中、多くの参加者が熱心にメモを取っており、高い関心が伝わってきました。発表後は参加者・登壇者がカジュアルに質問をし合いながら交流し、より深い情報や各社の悩みを共有する機会となりました。今後もこういったイベントを開催していきますので、ぜひご参加ください! ZOZOでは一緒に働く仲間を募集中です。ご興味のある方は以下のリンクからぜひご応募ください。 corp.zozo.com
アバター
こんにちは。千葉県の特産品として真っ先に思い浮かぶものがヨウ素 *1 な、データシステム部データ基盤ブロックの塩崎です。 この記事ではBigQueryストレージの費用を計算する方法と、費用を節約するための戦略について説明します。BigQueryストレージの費用計算をするために、まずストレージを2軸・8種類に分類し、それぞれの軸の視点から費用節約をする方法を紹介します。特にTime travel機能やFail-safe機能が関わると計算ミスをしやすくなるため、それらについても説明します。 ストレージの分類 最初にBigQueryストレージを分類するための2つの軸を説明します。1つ目の軸はライフサイクルで、これはテーブルの更新・変更・削除等の操作によって変化するものです。2つ目の軸は課金モデルで、これは非圧縮状態のデータ量で費用を計算するか圧縮済み状態のデータ量で費用を計算するかを決めるものです。 ライフサイクルは4種類の状態、課金モデルは2種類の状態が存在するため、これらの組み合わせは2*4=8種類になります。BigQueryストレージの費用の計算ミスはこれら8種類を正しく認識できていないことに起因して起こることがしばしばあります。そのため、まずはこれら8種類の分類を正しく理解することが大事です。 ライフサイクル ここから4種類のライフサイクルの状態について説明します。 Active Current Storage 最初に説明するのはActive Current Storageです。この用語は公式ドキュメントには載っていませんがとても重要な概念です。似た用語であるActive Storageという用語が公式ドキュメントで使われている一方で、その用語の意味が文脈によってまちまちであるため、いろいろな誤解が生じています。そのため、この記事では定義が曖昧なActive Storageという用語を極力使わずにActive Current Storageという用語で説明をします。 90日以内に作成・更新されたテーブルがActive Current Storageという状態になります。テーブルをロードして作成したり、既存のテーブルにINSERT・UPDATEをすると、この状態になります。 公式ドキュメントに載っているActive Storageと本記事のActive Current Storageは似ているものの別物であることを再度強調しておきます。 Long-term Storage Active Current Storageのテーブルを90日間更新せずにおくと、Long-term Storageに自動的に移行されます。また、Long-term Storageのテーブルに対してINSERT・UPDATE・DELETE等の更新処理を行うとActive Current Storageに戻ります。 Time travel Storage Time travel Storageは削除されたデータが一時的に配置される場所です。ファイルシステムにおける「ゴミ箱」のような場所です。 DELETE文で削除されたデータだけではなく、UPDATE文で上書きされたデータもTime travel Storageに送られます。これは1つのUPDATE文をDELETE文+INSERT文が組み合わさったものだと考えればわかりやすいです。 Time travel Storageに配置されているデータは、テーブルがまだ存在する場合には、 FOR SYSTEM_TIME AS OF 構文で参照できます。 cloud.google.com テーブルが削除されている場合は FOR SYSTEM_TIME AS OF の代わりにテーブルデコレーターを使って参照できます。 cloud.google.com Time travel Storageに配置されているデータは一定期間が経つと後述するFail-safe Storageに移動します。移動するまでの期間はTime travel windowという設定値で決まっています。Time travel windowの期間はデフォルトで7日で、最小2日〜最大7日の範囲で設定できます。 cloud.google.com Fail-safe Storage Fail-safe Storageは削除されたデータが一時的に保管される場所です。前述したTime travel Storageに保存されていたデータが次に送られる場所です。 Time travel Storageとよく似ていますが、以下の2点が異なります。 SQLやbqコマンドなどで復元できず、復元のためにサポート問い合わせが必要 保存期間を上書きできる設定はなく、7日間で固定されている Fail-safe Storageに送られてから7日間経過したデータは本当の意味で削除されます。 ライフサイクル 先ほど説明したライフサイクルの状態の変化を以下の図にまとめました。 課金モデル ライフサイクルの次に2つの課金モデルについて説明します。 cloud.google.com Logical課金 Logical課金は非圧縮状態のデータ量が計算式に使われる課金方法です。データ型毎のバイト数は以下のドキュメントに記載されており、テーブル内の全データに対してこの値を合算した値が使われます。 cloud.google.com Physical課金 Physical課金は圧縮済み状態のデータ量が計算式に使われる課金方法です。データの圧縮アルゴリズムなどの詳細は公開されていませんが、こちらの記事によるとデータによっては容量が1/20程度になることもあるそうです。 cloud.google.com なお、課金モデルはあくまで費用計算の時にのみ影響を与えるという点に注意が必要です。どちらの課金モデルを選んだとしても、実際には圧縮状態でデータが保存されています。そのため、以下のAthenaのパフォーマンスチューニングの記事にあるような圧縮・非圧縮によるパフォーマンスの差異は発生しません。 aws.amazon.com 変更方法 Logical課金を採用するかPhysical課金を採用するのかはデータセット単位で変更できます。変更するためのコマンドを実行してから実際に変更されるまでには24時間かかることと、一旦変更した後14日間は再変更ができない点に注意する必要があります。 cloud.google.com 費用 ここからストレージの分類毎の費用計算について説明していきます。ライフサイクル4種類、課金モデル2種類の組み合わせは8種類あります。 それぞれの費用を以下の表にまとめました。単位はUSD / GB / monthです。この記事ではUSマルチリージョンの費用を採用しています。他のリージョンの費用は異なりますが、傾向はUSマルチリージョンと同じです。 Logical課金 Physical課金 Active Current Storage 0.02 0.04 Long-term Storage 0.01 0.02 Time travel Storage 0 0.04 Fail-safe Storage 0 0.04 この表を使って考察をしていきます。 まずはActive Current Storageの行に着目して、Logical課金とPhysical課金について比較をしてみます。Logical課金はPhysical課金と比較して単価が半分であると分かります。しかし、Physical課金を採用すると圧縮済みのデータ量で計算されるためその影響も考慮する必要があります。圧縮の効果でデータのサイズが半分以下になるならば、Physical課金を採用したほうが安価になります。このことはLong-term Storageに対しても同様に言えます。実際に当社のBigQuery環境内にある多くのデータセットでは圧縮により半分以下のデータサイズになることが多いです。 次にTime travel Storageの行のLogical課金とPhysical課金の比較をしてみます。こちらについては先程よりも大きな違いがあります。課金モデルがLogical課金の場合はTime travel Storageの費用が無料な一方で、Physical課金の場合はActive Current Storageと同様の費用が発生します。Fail-safe Storageについても同様です。この違いを考慮せずに圧縮率だけを見てPhysical課金の方が安そうだから変更してしまうと、かえって費用を増加させてしまう可能性があります。 公式ドキュメントに載っている用語との対応 この記事で紹介した用語と公式ドキュメントの用語の対応を説明します。公式ドキュメントに書かれているActive Storageという用語の指し示すものが文脈によって異なるため注意が必要です。 料金表の用語との対応 BigQueryの料金表の用語との対応を考えます。以下の料金表には4つの用語が載っています。 Active storage Long-term storage cloud.google.com このActive Storageという用語は、本記事のActive Current Storageのみを指し示すようにも見えますが実際には異なります。Logical課金の場合はActive Current Storageのみを指します。Physical課金の場合はActive Storageに加えてTime travel StorageとFail-safe Storageのデータ量も合計したものを指します。特にPhysical課金の場合の後半2つが抜けやすいので注意が必要です。 INFORMATION_SCHEMA.TABLE_STORAGE 系のカラムとの対応 以下の記事の INFORMATION_SCHEMA.TABLE_STORAGE 系のカラムとの対応についても説明します。 cloud.google.com それぞれのカラムで取得できる情報が本記事のどれに対応しているのかを以下の表に示します。 カラム名 非圧縮か圧縮済か 定義 TOTAL_LOGICAL_BYTES 非圧縮 Active Current Storage + Long-term Storage ACTIVE_LOGICAL_BYTES 非圧縮 Active Current Storage LONG_TERM_LOGICAL_BYTES 非圧縮 Long-term Storage CURRENT_PHYSICAL_BYTES 圧縮済 Active Current Storage + Long-term Storage TOTAL_PHYSICAL_BYTES 圧縮済 Active Current Storage + Long-term Storage + Time travel Storage ACTIVE_PHYSICAL_BYTES 圧縮済 Active Current Storage + Time travel Storage LONG_TERM_PHYSICAL_BYTES 圧縮済 Long-term Storage TIME_TRAVEL_PHYSICAL_BYTES 圧縮済 Time travel Storage FAIL_SAFE_PHYSICAL_BYTES 圧縮済 Fail-safe Storage この表から注意が必要なことを読み取ってみます。 まず、Logical課金でのTime travel StorageとFail-safe Storageのデータ量を取得する方法がありません。取得できたところで結局これらの費用の単価はゼロなため取得できなくても実用上問題ないです。 次にPhysical課金にも目を向けます。Logical課金の場合の注意点は1つしかないですが、Physical課金では2つの注意点があります。まず、 TOTAL_PHYSICAL_BYTES はカラム名にTOTALという名前がついているものの、Fail-safe Storageを含んでいないことに注意が必要です。また、 ACTIVE_PHYSICAL_BYTES にはFail-safe Storageが含まれていないことも要注意です。特に先ほど紹介した料金表におけるActive Storageの定義と異なる点が罠になっています。料金表のActive StorageはFail-safeを含む一方で、 INFORMATION_SCHEMA.ACTIVE_PHYSICAL_BYTES は含んでいません。 Data retention with time travel and fail-safe内の用語との対応 以下のドキュメントに書かれているactive bytes/active storageという用語と本記事の用語の対応について説明します。ここでのactive bytes/active storageという用語は、本記事でのActive Current Storageに該当します。 cloud.google.com 文脈によるActive Storageという用語のばらつきについて ここまでで説明したようにActive Storageという用語(もしくはそれに似た用語)はドキュメントによって指し示すものが異なります。ある時にはActive Current StorageとTime Travel StorageとFail-safe Storageの3つを含みます。しかし、前者1つのみを指す場合も前者2つのみを指す場合もあります。そのため、Active Storageという単語を見かけた時には、その用語はどれを指しているのかを注意深く確認する必要があります。 テーブル毎・データセット毎の料金計算 この記事で紹介した知見を実践するために、テーブル毎・データセット毎の料金計算をしてみます。 まずは、以下のようにしてこの記事で紹介した4つのライフサイクルの状態毎、2つの課金モデル毎の容量をテーブル単位で取得します。Logical課金の場合のTime travel StorageとFail-safe Storageの容量は取得できませんが、それらは費用がゼロのため問題ないです。 select table_schema as dataset_name, table_name, deleted, active_logical_bytes / pow( 2 , 40 ) as active_current_logical_tb, long_term_logical_bytes / pow( 2 , 40 ) as long_term_logical_tb, (current_physical_bytes - long_term_physical_bytes) / pow( 2 , 40 ) as active_current_physical_tb, long_term_physical_bytes / pow( 2 , 40 ) as long_term_physical_tb, time_travel_physical_bytes / pow( 2 , 40 ) as time_travel_physical_tb, fail_safe_physical_bytes / pow( 2 , 40 ) as fail_safe_physical_tb, from `<プロジェクトID>`.`region-<リージョン>`.INFORMATION_SCHEMA.TABLE_STORAGE 次にそれぞれのテーブルの課金モデルがLogicalかPhysicalかを以下のクエリで取得します。課金モデルはテーブル毎ではなくデータセット毎に決定され、 INFORMATION_SCHEMA.SCHAMATA_OPTIONS に格納されています。この SCHEMATA_OPTIONS はSQLアンチパターンのEntity Attribute Valueのような構造をしているのでクエリを実行する時に注意が必要です。実際に、古くからあるデータセットは SCHEMATA_OPTIONS に storage_billing_model が格納されていないため、その場合の考慮を以下のクエリでは行ってます。 select dataset_name, ifnull(storage_billing_model, " LOGICAL " ) as storage_billing_model, from ( select schema_name as dataset_name, from `<プロジェクトID>`.`region-<リージョン>`.INFORMATION_SCHEMA.SCHEMATA ) left join ( -- SCHEMATA_OPTIONSにstorage_billing_modelが存在しないデータセットのためにleft joinをする select schema_name as dataset_name, option_value as storage_billing_model from `<プロジェクトID>`.`region-<リージョン>`.INFORMATION_SCHEMA.SCHEMATA_OPTIONS where option_name = ' storage_billing_model ' ) using (dataset_name) 最後に上記2つのクエリの結果をデータセットで結合させ、単価を掛け算するとテーブル毎の費用となります。実際にかかっている費用だけではなく、もしもLogical課金だったらPhysical課金だったらいくらになるのかも出力します。これによって、どちらの課金モデルを採用すると費用が安くなるのかを判断できます。 with storages as ( select dataset_name, table_name, deleted, storage_billing_model, ifnull(active_current_logical_tb, 0 ) as active_current_logical_tb, ifnull(long_term_logical_tb, 0 ) as long_term_logical_tb, ifnull(active_current_physical_tb, 0 ) as active_current_physical_tb, ifnull(long_term_physical_tb, 0 ) as long_term_physical_tb, ifnull(time_travel_physical_tb, 0 ) as time_travel_physical_tb, ifnull(fail_safe_physical_tb, 0 ) as fail_safe_physical_tb, from < 2 つめのクエリの結果> left join < 1 つめのクエリの結果> using (dataset_name) ) select *, if (storage_billing_model = ' LOGICAL ' , monthly_logical_cost_usd, monthly_physical_cost_usd) as monthly_actual_cost_usd, from ( select *, active_current_logical_tb * 20 + long_term_logical_tb * 10 as monthly_logical_cost_usd, (active_current_physical_tb + time_travel_physical_tb + fail_safe_physical_tb) * 40 + long_term_physical_tb * 20 as monthly_physical_cost_usd, from storages ) order by monthly_logical_cost_usd + monthly_physical_cost_usd desc そして、このクエリの実行結果をデータセットでGROUP BYするとデータセット毎のストレージ費用を計算できます。先程のクエリ結果をGROUP BYするだけなので、詳細なクエリはここでは省略します。 費用を節約するための戦略 最後にストレージ費用を削減するための戦略について紹介します。この章で書かれている内容を実践するためには開発工数が必要になることもあります。そのため、そもそもストレージ費用を削減することがベストなのかということを考える必要もあります。ストレージ費用ではなくクエリ実行費用を削減するほうが、コストパフォーマンス良く費用を削減できる可能性もあります。 パーティション分割 最初に紹介する戦略はパーティション分割です。ライフサイクルの状態がActive Current StorageになるのかLong-term Storageになるのかはテーブル単位ではなくパーティション単位で決定されます。そのため、データの変更が頻繁に発生する領域とそうでない領域を別のパーティションにすることによってLong-term Storageの比率を大きくできます。特にログを追記して蓄積するようなテーブルの場合はログのタイムスタンプでパーティション分割をすると、90日以上古いログの費用が半分になります。 また、このように設定したパーティション分割カラムはデータを参照するときのWHERE句に登場することも多いため、クエリ実行費用を削減する効果も見込めます。 Logical課金とPhysical課金の切り替え Logical課金かPhysical課金かはデータセット単位で設定できるため、先程のクエリでどちらのほうが安いのかを判断して切り替えると費用を削減できます。一旦切り替えた後14日間は切り替えできないため、作成された直後のデータセットに対して切り替えをすることには慎重になるべきです。ある程度の期間運用し、ストレージの傾向が安定してから切り替えを行ったほうが良いです。 圧縮率が高くなるようなデータ形式への変更 以下の資料によるとデータを取り込む前に前処理を施すと圧縮率の向上が見込めるそうです。確かに圧縮率を高めることでストレージ費用を削減できます。しかし、多くのケースでは先にクエリ実行費用を削減したほうがコスパよく費用を削減できると思います。 cloud.google.com Logical課金の方が安いテーブルとPhysical課金の方が安いテーブルのデータセットを分離 Logical課金の方が安いのかPhysical課金の方が安いのかはテーブル単位で決まる一方、どちらを採用するのかはデータセット単位でしか行えません。そのため、Logical課金のほうが安くなるテーブルとPhysical課金のほうが安くなるテーブルのデータセットを分離すると費用を削減できます。しかし、この方法は以下のような多くの副作用をもつため、基本的には採用しないほうが良いかと思っています。 データ利用者にとって分かり辛いデータセット分類になる危険性がある データセットの移動をする時、Long-term Storageに配置されていたデータがActive Current Storageに移動する。そのため、かえって費用が増加するかもしれない まとめ この記事ではBigQueryのテーブル費用をライフサイクルと課金モデルという2軸から体系的に説明しました。特にActive Current Storageという公式ドキュメントに載っていない概念を意識するとBigQueryの料金計算に関する解像度が上がります。公式ドキュメントに載っているActive Storageという曖昧な表現を見たときにはその用語の意味を明確にして読み進める必要もあります。 ZOZOではデータアナリスト、データマネージャー等のさまざまなポジションで一緒に働く仲間を募集中です。カジュアル面談も実施しておりますので、ご興味のある方は以下のリンクからぜひご応募ください。 corp.zozo.com www.wantedly.com *1 : 千葉県の水溶性天然ガス概要
アバター
はじめに こんにちは。データシステム部推薦基盤ブロックの新卒1年目の上國料( @Kamiko20174481 )と、5年目の宮本( @tm73rst )です。私たちのチームでは、ZOZOTOWNの推薦システムを開発しています。2024年7月のテックブログでは、ZOZOTOWNのホーム画面に表示される「 モジュール 」の並び順をパーソナライズする取り組みを紹介しました。 techblog.zozo.com モジュール とは、トレンドやキャンペーンなど特定のテーマに基づき商品群を表示する枠のことです。 モジュールの内容は企画チームの意図に基づいて設定されますが、ユーザーごとに関心や求めるコンセプトが異なるため、一律の表示ではなく最適な順序で並べることが重要です。 このように、ユーザーごとに適したモジュールを配置する仕組みを モジュールパーソナライズ と呼びます。本記事では、このモジュールパーソナライズの精度を向上させるために実施した「モジュールの多様性向上」と「受注系指標の改善」についてご紹介します。 パーソナライズ機能や推薦システムの開発に携わる方々の参考になれば幸いです。 目次 はじめに 目次 モジュールパーソナライズ Two-Towerモデルによる推薦 モジュールパーソナライズの仕組み モジュールパーソナライズをリリースしてわかった課題 1. 多様性の欠如 パーソナライズロジックの過度なクリック履歴依存 モジュール間での商品の重複表示 2. 購入促進の難しさ 改善のアプローチ パーソナライズの方針 Two-Towerモデルの精度向上 後処理の改善・導入 A/B テスト 概要 リリースを行う際の実験群の選定方針 結果 受注系指標 多様性に関する指標 Treatment1(クリック最適化モデル + 後処理)について Treatment2(カート投入最適化モデル)について Treatment3(カート投入最適化モデル + 後処理)について 今後の展望 パーソナライズのリアルタイム化 商品の並び順のパーソナライズ 推薦指標の定式化 最後に モジュールパーソナライズ ZOZOTOWNではユーザーの興味を引く商品を効果的に訴求するため、ホーム画面に多様なモジュールを表示しています。しかし、すべてのユーザーに同じモジュールを一律に表示すると、ユーザーによっては興味の薄いコンテンツが前面に出てしまい購買促進につながりにくいという課題がありました。 そこで、 ユーザーの嗜好に応じてモジュールの並び順を最適化する「モジュールパーソナライズ」 を導入しました。本手法の中核となるのが Two-Towerモデル です。 Two-Towerモデルによる推薦 モジュールパーソナライズは、以下の図に示す Two-Towerモデル を利用して構成されています。 Two-Towerモデルでは、 ユーザーの特徴 と 商品の特徴 をそれぞれ個別のニューラルネットワーク(タワー)で学習し、共通の埋め込み空間へマッピングします。類似した特徴を持つユーザーと商品が近い位置に配置されるよう学習されるのが特徴です。 具体的には、 ユーザーの属性 (例:年齢、性別、閲覧履歴など)を元にユーザー埋め込み(ユーザーの嗜好を表すベクトル)を計算し、 商品の属性 (例:カテゴリ、ブランド、価格帯など)を元に商品埋め込み(商品の特徴を表すベクトル)を計算します。 推薦する際は、ユーザー埋め込みと候補商品の埋め込みのコサイン類似度を算出し、ユーザーの嗜好に合った商品をランキング化します。 モジュールパーソナライズの仕組み ZOZOTOWNのホーム画面における推薦対象は 商品単体ではなく、複数の商品を含むモジュール です。そこで、以下の手順で モジュール単位のランキング を行います。 各商品の推薦スコアを算出(Two-Towerモデルによるスコアリング) モジュール内の商品のスコアを集約 統合スコアが高い順にモジュールを並び替え このフローにより、ユーザーの興味に合ったモジュールが上位に表示され、より最適なコンテンツを提供できるようになります。 モジュールパーソナライズをリリースしてわかった課題 モジュールパーソナライズの導入により判明した課題は、 多様性の欠如 と 購入促進の難しさ の2点です。 1. 多様性の欠如 多様性はやや抽象的な概念ですが、ここでは以下の2つの観点で課題を整理します。 パーソナライズロジックの過度なクリック履歴依存 ユーザーの商品クリック履歴は、特定のカテゴリや同一商品に偏る傾向があります。これは購入検討時に類似商品を比較したり、関心のあるカテゴリを集中的に探索するといった行動が要因です。しかし、従来のモデルは 直近のクリックデータに最適化 されているため、その履歴に強く依存してしまうリスクがあります。その結果、最近クリックしたカテゴリの商品ばかりが上位に表示され、ユーザーの探索の幅が狭まります。また、一時的に興味を持った商品や意図せずクリックした商品が学習へ影響を与え、ユーザーの本来の嗜好を正確に捉えにくくなる可能性もあります。 モジュール間での商品の重複表示 従来のZOZOTOWNのホーム画面では、同じ商品が複数のモジュールにわたって重複表示されるケースがありました。 特に、モジュールごとに横スクロールなしで表示される商品群は、サイト全体の印象やユーザーの探索行動に大きく影響を与えるため、重要な要素です。本稿では、これらの商品群を「ファーストビュー」と定義します。 なお、一般的に「ファーストビュー」は 縦スクロールなしで画面上部に表示される範囲 を指すことが多いですが、本稿では 各モジュール内で横スクロールなしに表示される商品群 を意味する点にご注意ください。 ファーストビューで同一商品が繰り返し表示されると、以下の問題が発生します。 商品の偏り :特定の商品やカテゴリへの露出が過剰になり、多様な商品が目に留まりにくくなる 新しい発見機会の減少 :同一商品の露出が増えることで、ユーザーが新しい商品に出会う機会が制限される 探索意欲の低下 :同じ商品が目立つことで新鮮さが薄れ、ユーザーの縦スクロールや横方向への探索意欲が低下しやすくなる このため、ファーストビューでの商品重複は、ユーザー体験の質を低下させる要因となっています。 2. 購入促進の難しさ 従来のロジックを用いたA/Bテストの結果、受注に関する指標のさらなる向上の可能性が明らかになりました。特に、パーソナライズロジックがクリック履歴に強く依存していることで、ユーザーの本来の嗜好を正確に捉えきれていない可能性があります。この影響により、推薦されたモジュール内の商品がユーザーの購入意欲を十分に喚起できず、購買へつながりにくい状況になっていると考えます。 改善のアプローチ パーソナライズの方針 課題を解決するために、図に示すように以下の対応をします。 Two-Towerモデルの精度向上 後処理の改善・導入 これにより、ユーザーの嗜好に即したモジュールのスコアリングを行い、後処理によってより多様な商品がユーザーに届くよう順序を最適化し、上述の課題を解決します。ここでいう「ユーザーの嗜好に即した」とは、 購入へつながる確度が高い ものを指します。 Two-Towerモデルの精度向上 今回モデルを改善する目的は、「ユーザーの短中期的な嗜好を学習し、それに即した多様な推薦をし最終的に購入に促す」ことです。主な改善ポイントは、以下の2点です。 最適化指標の変更 UserTowerの特徴量の変更 まず、 最適化指標 については、従来の「クリック」から「カート投入」に切り替えました。クリックは単なる興味関心の指標に過ぎないのに対し、「カート投入」はユーザーが実際に購入を検討した行動であり、より購買意向に近い嗜好を反映していると考えたためです。一方で、「購入」を最適化指標として採用しなかったのは、データの絶対量が少なく、学習が不安定になる懸念があったためです。そのため、 クリックよりも購買意向に近く、かつ十分なデータが確保できる「カート投入」 を最適化指標として選定しました。 次に、 UserTowerの特徴量 については、これまで「直近のクリック履歴(時系列データ)」のみを利用していましたが、 過去1か月のクリックログを活用する方針に変更 しました。中期的なログを学習することで、ユーザーの継続的な嗜好を捉え、より多様な商品を推薦しやすくなると考えています。具体的には、「商品」「ブランド」「カテゴリ」「カラー」などの複数の軸でユーザーの関心が高い要素を集計し、上位のものを抽出します。さらに、これらの情報に時系列の重み付けをし、直近の行動と過去の一貫性をバランスよく反映できるよう工夫しています。 細かな変更点としては、データ分割におけるトランザクション期間を1週間に設定し、訓練・検証・テストデータそれぞれで四分位範囲を用いたフィルタリングを行いました。これにより、学習から評価までの過程でデータ分布が大きく偏る(Skew)リスクを最小限に抑えつつ、分割の一貫性を高めることができます。 以下では、従来モデルを クリック最適化モデル 、改善後のモデルを カート投入最適化モデル と呼ぶことにします。 後処理の改善・導入 後処理を導入する目的は、「Two-Towerモデルでスコアリングされたモジュール群から、重複や偏りを抑えつつ多様な商品をユーザーに届け、体験価値を向上させる」ことです。主な改善ポイントは、以下の3点です。 モジュールのスコア計算手法を変更 類似モジュールの連続表示を抑制 関心度に応じたモジュールの再配置 まず、 モジュールのスコア計算手法の変更 についてです。従来は、モジュール全体の商品を対象にスコアを計算していました。今回はホーム画面の縦スクロールというUI特性に合わせ、 ファーストビューのみを対象にスコアを計算するよう変更 しました。これにより、ユーザーが実際に注目する領域に基づいた評価が可能となり、より適切なモジュールの順位付けが期待できます。 次に、 類似モジュールの連続表示の抑制 についてです。従来、同一商品を含むモジュールが連続して上位に表示されることがありました。これは、異なるモジュール間でスコアを独立に算出するため、高スコアの同一商品を含むモジュールが複数上位に配置されやすかったことが原因です。そこで、 各モジュールのファーストビューに同じ商品が多く含まれている場合、スコアが低いモジュールの順位を下げる仕組みを導入 しました。これにより、ホーム画面上での商品の重複表示を抑え、多様性の向上を図ります。 最後に、 関心度に応じたモジュールの再配置 についてです。埋め込みを用いたスコアリングには予測誤差が生じるため、ユーザーの本質的な嗜好を完全には捉えきれない可能性があります。そこで、 直近でユーザーが閲覧したもののクリックしなかったモジュールは迅速に下位へ移動させ、新しいモジュールを上位に推薦する仕組みを導入 しました。これにより、ユーザーの関心に即したモジュールを素早く提示し、飽きを防ぎつつ最適なモジュールを推薦できます。 A/B テスト 概要 新ロジックの効果を評価するために、ZOZOTOWN会員を対象として 5週間のA/Bテスト を実施しました。現行ロジックと前述のアプローチを組み合わせた 以下の4つのパターン を用意しました。また、テスト対象のユーザーが各実験群に均等に振り分けられるよう、4つのグループそれぞれに25%ずつ割り当てています。 実験群 説明 Control クリック最適化モデル Treatment1 クリック最適化モデル + 後処理 Treatment2 カート投入最適化モデル Treatment3 カート投入最適化モデル + 後処理 リリースを行う際の実験群の選定方針 A/Bテスト開始前に、プロジェクトメンバー間でリリース判断の基準を明確化しました。具体的には、受注系指標だけでなく、多様性に関する指標も考慮することを原則としました。ただし、多様性が向上してもKGIやKPIが悪化する場合はリリースを見送る方針とし、全メンバーで認識を統一しました。 結果 A/Bテスト結果のサマリを受注系の指標と多様性に関する指標に分けて以下に示します。 受注系指標 指標 備考 T1/C 比(%)  T2/C 比(%)  T3/C 比(%)  ホーム画面訪問者の受注金額 ホーム画面にランディングしたユーザーの合計受注金額 99.9 100.2 100.1 モジュール経由の受注金額 モジュールに表示された商品の合計受注金額 100.2 100.9 101.2 モジュール経由の受注商品点数 モジュールに表示された商品の合計受注商品点数 100.4 101.2 100.9 モジュール経由のカート投入数 モジュールに表示された商品の合計カート投入数 100.2 101.4 101.3 多様性に関する指標 指標 備考 T1/C 比(%)  T2/C 比(%)  T3/C 比(%)  モジュール掲載商品のユニーク閲覧数 表示商品の合計ユニーク閲覧数 101.8 99.8 101.4 モジュール掲載商品のユニーククリック数 表示商品の合計ユニーククリック数 100.5 100.0 100.4 モジュール間の閲覧商品の重複率 モジュール間で同じ商品を閲覧した割合 92.0 98.4 91.5 モジュールの重複率 前日と比較して同じモジュールが表示された割合 98.3 106.3 106.1 商品の多様性 ユーザーごとのユニーク閲覧商品数 / 全体の閲覧商品数 100.4 100.0 100.3 カテゴリの多様性 ユーザーごとのユニーク閲覧カテゴリ数 / 全体の閲覧商品数 100.3 101.3 101.6 ブランドの多様性 ユーザーごとのユニーク閲覧ブランド数 / 全体の閲覧商品数 99.8 99.0 99.2 Treatment1(クリック最適化モデル + 後処理)について 従来のロジックでは、特定の商品や子カテゴリに偏りが生じ、新しい商品の発見機会が制限されていました。しかし、商品の多様性を考慮した後処理を追加した結果、ユーザーが閲覧する商品のバリエーションが広がり、 多様性に関するほぼすべての指標が飛躍的に向上 しました。さらに、クリック数や閲覧ページ数の増加も確認され、ユーザーが新しい商品を発見しやすくなったことが示されました。このことから、後処理の導入が興味喚起の向上に寄与したことが実証されたと考えられます。 一方で、 受注系指標への大きな影響は見られませんでした 。その要因の1つとして、モジュールのスコアリングに使用した既存のクリック最適化モデルが、直近のクリックログに引っ張られ偏ったカテゴリや商品を推薦していた点が挙げられます。そのため、後処理を追加しても一部のケースではユーザーの嗜好を十分に反映できていなかった可能性があります。 Treatment2(カート投入最適化モデル)について カート投入最適化モデルは、「短中期的な嗜好を学習し、多様な推薦をすることで購入を促進する」という設計意図を実現し、 受注系指標が大幅に向上 しました。短中期的な嗜好の学習により、カテゴリや商品の偏りを抑えながら一貫性のある推薦をし、ユーザーに幅広い選択肢を提供した結果、売上向上に寄与したと考えられます。 一方で、「ブランドの多様性」は減少しましたが、定性評価ではユーザーの関心が高いブランドの露出が増加し、過去のクリック履歴に含まれていないブランドも、嗜好に基づく推薦が適切に機能していることが確認されました。 また、「モジュールの重複率」は増加しましたが、A/Bテスト結果を基に追加分析を行ったところ、重複したモジュールは売上と正の相関を持つことが判明しました。この結果から、モジュールの重複は必ずしもネガティブな影響を与えるとは言えず、むしろユーザーに適切な商品を継続的に提示することが有効な手段となり得ると考えられます。 しかし、 商品の多様性やユニーク商品に対する閲覧数・クリック数の向上は確認されませんでした 。この要因として、短中期的な嗜好を学習することで、ユーザーの過去の行動に基づいた推薦が強化され、一貫性のある提案が可能になったことが挙げられます。これにより、関連性の高い商品が表示されやすくなる一方で、ログの即時性が低下し、推薦内容の変化が抑制される傾向があると考えられます。 Treatment3(カート投入最適化モデル + 後処理)について カート投入最適化モデルと後処理を個別に適用した場合、それぞれ特定の指標には良い影響を与えたものの、一部の指標にはネガティブな影響も見られました。しかし、両者を統合したモデルでは互いの弱点を補完し合い、全体的なスコアの向上が確認され、各指標のバランスを取ることができました。 結論として、 カート投入最適化モデルによりユーザーの嗜好に即したスコアリングをするとともに、後処理でより多様な商品がユーザーに届くよう順序を最適化することで、受注系指標と多様性に関する指標の向上を両立させることができました 。 この結果を踏まえ、受注系指標および多様性に関する指標を総合的に評価した上で、Treatment3のリリースが決定しました。 今後の展望 モジュールパーソナライズのTwo-Towerモデルをカート追加最適化モデルに切り替え、後処理を追加することで、 多様性と受注系の指標の向上 を実現しました。今後は以下のような取り組みを行う予定です。 パーソナライズのリアルタイム化 商品の並び順のパーソナライズ 推薦指標の定式化 パーソナライズのリアルタイム化 カート投入最適化モデルへの切り替えにより、モジュール内の商品の重複率が増加し、ユニークな閲覧数やクリック数が減少しました。これは、短中期的な嗜好を学習した結果として生じたものであり、意図した効果ではあるものの、さらなる改善の余地があります。 現行のシステムでは、推論パイプラインが1時間に1回実行され、ユーザーのパーソナライズ情報が更新されます。従来のロジックでは、更新頻度を上げると直近の閲覧履歴に過度に影響され、ユーザーの短期的な嗜好が強く反映されるという課題がありました。しかし、カート投入最適化モデルでは、中期的な嗜好も考慮することで、短期的な変動に左右されにくい推薦が可能になっています。 この特性を活かし、リアルタイムでのモジュール更新を導入すれば、短期嗜好への過剰適応を抑えつつ、ユーザーの関心に応じた商品を最適なタイミングで推薦できると考えています。 商品の並び順のパーソナライズ 現在、一部のモジュールではユーザーごとに商品がパーソナライズされているものの、多くのモジュールでは共通の商品が表示されています。また、モジュールパーソナライズだけでは、ユーザーごとの微妙な嗜好の違いを十分に捉えきれない場合があります。例えば、同じカテゴリの商品を表示するモジュールであっても、ユーザーごとに好むブランド、価格帯、デザインの傾向は異なります。 現行のシステムでは、表示する商品の選定は行われているものの、ユーザーごとの関心度に応じた並び順の最適化は行われていません。そのため、より個々の嗜好に適した推薦を実現するには、商品の表示順をユーザーごとに最適化することが重要です。現在、このパーソナライズ機能の開発を進めています。 推薦指標の定式化 今回の取り組みでは多様性を重視しましたが、今後は新規性やセレンディピティなど、さらなる推薦指標の改善にも取り組む予定です。推薦指標は、企業やサービスごとに定義が異なるため、適切な指標の選定が重要になります。 そこで、私たちが目指すZOZOTOWNのホーム画面に最適な推薦指標を定義し、それをどのように改善につなげるかをチーム内で議論しながら具体化していきます。 最後に 本記事ではZOZOTOWNのホーム画面に表示するモジュールの並び順をパーソナライズするシステムとその効果について紹介しました。今回取り上げた部分以外にも改善すべき箇所が大量にあるので、これからも1つずつ改善していくことでユーザーにとってより良いZOZOTOWNを提供できるよう邁進していきます。 ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com
アバター
はじめに データシステム部検索技術ブロックの内田です。私たちはZOZOTOWNの検索精度改善や検索システムの運用効率化のためのメンテナンスなどに取り組んでいます。 これまでテックブログでご紹介してきた通り、ZOZOの検索改善チームではランキング学習(Learning to Rank)やクエリの意図解釈、ベクトル検索の導入など、比較的モダンなアプローチでZOZOTOWNの検索改善に努めてきました。先進的な技術を調査し、サービスの開発に応用することはサービスの品質改善において重要な取り組みです。 techblog.zozo.com しかし、モダンなアプローチをとる一方で、検索エンジンのベーシックな設定についてはメンテナンスする機会が徐々に減少していきました。設定内容や経緯を把握している開発メンバーの割合も減っていき、このままだと誰も触れない謎の設定になってしまうリスクがあったため、一度見直しを実施することにしました。 この記事では、全文検索エンジンの基礎的な設定について見直しを始める際に意識した内容を紹介します。これから検索システムを構築する方、全文検索エンジンの設定について学ぶ方の参考になれば幸いです。 全文検索システムの設定の基礎 全文検索とは、与えられたクエリにマッチするテキスト情報を持つ文書を発見する技術です。Luceneを代表とする全文検索エンジンは、文書に含まれるテキスト情報を解析して索引(インデックス)を構築することでクエリにマッチする文書の発見の高速化を実現しています。 ここで重要となるのが、 文書中のテキスト情報からインデックスに登録されるトークンをどのように抽出するか という問題です。トークン抽出の品質は、全文検索全体の精度に大きく影響を与えます。そのため、トークン抽出を担うアナライザー(解析器)の設定は非常に重要です。 Apache SolrやElasticsearchなどのLuceneベースの全文検索エンジンでは、アナライザーは以下のような三層構成をとります。入力が想定されるクエリや文書に含まれるテキスト情報に応じてそれぞれを設定します。クエリに対して適用するアナライザーと文書中のテキスト情報に適用するアナライザーは別々に設定できますが、出力されるトークンの一致が取れるよう設計する必要があります。 Character filter:入力されたテキストに対する加工処理 Tokenizer:トークンへの分割処理 Token filter:トークンに対する加工処理 テキスト情報に応じたアナライザー設定 N-Gramと形態素解析 先述した通り、検索エンジンに文書を登録する際にはテキスト情報をトークンに分割する必要があります。英語など西洋の多くの言語では空白文字で単語が区切られますが、日本語の文章は明確な区切り文字を持ちません。そのため、日本語で構成される文書に対しては、N-Gramと形態素解析に基づいたトークン分割が採用されることが多いです。ZOZOTOWNの検索システムでも検索対象フィールドに対しては、この2種のトークン化手法に基づいたアナライザーが設定されています。 N-Gramは文章をN文字ごとに区切ります。一方で形態素解析は辞書に基づき意味のある単位で文字列を区切ります。 利点 欠点 N-Gram ・検索漏れが少ない(再現率が高い) ・辞書が不要 ・検索ノイズが多い ・無差別に分割するため、インデックスのサイズが肥大化しがち ・N文字以下の文字列入力に対してトークンを出力できない 形態素解析 ・検索ノイズが少ない(適合率が高い) ・インデックスのサイズが小さめ ・品詞に基づくフィルタリングや変形が可能 ・辞書が必要 ・辞書に過不足があり単語を正しい位置で区切れなかった場合に検索精度が低下する (主に未知語による検索漏れが起こりやすい) この通り、N-Gramと形態素解析はそれぞれ得意とする領域が異なります。どちらを適用するかは取り扱うテキスト情報の特徴に合わせて検討する必要があります。例えば、ZOZOTOWNで取り扱う商品のテキスト情報には以下のような特徴があります。 商品名:主に名詞で構成されるため、品詞に基づく処理を必要としない。ファッション系の商品名はカタカナの連語で構成される固有名詞(未知語)であることが多く、形態素解析器では正しい位置で区切ることが難しい。 商品説明文:入力テキストが大きく、比較的多量のトークンが抽出される。日本語の文章で記述されていて、検索上意味を持ちにくい助詞などの形態素を数多く含むため品詞によるフィルタリングを行いたい。 これら2つの手法を適用したフィールドを横断的に検索するクエリを用いることで検索の精度を向上させることが出来ます。代償として検索処理の計算コストやインデックスのサイズおよび構築時間が膨らむため、それに見合った精度向上が期待できるかは検証するとよいでしょう。また、それぞれのフィールドに対するスコア重み付けの調整も重要です。 辞書 現在、ZOZOTOWNの検索システムに設定されている各種辞書の見直しに取り組んでいます。見直しを進める中で、逆に検索精度面に問題を発生させてしまっている設定が見受けられました。その一例を紹介します。 形態素解析器のユーザ辞書 ZOZOTOWNの検索システムは形態素解析器としてKuromojiを採用しています。Sudachiなど他の形態素解析器では挙動が異なる可能性があるためご注意ください。 形態素解析器の辞書は形態素解析処理の根幹となるため、検索の精度に大きく影響を及ぼします。文書中に出現する語句を多くカバーするためにユーザ辞書に単語を片っ端から追加してしまいがちです。ZOZOTOWNの検索システムの形態素解析ユーザ辞書にも過去に様々な語句が登録されていました。 ここで注意が必要なのが、ユーザ辞書に登録された語は優先的に区切られやすくなるというKuromoji tokenizerの挙動です。例えば、ZOZOTOWNではアクセサリーの略語である「アクセ」がユーザ辞書に登録されていました。これにより、「アクセ」を部分文字列に含む文章は概ね「アクセ」トークンを含むように分割されるようになります。結果として「アクセサリー」を意図して「アクセ」というクエリで検索したユーザに対して、全く関連のない商品を返してしまうことがありました。 GET _analyze { " tokenizer ": { " user_dictionary_rules ": [ " アクセ,アクセ,アクセ,カスタム名詞 " ] , " type ": " kuromoji_tokenizer " } , " text ": [ " アクセント " ] } // Output: { " tokens ": [ { " token ": " アクセ ", " start_offset ": 0 , " end_offset ": 3 , " type ": " word ", " position ": 0 } , { " token ": " ント ", " start_offset ": 3 , " end_offset ": 5 , " type ": " word ", " position ": 1 } ] } このような挙動は辞書に登録された短い語句で頻繁に発生していました。そのため、文書に登場する未知語を片っ端からユーザ辞書に登録するのではなく、以下のケースに絞って登録する方針で整理を進めています。 語句が意図せず区切られてしまっている場合、区切らせないためにその語句を登録する 途中で区切られてほしい語句(フレーズ)が区切られない場合、その語句の区切り方を登録する シノニムの辞書 検索の再現率を向上させるためにSynonym (graph) token filterを利用することがあります。辞書に語句の同義関係を記述することで、検索時にトークンの拡張を行えます。 Synonym token filterは、元のトークンとシノニム拡張で得られたトークンを同等の重みで扱う点に注意が必要です。スコア計算式によっては、拡張後のトークンにマッチした文書が元のトークンにマッチした文書よりも高いスコアを持つことがあります。ヒット件数を増やしたいという動機で軽率に同義関係を増やしてしまうと、検索の精度に悪影響を及ぼす危険性があります。 例えばZOZOTOWNでは、シノニム辞書に「リング」と「指輪」が同義語として登録されていました。「指輪」で検索した際に、ピンキーリングなどの指輪商品をヒットさせたかったものと思われます。これら2つの語は似た意味を持ちますが、厳密には同義ではなく「リング」は指輪に限らず輪形のもの全般を指す語です。結果として、指輪を探すユーザに対して、イヤリングやスマホリングなどを返してしまう不具合を発生させてしまっていました。 シノニム拡張は再現率を向上させるのに有用ですが、スコアの制御ができないため、同義ではなく類義の語まで適用範囲を拡張してしまうと思わぬ結果を招くリスクがあります。類義語でクエリを拡張したい場合は、アナライザー内で処理するのではなくクエリの構築時に重み付きのOR条件で記述する方が制御が容易になると思われます。ZOZOTOWNでもシノニム辞書で扱っている語句を整理して、一部はクエリ構築時に展開処理を施す方針を検討しています。 まとめ 本記事では、Elasticsearchのアナライザー設定について解説しました。また、ユーザ辞書の登録方針やシノニム拡張のリスクについて具体例を交えて説明しました。 ZOZOでは今後も引き続き、有益な検索結果を提供できるよう検索機能の改善に努めていきます。 おわりに ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com
アバター
はじめに こんにちは、計測システム部フロントエンドブロックの平田です。 私が所属する計測フロントエンドブロックでは ZOZOMETRY というスマートフォンを用いて身体計測し、計測結果を3Dモデルやデータとして可視化し、Web上で管理できるtoBサービスを開発しています。 このサービスのフロントエンドではReact(Next.js)を採用しています。更にそれらの知見を深めるために、NYで開催されたJSNation、React Summit US 2024、そしてWorkshopに参加してきました。 この記事では現地参加ならではの経験や、参加したセッションへの考察、Workshopで学んだ内容などを紹介していきます! はじめに JSNationとReact Summitとは? Day 1 - JSNation Day 2- React Summit After Party 気になったセッションについて Chrome DevTools 2024: Debugging and Performance Optimization for React Developers AIアシスタントによるデバッグの向上 React Developer Toolsによる拡張性と最適化 ローカルオーバーライド機能での実験と検証 パフォーマンス最適化ツールの活用 3Dレイヤーとアニメーションデバッグ まとめ Green Bytes: How Enhancing Web Vitals Contributes to Environmental Sustainability まとめ Day 3 - Workshop ワークショップ内容 Server Components Client Components Client Router Server Actions イベント全体を通じて感じたこと 最後に JSNationとReact Summitとは? JSNation と React Summit は、JavaScriptおよびReactに特化した国際的なカンファレンスで、 GitNation が主催しています。現地参加とオンライン参加のハイブリッド形式で開催され、それぞれJavaScript、React.js関連の様々なセッションが行われます。また、ネットワーキングやワークショップ、アフターパーティーなど、多彩なプログラムが提供されています。イベントでは最新の技術の動向を学び、世界中の開発者と交流する絶好の機会を体験できます。 様々な国や地域で開催されていますが、今回は2日間を通じてアメリカで開催されたJSNation、React Summitに参加してきました。ZOZOからは昨年オランダ・アムステルダムで開催された同イベントにもエンジニアが参加しており、今回も引き続き参加できることになりました。そして3日目は、1日通しで行われるワークショップに参加してきました。 日付 時間帯(EST) イベント 場所 2024/11/18 9:00 - 17:00 JSNation Liberty Science Center 2024/11/19 9:00 - 18:00 React Summit Liberty Science Center 2024/11/19 19:00 - 22:00 After Party Barcade & Hudson Hound 2024/11/20 9:00 - 18:00 Workshop Double Tree by Hilton それでは早速参加したそれぞれのイベントについて詳しく紹介していきたいと思います。 Day 1 - JSNation 公式サイト には「In New York」や「Manhattan views」と大きく書かれています。しかし実際の会場はNew York Cityの隣、Jersey CityにあるLiberty Science Centerです。間違えないようにしましょう。 会場に向かう途中で見た朝のマンハッタンの風景 最寄駅から会場に向かう途中の道 丁度紅葉シーズンだったので木々が秋色に色づいてました 会場のLiberty Science Center外観 会場に到着してチェックインすると、ステッカーやJSNationオリジナルロゴ入りのマグカップをもらいました。 階段もJS仕様になっています。 開場してすぐの8時過ぎに到着したのでまだ人がまばらでした。 Visitorバッジ。ラストネームのスペルを間違えて登録していました。 このQRコードには個人情報が紐づけられていて、イベント中のネットワーキングに大活躍します。このバッジは2日間を通して使用するため紛失しないようにしましょう。ちなみに私は2日目に新しいバッジが配布されると思い込みホテルに忘れてしまいました。 砂糖たっぷりの喉が焼けるほど甘いドーナツ。会場にはドーナツやペイストリー、フルーツなどが用意されており、朝ごはんを心配する必要はありませんでした。 参加者の中には会社のチーム単位で参加している人もいれば1人で参加している人もいました。私は近くにいたサウスカロライナ、ニューヨーク、ドイツから来た参加者と仲良くなり、そのまま一緒に行動しました。 軽く自己紹介をして、どんな仕事をしているか、どんな技術を使っているか、そしてどんなモチベーションでこのイベントに参加しているのかを話しました。自己紹介の際に自分が携わっているZOZOMETRYについて話したら、皆が口々に「クール」と言ってくれてちょっと嬉しかったです。 また、会場で出会ったほとんどの人が使っている技術はReactでしたが、ドイツ人エンジニアは出会った人の中で唯一Angularを使っていました。その人の会社は現在Angularを使って開発しているだけでなく、ドイツ国内でAngularの使い方を教えることもしているそうです。 そんなこんなで盛り上がっているうちにオープニングの時間となり会場に移動しました。 オープニングの様子。(写真提供:GitNation) 司会グループがラップを披露していました。(写真提供:GitNation) 最初のセッションの様子です。(写真提供:GitNation) 会場では、メイン会場ともう1つの会場で同時に2つのセッションが行われ、参加者は興味のあるセッションを選んで参加する形式でした。私は事前に参加するセッションをある程度決めておきました。 セッションの後には毎回質問ブースで個別質問できるSpeakers Q&A Roomもありました。(写真提供:GitNation) ランチメニューには多様性への配慮を感じさせるラインナップが揃っていました。(写真提供:GitNation) いくつかセッションに参加したり色々な人と話したりしていると、あっという間にランチタイムです。 ランチタイムには仲良くなった人々と合流して、のんびりと食事を楽しみました。直前のセッションがWebのパフォーマンスに関する内容だったため、どのようなツールを使っているかについて話が盛り上がりました。今のチームでDatadog RUMを使っていることを話すと「Synthetic Monitoring Tool」を勧められました。外部のエンジニアと気軽に相談できる環境はとても素敵ですよね。 技術的な話だけでなく、NYC観光情報についても盛り上がりました(残念ながら観光をする機会がなかったので、情報を活かす場はありませんでしたが)。また、最近の厳しい北米のジョブマーケットの話や、各国の休暇制度の違いについても話題になりました。特に、日本の企業では病気休暇が一般的には存在しないことに驚愕されました。様々な話題で盛り上がりとても楽しいひとときでした。前職まで西海岸の某所で働いていたこともあり、どこか懐かしい雰囲気を感じました。 またセッション以外には、スピーカーとカンファレンス参加者が意見を交わすディスカッションコーナーが設けられており、リスナーとしても参加可能でした。私はその時間を逃してしまったものの、果敢にディスカッションに参加した人から「周りのレベルが高すぎて、自信が無くなりそうになった」という感想を聞きました。 ディスカッションルームの風景(写真提供:GitNation) その他は企業ブースもたくさん来ておりノベルティハントしつつお話を聞いて回ったりしました。 「Storybook」ならぬ「Storyblok」という会社があり、CMSツールを提供している企業です。(写真提供:GitNation) セッション会場外の様子(写真提供:GitNation) 砂糖たっぷりのおやつ Day 2- React Summit 2日目はReact Summitです。この日もチェックイン後にマグカップとステッカーが貰えました。 React Summitの会場内観(写真提供:GitNation) 会場に入ると前日のJSNationと比べて参加者数や企業ブースも増え、より賑やかな印象を受けました。また、前日カジュアルな格好をしていた参加者がスーツ姿に変わっているなど、皆の気合いも一段と高まっているように感じました。 オープニング開始前に以前チームで導入を検討していたSentryのブースを訪れました。当時Sentryに対してほとんど知識がなく手探り状態だったので、導入コストやNext.jsとの相性など基本的なことから、使用方法、現在使っているDatadog RUMとの違いなどを質問しました。話を聞いた印象としてはDatadog RUMと似たような印象を受けたのでもう少し深く聞こうとしたところでタイムオーバーとなり少し残念でした。 企業ブース(写真提供:GitNation) React Summitのオープニング(写真提供:GitNation) React Futureのパネルディスカッション風景(写真提供:GitNation) 前日のJSNationではもちろんJavaScriptに関するセッションもありましたが、チーム内コミュニケーションに関するものなど、一般的な内容も多かった印象です。一方React Summitのこの日は、ほとんどのセッションがより技術的な内容にフォーカスしていたように感じました。 ランチメニュー。手前から3番目の豆腐を焼いた食べ物が美味しかったです デザート。イタリアンスイーツのカンノーリもあったのですが出遅れたせいで全て完売していました。(写真提供:GitNation) この日のランチタイムは、前日とは違うメンバーと一緒に過ごしました。セッションの感想や、どんな仕事をしているのか話しているうちに、気づけばピザ談義で大盛り上がりでした。 皆様によると、どうやらNYのピザやベーグルが世界一と評される理由は「水が綺麗だから」らしいのです。これを読んでいる皆さん同様私も最初は疑いましたが、後にChatGPT先生にも確認したところ、同じ説明をされてしまいました。ちなみにシカゴ出身の方曰く「シカゴピザこそが世界最高」だそうです。 こうしてピザ論争が続いたわけですが、実のところ私はピザが好きではないのでした。 企業ブースの中には高額商品が当たるイベントなども用意されていました。(残念ながら当選しなかったです。) イベントエンディングの様子(写真提供:GitNation) 気づけばあっという間に2日目も終了しました。この2日間はエネルギッシュな雰囲気に触れ、非常に刺激的で充実した時間を過ごすことができました。 After Party React Summitの後には、After Partyの場が用意されていました。2つのバーがイベント参加者専用に確保されており、1つは静かに会話を楽しみたい人向けの Hudson Hound 、もう1つは賑やかに過ごしたい人向けの Barcade でした。 私はゆっくりと会話を楽しみたかったのでHudson Houndへ行きました。イベント中に仲良くなった人や、その時初めて出会った人々とこの2日間のイベントはどうだったか、どのセッションが一番良かったかなど話しました。そこで聞いて驚いたことがあったのですが、中にはGitNationに招待されて参加費無料で来ている人もいるそうです。招待された人は、別のカンファレンスで登壇したことをLinkedInに投稿したところ、GitNationの目に留まり招待が来たそうです。興味がある方は試してみてもいいかもしれないですね。 Hudson Houndの内観。落ち着いた環境でゆったりと過ごせたので、非常にリラックスできました。 仲良くなった人々と。 Barcadeの内観 バー周辺の様子。綺麗で落ち着いていて路上バイオリニストなどもいて夜でも平和でした。 気になったセッションについて それでは、特に印象に残ったセッションを2つご紹介します。これらはGitNationのウェブサイトでも視聴可能ですので、ぜひチェックしてみてください。 gitnation.com Chrome DevTools 2024: Debugging and Performance Optimization for React Developers gitnation.com まず1点目はGoogle ChromeのエンジニアリングリーダーのAddy OsmaniさんのAIを活用したデバッグ支援やパフォーマンス最適化機能のセッションでした。 プレゼンテーションの様子(写真提供:GitNation) 以下、気になったポイントをまとめます。 AIアシスタントによるデバッグの向上 Chrome DevToolsの AIアシスタントパネル では、AIとチャット形式でトラブルシューティングが可能です。この機能により、UIコンポーネントのエラー分析やスタイル調整の提案を受けることができ、デバッグ作業が直感的に進められると感じました。 実際にReactアプリでエラーが発生した場合、AIアシスタントがエラー内容を要約し、解決策を提示する様子がデモで紹介されていました。このアプローチはデバッグ効率を大幅に向上させる可能性を感じました。 React Developer Toolsによる拡張性と最適化 React Developer Tools では、レンダー更新のハイライト表示機能が非常に有用です。不要な再レンダーを特定し、パフォーマンス向上につなげられる点が印象的でした。 さらに、Server Componentsのサポートが追加され、クライアントとサーバー側の処理を簡単に区別できるようになったことも、最適化やデバッグ作業の効率化に寄与していると感じました。 ローカルオーバーライド機能での実験と検証 Chrome DevToolsのローカルオーバーライド機能 では、元のコードを変更せずにスタイル調整やAPIレスポンスのモックが可能です。この機能を使うことで、バックエンドが未完成でもフロントエンドのデバッグや検証を進められる柔軟性が魅力的でした。 特にスタイル調整をローカルに保存し、セッションをまたいで変更内容を保持できる点は、デザインの検証作業を効率化できると感じました。 パフォーマンス最適化ツールの活用 Core Web Vitalsの分析機能 は、パフォーマンス改善の具体的な指標を提供し、ユーザー体験の向上に役立つと感じました。また、トレースアノテーション機能を使うことで、チーム内でデータ共有や注釈の付与ができ、パフォーマンス向上施策を共同で進められる点も印象的でした。 3Dレイヤーとアニメーションデバッグ DOM要素の階層構造を視覚的に確認できる 3Dビュー機能 は、インタラクションデザインやアニメーション調整に役立つと感じました。 特にアニメーションインスペクターを使用すると、リアルタイムでタイミングや効果を調整できます。この機能は触り心地の良いWebアプリケーション作成に貢献すると感じました。 まとめ このセッションを聞く前は、そもそもChrome DevToolsにAIアシスタント機能があることを知りませんでした。しかし、AIアシスタントや新しいデバッグ機能、パフォーマンス最適化ツールがWeb開発の効率を大きく向上させることを実感しました。 これらの機能を活用することで、より柔軟で視覚的にアプリケーションを構築できることがすぐに想像できました。計測フロントエンドブロックでの開発でもどんどん活かしていきたいです。 Green Bytes: How Enhancing Web Vitals Contributes to Environmental Sustainability gitnation.com もう一点気になったセッションはZEALのFull Stack Developerの Dimitris Kiriakakis さんのプレゼンテーションでした。 ウェブサイトの最適化は、ユーザー体験の向上だけでなく、環境負荷の軽減にもつながるという視点からの非常に興味深いものでした。このセッションでは、Web Vitalsの改善を通じてウェブアプリケーションのパフォーマンスを向上させ、CO2排出量を削減するための具体的な手法や実践例が紹介されました。 まず、インターネットのカーボンフットプリントについて取り上げられていました。インターネットは世界のCO2排出量の3.7%を占めており、これは航空業界に匹敵する規模だそうです。AI業界の成長に伴い、この数値は今後さらに増加する可能性が指摘されています。ウェブのカーボンフットプリントに影響を与える主な要因として、インフラストラクチャー、データ転送量、エンドユーザーのデバイス使用が挙げられていました。特にページ重量(Page Weight)が大きく影響し、ページサイズが大きいほどネットワーク使用量や電力消費量が増加するという内容は印象的でした。 次に、Core Web Vitalsに関する説明がありました。Googleは2020年にCore Web Vitalsを導入し、ユーザーエクスペリエンスを評価するための指標を標準化しました。具体的には以下の3つの指標が重視されています。 Largest Contentful Paint (LCP) :ページ読み込み速度を測定し、2.5秒以下が良好な体験とされる。 Interaction to Next Paint (INP) :インタラクティブ性を測定し、200ms以下が良好な体験とされる。 Cumulative Layout Shift (CLS) :視覚的な安定性を測定し、0.1以下が良好な体験とされる。 これらの指標を改善することで、パフォーマンスの向上と環境への負荷を減らすことの両方を実現できるという説明があったのですが、これは説得力がありますよね。 具体例として、意図的にパフォーマンスが悪化するように設計されたウェブアプリケーションのケースが紹介されました。Google Lighthouseによる評価では、LCPが13.2秒、CLSが0.367という非常に悪いスコアでした。しかし、以下の最適化を施すことで大幅に改善された事例が示されました。 プレゼンテーションの様子(写真提供:GitNation) 画像最適化 :モバイル向けに小型の画像を生成し、WebP形式に変換。 優先度の設定 :ビューポート内の主要要素を優先的に読み込み、他の要素は遅延ロード。 レイアウトシフトの排除 :安定したレイアウト構造を確保。 これにより、ページ重量は70%削減され、LCPは800ms、CLSは0.1以下という結果を達成したそうです。この事例を通して、具体的な改善策とその効果を明確に理解できました。 さらに、Chromeのパフォーマンスタブを用いたプロファイリングやインタラクションイベントの追跡を活用し、問題点を特定・改善する手法も紹介されました。特にINPスコアの最適化では、重いタスク処理や応答遅延を最小限に抑えることが重要であり、電力消費を抑えながらユーザーエクスペリエンスも向上させることができるとの説明が印象的でした。 このセッションでは、最適化によるCO2排出量削減の具体例も取り上げられていました。あるプラグインのサイズを1KB削減することで、アムステルダムからニューヨークへのフライト5回分のCO2排出を削減できたという報告は、最適化がもたらす環境への影響を強く実感させるものでした。また、ZEALのケースでは、LCPが13秒、CLSが1.775という非常に悪いスコアから、最適化によって劇的に改善された結果が示されていました。 加えて、エコフレンドリーなウェブ開発を支援するツールや手法についても紹介されました。 アセット最適化 :画像圧縮やコードのミニファイ化を通じてデータ量を削減。 グリーンホスティングプロバイダー :再生可能エネルギーを使用するホスティングサービスを選択。 コンテンツデリバリーネットワーク (CDN) :地理的に近いサーバーからコンテンツを配信し、データ転送量を削減。 まとめ これらのアプローチは、持続可能なソフトウェア開発を推進するうえで役立つものであり、企業や開発者にとって今後さらに注目される分野だと感じました。 また、Webのパフォーマンスを向上させることは、自分たちのプロダクトを技術的・ビジネス的に向上させるだけではありません。地球にも優しいという、普段あまり意識していなかった視点からもアプローチできることに気づきました。これにより、非常に良い学びを得ることができました。 今回紹介するのは以上になりますが、他にもアクセシビリティの話や最近フロントエンドフレームワーク界隈でじわじわと人気が上がっている Svelte の話など興味深いセッションがたくさんありました。また、セッションの後に他の参加者と感想を言い合ったり、どのように業務に活かせそうか話したりすることで、一層理解が深まりました。 Day 3 - Workshop 3日目はワークショップでした。私は Kent C. Dodds さんによる「React Future (Server Components and Actions)」のセッションに参加しました。このワークショップでは、まだ公式に安定リリースされていない Server Components と Server Actions についてハンズオン形式で学ぶことができました。Server ComponentsとServer Actionsがどのような役割を果たすのかを、実際にフレームワークを構築しながら理解を深めました。 ワークショップを選んだ理由は、現在計測チームの一部メンバーと共にZOZOMETRYの管理画面を開発しているからです( 関連記事 )。ZOZOMETRYの管理画面は社内メンバー専用のクローズドプロダクトであり、実験的な技術も採用しやすいため、フロントエンドで Next.jsのServer Actions を使用することに決めました。今回、このプロジェクトで初めてServer Actionsに触れたため、さらなる理解を深める目的でこのワークショップに参加しました。 インストラクターのラップトップとマスコットキャラクターのコアラちゃんです。 ワークショップ用のリポジトリをはじめ、オリジナルコアラステッカーなど、至る所がコアラちゃんまみれだったのです。なぜコアラちゃんなのか誰も突っ込んでいなかったため、その謎は解明しないまま今日に至ります。 ワークショップは、まず簡単なアイスブレークと自己紹介から始まりました。自己紹介の内容は、名前、出身地、仕事内容、好きなアイスクリームのフレーバーについてでした。最も人気があったフレーバーは、Cookie Dough(焼く前のCookie生地味)でした。参加者の内訳は、アメリカからが最も多く、次いでヨーロッパ、アジア圏からは私以外に韓国からの参加者がいました。ちなみに、ここでもZOZOMETRYのことを紹介したところ、「クールだ」と言ってもらえました。 ワークショップ内容 Server Components React Server Components(以下、RSC)は、従来のSPAアーキテクチャに代わる新しい手法として注目されています。クライアントとサーバーの役割を効率的に分担し、ストリーミング対応やインタラクティブなUI構築を容易にします。今後の発展に期待しつつ、まずは基本的な仕組みや実装を、作業を通して理解していきました。 Client Components RSCの革新の中心はClient Componentsにあり、これにより新しいアプローチが可能になります。サンプルアプリでは、ボタンをクリックしてテキストを編集し、Enterキーで送信する機能を実装しました。このプロセスを通じて、Client Componentsの重要性や役割について学びました。Client Componentsはインタラクティブな操作を処理するために欠かせない要素であり、ユーザーの操作に応じて即座に反応できる点が特長です。このアプローチにより、ユーザーエクスペリエンスの向上が期待でき、サーバーとクライアントの役割分担を明確にできます。今回のアプリ開発を通じて、Client Componentsが動的な機能やイベント処理を担当する重要な役割を果たすことを理解し、実際にその動作を確認することで、その有用性を実感しました。 Client Router アプリ開発において、スムーズで直感的なユーザーエクスペリエンスを提供するためには、クライアントサイドルーターが欠かせません。ここではClient Routerの役割とその利点について学びました。 Server Actions Server Actionsは、Client Componentsとサーバー間のデータやアクションを効率的にやり取りするための重要な要素です。ワークショップでは、フォームの操作におけるServer Actionsの活用方法を学びました。フォーム送信時に、クライアントからサーバーに対してアクションを呼び出し、その結果を動的に反映させる仕組みです。このように、Server Actionsを使うことで、サーバー側のデータ操作を簡潔に行い、クライアントとのやり取りがシンプルになります。 Server Actionsのポイントは、Client ComponentsからServer Actionsを直接呼び出すことができ、アクションの参照がクライアントに渡されることです。これにより、従来のフォーム送信のような間接的な手法を省き、より直感的で効率的なデータ操作が可能になります。 RSC図解(ワークショップより) 今回のワークショップでは、Server Components、Client Components、Client Router、Server Actionsについて学びました。これらの技術を活用することで、サーバーとクライアント間の役割分担が効率化され、パフォーマンスの向上やユーザーエクスペリエンスの改善が実現可能です。特に、Server Actionsを使ったデータのやり取りや、Client ComponentsによるインタラクティブなUIの実装は、今後のアプリケーション開発において非常に有用な技術です。これらの技術が今後どのように進化していくのか、非常に楽しみです。 本来数日間にわたって行う内容を1日で詰め込んだため非常にハードでしたが、周りの参加者と助け合いながら何とか乗り切ることができました。参加者の多くはフレンドリーで、協力して課題を解決していく様子は学生時代のようで楽しかったです。また、インストラクターとも距離が近く、カンファレンスの時よりもリラックスして話ができ、裏話なども聞くことができました(2024年は登壇しすぎてしんどかった等)。 ワークショップが終わった後は、他の参加者たちと「来年も会えるといいね」と言い合いながら、別れを惜しみました。 ワークショップの様子です。 少人数での開催だったため、非常にアットホームでフレンドリーな雰囲気でした。また、インストラクターは課題を完成させることよりも、他の参加者と多くの会話をすることを推奨していました。 余談ですが、写真を見ると分かるように半袖の参加者もいます。この日は最高気温が11度だったにもかかわらず室内は冷房が7度に設定されており、エアコンの風が髪をそよがせるほどでした。その結果、私は体調を崩しました。 お昼ご飯はアメリカンメキシカン、いわゆるTex-Mexでした。メキシカンが大好きなので嬉しかったです。 イベント全体を通じて感じたこと ゲットしたノベルティたち。 今回のカンファレンスは、国内外を通じて私にとって初めての参加となりました。これまで「ニューヨークの人々は西海岸の人々に比べて冷たい」と耳にしていたため、どのような雰囲気なのか少し緊張していました。しかし実際には、多くの参加者がとてもフレンドリーで交流を楽しむことができました。 また、最近ではアメリカの若者にとって日本が人気の旅行先となっているようで、日本を訪れた経験のある参加者にも多く出会いました。参加者層を見ていると、東海岸からのアメリカ人が最も多く、次いでカナダの東部からの参加者が目立ちました。一方で、西海岸からの参加者は意外に多くなく、ヨーロッパからの参加者も限定的でした。しかし、とあるイタリア人エンジニアによると、ヨーロッパからもそれなりに参加者がいたようです。アメリカやカナダ以外では、ESTAを利用して参加できる国からの参加者が多いように思いました。ちなみに、日本から片道13時間ほどかけてニューヨークまでやってきたと言うととても驚かれました。 セッションはオンラインでも視聴可能なため、「技術を学ぶ」という観点では動画配信でも十分に知識を得られるかもしれません。しかし、現地で世界中から集まったエンジニアたちと直接対話することで知識の吸収にとどまらず、彼らの働く環境やマインドセット、バックグラウンドについても知ることができました。自分の中の『当たり前』が、他の人にとっては異なることを実感できる貴重な体験は、視野を広げる素晴らしい機会となりました。 今回のカンファレンスを通じて著名なエンジニアや世界的企業に所属するエンジニアと交流できたことで、技術知識をアップデートしただけではなく、大きな刺激を受け日々の開発へのモチベーションも向上しました。そして何よりも非常に楽しい時間を過ごすことができました。今後も機会があれば今回のご縁を大切にし、現地で再会できることを楽しみにしています。 最後に 最後に、計測システム部フロントエンドブロックでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください! www.wantedly.com
アバター
ZOZO開発組織の2025年1月分の活動を振り返り、ZOZO TECH BLOGで公開した記事や登壇・掲載情報などをまとめたMonthly Tech Reportをお届けします。 ZOZO TECH BLOG 2025年1月には、前月分のMonthly Tech Reportを含め5本の記事を公開しました。その中でも特に注目度の高かった記事をピックアップしてご紹介します。 1月21日に公開した「 フロントエンドテストの正解って?FAANSにおけるテスト戦略の振り返りとこれから 」は、リアルな現場感を綴った記事です。2025年1月第4週のおすすめエントリーとして、 はてなブログのXアカウントにも取り上げてもらいました 。 Webフロントエンドにおけるテストには様々な論点がありますが、FAANSの事例を通じて、何か得られるものがあればうれしいですね。 techblog.zozo.com また、1月最終日に公開した「 BigQueryのアンチパターン認識ツールで独自のSQLリンターを開発しました 」では内定者アルバイトスタッフの成果物を紹介しています。 取り組む中で見つけたエラーに対してPull requestを送ってマージされることで、問題の発生を防げるようになりました。ZOZOではこのような取り組みを応援、そして歓迎しています! techblog.zozo.com ZOZO DEVELOPERS BLOG EC基盤開発本部 SRE部の三神と亀井による、ZOZOTOWNにおけるSREの魅力や日常を紹介する記事を公開しました。SRE部の組織構成から日々の業務、そして今後の展望についても触れています。ZOZOTOWNのSREに興味のある方は、ぜひご一読ください。 technote.zozo.com 登壇 LODGE XR Talk Vol.23 1月17日に開催された『 LODGE XR Talk Vol.23 』で、技術戦略部の諸星( @ikkou )が「CES 2025 Report」と題して1月上旬に参加したCES 2025でのXRの動向について登壇しました。 登壇後に公開したCES 2025のイベントレポート記事では、XRに限らずFashion TechやBeauty Techについても触れています。あわせてご覧ください。 techblog.zozo.com JR西日本・ファーストリテイリング・ZOZOが語るビジネス要件を踏まえたデータ基盤の構築 1月24日に開催された『 JR西日本・ファーストリテイリング・ZOZOが語るビジネス要件を踏まえたデータ基盤の構築 』で、データ基盤ブロック ブロック長の奥山( @pokoyakazan )が「 ZOZOを支えるリアルタイムデータ連携基盤の歴史とビジネス貢献 」というタイトルで登壇しました。 【ZOZOエンジニア登壇情報】 明日1/24(金) 12:00~13:15にオンラインで開催される『JR西日本・ファーストリテイリング・ZOZOが語るビジネス要件を踏まえたデータ基盤の構築』にデータシステム部の奥山 @pokoyakazan が登壇します🎙️ お気軽にご参加ください! https://t.co/OVMWx370aT #data_findytools — ZOZO Developers (@zozotech) 2025年1月23日 speakerdeck.com 掲載 教育新聞 昨年12月15日に開催した「 Girls Meet STEM〜ITのお仕事を体験しよう〜 」について、技術戦略部の長澤( @wiroha )が取材を受けた記事が、「 教育新聞 」に掲載されました。 www.kyobun.co.jp エンジニアtype エンジニアtypeが運営する音声コンテンツ『 聴くエンジニアtype 』の配信100回突破を記念した過去のおすすめ回を特集した記事に、CTOの瀬尾( @sonots )登場回が紹介されました。 type.jp 瀬尾が登場した第1回のテーマは「 エンジニアとして成長するために新人時代にするべきことは? 」でした。こちらは記事としても公開されています。未見の方はぜひチェックしてみてください。 type.jp AdverTimes. 昨年12月18日にAI・アナリティクス本部の川田が登壇した「 宣伝会議AI研究会 」の様子が、「 AdverTimes. 」に掲載されました。 www.advertimes.com その他 2025年3月期 第3四半期決算発表 1月31日に2025年3月期 第3四半期決算を開示しました。詳細は以下のリンクにある開示資料をご確認ください。 corp.zozo.com 以上、2025年1月のZOZOの活動をお届けしました! ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com
アバター