dely engineering blog

レシピ動画サービス「kurashiru」を運営するdelyのテックブログ

情報共有不足を打破するために試した7つのこと

はじめまして。

dely, Inc.でクラシルの開発に携わっている @sakura といいます。
本記事では、分析データを社内に情報共有するまで私がどんなことしたかを
赤裸々に紹介します。

この記事はデータ活用 Advent Calendar 2019の21日目の記事です。

はじめに

「こんなにも貴重なデータがあるのに全然活用できていない…てか誰も見ていない… 」 この状況を打破すべく一歩一歩やっていったことを共有します💪

泥臭くやったこと

スプレッドシートにしてハードルを下げる

f:id:sakura818uuu:20191217180009p:plain:w400
まずは、データをスプレッドシートにまとめました。

なぜかというと、そのデータがredashでしか閲覧できなかったからです。
エンジニア以外にとってはredashは心理的負荷が高く、スプレッドシートのほうが普段から使い慣れているので扱いやすいです。

弊社ではG Suiteを使用しているので、
スプレッドシートにすることでデータを見るハードルがぐっと下がりました。

データを整理して見やすく、負荷を少なく

f:id:sakura818uuu:20191217195835p:plain:w400

データの性質そのものではなくて、データを閲覧してほしい人を軸にして数千件のデータをカテゴリ分けしました。
上の画像は実際のスプレッドシートです。タブでカテゴリごとに分かれています。

何千件もあるデータを見るのは脳の負荷が高く疲れてしまいます。
従って、事前にカテゴリ分けをしてこの人はここだけ見ればOKというようにしました。

社内の複数人にメンションをつけて共有

f:id:sakura818uuu:20191217183124p:plain:w400

スプレッドシートが完成したら、データを見てほしい人にメンションをつけて共有しました。

特定のチャンネルで特定の人に「こういうデータがあって、ここの部分を利用できると思うんですがどうでしょうか」という風に、 チャンネル / メンションをつける人ごとにメッセージを変え、そのデータの効果が最大化するように努めました。
やってることはパーソナライズに近いかもしれないです。

オフラインで共有してアドバイスをもらう

f:id:sakura818uuu:20191217183352j:plain:w350

オンラインで伝えることは効率がよいですが、一番物理的距離が近く、必ず伝わるのはオフラインだと思い 2つのMTGで「貴重で役に立つデータがここにあるのでぜひ見てください」と共有しました。

その場で見てくださる方もたくさんいて、アドバイスを頂くこともできました。(このデータ役に立ちそう、といわれた時はうれしかったです)

Qiitaにも残して検索しやすく

f:id:sakura818uuu:20191217184230p:plain:w450

全社員が見れる社内の共有ドキュメントツールのQiitaを使い、軽くドキュメントを残しておきました。
slack内に残すと「あれ、あの情報どこいったっけ」となりがちなので、Qiitaにもまとめました。

ドキュメントに残すことで検索のヒットもしやすくなり、後から見返す時も探しやすくなります

bot作成してslackでかんたん確認

f:id:sakura818uuu:20191217185035p:plain:w450

定期的に確認したほうがいいデータだと思ったので、botを作成しました。
redashのデータを定期的にslackに通知することで、最新の情報をかんたんに確認することができます。

botの作成には色んなエンジニアさんが手伝ってくれました。ありがとうございます。

bot警察をしてデータを見る文化を作る

f:id:sakura818uuu:20191217191830j:plain

弊社のslackにはbotが何十個もおり、いくらかんたんに見れるといっても見過ごしてしまうことも多いです。
どうにかこのデータを見る文化を作ることはできないか、と考えました。

そこで、botが通知してくるデータを私自らが解釈・要約して投稿することもしていました。
botの内容に徐々にスタンプがつき、返信がつき、最近では私以外のメンバーが能動的に反応してくれることも多いです。 (すごい!)

まとめ

本記事では、分析データを社内に情報共有するまでにどんなことをしたかを赤裸々に紹介しました。

まとめとして、この経験から学んだ情報共有の五箇条を記しておきます。

f:id:sakura818uuu:20191218073226j:plain:w430

「情報共有難しい…💭」
と悩んでいる方の参考に少しでもなれば幸いです。





最後に告知です。delyではエンジニアを絶賛募集中です。
ぜひお気軽にご連絡ください。

https://www.wantedly.com/projects/329047

 

 

~OSSから学ぶ~ MVCフレームワークの保守性がモリモリ上がるクラス設計

こんにちは、delyコマース事業部エンジニアの小川です。

先月11月に入社し、エキサイティングな毎日を過ごしています。

この記事はdely Advent Calendar 2019 - Qiitaの24日目の記事です。

昨日はSREの松嶋さんが「AWS RunCommandを使ってEC2上に監視ダッシュボードをサクッと作る(Ansible+Terraform+Grafana編)」という記事を書いてくれましたので是非そちらも読んでみてください!

tech.dely.jp

コマース事業部では、現在「事業開発」と「ソフトウェア開発」がほぼ同時に進行しており、プロジェクトにおける確定要素と不確定要素が複雑に絡み合っています。 スピード重視でゴリゴリ実装していくのも興奮しますが、変化に耐えづらい実装をしてしまうと、その後の開発スピードに影響していまい、事業のスピードが落ちるなんて事にもなりかねません。

そこで、プロジェクトの保守性や拡張性をあげるには、どういった設計をしたら良いかを、OSSであるGitLabを一例として見ていきたいと思います。

今回は、model view controller 以外のどんなディレクトリ(=クラス)を導入しているか、それがどんな役割を担っているかを見ていきましょう。

それではいってみましょう。

GitLabが導入しているディレクトリ

GitLabはRailsで実装されています。app配下のディレクトリで、デフォルトで作成されないものを抽出すると以下のようになりました。

  • finders

さまざまな条件に基づいたコレクションを取得するクラス

  • graphql

Graphqlに関するクラス

  • policies

権限確認系に使われるクラス。独自のDSLで実装されている。

  • presenters

viewに関わるロジックやデータを持つオブジェクトをviewに提供するクラス

  • serializers

フロントで使われるJSONを構築するためのビジネスルールをカプセル化しているクラス

  • uploaders

CarrierWaveに依存しているUploader

  • services

ビジネスロジックが取りまとめられているクラス

  • validators

Activerecordが提供しているカスタムバリデーター

  • workers

SidekiqのWorkerクラス

今回はこの中から、個人的にあまり導入したことがない、finderspresentersあたりを見ていきます。

finders

finderクラスは「さまざまな条件に基づいたコレクションを取得する」責務を持つクラスになっています。

例えば、プロジェクトモデルの中でこのようなイシューを取得するメソッドを実装するより、

class Project
  def issues_for_user_filtered_by(user, filter)
    # たくさんのロジック...
  end
end

issues = project.issues_for_user_filtered_by(user, params)

下記のように実装すると、よりモデルを薄く保つことができるよ!っていうイメージですね。

issues = IssuesFinder.new(project, user, filter).execute

GitLabのFinderクラスは、基本的に#executeのみをパブリックメソッドとして持っているみたいです。

実装を見ていく

では実際にProjectsFinderを例に挙げて見ていきましょう。

まずはどこで#executeが実行されているか探してみます。 ありました、Admin::ProjectsControllerで以下のように実行されています。

ProjectsFinder.new(params: finder_params, current_user: current_user)
              .execute
              .includes(:route, :creator, :group, namespace: [:route, :owner])
              .preload(:project_feature)
              .page(finder_params[:page])

paramsにfinder_paramsを、current_userにはcurrent_userを指定しています。 find_paramsは、取得するプロジェクトの条件、つまりフィルタリングするパラメーターや、ソートの条件などのパラメーターを含めています。

では、ProjectsFinderの実装はどうなっているのでしょうか。

#initializeには、paramsとcurrent_userとproject_ids_relationをキーワード引数で渡します。

def initialize(params: {}, current_user: nil, project_ids_relation: nil)
    @params = params
    @current_user = current_user
    @project_ids_relation = project_ids_relation
end

#excuteの実装は以下のような形です。

def execute
    user = params.delete(:user)
    collection =
      if user
        PersonalProjectsFinder.new(user, finder_params).execute(current_user) # rubocop: disable CodeReuse/Finder
      else
        init_collection
      end

    collection = filter_projects(collection)
    sort(collection)
end

変数collectionに、フィルタリングのベースになる、プロジェクトのコレクションを代入しています。 その後、#filter_projectsでプロジェクトのフィルタリングをおこなった後、#sortにて結果のソートをおこなっていました。

フィルタリングとソートの条件は、#initializeの時に渡したparamsで指定しています。

メリットになりそうな事

パブリックメソッドである#executeが見通しがよく、コードリーディングしやすいと感じました。 デルメルの法則にも違反していなく、依存も少ない(浅い?)と言えそうです。

このコレクションの取得を、modelに#filterのようなメソッドで実装したらどうなるでしょうか? ProjectsFinderに実装されている、多くのprivateメソッドがmodelにも実装されることになります。しかもそのメソッド達は、#filterの結果を達成するために切り出されている(特に他のメソッドでは使われない)ロジックなので、そのメソッド達はfat modelになってしまう要因の一つだと思います。

取得のロジックが単純なうちは良いですが、上記まで複雑になってきたり、必要なパラメーターが増えてきたら、Finderクラスの実装を考えていいかもしれません。

ですが、プロジェクトの初期段階でも、取得系の実装が大きくなることが分かっている、かつ不確定要素がたくさんありそうならば、取得系のロジックをFinderに集約し、呼び出し側が変更に影響しないように実装するのもありだと思いました。

presenters

presenterは、viewに関わるロジックやデータを持つオブジェクトをviewに提供するクラスとなっています。 viewに直接書いてあるロジックや、modelにviewに関連するロジックやデータのメソッドは、presenterに実装します。

実装をみていく

使われ方を見てみます。今回はProjectPresenterを見ていきます。 presenterはviewで下記のように呼び出されていました。

-# @projectはProjectPresenterのオブジェクト
= render 'stat_anchor_list', anchors: @project.statistics_anchors(show_auto_devops_callout: show_auto_devops_callout)

パーシャルのreder時に、引数として ProjectPresenter#statistics_anchors の返り値を渡しています。 renderされているパーシャルは、

- anchors = local_assigns.fetch(:anchors, [])

- return unless anchors.any?
%ul.nav
  - anchors.each do |anchor|
    %li.nav-item
      = link_to_if anchor.link, anchor.label, anchor.link, class: anchor.is_link ? 'nav-link stat-link d-flex align-items-center' : "nav-link btn btn-#{anchor.class_modifier || 'missing'} d-flex align-items-center" do
        .stat-text.d-flex.align-items-center= anchor.label

のようになっています。

anchorsで渡されたデータを使って、リンクを生成していますね。 このパーシャルは、#link, #label, #is_link, #class_modifierのメソッドもつオブジェクトに依存しています。

では次に、ProjectPresenter#statistics_anchorsの実装見てみます。 下記はProjectPresenterの実装の一部です。

class ProjectPresenter < Gitlab::View::Presenter::Delegated  
  presents :project

  AnchorData = Struct.new(:is_link, :label, :link, :class_modifier, :icon)
  def statistics_anchors(show_auto_devops_callout:)
    [
      commits_anchor_data,
      branches_anchor_data,
      tags_anchor_data,
      files_anchor_data
    ].compact.select(&:is_link)
  end

  def commits_anchor_data
    AnchorData.new(true,
                   statistic_icon('commit') +
                   n_('%{strong_start}%{commit_count}%{strong_end} Commit', '%{strong_start}%{commit_count}%{strong_end} Commits', statistics.commit_count).html_safe % {
                     commit_count: number_with_delimiter(statistics.commit_count),
                     strong_start: '<strong class="project-stat-value">'.html_safe,
                     strong_end: '</strong>'.html_safe
                   },
                   empty_repo? ? nil : project_commits_path(project, repository.root_ref))
  end
end

ProjectPresenter#statistics_anchorsは#commits_anchor_dataや、#branches_anchor_dataを集めて返しています。

#commits_anchor_dataは、構造体であるAnchorDataを生成・返却しています。ProjectPresenterは、Gitlab::View::Presenter::Delegated(Rubyの標準のSimpleDelegatorを使用)を継承しているので、Projectのインスタンスメソッドである、#statisticsにもアクセスできます。

ちなみに、#branches_anchor_dataなど、#*_anchor_dataという命名のメソッドは全てAnchorDataを返却していました。

他にもオブジェクトの生成の仕方や、URL生成のヘルパーなど、しっかり作り込まれているのですが、長くなりそうなので割愛します。 オブジェクト生成の部分を端的に紹介すると、直接ProjectPresenter.newすることを禁止しており、下記のようなパターンで生成するようにしています。

# presentメソッドで生成するパターン
@project.present 

# Factoryクラスを使って生成するパターン
Gitlab::View::Presenter::Factory.new(@project).fabricate!

メリットになりそうな事

viewの表示のルールがpresenterに集約されていて読みやすいと思いました。 表示してはいけないものを表示してしまったなどの事故が起きにくくなりそうです。viewがコンフリクトした時の恐怖からバイバイできるのも個人的に好きです。

また、viewに比べかなりテストしやすくなっています。

導入するときには、presenterがどういった粒度で実装されるかをチームで決めておいた方が良いかもしれません。 GitLabはmodelに対してpresenterが実装されている風に見受けられました。(models/project.rbに対して、presenters/project_presenter.rbのイメージ)

プロジェクトによって、違う粒度での実装もありえると思います。例えば、viewで関連データが複雑かつ、多くのところで使われているパーシャルがあったら、それと1対1で対応させるようにすれば幸せになれそうですね。コントローラーにも同じロジックを書かなくて済むし、変更に対する影響範囲も明確です。もしコマース事業部で導入するとしたら、draperを使ったDecoratorとどんな住み分けするか(そもそも導入しない)とか、チーム内で議論してみたいです。

最後に

今回はこんな風にGitLabのクラス設計を見ていきました。 プロダクトの特性によって設計の仕方も変わってくると思うので、違うOSSも読んでみるのも楽しいと思います。

「保守性がモリモリ上がるクラス設計」と題しましたが、クラス設計に銀の弾丸はない気がするので、プロダクトの変化しやすい部分はどこか、設計することでどんな問題を解決したいかを、チーム内で議論を重ねて実装・検証することが良さそうです。

コマース事業部では事業も開発も挑戦することが多く、エンジニア・デザイナーを強く募集しています。 もし興味がある方がいましたら、お気軽にご連絡ください!

www.wantedly.com www.wantedly.com

AWS RunCommandを使ってEC2上に監視ダッシュボードをサクッと作る(Ansible+Terraform+Grafana編)

こちらは、dely advent calender 2019の23日目の記事です。
qiita.com
adventar.org
昨日は、サーバーサイドエンジニアのyamanoiさんが「画像管理をActiveStorageからCarrierWaveへ乗り換えた話」という記事を書きました。興味を持った方は、是非読んでみてください!
tech.dely.jp

こんにちは!
今年11月からdelyに入社しました開発部SREの松嶋です。
本記事では、Systems ManagerのRunCommand (Ansible-playbook)を使うことでより簡単に監視ダッシュボードを作ることができたので、その手順について紹介したいと思います。

はじめに

今年9月にGitHubまたはS3に保存しているAnsible-playbookを直接実行する機能がSystems Managerに導入されたのを覚えていますか。
この新機能によって、Ansibleを使うためにEC2にssh接続用の公開鍵の作成や管理をしなくてもplaybookを実行可能になりました。Ansibleのplaybookを作成するだけでサーバー設定ができるのは、設定や管理面で楽になりますよね。
ちょうど弊社でも運用やセキュリティの観点からSystems ManagerのRunCommandやAutomationを使用することで、sshしなくてもデプロイできる手順に順次置き換えている最中です。
aws.amazon.com

また今年の11月には、Grafana6.5がリリースされました。このアップデートによって、AWS Cloudwatchのメトリクスをより効率的に監視できる機能が新しく追加されました。例えば、ワイルドカードを使って動的なクエリを書くことが可能になったり、事前構築されたダッシュボードが用意されるようになったため素早くモニタリング開始することができるようになっています。
aws.amazon.com

そこで、今回はこれらの新機能を試すために、Systems Managerの公式ドキュメント「AWS-ApplyAnsiblePlaybooks」を使ってRunCommandでGrafanaの監視ダッシュボードを構築してみたいと思います。

今回使ったものは、以下の通りです。

  • Grafana 6.5.2
  • Nginx 1.16.1
  • Terraform 0.12.12
  • AWS-ApplyAnsiblePlaybooks (Systems Managerドキュメント)

Grafanaプロビジョニング用設定ファイルの用意

現在のGrafanaでは、データソースやダッシュボードをファイル管理することが可能となっています。そのため、事前に設定ファイルを用意しておけば、Grafanaの起動と同時にモニタリングを開始することができます。

まずは、データソース設定用ymlファイルを用意します。ここでは、データソースのタイプと認証方法の設定を記載します。セキュアな情報が必要となるのでAWSのパラメータストア等を使用して安全に管理してください。

  • cloudwatch-datasource.yml
apiVersion: 1

datasources:
  - name: cloudwatch
    type: cloudwatch
    jsonData:
      authType: keys
      defaultRegion: ap-northeast-1
    secureJsonData:
      accessKey: $AWS_ACCESSKEY
      secretKey: $AWS_SECRETKEY

続いて、ダッシュボード設定用ymlファイルを用意します。optionsのpathはダッシュボードのjsonファイル置き場を指定しています。

  • cloudwatch-dashboard.yml
# # config file version
apiVersion: 1

providers:
 - name: 'cloudwatch'
   orgId: 1
   folder: ''
   folderUid: ''
   type: file
   options:
     path: /var/lib/grafana/dashboards #dashboard jsonファイル置き場

監視ダッシュボード用jsonファイルは、公式サイトからダウンロードしました。今回は、以下3つのダッシュボードjsonファイル(EC2,EBS,Billding)を用意しました。

  • amazon-ebs_rev1.json
  • amazon-ec2_rev1.json
  • aws-billing_rev13.json

公式のダッシュボードは、他にもLambdaやCloudwatchLogs、RDSがあります。ここはお好みのものをどうぞ。
grafana.com

Ansible-playbookの作成

RunCommandで使用するplaybookは以下のような構成にしました。playbookの中身は、grafana及びnginxのインストール、設定をするタスクを記載しています。これらをzipファイルにまとめて後ほどs3にアップロードします。

.
├── README.md
├── main-ansible.yml
└── roles
    ├── grafana
    │   ├── files
    │   │   ├── amazon-ebs_rev1.json
    │   │   ├── amazon-ec2_rev1.json
    │   │   ├── aws-billing_rev13.json
    │   │   ├── cloudwatch-dashboard.yml
    │   │   └── cloudwatch-datasource.yml
    │   ├── handlers
    │   │   └── main.yml
    │   ├── tasks
    │   │   └── main.yml
    │   └── templates
    │       └── grafana.ini.j2
    └── nginx
        ├── files
        │   └── grafana.conf
        ├── handlers
        │   └── main.yml
        └── tasks
            └── main.yml

10 directories, 13 files

使用したplaybookのサンプルをgithubにあげていますので、参考までに。
github.com

terraformで環境構築

ここまで準備ができたら、terraformで必要なものを構築していきます。
今回Grafanaサーバー用に構築したのは、以下の通りです。VPCやサブネットは既存のものを使用しました。

  • EC2インスタンス
  • セキュリティグループ
  • IAMロール
  • IAMポリシー
  • S3バケット
  • S3オブジェクトのアップロード
  • Route53のAレコード

EC2インスタンスに付与するIAMロールに関しては、RunCommandの実行やGrafanaがCloudwatchのメトリクスを取得できるように以下のIAMポリシーをアタッチしておく必要があります。

  • Cloudwatchのメトリクス取得
  • 作成したS3バケットのアクセス権限
  • AmazonEC2RoleforSSM (Amazon管理ポリシー)

IAMロールとアタッチしたポリシーの例は、github上に置いているので参考にしてみてください。
github.com


Terraformの実行は、GithubActionを使うとGithub上でterrraform initからvalidateやshow, applyまで完結するのでおすすめです。GithubActionで実施したplanやapply履歴もgithubで確認することができます。
github.com

Ansible-playbookの実行

Systems Manager > Run CommandからAWS公式コマンドドキュメントの「AWS-ApplyAnsiblePlaybook」を選択します。
コマンドのパラメータは、以下のように設定しました。Source InfoはS3バケットに保管しているオブジェクトURLを記載してください。

Source Type: S3
Source Info: {"path":"object-url"} 
Install Dependencies: True
Playbook File: grafana-ansible/main-ansible.yml
Extra Variables: SSM=True
Check: False
Verbose: -v

後はRunCommand先のインスタンスを選択し、ログが必要な場合はCloudwatchLogsまたはS3に出力するように設定して実行すればOKです。
成功すれば以下のように表示されます。

f:id:akngo22:20191222210405j:plain
RunCommand実行結果

S3にアップロードしておくだけで、ssh経由せずにAnsible-playbookを実行することができるのはかなり便利だと思いました。ただ、デバックオプションを使ってログ出力していてもplaybookのどのタスクで失敗したのか表示されずデバッグしづらかったので、事前にplyabookが想定通りに動作することを確認の上で使う必要があると思います。この点は、RunCommandの実行結果で見れるようになると良いですね。

Grafana確認

Ansible-playbookの実行が成功したら、Grafanaにログインしダッシュボードを見てみましょう。ダッシュボード一覧にプロビジョニングしたダッシュボードが表示されていることが確認できると思います。
Grafana6.5では、ワイルドカードを使えるようになったため動的なクエリに対応できるようになり、AutoScalingでEC2インスタンスが増減しても自動でダッシュボードに反映されるようになっています。

f:id:akngo22:20191222205423j:plain
EC2インスタンスのダッシュボード

また、グラフをクリックすると「View in Cloudwatch console」というCloudwatchコンソール画面に遷移するためのディープリンクがコンテキストメニューに追加されていることが確認できます。このリンクをクリックすれば、Cloudwatchコンソール画面に飛び、対象メトリクスを表示させることも可能となっています。

f:id:akngo22:20191222205024j:plain
ディープリンクが表示される
f:id:akngo22:20191222205106j:plain
Cloudwatchコンソール画面に遷移できる


まだ、発展途上な感じではありますが今後よりAWS Cloudwatchと親和性が高くなる予感がするので、さらに使いやすくなるのではと思います。

最後に

delyではSREを大募集しています!興味ある人は気軽にまずはオフィスに遊びにきてください!
www.wantedly.com

delyの開発部について知りたい方はこちらをご覧ください!
speakerdeck.com

画像管理をActiveStorageからCarrierWaveへ乗り換えた話

本記事は dely Advent Calendar 2019 22日目の記事です。

qiita.com
adventar.org


昨日はiOSエンジニアのknchstが「“ダーク“な2019年」という記事を書きました。
tech.dely.jp


こんにちは、delyでサーバーサイドエンジニアをやっているyamanoiです。

弊社のとあるプロダクトにて画像アップロード処理周りに、ActiveStorageを使用していたのですが、使いづらい点がいくつかあったため、採用実績があったCarrierWaveへ乗り換えました。
この記事ではなぜ乗り換えたのかと、乗り換える手順を書いていきたいと思います。

なぜActiveStorageから乗り換えたのか

1. CDNとの相性が悪い

ActiveStorageはアタッチされたモデルのurlメソッドを使用するとActiveStorageが定義した /rails/active_storage/blobs/* へのパスを生成します。
このパスへアクセスすると、各クラウドストレージ上のオブジェクトに対する一時的な認証コード付きURLへリダイレクトし、画像を取得することができます。
このURLをキャッシュしてしまうと、URLが期限切れになってしまうと画像が表示されなくなってしまいます。

ActiveStorageとCDNを併用するにはActiveStorageの機能を独自に拡張することで利用はできるのですが、拡張を行うことによってシステムが複雑になってしまい、アップデートの障壁になったりすることが容易に想像できるため今回は避けました。

2. 画像のリクエストがRailsに向いてしまう

ActiveStorageを有効にするとActiveStorage用のroutingが新しく追加されます。
ActiveStorageを用いて画像を取得する際はすべてこのルーティングを経由する必要があります。
静的ファイルはパフォーマンスの観点からアプリケーションサーバーを通さずnginxやs3等のバケットから直接配信したいですよね。

また追加されるルーティングは自分で定義しているconfig/routes.rbの後にロードされるため、以下の様なルーティングを定義しているとActiveStorage側のルーティングにマッチする前にルーティングが解決されてしまい、画像が正しく表示されない問題に直面しました。

  get "*path", controller: 'front', action: 'spa', via: :all

3. DBへのリクエストが頻繁に走る

ActiveStorageはactive_storage_attachmentsactive_storage_blobsの2つのテーブルを作成し、そこに画像のメタ情報やモデルとの関連を保持します。
ActiveStorageを使う場合は少なくともアタッチするモデルと1対1の関連が発生します。
何も考えず使用すると容易にN+1を誘発します。そのためActiveStorageではN+1を回避するためのメソッドが用意されています。
また上記の2つのテーブルですべてのモデルに対しての画像を扱うため、レコード数の多いテーブルが複数存在するとレコード数が増加し、パフォーマンスに影響が出てしまう可能性もありそうです。


CarrierWaveへ乗り換える手順

1. 設定ファイルを消す

config/application.rb
でrails/allしている場合は不要なファイルもロードしてしまうので必要なもののみをロードするように変更します

デフォルトだとconfig/application.rbに以下の様な記述があると思いますが、これだとActiveStorageもロード対象になってしまうので使うものだけをロードするように変更します。
Before

require 'rails/all'

After

require "rails"
require "active_model/railtie"
require "active_job/railtie"
require "active_record/railtie"
require "action_controller/railtie"
require "action_mailer/railtie"
require "action_view/railtie"
require "sprockets/railtie"
require "rails/test_unit/railtie"

2.CarrierWave gemの追加と書き換え

CarrierWave gemを入れて実際にコードを置き換えていきます。
置き換える点で厄介になりそうなところはActiveStorageのvariantを使用している場合です。

ActiveStorageではアタッチされている画像に対してvariantメソッドを使うことでリサイズ処理を手軽に実現することができます。

    <%= image_tag @post.thumbnail.variant(resize:'50x50').processed %>

様々なサイズを気軽に生成することができるので便利な機能なのですが、CarrierWaveではUploaderクラスに予めversionとして画像のパターンを定義しておく必要があります。
今回のプロダクトではこのvariantの機能をフル活用している場所はあまり無かったのでそこまで問題にはなりませんでした。

3. 移行スクリプト作る

弊社の場合、ActiveStorageで保存された画像はs3に置いてあり、移行するにあたってファイルの保存場所を変更する必要があったため移行するスクリプトを作りました。
ActiveStorageは1つのバケットにフラットに画像を保存するため、パスを特定し、置き換えていきます

↓サンプルコード

class FileDataStringIo < StringIO
  attr_accessor :original_filename, :cnotent_type

  def initialize(*args)
    super(*args[2..-1])
    @original_filename = args[0]
    @content_type = args[1]
  end
end

class ActiveStorageBlob < ActiveRecord::Base; end

class ActiveStorageAttachment < ActiveRecord::Base
  belongs_to :blob, class_name: 'ActiveStorageBlob'
  belongs_to :record, polymorphic: true
end

ActiveStorageAttachment.all.each do |attachment|
  blob = attachment.blob
  key = blob.key
  filename = blob.filename
  record = attachment.record
  name = attachment.name
  content_type = blob.content_type

  s3 = Aws::S3::Resource.new(region: 'ap-northeast-1')
  obj = s3.bucket(ENV["ACTIVE_STORAGE_S3_BUCKET"]).object(key).get
  data = obj.body.read

  io = FileDataStringIo.new(filename, content_type, data)
  record.send("#{name}=".to_sym, io)
  record.save!
end

4. テーブルの削除

最後にActiveStorageの有効時に生成されたテーブルを削除します。

class DestroyActiveStorageTables < ActiveRecord::Migration
  drop_table :active_storage_blobs
  drop_table :active_storage_attachments
end

まとめ

Rails備え付けの機能だからと言ってすぐ取っつかず、開発しているプロダクトの要件をしっかり満たせるかを検討しながら、Gemの選定を行いましょう

最後に

delyではサーバーサイドエンジニアを募集中です。ご興味ありましたらぜひこちらから!
note.com
www.wantedly.com


delyの開発について知りたい方はこちらもあわせてご覧ください!
speakerdeck.com

“ダーク“な2019年

こんにちは!クラシルiOSアプリを開発しているknchstです。

この記事は「dely Advent Calendar 2019」の 21 日目の記事になります。

qiita.com

adventar.org

昨日は Android チームの tummy による「何もわからない状態からいち早く脱するためのコードリーディング法(Android 版)」という記事でした。

tech.dely.jp

ダークモードな2019年

WWDC2018で発表された macOS Mojave にて実装されたダークモードを皮切りに世界的にダークモードへの対応が進んできました。しかしダークモードについては様々な意見があり、その評価や必要性についても賛否両論です。

iOSやAndroidのような主要なモバイルプラットフォームがOSレベルでダークモードをサポートしたことにより、Twitter, Instagram, Slackなどメジャーなデベロッパーたちは早々にダークモードに対応しました。

Dark Mode Listというサイトではダークモードに対応しているアプリの一部が確認できます。

darkmodelist.com

ダークモードがもたらした功罪

ダークモードに対する一般的な評判はとてもいいように感じます。iOSやAndroidがダークモードに対応する前からLINEが黒のテーマをリリースした時や、SpotifyのアプリのUIがかっこいいという声はよく聞きました。

さらにウォールストリートジャーナルは、ダークモードを強く推奨している記事を書いていました。そしてその記事を今月のはじめに再びツイートしていました。しかしながら面白いことに当のWSJのアプリはこの時点ではダークモードに対応していなかったのです。ダークモードが追加されたのは最近の話です。

この動きはメディアに限らず個人レベルでも起きつつあります。そう、ダークモードハラスメントが起こっているのです。以下は著名なeスポーツプレイヤーのツイートです。

なぜダークモードはここまで人気なのでしょうか。

いち早く一般ユーザー向けのサービスでダークな外観を提供していたSpotifyのディレクターだったMichelle Kadir氏はインタビューで以下のように述べています。

www.fastcompany.com

We believe that when you have music or art that’s very colorful and very artistic, and you have beautiful cover art for music, that it really shows more clearly visible in a product like this, when it’s about entertainment. Everything else settles in and isn’t as much in the way when you have a white background

Spotifyがダークな外観をアプリに採用したのは多くの背景パターンをユーザーに対してテストした結果ダークな外観がユーザーに好まれることが分かったからです。

さらにダークモードの人気は近年の人々の日常のニーズを反映していると言えます。 おそらく誰もが照明が暗い空間でスマホをいじる人にイラついたことがあると思います。ダークモードがあるおかげで他人の邪魔をせずに暗闇でスマートフォンを使用することができます。

例えば映画館・美術館などの照明が灯っていないもしくは非常に暗い状況下での使用です。あるいは、パートナーが近くで寝ている間にスマートフォンを操作したい場合などもです。

さらにダークモードにすることにより憎っくきブルーライトを軽減し、安眠に貢献してくれることでしょう。

また、スマートフォンのディズプレイへの様々な工学的理由によって採用されている有機EL(OLED)との相性の良さもダークモードの普及をさらに促すことになると思います。今年のGoogle I / Oでのセッションによると、Android Qのダークモードは、「有機EL(OLED)下での利用でバッテリー消費を最大60%削減できる」と述べていました。

しかしながらダークモードが全てのユーザーに適しているかは疑問があり現在でも議論がつづけられています。以下のQuoraの質問ではメガネをかけている人や乱視など状態にある人にとってはダークモードが眼精疲労を助長する恐れがあると主張しています。

www.quora.com

以下の2008年のブログで引用された論文では、黒地に白のテキストが白地に黒よりも26%読みやすいと主張しています。ただ、その論文は1980年代のスクリーンを使用しているため、今の私たちが利用しているディズプレイなどで再度研究してみる必要があるかもしれません。

tatham.blog

ダークモードとレガシー

ダークモードに対応するということは、既存のサービスのUIと闘う必要があります。ただ単に背景を黒に変更すればいい、という訳ではありません。

以下記事に書かれているAlibabaアプリのダークモード対応では、色の最適化の重要性について主張しています。

www.uisdc.com

Alibabaのブランドカラーである明るいオレンジを使用する時、暗い背景に適しているか考慮する必要があります。あるいはブランドカラーを拡張させた補助色を利用することも検討できます。これにより、コントラストを維持し、より疲れにくく美しさを保ったUIを提供することができます。

f:id:knchst:20191221175537p:plain

また別の例としてロシアの検索エンジンを提供しているYandexが開発しているメールクライアントではダークモードに対応する際に大きな課題にぶつかりました。

habr.com

Eメールは古くからあるコンテンツ形式であるため、ほとんどのメールクライアントは背景色が白であるという前提で設計されているため、メール本文ないに利用される画像は背景が白で作られていることが多くあります。また、一部ではメールテンプレートの背景色を調整している可能性もあります。

このためメールクライアントがダークモードに対応すると以下のような問題が発生します。Amazonのロゴ部分は背景が白い画像であるためダークモードにしたときに不自然に見えます。

f:id:knchst:20191221175654p:plain

おわりに

ダークモードについて話してきましたが、サービスに本当にダークモードが必要かも検討する必要があるかもしれません。Spotifyの事例ではそれがユーザーに最適であるということがテストによってわかったのに加え、サービスのコンテンツがダークモードとの相性が良かったので採用されました。自分はなんでも間でもダークにすればいいとは思いませんし、ダークモードもあまり使っていません。

しかしながら、Appleにプラットフォームの開発者である以上Appleの決定には従わざるおえないのも事実です。来年もいろんな意味でダークな一年になりそうですね。。

delyでは一緒に食の課題を解決してくれるエンジニアを募集しています!興味ある人は気軽にまずはオフィスに遊びにきてください!

www.wantedly.com

speakerdeck.com

何もわからない状態からいち早く脱するためのコードリーディング法(Android 版)

f:id:rnitame:20191218093616p:plain

こんにちは。

dely の開発部でクラシルの Android を担当している tummy です。 2019 年 12 月から dely に入社して、たくさんキャッチアップしながら初めての施策を実装しています。ついていくので精一杯です。。笑

この記事は「dely Advent Calendar 2019」の 20 日目の記事になります。

qiita.com

adventar.org

昨日は同じ Android チームの kenzo による「エンジニアは体が資本でしょ。と思って始めた習慣とその続け方」という記事でした。 自分は三日坊主になることが多いので、次になにか継続しようと思ったときはアドバイスをもらおうと思います。

tech.dely.jp

今回は、新しいプロジェクトに入った際のキャッチアップ時に行っている、担当する Android アプリの中身を「なるべく早く」「ざっくりと」把握するためのコードリーディング方法を紹介できればと思います :)

目的

何がどこにあってどう使われているか、を粗方把握している 状態を目指します。この状態になっていれば、とりあえず該当箇所を見に行けるため一次調査が自分で行えると考えているからです。

まずやること

  1. build.gradle で何が入っているのか見る
  2. デバッグ周りを調べる
  3. package 構成を見る

以上の 3 つです。

まず、build.gradle を見ます。ライブラリの一覧が記述されているので通信ライブラリや Android SDK まわりで使っているものを確認できます。また、flavor や Lint の設定なども記述されているため、手元で開発するときの環境が把握できます。

その後、デバッグまわりを調べてデバッグメニューが実装されているかどうか、Stetho やこのあとも紹介する Hyperion などのデバッグツールが入っていないか見ます。

そして、package 構成を見に行きます。 ここまでやったあとにコードを読みに行きます。

コードを読む方法

PR を見に行く

コードを読めばなんとなくやっていることはわかりますが、「なぜこういう実装になっているのか?」という箇所を発見した場合、プルリクに書いてあればそれを元に理解することができる可能性があります。Find Pull Request という Intellij プラグインがあるのでそれを活用します。

plugins.jetbrains.com

f:id:rnitame:20191218113909p:plain
List Pull Requests にチェックを入れて Annotate すると Pull Request の番号が表示される

GitHub 等を使ってプルリクベースで開発していることが前提になりますが、こういった情報も概要を掴む上で助けになってくれるはずです。

Android Studio の機能フル活用

ショートカットをたくさん使うのはもちろんですが、他に特に便利なものを紹介します。

Breakpoint にひっかけて Frames を見る

ブレークポイントを任意の場所(例えばどこから遷移してくるのか知りたい Activity の onCreate)に置き、Debugger を起動してブレークポイントの場所を通ると Debug のビューが開くと思います。 そこに Frames という欄があり、アプリ起動からそのブレークポイントに至るまでが表示されています。

f:id:rnitame:20191216160948p:plain

上記の場合、「MainActivity の initListener メソッドで onClick が呼ばれ、SecondActivity の Companion Object 内にある createIntent が呼ばれた」と読むことができます。 このことから、MainActivity から SecondActivity に飛ぶ際の Intent が SecondActivity 内のメソッドで作られている事がわかります。

Layout Inspector

実行中のアプリのレイアウトがどういった構造になっているか確認することができます。 Tools > Layout Inspector から起動できます。

f:id:rnitame:20191216160431p:plain
Layout Inspector を起動したところ

2 つ以上 Activity を開いていれば、起動時にダイアログが表示されて Activity のクラス名が判明します。実機で触ってみるフェーズにおいて、こちらを併用しながらコードを読んでいくと捗るかと思います。

f:id:rnitame:20191216160301p:plain
スタックに溜まっている Activity のリストが表示される

※ Android Studio 4.0 から使える Live Layout Inspector もあるとより便利ですね!

Hyperion-android の活用

hyperion-android というデバッグライブラリがあり、Timber の中身やクラッシュログを確認することができます。またこのライブラリのサードパーティで、任意のアイテムを追加できるものがあり、今回はそちらを活用します。

github.com

どのクラスにいるかを Toast で出す

Application クラス内で以下のように実装します。

   override fun onCreate() {
        super.onCreate()

        val listener = object : ActivityLifecycleCallbacks {
            private var activityName: String? = null

            override fun onActivityPaused(activity: Activity) {
            }

            override fun onActivityStarted(activity: Activity) {
            }

            override fun onActivityDestroyed(activity: Activity) {
            }

            override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {
            }

            override fun onActivityStopped(activity: Activity) {
            }

            override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
            }

            override fun onActivityResumed(activity: Activity) {
                activityName = activity.javaClass.simpleName
            }

            fun getCurrentActivityName() = activityName
        }
        registerActivityLifecycleCallbacks(listener)

        val item = SimpleItem.Builder("debug")
            .image(R.drawable.ic_launcher_foreground)
            .text("Show class name")
            .clickListener {
                Toast.makeText(this, listener.getCurrentActivityName(), Toast.LENGTH_LONG).show()
            }
            .build()

        SimpleItemHyperionPlugin.addItem(item)
    }

f:id:rnitame:20191216151312g:plain
実際にうごかすとこのようになります

FragmentLifecycleCallbacks を使えば Fragment でも同様に取得できると思います。

まとめ

最初からすべてを把握するのは大変難しく、時間を要します。そのため、基本的な設計とメインループだけわかっていれば最初の入りとしては上出来で、他の画面等については施策やレビュー等でいじった際に理解していけば良いと考えています。 自分は Android Studio やちょっとした実装をすることでカバーしていましたが、他にこういうツールも便利ですよっていうのあったらぜひ教えて下さい!

明日は iOS エンジニアのふくさんが、「“ダーク“な2019年」というタイトルでお届けします。

さいごに

dely では Android エンジニアを絶賛募集中です、ご興味あればこちらのリンクからお気軽にエントリーください!

www.wantedly.com

開発チームについて詳しく知りたい方はこちらから 🙆

wevox.io

note.com

『Deep Neural Networks for YouTube Recommendations』を紹介します

www.youtube.com

はじめまして。
dely, Inc. の @sakura です。

この記事は Google Products Advent Calendar 2019 - Adventar の20日目の記事です。 昨日は新坂さんのGoogle Homeの記事でした。Google Homeに関する様々な実体験が書かれており、とてもほっこりするエントリでした。

polasleep.hateblo.jp


本記事では、YouTubeのレコメンドの仕組みについて書かれている論文『Deep Neural Networks for YouTube Recommendations』を紹介します。

なぜ読んだのか

YouTubeは普段から利用するサイトの一つであり、興味があったからです。
また、社内では「サーベイチャレンジ」という試みがありせっかくの機会なので読んでみました。

tech.dely.jp

本論文について

f:id:sakura818uuu:20191219173446p:plain
本論文の1ページ目

論文へのリンク https://static.googleusercontent.com/media/research.google.com/ja//pubs/archive/45530.pdf

タイトル: Deep Neural Networks for YouTube Recommendations
著者:Covington, Jay Adams, Emre Sargin

これは2016年に発表された論文で、YouTubeのレコメンドに関する技術について書かれています。

イントロダクション

さっそく本論文の内容に入っていきます。

※ここから先は書き言葉になります。そうしないと〇〇らしいです。〇〇そうです。の文章のオンパレードになってしまい読みくいため

f:id:sakura818uuu:20191219173222p:plain:w200
イントロダクションには、YouTubeのレコメンドは3つの大きな挑戦があると書いている。

1.スケールの問題

既存のレコメンドアルゴリズムの多くは小さな問題ではうまく機能することが証明されているが、YouTubeの規模では機能しない
YouTubeの規模になるとユーザーデータも莫大にあるからそこをどう対処していくか。

2.新鮮さの問題

毎秒何時間もの大きな動画がアップロードされるのでレコメンドシステムはこれに耐えうる性能でなければならない。
また、ユーザーは新しいコンテンツを好むことはわかっているがそれと引き換えに適合度(関連性)や既存のコンテンツとのバランスは慎重に考慮する必要がある。

3.ノイズの問題

YouTubeでの過去のユーザー行動は、スパース性(まばらという意味)と色んな観測不能な外部要因があるから予測がかなり困難。
加えて、各ビデオのメタデータの構造が不十分である。
従って、アルゴリズムを作るにあたってこのような特性を十分に加味しておかなければならない。

わかったこと

論文を読んでわかった部分のいくつかを紹介します。
全てについては解説していないのでご了承ください。

レコメンドシステムの2層のニューラルネットワーク

f:id:sakura818uuu:20191219174303p:plain
Figure 2: Recommendation system architecture demonstrating the “funnel” where candidate videos are retrieved and ranked before presenting only a few to the user.

レコメンドシステムには2層ニューラルネットワークを使用している。
1つはレコメンド群の候補たちを生成するもの、もう1つはそれらをランキングのように並び替えるものである

前者には特徴量行列としてユーザーのYouTubeアクティビティ履歴を使用しているとのこと。
また、前者には協調フィルタリングも使用していてパーソナライズを実現している。協調フィルタリングにはアイテムの類似度のみならず、ユーザー同士の関係性も必要になってくるがそこはどんなビデオを見ているか、どんな検索クエリを入力しているか、あとは一般的な人口統計などの情報を使っているらしい。

暗黙的フィードバック

f:id:sakura818uuu:20191219174447p:plain
YouTubeのGood/Badボタン

YouTubeにはGood/Badボタンなど様々な明示的フィードバックがあるが、モデルをトレーニングする時は暗黙的なフィードバックを使う。

暗黙的なフィードバックとはなにかの説明については以下のブログがわかりやすかったです。

この記事では,ユーザの行動履歴をフィードバックと呼びます.一言にユーザからのフィードバックと言っても様々な種類があります.例えば,ユーザがアイテムに対して点数(rating)をつけたものや,like・dislikeのようにそのアイテムを好きか嫌いかを表すものがあります.このタイプは,ユーザがそのアイテムを好きかどうかがはっきりとわかるので,明示的フィードバック(explicit feedback)と呼ばれます.一方で,単なる閲覧・購入履歴などのデータも存在します.このデータからは,閲覧したけど好きだったかどうかはわかりませんし,購入したけど満足したかどうかはわかりません.したがって,このタイプは暗黙的フィードバック(implicit feedback)と呼ばれます.

「推薦」の定式化から推薦システムを理解する | カメリオ開発者ブログ

アルゴリズムに使われている情報

論文中に記載されていただけでもこれだけの情報がレコメンドアルゴリズムに使用されている可能性があることがわかった。
3.3 Heterogeneous Signalsなどに記載されている。

・ユーザーのYouTubeアクティビティ履歴
・ユーザー間の類似度
・ビデオ間の類似度
・ユーザーの視聴履歴
・ユーザーの地域
・ユーザーの性別
・ユーザーのログイン情報
・ユーザーの年齢
・ユーザーはいくつこのチャンネルの動画を見ているか
・ユーザーがこのトピックに関するビデオを最後に見たときはいつか
(・もしレコメンドした動画をユーザーがみなかったらその動画は降格させる

など

CTRより視聴時間

Ranking by click-through rate often promotes deceptive videos that the user does not complete (“clickbait”) whereas watch time better captures engagement [13, 25].

[13] E. Meyerson. Youtube now: Why we focus on watch time. http://youtubecreator.blogspot.com/2012/08/ youtube-now-why-we-focus-on-watch-time.html. Accessed: 2016-04-20.

[25] X. Yi, L. Hong, E. Zhong, N. N. Liu, and S. Rajan. Beyond clicks: Dwell time for personalization. In Proceedings of the 8th ACM Conference on Recommender Systems, RecSys ’14, pages 113–120, New York, NY, USA, 2014. ACM.

「CTRで測定するランキングはしばしばdeceptive(こすい、詐欺的)なビデオを促進するものだ。それよりも視聴時間を見るほうがユーザーのエンゲージメントをよく捉えているといえるだろう。[13,25]」

と引用して記載されている部分がある。

レコメンド群の候補たちを生成するモデル

f:id:sakura818uuu:20191219174638p:plain
Figure 3: Deep candidate generation model architecture showing embedded sparse features concatenated with dense features. Embeddings are averaged before concatenation to transform variable sized bags of sparse IDs into fixed-width vectors suitable for input to the hidden layers. All hidden layers are fully connected. In training, a cross-entropy loss is minimized with gradient descent on the output of the sampled softmax. At serving, an approximate nearest neighbor lookup is performed to generate hundreds of candidate video recommendations.

図はレコメンド群の候補たちを生成するモデルを説明しているものだ。

視聴履歴や検索履歴をベクトル化して、そこにユーザーの地理的な情報や年齢、性別などの情報をベクトル化してくっつけている。
それをReLU関数に3回かけてsoftmaxでトレーニングしてクラスの確率を推定してる。(多クラス分類なので
最後に、上記でやった結果を最近傍探索して上位N件を取り出している。

まとめ

YouTubeのレコメンドに関する論文
Deep Neural Networks for YouTube Recommendations』を紹介しました。

YouTubeってついつい見て時間経っちゃってる時ありますよね。
その裏ではこんなことが起こっているんだな、と一端を知ることが出来ました。

機械学習に詳しいわけではないので理解できない部分も多々ありましたが、
YouTubeのレコメンドの仕組みが少しでも理解できて感動しました。

もし、YouTubeに興味がある方がいれば読んでみると面白いかもしれません。

さいごに

最後に告知です。delyではエンジニアを絶賛募集中です。
ぜひお気軽にご連絡ください。

https://www.wantedly.com/projects/329047