Quantcast
Channel: クックパッド開発者ブログ
Viewing all 801 articles
Browse latest View live

大きな Rails アプリケーションをなんとかしよう。まずは計測と可視化からはじめよう。

$
0
0

こんにちは、技術部開発基盤グループの id:hogelogです。

RubyKaigi 2018 楽しかったですね。僕はおそらく RubyKaigi 2010 以来の久しぶりの参加でした。ああいう場の楽しさを思い出し、また今回はスポンサーブースから RubyKaigi に参加するという学生の頃は知らなかった楽しみも新たに知り、RubyKaigi を満喫させていただきました。

さて今回はそんな RubyKaigi で取り戻した Ruby に対する感情と関係あるようなないような、最近自分が取り組んでいるお台場プロジェクトとプロジェクト内で実施している計測と可視化について紹介します。

お台場プロジェクトの発足

クックパッドの開発といえば数年前までは cookpad_all という一つのリポジトリの中に詰め込まれた巨大なモノリシック Rails アプリケーションを社内のエンジニアが寄ってたかって開発するというのが典型的な開発スタイルでした。世界でも類を見ない規模の巨大な Rails アプリケーションの開発であるため、もちろん多様な技術的困難が発生していましたが様々な技術を用いてアプリケーションをメンテナンスし Rails の良さを損なわず開発が進められるように努力していました。*1

しかしその後クックパッドでも徐々にモノリシックアプリケーション構成から Microservices 構成への移行が進んでいきました。 techlife.cookpad.com

そして気づけば cookpad_all は社内に数多く存在する他のアプリケーションと比較してずいぶんと古臭い、触ることが忌避されがちなアプリケーションの代表となっていました。 https://cookpad.com/のバックエンドの大部分を支える重要なアプリケーションであるというのは変わっていないのに。そこで始まったのがお台場プロジェクトでした。お台場プロジェクトとはなんなのか。その全貌を語るのはまた別の機会としますが、実施することは端的に言えば cookpad_all というアプリケーションの実装の改善です。

お台場プロジェクトではレガシーなシステムの削除、未使用コードの削除、システム分割など様々なことをおこなっており、 id:riseshiaが取り組んだ Ruby の lazy loading の仕組みを利用して未使用の gem を探す - クックパッド開発者ブログや RubyKaigi 2018 LT で発表した Find out potential dead codes from diffもその一環です。

以下ではお台場プロジェクトを進めるにあたって取り組んだ cookpad_all 関連メトリクスの計測について紹介します。

cookpad_all 関連メトリクスの計測

cookpad_all の開発における困難を改善するといってもどう改善されているのか記録し、可視化しなければなにもわかりません。

そこでお台場プロジェクト開始初期にまず cookpad_all に関するメトリクスを計測し、社内で稼働させている InfluxDB に記録し、Grafana でダッシュボードを作成しメトリクスを可視化できている状態を作りました。

f:id:hogelog:20180607161915p:plain

具体的には現在 cookpad_all では以下のようなメトリクスを可視化し、改善を進めながら経過を観測し続けています。

  • CI Duration
  • App Load Time (development / production)
  • Loaded File Count
  • Code Statistics
  • GemCollector Up-to-date Point
  • Dependent Gem Count

CI Duration

これは Jenkins で実行している CI にかかった時間の計測です。気をつけることとしては失敗した時の実行時間は不安定になることが多いので、成功した時の時間のみ記録していることです。上記の図で示すように 2017/7 〜 2018/6 現在に至るまで、長いものでは 10 分程かかっていたものが 7分程度まで実行時間が削減されています。

App Load Time

開発者が手元で bin/rails sした時にアプリケーションが動き出すまでの遅さはわかりやすく辛い箇所です。cookpad_all の各アプリケーションでは定期的に以下のようなスクリプトを実行しアプリケーションのロードにかかった時間を計測しています。

defprofile_app_load_timeBenchmark.measure do
    system("./bin/rails r '1;'") orraise"error"endend# Warming disk cache, ...
puts profile_app_load_time

3.times do
  result = profile_app_load_time
  puts result
  influx.write_point("cookpad_ci_app_load_time", tags: { app: app }, values: { load_time: result.real })
end

Loaded File Count

これはアプリケーションのロードが終わった時点での $LOADED_FEATURESの数です。この数字は依存 gem の追加や削除、大規模なコード削除などで大きく数字が動き、アプリケーションになにか大きな変更があったことの観測に役立っています。

Code Statistics

bundle exec rake stats*2の数字を記録するものです。この数字も時々誰かがどこか外部で「クックパッドの巨大 Rails アプリケーション」の発表をする時に計測する程度で、定点観測はおこなわれていませんでした。

f:id:hogelog:20180607172035p:plain

大きなシステム分割などにより時々グッと下がっている以外にも、日常的なコード掃除などで地道ながらもコード削減が進んでいることがダッシュボードを見るだけでわかるようになりました。

Dependent Gem Count

依存している gem の数の記録です。数が増えれば増えるほど gem の依存関係が深くなり、新規 gem の導入や既存 gem の更新などが難しくなっていきます。

f:id:hogelog:20180607161805p:plain

このメトリクスは git のログを遡り 2011 年頃からの値を計測してみましたが、依存する gem 数はお台場プロジェクトが始まるまでは増える一方でありアプリケーションを小さくしていこうという開発の流れはほぼ存在していなかったことがわかります。

ちなみに一瞬依存 gem 数 が400個を超えたところがあるのが目を引くかもしれないので説明しておくと、これは aws-sdk を v2 -> v3 にアップグレードし、その後で必要な aws-sdk-* のみに絞るよう修正したためです。

GemCollector Up-to-date Point

これは この gem を使っているアプリケーションを探す - クックパッド開発者ブログで紹介した GemCollector で出している gem の最新度を記録しているものです。

f:id:hogelog:20180607161828p:plain

この値は相対的なものであるため、gem のバージョンアップに追従していかないとどんどんポイントが下がっていきます。対応をおこたっていくといどんどんアプリケーションがレガシーになっていく状況を把握するのに非常に便利なグラフになっています。

まとめ

クックパッドでは現在巨大モノリシック Rails アプリケーションに頼った開発から Microservices 構成のアプリケーション群を組み合わせて使ったサービス開発への急速な移行段階にあります。その中で最後に残されている巨大 Rails アプリケーションを改善していくためのメトリクス収集と可視化ダッシュボードについて紹介しました。

私達はそういうことを一緒にやっていく仲間をもっともっと求めています。定型文じゃなくて本当に求めています。採用への応募またはどんな会社なのか聞くために遊びに来たいみたいなお声がけ、お待ちしております。

*1:どんな技術を用いていたか詳しくは Ruby on Ales 2015 で @amatsudaが発表した The Recipe for the World's Largest Rails Monolithなどで詳しく説明されています

*2:実際にはちょっと特殊なディレクトリ構成に対応するため cookpad:stats という独自タスクを定義しています


Header Bidding 導入によるネットワーク広告改善の開発事情

$
0
0

こんにちは。メディアプロダクト開発部の我妻謙樹(id:kenju)です。 サーバーサイドエンジニアとして、広告配信システムの開発・運用を担当しています。

cookpad における広告開発

2015年11月に、"クックパッドの広告エンジニアは何をやっているのか"というタイトルで、広告開発部の開発内容について紹介する記事が公開されていますが、それから 2 年余り経過し、広告配信システム周りの状況も大きく変化しました。はじめに、現在の cookpad における広告開発の概要について、軽くご紹介します。

まず、私が所属しているメディアプロダクト開発部では、広告配信システムに加え、動画配信サービスの開発も担当しています。過去には同じチームから、動画配信周りの技術について以下のような投稿もありますので、そちらもご覧ください。

広告配信システムの開発で担当しているサービスの一覧は、以下の通りです:

  • 広告配信サーバーの開発 (Rails)
  • 社内向け広告入稿システムの開発 (Rails)
  • 広告ログ基盤の運用(Python, Kinesis Streams, DynamoDB, Lambda)
  • 広告配信用 SDK の開発(各プラットフォームに準拠。WEB 向けは JavaScript)

プロジェクト別で言えば、以下がスコープです:

  • 既存自社広告商品の改善
  • 新広告商品の新規開発
  • ネットワーク広告商品の開発や改善

例えば、昨年末から今年はじめにかけて、とある新広告商品の開発に携わっていたのですが、その時のプロジェクトの一部についてスライドが公開されているので、そちらもご覧ください。

今回は、その中でも「ネットワーク広告商品の開発や改善」における一プロジェクトについてご紹介します。

背景

cookpad では、以下の 2 種類の広告を配信しています。

  • 自社広告 1
  • ネットワーク広告 2

この内、ネットワーク広告においては、 Supply Side Platform (以下、SSP)各社と連携して複数のアドサーバー経由で広告を配信しているのですが、それらの広告は頭打ちになってきています。そのため、広告の収益改善に取り組む必要がありました。

安易に広告枠を増やすことはユーザー体験の低下やネットワーク負荷増加に繋がるため、避けなければなりません。したがって、現在配信されている広告の買付け額や配信フローを改善させる必要があります。

そこで、近年日本でも導入が進んできている Header Bidding と呼ばれる仕組みを導入することになりました。

About Header Bidding

Header Bidding とは、アドサーバーに広告のリクエストをする前に、SSP 各社に広告枠の最適な額を入札します。

仕組み的には、<head>タグ内(= Header)で事前に入札リクエスト(= Bidding)を行うことから、"Header Bidding"と呼ばれています。

Without Header Bidding

例えば、Header Bidding を経由しない、従来のパターンでの広告枠買付け方式を見てみましょう。ここで図の用語の定義は以下の通りとします:

  • Client ... 広告を表示させる側ここでは cookpad の本体サイト
  • Ad Server ... アドサーバー
  • SSP ... SSP各社
  • Floor ... フロアプライス 3
  • Bid ... 入札結果 4
  • Winning bid ... 買付けに成功した入札価格

f:id:itiskj:20180613203016p:plain

既存のアドサーバーの入札ロジックは、基本的にウォーターフォール方式(5)で買付けが行われます。したがって、上記図のケースの場合、

  • 広告枠に対してフロアプライスが $1.0
  • SSP α の入札結果が $0.8(フロアプライス以下)
  • SSP β の入札結果が $1.2(フロアプライス以上)

上から順番に問い合わせていった結果、最初にフロアプライス以上の価格で入札してきた「SSP β」の広告が表示されることになります。

ここで「SSP δ」の入札結果が、$2.0 であることに着目してください。もし、「SSP δ」入札結果を反映できていたら、その広告枠の価値は $2.0 になります。つまり、広告枠本来の価値は $2.0といえます。しかし、ウォーターフォール形式の仕組み上の制約によって、$1.2 の入札結果が反映されてしまいました(差し引き $0.8 の機会損失ですね)。

これを解決するのが、Header Bidding です。

With Header Bidding

次に、入札サーバを挟む場合で見ていきましょう。なお、この場合は後述する Server-to-Server 方式で説明していきます。

f:id:itiskj:20180613203037p:plain

この場合、

  1. Client -> Bid Server ...Header Bidding を入札サーバにリクエスト
  2. Bid Server -> SSP ... 入札。このとき、各社への入札は 同じタイミングで入札される
  3. Bid Server -> Client ... 入札結果を返す。ここでは、「SSP δ」が $2.0 で買付けをすレスポンスを返してきたので、Winning bid が $2.0 になる
  4. Client -> Ad Server ... 入札結果が返ってきてから広告をリクエストする。このとき、Winnig bid を伝えることで、「SSP δ」広告が返却されることになる 6

という順番で処理が実行されます。Header Bidding を利用しないケースと違って、一番買付け額が高い SSP の入札結果が反映されたことがわかります。

ポイントは、

  • アドサーバーにリクエストする前に入札を行うこと
  • SSP 各社へのリクエストが並行に行われること

です。これによって、本来失われてしまっていた入札価格を最適化することができます。

Client vs S2S Header Bidding

Header Bidding には

  • Client Header Bidding
  • Server-to-Server Header Bidding

の2種類の方式が存在します。

Client Header Biddingは、クライアント側で入札を行う形式です。技術的には、<script>タグ内で、入札先の SSP 一覧を指定して、それぞれに入札リクエストを行います。それらの入札リクエストの結果を待って、一番 eCPM の優れた入札結果を選択する形式です。

Server-to-Server Header Bidding は、サーバー側で入札を行う形式です。Client Header Bidding との違いは、入札サーバーに 1 回だけリクエストを送信すれば良い点です。また、入札ロジック(例:SSP 各社からの入札結果の待ち合わせ、タイムアウト処理、入札結果の比較)を入札サーバーが担ってくれるので、クライアント側の責務が大幅に削減されることです。

現在は、Server-to-Server 方式が主流です。

Header Bidding Services

なお、Transparent Ad Marketplace(以下、TAM)という、Amazon が提供する Header Bidding の広告サービスを採用しています。

設計・実装

基本的には、TAMの提供するドキュメントに沿って、<head>タグ入札サーバーにリクエストするスクリプトを埋め込めば、導入は完了します。

しかし、弊社の場合

  • 独自の広告入稿・配信サーバーを介して自社広告・ネットワーク広告すべての広告を配信している
  • しかも、ページごとに配信される広告スロット(7)は静的ではなく動的に変化する

といった制約のため、スムーズな導入ができず改修が必要でした。

以下の図が、Header Bidding を行うまでの大幅な流れです。ここで、

  • ads ... 社内広告配信サーバ
  • display.js ... 広告表示用の JavaScript SDK
  • cookpad_ads-ruby ... display.js を埋め込むための Rails 用ヘルパーを定義した簡易な gem
  • apstag ... TAM の提供する Header Bidding 用ライブラリ
  • googletag ... DFP の提供するアドネットワーク用ライブラリ

だとします。なお、googletag の公式ドキュメントは、https://developers.google.com/doubleclick-gpt/referenceからご覧になれます。

f:id:itiskj:20180613203104p:plain

Header Bidding を実行するまでの大まかな流れは、以下の通りです:

  1. JavaScript SDK が、広告配信サーバーから表示すべき広告リクエストする
  2. 広告にネットワーク広告が含まれている場合、Header Biddingの一連の処理を開始する
  3. まずは、apstag, googletag それぞれの初期化を行う(例:デフォルトのタイムアウト設定)
  4. apstag を用いて TAM に Header Bidding リクエストを送る
  5. 入札結果をもとに、DFP に広告リクエストを送る
  6. DFP から広告リクエストが返却されたら、広告を表示する

ポイントは、Header Bidding をリクエストしている間、

  • googletag.pubads().disableInitialLoad()で DFP リクエストを中断し、Header Bidding を行う
  • 入札結果が返ってきたら、googletag.pubads().refresh([opt_slots...])で広告のレンダリングフローを再開する

という点です。

結果

以上を持って、Header Bidding を導入するまでの一連の流れを説明してきました。具体的な数字はここでは伏せますが、今回の導入によってネットワーク広告の収益改善を実現することができました。

新たな課題

広告レンダリングフローのパフォーマンス悪化

しかし、ここで新たな課題も発生してしまいました。

それは、Header Bidding リクエストの分、ネットワーク広告が表示されるまでのレイテンシが増加してしまった、という点です。

広告が表示されるまでの一連のレンダリングプロセスを、以下の図に示しました。

Processing, DOMContentLoaded, loadは、ブラウザが HTML/CSS をパースしてレンダリングするまで一連のフローの一般的用語です。気になる方は、Ilya Grigorik(8) による"Measuring the Critical Rendering Path"をご覧ください)

f:id:itiskj:20180613203121p:plain

ぱっと見て気づくのは、社内広告配信サーバの ads へのリクエストから、Header Bidding 、そして DFPへのリクエストまですべてがシリアルに実行されていることです。今回 Header Bidding を導入したことによって、その分レイテンシが増加したのです、大体 150 ~ 400 (ms) と、かなり致命的なパフォーマンス低下になってしまいました。

広告レンダリングフローの可視化がされていない

筆者の肌感で「150 ~ 400ms」と説明しましたが、実はクライアント環境で実行されるまでの広告レンダリングフローは、今まで計測・可視化されていませんでした。

上記で挙げた広告レンダリングフローのパフォーマンスを改善したいものの、ボトルネックが正確にはどこになるのか、計測するまでわかりません。計測できないものは改善できないと言われるように、まずは計測・可視化のフローを導入しました。

ここで幸いなことに、Fluentd にログを流し、分析可能なデータウェアハウス(cookpad の場合は、現状 Redshift)にテーブルを構築するまでの仕組みはすでに存在していました。したがってクライアント側と多少のテーブル定義を書くだけで実現できました。

(補足:cookpad におけるデータ活用基盤については、"クックパッドのデータ活用基盤"をご覧ください。)

以下は、取得したログデータをもとに可視化してみた様子です。ログを先日から取得し始めたばかりなので、可視化のフローはまだ未着手です。社内で推奨されている BI ツールにダッシュボードを作り、定点観測できるところが直近のゴールです。

f:id:itiskj:20180613203133p:plain

広告レンダリングのクリティカルパスは任意のタイミングでロガーを仕込むだけですが、DFP の場合、googletag.events.SlotRenderEndedEventを利用すると、広告枠が表示されたタイミング、広告が "Viewable"(9)になったタイミングでイベントを取得できます。

対策と今後の展望

以上が、Header Bidding の導入から、新たに浮上した課題への対策の説明でした。直近ですと、以下に取り組んでいく予定です。

  • 広告レンダリングフローの可視化フェーズ
  • 広告レンダリングフローの最適化

広告レンダリングフローの最適化

「広告レンダリングフローの最適化」では、自社の広告配信サーバーへのリクエストのタイミングを、今より前倒しにする方針で設計及び PoC の実装を行っている段階です。

具体的に言うと、現在 HTML ファイルの <body>下部で広告レンダリングフローを開始しているのですが、それを <head>タグの可能な限り早い段階で開始するように改善をする必要があります(過去の設計の都合上、広告配信サーバーへのリクエストは、各広告スロットの HTMLElement 要素が レンダーツリー10に挿入され、実際に描画されるタイミングにブロックされている)。

f:id:itiskj:20180614112206p:plain

パフォーマンスの可視化及びレンダリングフローの最適化についても、また別の機会にご紹介したいと思います。

まとめ

アドテク関連のエンジニア目線での事例紹介や技術詳解はあまり事例が少ないため、この場で紹介させていただきました。技術的にチャレンジングな課題も多く、非常に面白い領域です。ぜひ、興味を持っていただけたら、Twitterなどからご連絡ください。

また、メディアプロダクト開発部では、一緒に働いてくれるメンバーを募集しています。少しでも興味を持っていただけたら、以下をご覧ください。


  1. 自社広告 … 自社独自の営業チームが、直接広告主と契約を結び配信している広告。自社で配信されるクリエイティブを運用できるため、意図しない広告が配信されることがない。

  2. ネットワーク広告 … 他社の広告配信会社が提供している広告配信サーバーを経由して、広告の買付け・配信を行う広告。各社が提供する <script>タグを HTML に埋め込み、返却された広告を <iframe>にレンダリングする形が一般的。

  3. フロアプライス … 最低落札価格のこと。例えば、「フロアプライス $0.8」広告をリクエストしたとき、$0.8 以下の広告枠の買付けは行わない。

  4. 入札結果 …SSP 各社が、広告枠をいくらで買い付けるかを示す価格。これがフロアプライスより低い場合、広告枠に広告が表示されることはない。

  5. ウォーターフォール方式 … SSP 各社定義した順番で一つ一入札していく方式。「滝」語義が表すとおり、上から順に問い合わせ得ていく様子からこの名前で呼ばれる。

  6. アドサーバー側にどのように入札結果を伝えるかは、各アドサーバー側の仕様や実装に依存する。例えば TAM が DFP に対して Header Bidding を行う場合、Key/Value Targetingの仕組みを使っている。

  7. 広告スロット … 広告枠が表示される枠のこと。

  8. Ilya Grigorik … Google のエンジニアで、“High Performance Browser Networking”の著者、と言えばわかる方も多いかもしれません。完全に蛇足ですが、尊敬しているエンジニアの1人で、彼の著作をきっかけ Web の裏側に興味を持ちました。

  9. Viewable Impression … 広告が「ユーザーに見える状態」になったかどうかでインプレッションを測定している。詳細については、“Viewabiliity and Action View”を参考のこと。

  10. Render Tree … https://developers.google.com/web/fundamentals/performance/critical-rendering-path/render-tree-construction

無理をしないコードレビュー

$
0
0

会員事業部の三吉です。 クックパッドでは、GitHub Enterprise の Pull Request を使ったコードレビューを広く実施しています。 この記事では、私がコードレビューすることに対する苦手意識をなくすために意識したことを紹介します。

クックパッドでは、テックリードや新卒、インターン、バイトといった肩書きに関係なく、誰もがレビュワー・レビュイーになります。 チームやプロダクトによって開発ルールは少しずつ異なりますが、私の所属する会員事業部では、PR を出したときに GHE やチャットで部内のエンジニアにメンションして、その時にレビューできる人がレビューするという形を取っています。

私は、昨年2017年に新卒入社したのですが、それまでは個人開発や研究用のコードしか書いたことがなく、短期インターンシップを除くチーム開発の経験がありませんでした。 配属当初からコードレビューすることは求められていのですが、はじめの数ヶ月間はレビューすることに対して苦手意識があり、なかなか積極的にレビューに参加することができませんでした。

コードレビューの難しさ

コードレビューに対する苦手意識は、自分のレビュー内容に自信が持てないことによるものでした。 コードレビューは、真剣に取り組もうとすると非常に難しい作業です。 以下、「見るべき項目の多さ」「文脈によるレビュー内容の変化」「開発速度とのトレードオフ」の3点からコードレビューの難しさを見ていきます。

見るべき項目の多さ

コードレビューでチェックするべき点は非常に多いです。 以下に、ざっくりとレビューの観点をカテゴライズしました(順序に深い意味はありません)。

  • 挙動(意図どおり動作するか)
  • バグ
  • セキュリティ
  • 可読性
  • テスト
  • パフォーマンス
  • ドキュメンテーション
  • 設計 *1
  • ……などなど

これらは、それだけで分厚い本が書けるようなカテゴリであり、それぞれについて一般的な原則や社内のルールがたくさんあります。 ちょっとした変更であればともかく、毎回のコードレビューでそれらを厳密に網羅することは不可能で、どこかで妥協する必要があります。

文脈によるレビュー内容の変化

では、どこで妥協するのかというと、それはレビュー対象のコードの文脈に依存します。

極端な例を挙げると、ユーザーテスト用のプロトタイプと、決済周りのデータ処理を行うジョブとでは、どのくらい細かくレビューするべきか変わってきます。 前者は、あくまでプロトタイプであって、テストするのに問題なければ最悪バグが含まれていても構いません。 それに対し、後者にバグが含まれていると、深刻なデータ不整合が起きたり、ユーザーに不利益が出たりすることになります。 後者をレビューするときには、テストの内容やエンバグの可能性の丁寧なチェックが必要です。

他にも、変更頻度の高い箇所であればメンテナビリティを考慮したり、検索などリクエストの多い部分であればパフォーマンスを意識したりと、コードの位置する文脈に依存して、重点を置くべき観点が変わってきます。

開発速度とのトレードオフ

レビュー時どこに重点を置くべきかは、文脈だけでなく、レビューに割ける時間によっても変わります。

コードレビューは思いのほかコストのかかる作業です。 レビューしているあいだレビュワーは他の作業をすることができず、また、レビュイーにも指摘箇所の修正だけでなく、レビューがつくたびに発生するコンテキストスイッチの負担があります。

しかし、コードレビューにはそのコストをかけるだけの価値があります。 私たちがコードを書く目的は、ユーザーに価値を届けることであり、コードレビューの目的も変わりません。 コードレビューは、届ける価値の品質を担保するために必要な作業です。

とはいえ、サービス開発においては、その価値をすばやく届けることも非常に重要です。 レビューに不必要なまでに時間をかけて、高速な開発サイクルを回せなくなっては本末転倒です。 品質と速度とのトレードオフから、レビューにどの程度コストをかけるべきか考える必要があります。

コードレビューをするときに意識していること

以上にみてきたように、コードレビューは、無数の項目について、コードが置かれた文脈から優先順位をつけ、開発速度と品質を最大化するような時間でチェックしていく、というとても困難な作業です。

……と、これが理想のコードレビューかもしれませんが、人間が意識的にできるものではありません。 重要なのは、そういった難しさがあることを知った上で、できる範囲でやることです。 とはいえ、コードレビューに慣れない頃は、できる範囲でやったレビューには多くのヌケモレがあるように思えて不安になったり、しっかりレビューしたときには時間をかけ過ぎではないかと不安になったりしていました。 ここからは、そうした不安や、それに起因する苦手意識をなくすために私が行っている工夫を紹介します。

レビューした範囲を明示する

自分のレビューが不十分に感じられたときは、何を見たか、あるいは、何を見られていないかを Review summary などに書くようにしています。

f:id:sankichi92:20180618184907p:plain

こうすることで、他のレビュワーに重点を置いて見てほしい場所を伝えています。 広く薄く見ただけであれば「ざっと見ました」というコメントでも構いません。

また、自信のないときは、素直に他のレビュワーに依頼します。

f:id:sankichi92:20180618184918p:plain

重要な定数などについて、仕様と照らし合わせて正しいことを指差し確認するのも、ヌケモレを防ぐのに大事です。

f:id:sankichi92:20180618184933p:plain

わからなかったら質問する

もちろん、他のレビュワーが現れることを期待できない場合は、ひとりで全範囲をレビューする必要があります。 そういう時に自信を持てない箇所、わからない箇所を見つけたら、わかった気になるまで読むのではなく、質問することが重要です。

次のように、質問に答えることで、実装者がミスや考慮漏れに気づくということも少なくありません。

f:id:sankichi92:20180618184946p:plain

コードから汲み取ったことを言語化して、その認識であっているか確認するだけでも効果があります。 込み入った内容であれば、実装者と一緒にペアレビューするという方法も有効です。 ペアレビューで理解した内容を PR にコメントするとなお良いです。

コードレビューには、問題点を見つけるだけではなく、そのコードの理解者を増やして属人性をなくす機能もあります。 GHE に残った質問のログが数年後に再び役立つということも珍しくありません。

nits や IMO, MUST といったラベルを利用する

先にも述べたように、サービス開発ではスピードも重要です。 本質的でない修正に時間をかけるよりも、先にリリースした方が良い場合もあります。

とはいえ、レビューしていると、どうしても細かいところが気になってしまうものです。 そうしたときは、[nits] や [IMO] といったラベルをレビューコメントの先頭につけて、修正の判断を実装者にゆだねます*2

f:id:sankichi92:20180618184957p:plain

逆に、どうしても修正してほしい場合は [MUST] ラベルをつけます。 すぐに修正できない問題であれば、その PR で修正するのではなく、Issue にして後日修正するのも有効です。

おわりに

以上が、私の意識している「無理をしないコードレビュー」です。 レビューすることに慣れて、日常化すれば特に意識する必要はなくなります。 しかし、1年前の私にとってそれはとても難しいことだったので、当時の私に伝えるつもりでコードレビューするときのコツを書いてみました。

コードレビューする機会が増えて感じるのは、レビュワー側も非常に勉強になるということです。 レビューする時の視点は、単にコードリーディングする時の視点とは違います。 エンバグはないか、テストは必要十分か、などなど普段以上に神経をとがらせて見ることになります。 そして、その視点はそのままコードを書く時にも活かすことができます。

また、今回はコードレビュー「する」ときの工夫に焦点を当てました*3が、「される」ときの工夫も重要です*4。 他にも、クックパッドでは、コードレビューのコストを下げるための自動化等の取り組みも行っています*5

コードレビューのスタイルは、組織や開発するプロダクトの性質によって変わってくるものだと思いますが、この記事が少しでも参考になれば幸いです。

*1:大きな変更であれば事前に設計レビューを行います。

*2:リポジトリにもよりますが、クックパッドでは実装者がマージを行うことが多いです。

*3:コードレビューすることに関する記事として、他にも たのしくなるコードレビューがあります。

*4:開発速度を上げるための Pull-Request のつくり方など。

*5:Android開発のコードレビューbotを乗り換えた話など。

RubyKaigi 2018 ありがとうございました!

$
0
0

人事部の@mamiracle__です。好きな Ruby のメソッドは Enumerable#entriesです。みなさまからの entriesをお待ちしています💖

さて、クックパッドは先日の RubyKaigi 2018に Ruby Committers Sponsor と Network Sponsor としてスポンサーをいたしました。私たち人事メンバーも、ブースやドリンクアップを通じて、RubyKaigiを盛り上げることに貢献できたのではないかとおもっています。

会期中には、クックパッドに所属する桑原仁雄(@pocke)、Kirk Haines(@wyhaines)、 笹田耕一(@ko1)、遠藤侑介(@mame)がスピーカーとして登壇し、심 상용(@riseshia)がライトニングトークを行なっています。また、RubyKaigi運営では、オーガナイザーとして @nano041214@asonas@sorahが、スタッフとして @mozamimyが活躍してくれました。

イベント参加のご報告として、当社所属のメンバーの発表やブースの紹介をいたします!!!

発表

A parser based syntax highlighter

桑原仁雄(@pocke)からはパーサーベースのシンタックスハイライターであるIroに関して発表がありました。通常のシンタックスハイライターで利用されている正規表現では、複雑なプログラム表記でうまく機能しなくなってしまいがちですが、パーサーベースであればそういった問題が生じないそうです。また、gemとして提供されているので、Vimだけではなく色々なエディタでも活用できるとのことです。

また、RubyKaigi効果で意識が高まった本人からも力強い宣言がでておりますので、ぜひ今後にご期待ください!

It's Rubies All The Way Down

自身が2001年以来の Rubyist であると紹介した Kirk Haines(@wyhaines)からは、主に Web アプリケーションを動かすときのテクノロジスタックについて、それぞれの時代における Ruby で作られたソフトウェアの紹介や、概念実証(Proof of Concept)が示されたりしました!

特に Web アプリケーションにおいては Rack の登場がキーポイントになり、ミドルウェアとWeb サーバーの関係がそれ以前と比べてシンプルになったという説明は納得できました。また、10年以上前の Ruby では、ロガーやロードバランサーなどをやるには遅すぎると言われることがありましたが、近年の Ruby では十分なパフォーマンスが確保できることも示され、Ruby の進化がよくわかりました。

Guild Prototype

Ruby の新しい並行プログラミングモデルである Guild について、笹田耕一(@ko1)から発表がありました。冒頭では、シングルプロセスで合計40コアのCPUを使いきるデモがされていて、未来感ありましたね。

従来の並行プログラミングでは、データを共有するために競合状態をプログラマが頑張るか、データを共有しないという手法が主にとられてきました。Guild では、メンバーシップという概念を導入することで、容易にデータを共有しつつも、マルチコアを活用できるようにしています。Rubyで実用段階になるのが、今から楽しみです!

Type Profiler: An analysis to guess type signatures

遠藤侑介(@mame)の発表では、提案されている複数のRubyの型システムを概観したあと、Ruby 3に必要となってくる型データベースのために、型プロファイラーの導入が提案されました。型プロファイラーは、いくつかの手法で型を推測するための機構です。静的解析や動的解析をつかって、いい感じに型を推測できないかという試みです。それぞれの手法にメリットやデメリットがあって、まだまだ難しいところも多いようですが、 Ruby の改善が着実に進んでいる様子が伝わってきました。

また、ブースの質疑応答タイムでは、海外のファンからチョコレートのプレゼントをもらうなど、大人気ぶりを発揮していました!

ライトニングトーク

Find Out Potential Dead Codes from Diff

Cookpad のような長期でメンテナンスされているコードでは、どうしてもどこからも呼び出されないデッドコードが生まれてしまいます。심 상용(@riseshia)のライトニングトークでは、未使用コードの差分からデッドコードを検出する手法について発表がありました。クックパッドでも実際に使われて効果がある手法とのことです。

ブースやグッズ

クックパッドが海外展開している国を示した世界地図や、「Cookpad storeTV」、ユーザーボイスやミッションなどを詰め込んだオリジナルボードなどを展示しました。

f:id:cookpadtech:20180614152425j:plain(地図にはクックパッドが展開している68カ国を⭐シールで示しました)

ノベルティには今年初めて作った「ロゴ入りお箸」や、仙台をイメージした「ずんだ餅どら焼き」を用意しました。どら焼きは、仙台の老舗和菓子屋さん「こだまのどら焼き」さんに相談をして作っていただきました。控えめな甘さのずんだ餡と、もちもちの食感が美味しくてあっという間に完売してしまいました!

【RubyKaigi 2018のスポンサーで仙台にきています♦️】 クックパッドは毎年、Rubyというプログラミング言語のカンファレンス #RubyKaigi に協賛しています🤗 2018年は5/31〜6/1まで #仙台 での開催なので、 #ずんだ餅どら焼き を作ってみました❤️ 仙台の老舗 #こだま さんのどら焼きです! クックパッドでは、ユーザーさんにより速く良い価値を届けるために、技術もすごく大事にしているんですよ! 今後はそういったこともご紹介できればと思います🎶 #クックパッド #テクノロジーカンパニー #どら焼き #ずんだ餅 #仙台名物 #こだま #cookpadorayaki #クックパッどら焼き #rubykaigi #rubykaigi2018 #cookpad #makeeverydaycookingfun #毎日の料理をたのしみにする #♦️ #💎 #🖥 #🥞 #🤗

クックパッドHRさん(@cookpad_hr)がシェアした投稿 -

その他ブースでは、クックパッドでRubyフルタイムコミッターとして活躍する遠藤侑介(@mame)と、仙台のずんだ豆にちなんで「mameさんの豆つかみ」も実施!CTOの成田一生(@mirakui)をはじめとして、まつもとゆきひろさん(@matz)さんやAaron Pattersonさん(@tenderlove)さんも参加してくださる盛況なイベントとなりました!

f:id:mamiracle:20180614153621j:plain(今回の最速王はクックパッドHRメンバーで叩き出したタイムは0:06:92でした!)

Cookpad X RubyKaigi 2018: Day 2 Party

2日目の夜に開催したパーティには、90名近くのゲストが参加してくださいました!RubyKaigiの会期中にパーティを企画するのは、クックパッドにとってこれが初めてでドキドキしていましたが、多くの方にお楽しみいただけて本当に良い機会となりました!

【Asakusa.rb × Cookpad】Meetup after RubyKaigi 2018

RubyKaigi 2018終了後の翌火曜日には、Asakusa.rbとのコラボレーションイベントを開催しました!Railsコミッターであり、Rubyコミッターの松田明さん(@amatsuda)に司会を務めていただき、RubyKaigi 2018 の余韻を楽しみました。

この日は「Rubyコミッターによる RubyKaigi 2018の見どころ振り返り」「RubyKaigi 2018会期中に決まったRubyの次の方向性について」というふたつのテーマを設定。笹田耕一(@ko1)と遠藤侑介(@mame)を中心に、飲みながら食べながら語り合いながら、会場全体で楽しむことができました。

f:id:mamiracle:20180614153523j:plain(クックパッドイベント恒例のキムラシェフの絶品ご飯)

おわりに:Rubyとクックパッドについて

クックパッドは、2008年にRuby on Rails へとリニューアルしてから、世界的にも大規模な通用事例として知られてきました。Ruby への貢献を現在も強化しており、二人のフルタイムコミッターを迎え入れクックパッドで次世代Ruby の開発に取り組んでいます。

また、技術をわたしたちが使うことはもとより、社外にも共有することで価値を生むようなソフトウェアやライブラリは、積極的にオープンソース化を行い公開しています。

今後もRuby を含む、さまざまなオープンソースソフトウェアの発展に貢献できるよう、クックパッド一同頑張りたいと思います!

次は8月30日から開催されるiOSDC Japan 2018に参加する予定です。みなさまにお会いできることを楽しみにしています😄

f:id:mamiracle:20180618181226j:plain(来年こそはRubyKaigiに参加した社員全員で集合写真撮るぞ!)

クックパッドでRubyを書きたいなと思ったら... - バックエンドエンジニア(料理動画・広告配信) - UXエンジニア - バックエンドエンジニア(決済基盤) - Webアプリケーションエンジニア - セキュリティエンジニア - ソフトウェアエンジニア (Site Reliability)

Tokyo Machine Learning Kitchen

$
0
0

Hello, I'm @lunardog. I work in Cookpad's Research and Development team as a machine learning researcher. I also host Tokyo Machine Learning Kitchen events. If you'd like to attend, you can sign up using meetup.com or connpass.

I first heard about Cookpad back in 2012 when I joined Tokyo Rails Meetup. At the time, Rails was immensely popular in the developer community and many coders, if not working with Rails in their day jobs, would learn Rails and build hobby projects in Rails after hours. I remember being quite impressed by the talks, especially the ones by Cookpad employees. I was amazed by Cookpad's hospitality and willingness to share the event space, the expertise of the staff and even contribute to open source projects. Cookpad was truly the cornerstone of the Rails community for Tokyo.

f:id:lunardog:20180625165758j:plain (A talk at the Tokyo ML event)

Fast forward to now. Machine Learning in general and Deep Learning in particular is the hot topic. I joined Cookpad's newly created Research and Development department as a Machine Learning researcher. Cookpad is, as it was those years ago, a dynamic, fast-paced company with a positive attitude to open source and sharing knowledge, be it recipes, open-source code or datasets for machine learning researchers. I have decided to make it my mission to contribute to the community of Deep Learning researchers in Tokyo by organizing Tokyo Machine Learning Kitchen events. Much like Tokyo Rails, the ML Kitchen events are a place where professionals and hobbyists can get together to share their interests, network and enjoy a good meal.

The focus of ML Kitchen events is networking between practitioners of Machine Learning. You can expect presentations on:

  • professional or personal ML-related projects
  • machine learning, deep learning, data science walkthroughs
  • paper reviews: own or found somewhere
  • reviews of machine learning frameworks and libraries
  • really interesting talks about math and statistics

To further facilitate networking, Cookpad provides snacks prepared in the kitchen where the event takes place. This is why it's called "ML Kitchen", by the way. It takes place in a kitchen.

Talks are in English, but it's not a problem to do a talk in Japanese, if the slideshow is in English. We usually have a keynote talk, followed by a snack break and a series of lightning talks. We end the evening with more networking, exchanging of business cards, eating and drinking.

f:id:lunardog:20180624183031j:plain

If you'd like to share something with the Machine Learning community in Tokyo, contact me through our group page or meetup or raise an issue in our github repository.

Hope to see you soon!

目的に向かって作り込む、ユーザーを動かすためのデザインの考え方

$
0
0

投稿開発部のデザイナー平塚です。クックパッドにレシピを投稿してくれるユーザーのための機能やサービス全般のデザインを担当しています。
今回は最近投稿開発部で行なったキャンペーンのLPのデザインを具体例に、私の考えるデザインプロセスをご紹介します。

施策の概要

日頃からクックパッドを利用してくれているユーザーに感謝を込めて、投稿した自分のレシピでレシピ本を作れるというキャンペーンを実施しました。 伝える手段としてキャンペーンLPを作成し、対象者にはメールで告知しました。

作成したLPはこちら

LPデザイン

デザインコンセプトを決める際に考えること

大きく分けて、ユーザーの体験とクックパッドが伝えたいことの2つを考えてデザインに落とし込んでいきます。

A. ユーザーの体験を整理する

まず、LPを見たユーザーにどうしてほしいかを考え整理していきます。
ユーザーにレシピ本を作ることが目的だと思ってもらうことを考える上で意識しました。ユーザーストーリーをもとに要点を洗い出します。

ユーザーストーリー

LPのゴールは「あのレシピ載せたいな!」と思って応募してくれることで、それを実現するためには、LPを見たユーザーに以下の3点が起こることが重要になりそうです。

  • 1.面白そう!魅力的!私も作りたいと思う
  • 2.手元にどんなものが届くのかがイメージできる
  • 3.どうやって参加できるのかが分かる

この3点をベースに構成していきます。

1. 面白そう!魅力的!私も作りたいと思わせるには?

LPを開いてすぐにユーザーの興味を掴めるよう、ファーストビューでどんな嬉しい体験ができるのかが想像できる状態を目指します。
キャッチコピーはユーザーインタビューやターゲットユーザーに近い社内スタッフの意見をもとに決定しました。 またレシピ本のイメージ写真をキャッチコピーの直下に置くことで、訴求する体験と合わせてレシピ本をユーザーがより想像しやすくなるようにしました。

2. 手元にどんなものが届くのかをイメージさせるには?

さらにレシピ本のイメージを膨らませ、ユーザーの頭の中でレシピ本がより鮮明になるようにします。
レシピ本のサイズや質感、厚み、それにどんな情報が載せられるのかが分かる写真を用意しました。

3. どうやって参加できるのかを伝えるには?

レシピ本に興味を持ってもらえたところで、自分はキャンペーンに参加できるのか?どうやって参加するのか?を説明します。
応募条件 → 締め切り → 具体的な流れ という順で重要な情報から先に記載していき、説明が一通り完了したところで応募導線を配置しました。

これらをもとにワイヤーフレームを起こして、情報の優先順位付けや読ませるもの・見せるものの棲み分けをしていきます。

ワイヤーフレーム

ワイヤーフレームができたらチームで議論を進めます。
キャッチコピーについてはディレクターがテコ入れしてくれてとても良くなりました。

B. 伝えたい世界観を整理する

ワイヤーフレームの作成と同時並行で、デザインコンセプトを固めていきます。
クックパッドが伝えたいこと・ユーザーに感じて欲しいことを整理し、ビジュアルに落とし込みます。 今回は洗い出した要点の中でも「面白そう!魅力的!私も作りたいと思う」部分をさらに掻き立てるようなデザインを目指しました。

キーワードの洗い出し

まず「何を感じたらレシピ本を作りたいと思うか」を考え言語化します。
ユーザーインタビューでレシピ本について話してくれた内容をもとにキーワードを洗い出しました。 そこから、紙ならではの魅力や、普段ユーザーが料理メモやノートに整理するときに思っていることを中心にキーワードを選定しました。

  • レシピ本
  • ぬくもり感
  • 手作り感
  • 手書き感

レシピ本キャンペーンの世界観を可視化するために、選定したキーワードを表現できる要素は何かを考えていきます。

紙の質感、ぬくもり感

レシピ本を手にとってページをめくる時に感じる紙ならではの質感・ぬくもり感を表現したいと思い、テクスチャの素材で表現できないか考えました。
雑誌や料理本を参考にしたり、Photoshopのテクスチャの素材を探して決めました。

手作り感、手書き感

これは丸みがあって人が書きそうなフォントで表現しようと考えました。
ただユーザーインタビューで見せていただいた普段書く料理メモやノートのような雑な感じとは差別化したいので、字として崩れすぎていない・きれいで憧れるようなフォント選びを意識しました。
出せるだけフォントを集めてPhotoshopで並べ、スマホ画面でのサイズ感、カーニングの扱い、ひらがな・漢字・数字のバランスなどを見て厳選していきます。

レシピ本の質感と手書き感から連想するイメージが見えたのでそれに合う色を探します。
基準としては「明るい印象を持ちつつ、落ち着きがある」で、前述で決めたデザインとの相性から緑を連想し、彩度低め・明度高めでカラーパレットを作成しました。
最終的にクックパッドのデザインシステムに定義されている Mint #40b2aaが考えていたものと近かったため、カラーパレットとしました。

クックパッドであることが分かる

LPなのでクックパッドであることが分かる要素も重要です。
ここまで考えたデザインがクックパッドらしさと少し離れているなと感じたので、色を足すことにしました。
ここでもクックパッドのデザインシステム Mint #40b2aaと相性の良い Orange #ff9933を選定し、LPデザインのアクセントカラーとして使用しました。

まとめ

ユーザーの体験整理と伝えたい世界観の整理を同時並行することで、デザインコンセプトは行き過ぎていないか、逆に体験を意識しすぎてメッセージ性がないものになっていないかバランスを見ながら進めることができます。

デザインコンセプトを考える

今回はキャンペーンLPを具体例にあげましたが、このデザインプロセスはアプリUIなど他のデザインでも同じように考えることができます。
デザインには様々な手法があり、今回ご紹介したものが万能ということではありませんが、自分の中ではデザインしやすいやり方なので数ある方法の一例として捉えていただければと思います。

最後になりますが、クックパッドではより良いユーザー体験を届けていきたい!というデザイナーを募集中です。 クックパッド株式会社 採用情報

Firebase ML Kitで自作のカスタムモデルを使って料理・非料理画像を判定できるようにした

$
0
0

会員事業部の山下(@farmanlab)です。 Androidエンジニアとしてクックパッドアプリの開発を担当しています。

今回はGoogle I/O 2018で新しく発表されたML Kitをクックパッドのデータで学習したモデルを使って検証した話をします。

機械学習モデルの利用にあたって、研究開発部の菊田(@yohei_kikuta)の協力の元で検証を行いました。

これからお話する内容がイメージしやすいよう、 クックパッドの料理・非料理を判別するモデルを動かした実機デモをお見せします。

これは料理と判定された確率がfood、料理ではないと判定された確率がnon-foodというラベルのスコアで表示されているデモです。 (非)料理画像において(non-)foodのラベルのスコアが大きくなり正しく判別できていることが分かります。

f:id:farmlanlabdev:20180704182402g:plain

  • モデルは MobileNetV2
  • tensorflow-gpu==1.7.1で学習してTOCOでTensorFlow Liteのモデルを作成
  • アプリ側では入力データやモデルの重みはfloatで処理

ML Kitとは

ML Kitとはモバイルアプリ向けに機械学習機能を組み込むことができるSDKです。 Firebaseの機能の一つとして提供されており、Android/iOSに簡単に導入することができます。 大まかに以下のような特徴があります。

  • デフォルトで以下の機能が利用可能

    • 文字認識
    • 画像ラベリング
    • バーコードスキャン
    • 顔検出
    • ランドマーク認識
  • オンデバイスモードとクラウドモードで利用可能

オンデバイスモードでは端末にモデルをダウンロードすることでオフラインで動作し、無料で使うことができます。 クラウドモードはCloud Vision APIを使ってオンデバイスよりも精度の高い情報を得ることができる代わりに Firebaseの課金プランをBlazeにする必要があり、一定回数以上の利用は有料です。

  • 独自のカスタムモデルを利用可能

デフォルトで提供される機能以外に独自の機械学習モデルを利用することができます。 ちなみにカスタムモデルの利用にあたってはFirebaseの課金プランをBlazeにする必要はありません。

  • Android Neural Networks API(NNAPI)との連携

ML KitはAndroid8.1で導入されたNNAPIとの連携がSDKに含まれているため、 開発者がNNAPIに関するコードを書く必要がありません。

ML Kitとカスタムモデルを導入するまでの流れ

以下のステップでカスタムモデルをML Kitで使えるようにします

  1. アプリの依存関係にML Kitを追加
  2. TensorFlowの機械学習モデルをTensorFlow Liteのモデルに変換する
  3. FirebaseでTensorFlow Liteのモデルをホストする
  4. アプリでFirebaseのモデルをバンドルする
  5. バンドルしたモデルを利用して推論する

1と3については公式に詳しいので、説明はそちらに譲ります。 ここでは2と4と5について掘り下げていきましょう。

ML Kitはクイックスタートサンプルが用意されているので、 このサンプルに元にしつつ、自分たちのモデルを動かす上で必要になるポイントも加えて説明していきます。

2.TensorFlowの機械学習モデルをTensorFlow Liteのモデルに変換する

サンプルを動かす場合はGitHubのレポジトリmobilenet_quant_v1_224.tfliteが用意されているので、特に準備をする必要はありません。

自分のモデルを使う場合は、一言で言えばTOCOというTensorFlowのモデルからTensorFlow Liteのモデルに変換するツールを使えばいいのですが、自分で学習したモデルを使うには注意を要します。 ここではその部分を詳しく解説します。

今回はクックパッドで使われている料理・非料理判別モデルを実装します。 モデルはサンプルに倣って基本的にはMobileNetV1を使います。 冒頭で示したようにMobileNetV2でも実装ができていますが、これはV1の実装ができれば(モデルアーキテクチャ以外)全く同様にできるため、ここでは試行錯誤の過程を紹介する意味でもV1の話をします。 それぞれのモデルの詳細はこの記事では解説しませんが、学習済みのモデルはV1はこちらV2はこちらにあります。

自分たちが準備したデータでモデルを学習する部分には新しいことはなく、tensor-for-poets-2のコードtensorflow/hubのコードがそのまま使えます。 今回は料理・非料理の二値分類を対象としました。

ここで作成したTensorFlowのモデルからML Kitで適切に動作するTensorFlow Liteのモデルを作るところで苦戦したので、気をつけるべき点と共に手順を紹介します。

TensorFlow Liteモデル(.tflite)の作り方

ここでは、TensorFlowで学習して作成した xxx.pbファイルから model.tfliteファイルへ変換することを考えます。 例えば、model_graph.pbmodel.tfliteファイルに変換するコマンドは以下のようになるイメージです。

toco \
  --input_file=/tmp/model_graph.pb \
  --input_format=TENSORFLOW_GRAPHDEF \
  --output_file=/tmp/model.tflite \
  --output_format=TFLITE \
  --input_arrays=input \
  --output_arrays=final_result

このように、TensorFlowでモデルの学習をする場合はそのままTOCOを使えば.tfliteを作れるため、変換それ自体に困難はありません。 ただし、TensorFlowのバージョン依存性が強いので注意が必要です。 この記事における我々の結果は全てtensorflow-gpu==1.7.1で実行したものとなります。

他のフレームワークでモデルを学習する場合は一旦TensorFlowのモデルに変換する必要がありますが、変換用のライブラリは色々出てるので、標準的なoperationのみを使っていれば可能だと思います。 後述しますが、TensorFlow Liteではでサポートしているoperationはまだ限定的なので、特殊なoperationを含むモデルを使う場合は自分でTensorFlow Lite側の実装をする必要があります。

また、コンバーターであるTOCOは重みの量子化などのオプションも有していて、これを使ってfloatで重みを扱うモデルから量子化して扱うモデルを作ることもできます(正確には、Fake quantizationという、重みはuint8で扱うが出力はfloat32として扱う機能が提供されています)。

サンプルと同じようにやってみて上手くいかなかった話

単純に考えれば、サンプルで動いているモデルに基づき、自分たちのデータを使って再学習したモデルをTOCOを使って.tfliteファイルに変換するだけで上手くいくはずです。 ML Kitのサンプルではmobilenet_quant_v1_224.tfliteという重みが量子化されたMobileNetV1が使われているので、とりあえずMobileNetV1の量子化バージョンMobilenet_1.0_224_quantから再学習したretrained_graph.pbを使いfood-non-food.tfliteを作成します。 変換コマンドは以下のものを使用しました。

IMAGE_SIZE=224
toco \
  --allow_custom_ops \
  --input_file=/tmp/retrained_graph.pb \
  --input_format=TENSORFLOW_GRAPHDEF \
  --output_file=/tmp/food-non-food.tflite \
  --output_format=TFLITE \
  --input_shapes=1,${IMAGE_SIZE},${IMAGE_SIZE},3 \
  --mean_values=128 \
  --std_values=128 \
  --inference_type=QUANTIZED_UINT8 \
  --input_arrays=input \
  --output_arrays=final_result

先程の例と比べると入力のshape指定や値の標準化なども入っています。 オプションの--allow_custom_opsに関しては、これをつけないとcustom opがないというエラーが出るのでつけています。 「それでは動かないのでは?」という自然な疑問が湧きますが、一方でサンプルで動いているモデルと同じだ(と思われる)ので動くだろうという期待もそれほど悪くないものに思えます。

しかしながら、結果はダメで、サンプルのモデルだけ置き換えると例えばDidn't find custom op for name Dequantizeなどというエラーを吐きます。 これはTensorFlow Lite側で計算グラフのoperationが実装されていないことを意味しています。 operationが無いということで、選択肢は自分で頑張って実装するかサポートされているoperationだけでモデルを作るかです。 そもそもちゃんと動くか分からない状況なので、試すまでのスピードや余計なバグの原因を混入させないという意図で、後者の方法で進めることにしました。

以降では重みをfloatで扱うモデル(以下floatモデル)をどのようにすれば動かせるかを紹介しますが、そもそもサンプルにおいて量子化されたモデルがどのように動いているかに関してはよく分かっていません。

floatモデルを動かすまでの試行錯誤

まず試したのは、floatモデルをfake quantizationして扱うという方法です。 ML Kitのサンプルが量子化されたモデルを扱っているのでこれが既存のスクリプトを書き換えずに実行する近道に思えます。 Mobilenet_1.0_224を元に再学習したretrained_graph.pbを以下のコマンドで量子化されたfood-non-food.tfliteに変換します。

IMAGE_SIZE=224
toco \
  --input_file=/tmp/retrained_graph.pb \
  --input_format=TENSORFLOW_GRAPHDEF \
  --output_file=/tmp/food-non-food.tflite \
  --output_format=TFLITE \
  --input_shapes=1,${IMAGE_SIZE},${IMAGE_SIZE},3 \
  --mean_values=128 \
  --std_values=128 \
  --default_ranges_min=0 \
  --default_ranges_max=6 \
  --inference_type=QUANTIZED_UINT8 \
  --input_arrays=input \
  --output_arrays=final_result

オプションのdefault rangeがfake quantizationに必要な情報で、活性化レイヤーでの値の取りうる範囲を指定して量子化の際の情報として使います。 理想的には学習時の結果を保持して使うものですが、MobileNetV1はReLU6を使っているためこのように指定できます。

これで作ったモデルはエラーは吐きませんが、予測のスコアが[0.7,0.3]辺りをうろついてあまり変化しないという結果になりました。 この結果から推察するに入力の画像の取り扱いや重みがちゃんと入ってるかなどが怪しいところですが、いくつか調べてみても解決法は見つかりませんでした。 ML Kitの世界に行ってしまうとどこに問題があるか(モデル変換にバグがあるのかアプリ側にバグがあるのか)のデバッグが難しいということもあります。

ということで残りは素直にfloatモデルを作ってアプリ側でもfloatモデルを扱うように変更するという方法です。 モデル変換は以下で実施しました。

IMAGE_SIZE=224
toco \
  --input_file=/tmp/retrained_graph.pb \
  --input_format=TENSORFLOW_GRAPHDEF \
  --output_file=/tmp/food-non-food.tflite \
  --output_format=TFLITE \
  --input_shapes=1,${IMAGE_SIZE},${IMAGE_SIZE},3 \
  --mean_values=128 \
  --std_values=128 \
  --inference_type=FLOAT \
  --input_arrays=input \
  --output_arrays=final_result

これで得られたモデルをそのまま動かすとInput 0 should have 150528 bytes, but found 602112 bytesというエラーに遭遇します。 モデル的にはfloat32で扱うところを入力としてはuint8を想定しているために不整合が起こっているように見えます。

これは元々アプリ側では量子化されたものを扱おうとしていたのだから自然なエラーだと思われ、アプリ側を適切に変更すれば動くことが期待できます。 いずれにせよ、モデルを作る側にできるのはここまでなので、以降でこのモデルのアプリへの取り込みとアプリ側でどのように扱えば正しく動かせるのかを説明していきます。

4.アプリでFirebaseのモデルをバンドルする

では、作成したモデルをML Kitに取り込んでみましょう。 ここでの手順は他のカスタムモデルを取り込む手順と違いはありません。

ML Kitでカスタムモデルを使う際の大まかな構成は以下のようになっています。 ML Kit構成

アプリにモデルをバンドルするときにはFirebaseModelManagerを利用します。

FirebaseModelManagerに定義されている、FirebaseLocalModelSourceFirebaseCloudModelSourceのインスタンスをそれぞれ引数に取る registerLocalModelSourceregisterCloudModelSourceメソッドを使って利用するモデルのバンドルを行います。 もちろん、ローカルモデルのみ、クラウドモデルのみを利用することも可能です。

ローカルモデルの指定

val localSource = FirebaseLocalModelSource.Builder("food-non-food")
    .setAssetFilePath("food-non-food.tflite")
    .build()

Builderのコンストラクタにはモデルを識別するための任意の文字列を渡します。 Assetsフォルダ内のtffileを参照する場合にはsetAssetFilePathを、それ以外のフォルダを参照する場合はsetFilePathでファイルを指定します。

クラウドモデルの指定

val conditions = FirebaseModelDownloadConditions.Builder().requireWifi().build()
val cloudSource = FirebaseCloudModelSource.Builder("food-non-food")
    .setInitialDownloadConditions(conditions)
    .setUpdatesDownloadConditions(conditions)
    .enableModelUpdates(true)
    .build()

FirebaseModelDownloadConditionクラスでCloudモデルをダウンロードするための条件を設定することができます。 FirebaseCloudModelSource.Builderのコンストラクタにはステップ3でFirebaseにホストしたモデルの名前を指定します。 enableModelUpdatesをtrueにするとFirebaseにホストしたモデルに更新があった場合にモデルをFirebaseから更新するようになります。 この仕組みのおかげでアプリをアップデートすることなく最新の学習モデルを利用することが可能です。

モデルの登録

FirebaseModelManager.getInstance().apply {
    registerLocalModelSource(foodNonFoodLocalSource)
    registerLocalModelSource(attractivenessLocalSource)
    registerCloudModelSource(foodNonFoodCloudSource)
    registerCloudModelSource(attractivenessCloudSource)
}

FirebaseModelManagerのインスタンスを取得して、登録メソッドで渡します。 ML Kitの構成にもあるように複数の機械学習モデルを利用することも可能です。 ここでは料理・非料理の判別モデルと料理の魅力度推定モデルを登録しています。

推論モデルの指定

val options = FirebaseModelOptions.Builder()
    .setLocalModelName("food-non-food")
    .setCloudModelName("food-non-food")
    .build()
val interpreter = FirebaseModelInterpreter.getInstance(options)

FirebaseModelOptionsクラスで推定を行う機械学習モデルの指定を行います。 Firebase(Cloud|Local)ModelSource.Builderのコンストラクタに指定した名前を指定することで、 FirebaseModelManagerに登録した機械学習モデルを使用することができます。

このFirebaseModelOptionsを使って、実際に推定を行うFirebaseModelInterpreterのインスタンスを取得します。

5.モデルを使って推定する

いよいよ、カスタムモデルを使って推定を行います。

入出力のデータを指定する

FirebaseModelInputOutputOptionsを使って、学習モデルのinputとoutputのデータを指定します。

クイックスタートサンプルではbyte値を扱うコードが紹介されていますが、 今回利用する学習モデルはfloat値を扱うように作成しているので、floatの多次元配列がinputデータになります。

outputデータは画像がモデルによって予測されるカテゴリのいずれかの確率であるfloat値の多次元配列(softmaxの出力)です。 カテゴリ一覧を表すテキストファイルをassetsフォルダなどに配置して読み込みます。 今回は、料理・非料理判別モデルのカテゴリを表す

food
non-food

という内容のテキストをlabel.txtというファイル名でassetsフォルダに配置したと仮定します。

val labelList = activity.assets.open("label.txt").reader().use {
    it.readText()
}.split(System.lineSeparator())

val ioOptions = FirebaseModelInputOutputOptions.Builder()
    .setInputFormat(0, FirebaseModelDataType.FLOAT32, intArrayOf(1, 224, 224, 3))
    .setOutputFormat(0, FirebaseModelDataType.FLOAT32, intArrayOf(1, labelList.size))
    .build()

Bitmapからinputデータを作成する

まずはinputデータを格納するための多次元配列を作成します。 なお、執筆時点でのML KitはByteBufferには対応していますが、FloatBufferには対応していませんでした。

// 定数値val IMAGE_MEAN = 128val IMAGE_STD = 128.0f// inputデータを格納する配列を作成val imageData = Array(1) { Array(224) { Array(224) { FloatArray(3) } } }

val imageValues = IntArray(224 * 224)

// 224×224にリサイズしたBitmapからpixel値を取得
resizedBitmap.getPixels(imageValues, 0, resizedBitmap.width, 0, 0, resizedBitmap.width, resizedBitmap.height)
var pixel = 0for (i in0 until 224) {
    for (j in0 until 224) {
       imageValues[pixel++].let {
           imageData[0][i][j][0] = (Color.red(it) - IMAGE_MEAN) / IMAGE_STD
           imageData[0][i][j][1] = (Color.green(it) - IMAGE_MEAN) / IMAGE_STD
           imageData[0][i][j][2] = (Color.blue(it) - IMAGE_MEAN) / IMAGE_STD
       }
    }
}

val inputs = FirebaseModelInputs.Builder()
            .add(imageData)
            .build()

次に学習モデルをTOCOで作成したときの IMAGE_SIZE=224に合わせて224×224サイズにリサイズし、リサイズしたBitmapからpixel値を取り出します。

ここで重要なのが、pixelの各RGB値に対して、IMAGE_MEANとIMAGE_STDを使って演算をしている点です。 tfliteへの変換時に

toco \
  ...
  --mean_values=128 \
  --std_values=128 \
  ...

とMEANとSTDの値を指定しているので、モデル推論時にいい感じにやってくれるように思います。 しかし、実際には予め計算した値をinputとして与える必要があります。 こうして得られたinputの多次元配列データを FirebaseModelInputs.Builderaddメソッドに渡してやります。

推論結果を得る

入出力のデータを指定するで指定したoptionと、Bitmapからinputデータを作成するで得たinputデータを FirebaseModelInterpreterrunメソッドに渡すと推論が実行されます。

interpreter.run(inputs, options)
.addOnSuccessListener { outputs ->val result = outputs.getOutput<Array<FloatArray>>(0)[0]
    result.mapIndexed { index, value ->
        Pair(labelList[index], value)
    }
}

addOnSuccessListenerが受け取るTaskからgetOutputすることで推論結果を得ることができます。 今回はfloatのモデルを使ったのでgetOutputで得られる型はfloatの多次元配列です。 ここではlabel.txtで指定したカテゴリのインデックスと出力値をマッピングしています。

結果

実際に実機で動かしたデモをお見せします。

一つ目は冒頭でもお見せした料理・非料理判別です。 MobilenetV1とMovileNetV2の両方で実装しましたが、大きく変わるところはないので後者の結果のみを改めてお見せします。 実機デモ1

餃子やグラタンやパスタといった料理画像ではfoodのスコアが高くなり、ゴリラや紫陽花などの非料理画像ではnon-foodのスコアが高くなっていることが確認できます。

二つ目は魅力度推定です。 これは学習データとして料理の見栄えを5段階評価(数字が高いほど見栄えが良い)したものを準備し、回帰モデルを学習したものになります。 こちらはMobileNetV1のみで実装しましたのでその結果をお見せします。 実機でも2

数枚でかつ主観的な評価とはなりますが相対的に見栄えの良いと思われる画像に高いスコアが付与されており、モデルが期待通りに動いていることが確認できます。

ということで自分たちで作成した分類モデルを回帰モデルがML Kitを使って実機で動かすことができました! 今回は動かすことが目的であったため正答率や処理速度などの各種指標はまだ詳細には調べていませんが、これは単なるアプリのプロファイリングの話なので難しいところはありません。

まとめ 

今回、Google I/O 2018で発表された最新技術であるML Kitの現状をクックパッドの機械学習モデルを使って検証した話をしました。

ML Kitはまだβ機能として提供されているので、対応しているモデルのオペレータが少なかったり、 量子化されたモデルを上手く動かす情報が不足していたり、発展途上であることは確かです。 しかし、一度モデルを構築してしまえばオンデバイスで動作させることができますし、Firebase経由でモデルのアップデートも簡単にできます。

一方でオンデバイスで動作することは、常に最新のモデルを利用するようにコントロールできないということでもあるので、 設計する上で注意しなければなりません。

今後ますます機械学習を活用したサービスや事例が多く出てくると思いますし、 ML Kitは機械学習機能をモバイルに組み込むための有効な手段の一つであると感じました。

クックパッドではML Kitのような最新技術を利用したモバイルアプリ開発や研究開発がしたい!というエンジニアを募集しています。

興味がある方は採用ページ、または@farmanlab or @yohei_kikutaまで!

x3 Speed Up Android CI at Cookpad

$
0
0

海外事業部の松尾(@Kazu_cocoa)です。こちらは、私たちの x3 Speed Up Android CI at Cookpadに公開した記事の日本語訳です。英語でご覧になりたい方は原文を一読ください。

以下に登場するAndroidアプリは海外版のクックパッドアプリとそれにまつわる環境を指します。国内のものと構成も異なるところがありますので、そのあたりを頭に入れつつ読んでいただければと思います。


この記事では、現在のAndroidアプリ開発向けCI環境の紹介とその環境構築の流れを紹介します。現在の環境では、GitHub上に作成されたプルリクエストへのプッシュ毎にビルド・テストの実行含めて処理が 7分程度で完了します。これらの処理にはAPKの作成、各種テストの実行が含まれます。

以前は、私たちのCIはプッシュ毎に 合計で25分程度かかっていました。そのころは、2つの役割の異なるJenkins Jobを並列実行させていました。合計25分はそれらを含めた合計値です。

以下では、どのようにしてこのような環境を構築したのかを知ることができます。テストの実行環境として、エミュレータの並列起動とそれを用いたテスト実行の話を載せています。この記事ではCIにおけるビルド/テスト環境に焦点を当てるため、他の話は行いません。

マルチモジュール、1000以上のテストケース

まずは対象となるAndroidプロジェクトの概要の共有です。

Androidアプリ

  • 20個のモジュールを保持
  • unit/instrumented/Espressoを含む、合計1000以上のテストケースを保持
  • ビルド毎、社内配布用のapkを配布

現在のCI環境

現在のCI環境は以下で構成されています。

  • AWS上に構築されたJenkins環境
    • Master/Slave構成(Android向けのJenkins Slaveには i3.metal instanceを利用)
    • Jenkins環境はAndroid以外のプロジェクトも利用
  • GitHubへのプッシュ毎に実行されるタスク
    • unit/instrumented/Espresso testsの実行
      • 取得可能なものはカバレッジの取得も含む
    • 必要なapkのビルド
  • テスト端末
    • 合計14個のエミュレータを作成・起動し、並列にAndroid APIの必要な instrumented/Espresso テストを全て実行
  • ガイド

擬似的な Gradleタスクを用いて上記を模倣すると、以下のような処理がプッシュ毎に実行されています。

./gradlew clean
./gradlew checkLicenses
./gradlew testAllUnitAndInstrumentedTests
./gradlew assembleOurInternalApk
./gradlew uploadApkForInternal

GitHubへのプッシュを契機に以下のような流れで処理が Jenkins Slave 上で実行されます。

GitHub <----> Jenkins (master) <-----> Jenkins (slave)
                                        Build/All Tests

旧CI環境

変更する前の環境は以下です。

  • AWS上に構築されたJenkins環境
    • Master/Slave構成(Android向けのJenkins Slaveには i3.metal instance 以外を利用)
    • このJenkins環境はAndroid以外のプロジェクトも利用
  • GitHubへのプッシュ毎に実行されるタスク
    • unit/instrumented/Espresso testsの実行
      • 取得可能なものはカバレッジの取得も含む
    • 必要なapkのビルド
  • テスト端末
  • ガイド

旧CI環境では、以下のように2つの Jenkins Slave を用意し、実施するテストを分ていました。

GitHub <----> Jenkins (master) <-----> Jenkins (slave) 1
                        |              Build/non-UIテスト
                        |------------> Jenkins (slave) 2
                                        Build/UIテスト

上の図に出ている non-UIテストとは、unit/instrumentation-based JUnitを指す、UI描画を含まないテストをさします。これらにはGenymotion Cloudを使い、いくつかのビルド含む18~20分かけてビルドしていました。 UIテストとは、Espressoを使ったテストです。OpenSTFを使いテストを実行していました。いくつかのビルド含む、合計で15分程度かかるジョブです。

2つの異なるJenkins job環境

UIテストはOpenSTFを用いて実行していた、と上記で書いています。

AWS上では、ARMベースのAndroidエミュレータしか実行できませんでした。そのため、UIテストは特に実行/描画速度が遅く、UIの描画を含むテストを安定して実行することが厳しかったです。私たちの環境では、Genymotionも多少なりもとも制限を有していました。そのため、UI描画が不要なテストと、UI描画が必要なテストは分けて実行していました。

各々のJenkins jobは並列して実行されるようにしていました。それぞれ、15分程度は少なくとも実行環境まで時間がかかります。

このくらいの待ち時間になると少しコーヒーで一息つくことができる感じですね。

f:id:kazucocoa:20180705183344j:plain

よりJenkins Jobを細かく分割などすれば時間は短縮することはできますが、Jobの管理などが複雑になってきます。

なぜAWSを使っているか

現在、私たちは可能な限り自分たちで物理リソースを保守したくありません。私たちはAWSを主に利用しています。

強力な物理マシンを用意しそれをCIとして利用できれば今回と同様のことを実現できるでしょう。その場合は環境を拡大するさい、そのつど端末購入が必要になってきます。現在のiOSビルド環境に似たようなものですね。それは保守コストや将来的にもチーム拡大の足枷になるとふんでいました。

そのため、私たちはAWS上で利用可能なそのようなマシンを待っていました。

AWSのベアメタルインスタンス

昨年の終わり頃、Bare Metal Instances for EC2が発表されました。その環境下では、Androidのx86エミュレータによる実行が可能になると期待できていました。Androdエミュレータ向けのVMアクセラレーションの各種機能を利用できることも期待できました。

少し、x86エミュレータについて説明を残しておきます。

Googleは公式のAndroidエミュレータを公開しています。そこから、私たちはいくつかのCPUアーキテクチャ上で動作するエミュレータを利用することが可能です。その中で、 x86とはInetel Processor上で動作可能なものを指します。

AWS上では、元々はARMエミュレータだけが利用可能で、x86エミュレータは利用不可能でした。一方、ベアメタルインスタンスではx86エミュレータも利用可能です。

新しい環境

Amazon EC2 Bare Metal Instances の一般向け公開をお知らせしますによってベアメタルインスタンスが利用可能になったのち、私たちはJenkins Slaveとしてその環境を検証し、使いはじめました。その途中でいくつか問題にぶつかったので、その経験を共有したいと思います。

i3.metalでAndroid環境を構築する

i3.metalの設定

現在、ベアメタルインスタンスは入手可能な地域が限られています。まずは利用したい地域でベアメタルインスタンスを入手可能か確認してください。 ベアメタルインスタンスはEC2向けの通常のインスタンス起動ウィザードから作ることが可能です。ウィザード起動後、AMIを選択してウィザードを次に進めてください。表示されるインスタンス一覧の最下部に i3.metalの表記を見つけることができるはずです。その i3.metalがこのベアメタルインスタンスです。i3.metal選択後、各々の環境にあった設定を選んでいき、インスタンスを作成してください。

なお、以下ではUbuntuをベースとしたAMIで動作を確認しています。そのほかの環境では手順が異なるところがあるかもしれませんのでお気をつけください。 i3.metalは起動までに少し時間がかかります。その間、エスプレッソでも飲みながら待ちましょう。

f:id:kazucocoa:20180705183349j:plain

Android SDKの入手

まずは Android SDK コマンドラインツールを取得する必要があります。ダウンロードページからLinux向けのものを入手してください。 i3.metalインスタンス上でそれらを展開した後、 ANDROID_HOMEANDROID_TOOLSの設定を忘れずに行なってください。

各種アクセラレーションを有効にする

i3.metalを使った大きな理由は、先でも少し述べたようにAndroidエミュレータの各種アクセラレーション機能を使うことです。この機能はARMエミュレータでは利用できません。そのため、上で述べたように私たちはOpenSFTやGenymotionをAndroid CIに使っていました。

VMアクセラレーション

Configure VM accelerationでは、VMアクセラレーション環境の構築を知ることができます。

私たちはUbuntuベースのi3.metalインスタンスを構築しています。そのため、Ubuntu KVM Installationに沿って環境構築を進めました。環境構築の後、以下のような入力に対して出力が得られたのであればでは、VMアクセラレーション環境環境の構築は完了です!

$ sudo /home/ubuntu/android/tools/emulator -accel-check
  # accel:ad
  # 0
  # KVM (version 12) is installed and usable.
  # accel

SwiftRenderによる描画のアクセラレーション

描画に対するアクセラレーション環境はいくつかの種類提供されています。 VMアクセラレーションだけでinstrumented testsに対する高速化は十分です。ただ、UIの描画のからむEspressoのテストも含んでくるとそれでは足りません。この環境構築が必要となってきます。

この中で、私は1つ問題に出くわしました。

OpenGL 関係の描画問題

まず、私は hostモードを使ってみました。OpenGLが利用可能であれば i3.metalでも macOSなど同様の描画が可能だと期待したためです。しかし、Could not initialize OpenglES emulationというエラーに出くわし、エミュレータを起動することはできませんでした。

Elastic GPUを用いることができるかと期待しましたが、この時は利用することができませんでした。

次に、私はエミュレータの -no-windowモードを利用しました。エミュレータの起動には成功したのですが、Espressoのテストを実行すると以下の例外が発生してテストが中断されました。

E AndroidRuntime: DeadSystemException: The system died; earlier logs will point to the root cause
W System.err: java.lang.Throwable: tname=main - android.os.DeadSystemException
W System.err:    at adgh.a(PG:17)
W System.err:    at adgh.uncaughtException(PG:20)
W System.err:    at java.lang.Thread.dispatchUncaughtException(Thread.java:1955)
W System.err: Caused by: java.lang.RuntimeException: android.os.DeadSystemException
W System.err:    at android.app.ContextImpl.registerReceiverInternal(ContextImpl.java:1442)
W System.err:    at android.app.ContextImpl.registerReceiver(ContextImpl.java:1394)
W System.err:    at android.app.ContextImpl.registerReceiver(ContextImpl.java:1382)
W System.err:    at android.content.ContextWrapper.registerReceiver(ContextWrapper.java:609)
W System.err:    at ajmt.a(Unknown Source:10)
W System.err:    at akmp.a(Unknown Source:117)
W System.err:    at akma.a(Unknown Source:12)
W System.err:    at akmu.a(Unknown Source:7)
W System.err:    at aklm.a(Unknown Source:8)
W System.err:    at ajqf.a(Unknown Source:2)
W System.err:    at ajpo.handleMessage(Unknown Source:11)
W System.err:    at android.os.Handler.dispatchMessage(Handler.java:106)
W System.err:    at android.os.Looper.loop(Looper.java:164)
W System.err:    at android.app.ActivityThread.main(ActivityThread.java:6494)
W System.err:    at java.lang.reflect.Method.invoke(Native Method)
W System.err:    at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:438)
W System.err:    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:807)
W System.err: Caused by: android.os.DeadSystemException
W System.err:    ... 17 more
I Process : Sending signal. PID: 3501 SIG: 9

これはQtの問題だと予測したので、apt-get install xorg openboxQT_QPA_PLATFORM='offscreen'を試してみました。しかしそれでも改善はしませんでした。

解決策を試行錯誤している中で、最終的にSwiftRenderが助けてくれました。no-windowオプションと合わせてエミュレータを起動した結果、正しくエミュレータが起動し、Espressoのテスト実行も完了することができました。 Configuring graphics acceleration on the command lineにも書かれている通り、これは swiftshader_indirectモードによるエミュレータ起動で利用することができるものでした。このライブラリはOpenGL ESをCPU処理を使い実現するものです。

まとめ

ここの章は少し長かったので軽くまとめます。

  • VMアクセラレーションを有効にするためにKVMをインストールする
  • 描画のアクセラレーションを有効にするためにSwiftRenderを有効にする
    • このライブラリは $ANDROID_HOME/emulator/lib64/gles_swiftshaderに見つけることもできます
  • エミュレータは "-no-boot-anim -no-snapshot-save -netfast -noaudio -accel on -no-window -gpu swiftshader_indirect"のオプションで起動する

ここで、 i3.metalインスタンスのAndorid向けビルド環境構築の説明は終わりです。こちらをJenkins Slaveとして利用可能にすると、Android向け Jenkinsインスタンスとして利用可能になります。

最後に1つ、 i3.metalインスタンスの良い点を以下にあげておきます。実は、個人的には以下の環境を手に入れることがベアメタルインスタンスの、もう一つの大きな目的でした。これにより、短時間のAndroidテスト環境を構築することができます。

CI上でテストを並列実行する

i3.metalインスタンスは非常に高性能です。72コアや、大量のメモリを搭載しています。そのため、苦無く複数エミュレータを同時に起動可能です。

エミュレータの複数同時起動により、Androidテストを実行するときに必要となる adb installの1つのエミュレータに対する実行頻度を下げることができます。shardingの機能も利用することができます。エミュレータの起動数を上げることで、テストの並列実行数をあげることもできます。

テスト対象となるapkのインストール時間はAndroidテストの実行の中で時間を必要とする処理です。私たちのプロジェクトにはモジュールが20個あります。その中で、14個のモジュールにAndroidテストが存在します。そのため、14モジュール分のandroidTest実行における adb installが処理される必要があります。それが直列で行われる場合、それなりに時間がかかってしまいます。それを解決するために、各々のモジュール1つに対して1つ専用のエミュレータを作成し、並列してそれらを実行できるようにしました。

以下のGIFアニメーションが並列実行の例です。現在は、このような状況が合計で14個、Jenkins Slave上で動作しています。並列実行を達成するためにcomposerswarmerを利用しました。(感謝も含めて、何か問題や追加したい機能があればPRを送るなども行いたいですね。)

f:id:kazucocoa:20180705183355g:plain

以下のスクリプトは複数のサブプロセスを操作するbashスクリプトの例です。これにより、シェルのサブプロセスでバックグラウンド実行するAndoridテストの終了を待ち、結果を集計してJenkinsの成功/失敗をまとめることができるようになっています。

function wait_and_get_exit_codes() {
    children=("$@")
    EXIT_CODE=0
    for job in "${children[@]}"; do
       echo "PID => ${job}"
       CODE=0;
       wait ${job} || CODE=$?
       if [[ "${CODE}" != "0" ]]; then
           EXIT_CODE=1;
       fi
    done
}

EXIT_CODE=0
children_pids=()

# composerスクリプトを `&` 指定で実行
children_pids+=("$!")

wait_and_get_exit_codes "${children_pids[@]}"

exit "${EXIT_CODE}" # いずれかのサブプロセスの終了コードが1の場合だと、ここが1になる

Conclusion

読んでいただいてありがとうございます。

上記により、 i3.metalインスタンスを使ったAndroid CIの環境構築方法を知ることができました。また、複数エミュレータ起動によるテストの並列実行の様子も見ることができたかと思います。

これらの対策により、AndroidのCI環境は今までよりも高速に、安定するようになりました。全てのプッシュに対するCI実行は7分程度で完了します。まだチューニングの余地はあるので、必要であればより最適化する余地もあります。ベアメタルインスタンスは価格がほかのインスタンスよりも高価なので、より効率的な利用ができるようにしていくことも将来的な挑戦でもあります。

最後に、検証・本番環境構築にKohei Suzukiさん、 Takayuki Watanabeさんの協力もいただきました。ありがとうございます。


Firebaseを活用したiOSアプリ開発事例

$
0
0

こんにちは。新規サービス開発部の中村です。

最近Komercoで販売されている鉄のフライパンが欲しいです。クリエイターさんたちの作品は見ているだけで本当に楽しいですね。

そんなKomercoはバックエンドにFirebaseを活用していますが、実は弊社からKomercoの他にもFirebaseを活用したサービス「Cookin'」をリリースしています。

本稿ではCookin'のFirebaseを活用した事例についてご紹介します。

Cookin'とは

Cookin'は料理動画撮影アプリです。手順ごとに3秒間取るだけで1本の料理動画が投稿できるサービスとして、2017年12月にiOSアプリとしてリリースしています。

このサービスの特徴は、簡単に料理動画が作成できるほかに、投稿から斬新なアイデアを得られたり、コメント欄から料理のコツやポイントを気軽に質問できるところです。

料理をしながら動画撮影するのは難しいですが、慣れると楽しいです。

App Store リンク

誠に勝手ではございますが、Cookin’は提供サービスの見直しにともない、2018年7月末をもちまして、提供を終了いたします。ご利用のお客様には、ご迷惑をおかけいたしますことを深くお詫び申し上げます。サービス終了に関するご案内はこちらをご覧ください。

FIrebaseの活用事例

先ほど述べた通り、Cookin'のバックエンドはFirebaseを活用しています。Firebaseには様々なプロダクトが用意されていますが、Cookin'では以下のプロダクトを活用しています。

アプリの開発とテスト ユーザー層の拡大と利用促進
Authentication Cloud Messaging
Cloud FIrestore Google Analytics for Firebase
Cloud Storage
Cloud Functions

以降ではこれらの概要と活用事例をご紹介します。

Authentication

Authenticationは安全な認証システムを提供しています。メールアドレスとパスワードの組み合わせ、電話番号認証、匿名認証、Google、Twitter、Facebook、Githubのログイン等をサポートしています。

活用事例

ユーザーの認証に匿名認証を活用しています。これにより、データベースやストレージのセキュリティーを堅牢にしつつ、初回起動時のアカウント作成プロセスを省略することでユーザーの離脱を防いでいます。

匿名ユーザーは必要に応じてメールアドレスとパスワードでの認証に切り替えることも可能です。

匿名ユーザーのアカウントを停止したい場合は一手間必要になります。詳細はこちらのブログ記事をご覧ください。

Cloud Firestore

Cloud FirestoreはNoSQLドキュメントデータベースです。オフラインの場合でもデータにアクセスして変更を加えることができ、オンラインに復帰すると自動的に変更したデータを同期します。スケーリングは自動的に行われ、セキュリティールールを書くことでセキュリティーを堅牢にできます。

活用事例

  • 投稿情報、コメント、ユーザー情報等を保存しています。
  • 投稿画面に閲覧しているユーザーのアイコンをリアルタイムで表示しています。このようなリアルタイム機能を素早く実装できることも大きな特徴です。

    閲覧ユーザーアイコンのキャプチャ

  • ユーザーページの投稿一覧は、クエリ機能を利用して全体の投稿から特定のユーザーの投稿を取得して表示しています。また、クエリを利用してソートやページングを実装しています。

    全体の投稿から特定のユーザーの投稿を取得するクエリの例

    postsRef.whereField("authorID", isEqualTo: user.id)

    ユーザーページのキャプチャ

現時点ではCloud Firestoreにバックアップ機能が提供されていないため、バックアップする場合は自前で行う必要があります。

Cloud Storage

Cloud Storageは写真や動画等の容量が大きなファイルを保存できます。Cloud Firestoreと同様オフラインサポートと高いスケーラビリティを備え、セキュリティールールを書くことでセキュリティーを堅牢にできます。

活用事例

  • 動画やサムネイル画像を保存しています。

  • セキュリティールールの例を上げると、Cloud Storageにアップロードされる動画のファイルサイズを1MB以下に制限するために、下記のセキュリティールールを書いています。

セキュリティールールの例

service firebase.storage {
  match /b/{bucket}/o {
    match /version/1 {
      match /video/{videoID}/file/{file} {
      
        // 認証済みユーザーのみ動画ファイルの読み込みが可能
        allow read: if request.auth != null;
        
        // 動画ファイルが書き込まれる際の条件
        allow write: if (request.auth != null && request.resource == null)
                     || (request.auth != null &&
                     // 1MB 以上の動画ファイルは許可しない
                     request.resource.size < 1 * 1024 * 1024 &&
                     request.resource.contentType.matches('video/.*'));
      }
    }
  }
}

Cloud Functions

Cloud FunctionsはCloud FirestoreやCloud Storageへのデータの追加や変更、またはHTTPSリクエストによりトリガーされたイベントに応じてバックエンドコードを自動的に実行できます。

活用事例

  • プッシュ通知やSlackへのメッセージ送信に活用しています。例えば、ある投稿に新しいコメントが書き込まれたときに、投稿者にコメントが書き込まれたことをプッシュ通知で知らせたり、アプリから不適切な投稿が報告されたときにSlackにその報告を流しています。

    Cloud Functionは1つのイベントを元に複数回トリガーされることがあるため、関数は何回実行されても問題ないように実装しておく必要があります。詳細はこちらのブログ記事をご覧ください。

  • 関数のディレクトリ構造・命名は以下のようにしています。これにより、コードを修正をするときにどのイベントから実行される関数なのか把握しやすくしています。

それぞれのイベントから実行される関数が明確になります。

├── functions
│   ├── auth
│   ├── db
│   │   ├── comment
│   │   │   └── onCreate.ts
│   │   ├── feedback
│   │   │   └── onCreate.ts
│   │   ├── post
│   │   │   └── onCreate.ts
│   │   └── report
│   │       └── onCreate.ts
│   └── storage
├── index.ts
├── test

どのイベントから実行される関数なのかファイルの中を確認しないと分からないため把握しにくいです。

├── functions
│   ├── notifyPost.ts
│   ├── notifyComment.ts
│   ├── notifyReport.ts
├── index.ts
├── test

関数のテスト

テストフレームワークはJestを利用しています。

テスト方法はオフラインモードとオンラインモードの2種類あり、オフラインモードで行う場合、データベースの書き込みを全てスタブしなければならないため、オンラインモードで行っています。

オンラインモードではデータベースへの書き込みやユーザーの作成などが実際に行われ、テストコードがその結果を検査できるように、テスト専用のFirebaseプロジェクトとやり取りするテストを作成します。

注意点として、Firebase Test SDKのfirebase-functions-testnpmモジュールをテスト専用のFirebaseプロジェクトの構成値で初期化した後、メイン関数ファイルをモジュールとしてインポートしなければなりません。

この順序を守らないと予期しないFirebaseプロジェクトとやり取り(書き込み・読み込み)が行われてしまうことがありました。

防止策として、JestのsetupFilesのタイミングでfirebase-functions-testnpmモジュールを初期化するようにしています。

Cloud Messaging

Cloud Messagingは、iOS、Android、ウェブ(JavaScript)クライアントアプリに通知メッセージを送信することができます。

活用事例

Cloud FunctionsからAdmin FCM APIを利用してプッシュ通知を送信しています。

ユーザーが料理を始めたことを知らせる通知自分の投稿にコメントが届いたことを知らせる通知の2種類を送信しています。

Google Analytics for Firebase

Google Analytics for Firebaseは、最大で500種類のイベントに関するレポートを無料で無制限に生成できます。Firebaseのコンソールからダッシュボードを見ることができます。

活用事例

ユーザーの動画閲覧から動画投稿までのファネルを作成して活用しています。

その他のライブラリ

以降はFirebaseの他に活用しているライブラリの一部をご紹介します。

Pring

データの読み込み・書き込みにPringを活用しています。

PringはCloud FirestoreとCloud StorageのO/Rマッパーで、Cloud FirestoreとCloud StorageのAPIを意識せずにデータの読み込み・書き込みができます。

Pringを活用すると開発スピードを加速させることができます。

FilterCam

iOS SDKのCore Image FIlterを適用した動画を簡単に作成することができます。

撮影した料理がより魅力的になるように彩度と中間色の明るさをやや上げたフィルターをFilterCamに適用して活用しています。

適用前(左) 適用後(右)

まとめ

本稿を通してiOSアプリのバックエンドにFirebaseを活用した事例をご紹介しました。

Firebaseは開発が活発で新しい機能が追加され続けているので、今後もより使いやすくなることが期待できます。プロダクトの性質に合わせて、うまく活用できれば大きなメリットが得られるのではないでしょうか。

最後になりましたが、冒頭に記載しました通り、Cookin'は7月末をもちまして提供を終了いたします。これまでCookin'を支えてくれた皆さまに感謝申し上げます。ありがとうございました。

新規決済手段導入に際し、なるべく丁寧にテストケースを作成した話

$
0
0

会員事業部の日高尚美(@natan3)です。 半年前になりますが、クックパッドでは Android ユーザ向けにプレミアムサービスの決済手段の一つとして Google Play 決済を導入しました。

ユーザに新たな機能を提供する前には、何らかの形で開発者側での検証が必要です。

Google Play 決済導入バージョンのリリースは、ユーザのお金を扱うこともあり、不具合が起きた際にサービス全体の信用に関わる、非常にリスクの高いリリースでした。 それに伴い、検証もできる限り万全に行わなければなりません。

そのため、なるべく丁寧にテストケースを作成し、それをもとに検証を実施することで新機能が期待通りに実装されていることを担保しました。 丁寧にテストケースを作成したから、というだけではもちろんありませんが、リリースから半年経った今でも Google Play 決済周りの目立った不具合はまだ見つかっておりません。

今回作成したテストケースの紹介

今回作成したテストケースの一部をご紹介します。 実際に利用したものと表現を変えてはいますが、雰囲気は伝わるかなと思います。

f:id:nano-041214:20180718194510p:plain

含まれている項目は以下のとおりです。

  • 各画面で起こりうる状況の組み合わせ(上図で言うところの前提条件)
  • 前提条件を再現するシナリオ
  • シナリオを達成するための手順
  • 手順に対応した期待する振る舞い
  • 期待する振る舞いが得られたかどうかのチェック欄

これを元に動作を確認しました。

それぞれのケースにて、想定した通りの画面が表示されていれば問題ありません。 しかし、実装によっては、期待する振る舞いが得られない場合も出てきます。

テストケースにて期待する振る舞いが本来あるべき姿なので、その場合には実装に手を入れる必要があります。 すべての操作に対して期待する振る舞いが得られる状態にすることが、テストケース作成の目的であり、検証のゴールとなります。

Google Play 決済のテストケース作成までの流れ

なにもないところからいきなりテストケースを作成することは難しいため、 画面遷移図とそれらの分岐条件、各 API の返す異常系一覧をもとに、テストケースを作成しました。

テストケースを作成する流れは以下のようになります。

  1. 各画面で起こりうる状況を整理する
  2. 前提条件を再現するシナリオを作成する
  3. そのシナリオをなぞるための手順を埋める
  4. その手順を行うことで期待されるアプリの振る舞いを埋める

以降の文章で、各項目について解説します。

各画面で起こりうる状況を整理する

テストケースにおいて重要なのは 網羅性です。

そのため、画面遷移図の各分岐である API 接続箇所や画面遷移時にて起こりうるユーザ状態など、前提条件を構成する各状況を洗い出します。

実際の遷移図ではありませんが以降の理解のために Google Play 決済導入時の画面遷移のイメージ図を添えておきます。

f:id:nano-041214:20180718194451j:plain

Google Play 決済導入プロジェクト初期リリースではクックパッドにログイン済みの無料ユーザのみを対象としていたため *1、 各分岐において以下のような状況の組み合わせを、検証すべき条件として考えました。

  • ユーザの状態がどうか
    • ログイン済みユーザ or ゲストユーザ
    • プレミアムサービス会員ユーザ or 無料ユーザ
  • Android のクックパッドアプリのバージョンが決済対応バージョン以降かどうか
  • 決済可能端末かどうか
    • クックパッドがプリインストールされているらくらくフォンなどの場合、Google Play ストアアプリがないため Google Play 決済も利用できない
  • 異常な操作をした場合でも、何らかの方法で本来あるべき状態に復帰可能かどうか
    • この画面を表示している状態で Web からクックパッドのユーザ登録を解除してみる
    • この画面を表示している状態で Web からクレジットカード決済でプレミアムサービスになってみる
    • ここでフライトモードにしてみる

もちろん全ての組み合わせが成立するわけではありません。 例えば決済可能端末かどうかについては、決済不可能端末を伝える画面にて遷移がストップするため、以降の画面については考えない、といった具合です。

前提条件を再現するシナリオを作成する

それぞれの状況が洗い出せると、それらを組み合わせることでユーザが何をしようとしたかのシナリオが書き出せます。

状況の羅列にはなりますが、シナリオを作成しておくことで見通しが良くなり、仕様の考慮漏れについて気づきやすくなります。

例えば、

「ログイン済みユーザが Google Play 決済用画面を開いた状態で Web からクレジットカード決済でプレミアムサービスになった場合、
 Google Play で決済するボタンを押すと既に課金済みであることを示すダイアログが表示される」

といったものです。

そのシナリオをなぞるための手順を埋める

シナリオまで埋め終わると安心してしまいがちですが、書き手以外が再現可能なレベルで手順を埋めるまでがテストケース作成です。

テストケースは、一度作成すると、製作者本人が望もうが望まなかろうが再利用される可能性が高いです。 現にクックパッドでは、作成されたテストケースは今後のテストケース作成の参考にできるよう、社内で共有されています。

コードや文章に限らず、テストケースも、書いた本人ですら暫く経つと書いた内容やなぜこう書いたかを忘れがちです。 テストケースが雑に書かれていた場合には、この記述が当時何のために書かれていたのかを考古学する必要が出てきます。

そのため、なるべく丁寧に書きましょう。

先程の

「ログイン済みユーザが Google Play 決済用画面を開いた状態で Web からクレジットカード決済でプレミアムサービスになった場合、
 Google Play で決済するボタンを押すと既に課金済みであることを示すダイアログが表示される」

というシナリオの例に対しての手順を具体的に書くと、

  1. 無料ユーザでログインした状態でアプリのトップページを開く
  2. サイドメニューのプレミアムサービス会員登録導線をタップする
  3. Google Play 決済を利用するボタンを押す
  4. ブラウザアプリを起動し Web からクックパッドのトップページにアクセスする
  5. Web から先ほどと同一のユーザでログインする
  6. Web のトップページのプレミアムサービス会員登録導線を開く
  7. Web の登録ページからクレジットカード決済を利用してプレミアムサービスに登録する
  8. アプリの画面に戻り、プレミアムサービスに Google Play で決済するボタンを押す

といったものになります。

その手順を行うことで期待されるアプリの振る舞いを埋める

こちらも先程の手順同様、丁寧に書く必要があります。

先程の手順の例に対応するアプリの振る舞いとしては以下のようになります。

  1. 無料ユーザ向けのトップページが表示される
  2. プレミアムサービスを訴求する画面が表示される
  3. Google Play 決済用の画面が表示される
  4. Web 版のクックパッドのゲスト向けページが表示される
  5. 先ほどと同一ユーザでログインしたトップページが表示される
  6. Web のプレミアムサービスを訴求する画面が表示される
  7. プレミアムサービス登録完了ページが表示される
  8. 既にプレミアムサービス登録済みであることを伝えるダイアログが表示される

異常系であれば、どのような操作をすれば正常系に復帰できるかを伝える画面が出ていることを期待します。 そのため、ある操作の流れで何らかの異常を伝える画面を見たユーザが、その内容を読んだ際に混乱しないかについても確認ができます。

テストケースを作成して得られた良い副作用

テストケースを作成して得られる一番の良い副作用は、仕様の考慮漏れに気づけることです。

テストケース作成前は、Google Play ストアによる決済用システムダイアログが出ているときにブラウザアプリから別な決済手段にてプレミアムサービスを登録したらどうなるのか? というケースについて考えられていませんでした。

Google Play ストアの決済用システムダイアログが出てから決済処理完了までは、クックパッドではその経過状況を知ることが不可能です。 そのため、残念ながら決済完了時に Google Play の購読情報をクックパッドに送る際に二重課金が起きていることを伝えることになります。 幸いにも既に別で考慮していたケースと同様にユーザに重複課金を伝え、問い合わせを促す画面を表示できるような実装になっておりました。

また、テストケースを作成することで、検証の手順の理解や進捗が属人化しなくなるため検証を分担したりダブルチェックしたりできます。

加えて、きちんとやったから大丈夫、という安心感がえられます。

もちろんリリースして運用に入るまで何が起こるかはわからないですが、 最大限やることはやった!という安心感を持ってリリースできるので精神衛生上良いです。

テストケース作成期間について

テストケース作成に着手してから、検証し、修正完了するまでに 1ヶ月ほどかかりました。 開発期間全体のおよそ 1/3 もかかってしまった理由は、テストケース作成と並行して API 実装を始めとした他の作業などもやっていたためです。

しかし、効率的な開発を目指す上で、必ずしもテストケース作成期間を短くする必要はないと考えています。

というのも、仕様を煮詰めながらテストケースも整理していくことで、結果的に考慮漏れによる実装の手戻りを減らすことができるからです。 仕様を煮詰めている段階でテストケースも同時に作成していくことは、回り回って効率的に開発を進めることができると感じました。

最後に

検証を怠ることは、ユーザに検証をさせることと同義です。

もちろんすべてのケースを想定したテストケースを作成するのは不可能です。 しかし、ユーザにサービスを提供する以上、リリースまでに最善をつくすことに価値があると考えております。

テストケースは技術部品質向上グループチーム(以下 QIT: Quality Improvement Team)と密に協力して作成しました。 QIT はクックパッドのアプリを利用したユーザが技術的な問題で残念な思いをする体験を減らすための専門家集団です。

このように、クックパッドでは新機能リリースプロジェクトのために様々な分野のエンジニアが一丸となって取り組みます。

そのため、クックパッドではアプリケーションの品質向上に興味のあるエンジニアや、企画から、実装、テストケース作成までを一貫して行うエンジニアリングに興味のあるエンジニアを募集中です。 興味がある方は採用ページまで!

*1:現在はクックパッドに会員登録、もしくはログインしていないユーザでも Google Play 決済を利用してプレミアムサービスを利用できます

スマートまな板による料理支援

$
0
0

研究開発部アルバイトの佐藤です。今日はアルバイト期間中に取り組んでいたまな板にレシピを表示する装置について紹介します。

背景

レシピ本をキッチンに持ち込む以外にも、キッチンでスマホ上から検索することによってレシピを見る機会が増えています。しかし、キッチン内でタブレット端末やスマホでレシピを見る問題点として以下が挙げられます。

  • デバイスが水や油で汚れず、レシピが見やすい位置に置きたいが、スペースの都合上難しい
  • 汚れた手で端末の画面を料理中に触って操作しなくてはならない

また、最近ではAmazon EchoやGoogle HomeなどのスマートスピーカーでCookpadのレシピを検索し、タブレット端末やスマホでレシピを保存することができます。その発展として、レシピの読み上げやEcho Showなどの端末を用いたレシピ表示なども考えられますが、端末と同様に映像などの表示位置が固定されてしまうという問題が挙げられます。また、音声での入力の他にジェスチャなどもとりいれることができればより視覚的な操作も可能ではないかと考えられます。

このような問題に対して取り組んでいるプロジェクトはいくつかあります。例として2つのプロジェクトを紹介します。

こちらのプロジェクトではユーザーの動作やキッチン台の上のものを認識して、端末に現在の動作に合わせた作業内容を表示します。切り方の動画の再生なども端末上で行っていますが、再生するには端末を操作する必要があります。また、この装置では切っている食材を認識するためにまな板自身にセンサなどを取り付ける必要があります。

こちらの論文ではカメラ・プロジェクタ・対話ロボットを連携させた調理支援システムが提案されていますが、対話ロボット1台、カメラ2台、プロジェクタ3台とかなり大掛かりなシステムとなっています。

スマートまな板

f:id:sss3p:20180720111838p:plain

そこで図のように天井に装置を設置することによりキッチン用品には非接触のスマートまな板を開発しました。このまな板の特徴はまな板には何も手を加えないことです(つまり、正確にはスマートまな板でなく、レシピプロジェクターです)。具体的には次のことを目標に開発しました。

  • プロジェクターでレシピや操作画面をまな板に投影し、視線の移動の少ない情報提供
  • 作業台の上に装置を置かないことによる広い作業スペースの提供
  • webカメラとRaspberry Piによる画像処理で人の手を検知し、画面に触らない操作

今回の対象者

以下のような問題に困っている料理初心者を対象ユーザとしました。

  • にんじん、たまねぎ、じゃがいもの剥き方・切り方がわからない
  • だしのとり方がわからない
  • ケーキをどのようにデコレーションすれば良いかわからない

今回実装した機能

上記の様な料理初心者に対して、にんじん、たまねぎ、じゃがいも、だしが材料にすべて入っている肉じゃがの調理とケーキのデコレーションを支援するような機能を実装しました。 料理初心者への支援としてまな板に映像の投影を行い、次のようなものを視覚的に提供しました。

  • にんじん、たまねぎ、じゃがいもの剥き方・切り方の手順動画:文字だけでは中々習得の難しい包丁の具体的な使い方を、まな板の左上に動画を流すことで視覚的に伝える
  • 材料と調味料のチェックボックス:何が準備できていて次に何を準備すべきか判断しやすいようにする
  • 手順表示:一文ずつ手順を表示していく。また、時間が手順に書いてあった場合はタイマーを起動する
  • デコレーションケーキの下書き:デコレーションケーキの下書きを投影し、デコレーションの位置ガイドとして使えるようにする

実装方法

webカメラ・小型プロジェクター・Raspberry Piを用いて実装を行いました。 開発言語はPython3、使用ライブラリはtkinter、opencv2です。 詳細は以下のようになっています。

使用物名 型名など
webカメラ Logicool HD720p
小型プロジェクター iOCHOW iO4 ミニ プロジェクター
Raspberry Pi Raspberry Pi 3, raspbian gnu/linux 9
Python3 Python 3.5.3
tkinter version 8.6
opencv2 version 3.4.1

手の認識

簡易的なデモ機の実装としてカラートラッキングを用いました。具体的には手の肌色をトラッキングすることで手の位置を捉えて、画面操作ができるように実装しました。

デモ

デモ中の写真をいくつか紹介します。

  • スタート画面 f:id:sss3p:20180720111936j:plain
  • メニュー選択 f:id:sss3p:20180720112025j:plainf:id:sss3p:20180720112045j:plain
  • 材料一覧表示 f:id:sss3p:20180720112117j:plain
  • 手順表示 f:id:sss3p:20180720112244j:plain

気づき

実装したスマートまな板では視線の移動の少ない情報提供、広い作業スペースの提供、画面に触らない操作を実現することができました。 実際に実装してみて気づいたことは以下です。

  • 視線の移動の少ない情報提供:
    • 切り方動画をまな板の左上に表示することによって、動画を確認しながら作業することができた
    • 作業によってはまな板に投影するよりもキッチンの壁に投影したほうが良い場合もある(まな板に投影したほうが良い場合は食材を切るときの動画での切り方確認で、キッチンの壁に投影したほうが良い場合はレシピ表示とタイマー)
  • 広い作業スペースの提供:
    • スマホやタブレット端末を作業台の上に置く必要がないため、広い作業スペースを確保できた
  • 画面に触らない操作:
    • 今までレシピを確認するために画面操作する度に手を拭いたり洗ったりしていたことがなくなった

デモ

実際にスマートまな板を数人に体験していただきました。体験後、頂いた意見をいくつか紹介します。

  • 材料チェックリストがまな板の上で操作できるのは便利
  • まな板の上で切り方動画を見られるのは面白い
  • 実用化するんだったら、スマートスピーカーと組み合わせて提供する情報や選択肢によって音声か映像か使い分けたほうがよさそう

また、いくつかの改善点や追加機能がみつかりました。

  • ユーザーの動作によって投影位置を変える機能(ユーザーの作業している場所を検知して、作業の邪魔にならないようなスペースへ画面を移動・縮小させる)
  • まな板の上に置かれた材料を認識したレシピ検索
  • まな板の上に置かれた材料の重さを概算し、レシピで指定されている重さによって切り方を投影する機能
  • 自動でデコレーションの下書きを拡大縮小したり移動したりしてケーキに下書きを合わせてくれる機能

現在の実装では以下の問題が発生しています。

  • 肌色の位置を手の位置と認識しているため、色の似ている木のまな板などを誤認識
  • 指先や指を認識していないため手首などを誤認識するなど認識精度が低い そのため、手の認識専用デバイスを利用しない場合はニューラルネットワークを用いて手を手としてラベル付したり、手の形を認識するなどの実装に変更することが考えられます。 また、Leap Motionなどの外部デバイスを用いて手の認識を行うということも考えられます。

まとめ

キッチンでレシピを確認するときに、視線の移動の少ない情報提供、広い作業スペースの提供、画面に触らない操作を実現できるスマートまな板の開発に取り組みました。

実際に実装することによって、提案の有用性や改善点を見つけることができました。

今後の展開としてはスマートまな板を用いたアプリの開発などが考えられます。具体的には切り方動画をまな板で再生できることや材料・手順のまな板への投影を用いて、子供・初心者向け料理学習アプリなどを実装することにより、より料理初心者への支援ができると考えられます。

Chaos Engineering やっていく宣言

$
0
0

技術部のヨシオリです。

Netflix が Chaos Engineering の論文を公開して 2 年ほど経ちました。
クックパッドは最近、 Chaos Engineering を導入する事を決めました。
この記事ではその背景を紹介したいと思います。

そもそも Chaos Engineering とは

Netflix では Failure Injection Testing として、営業時間中に意図的に障害を起す事をやっていました。Chaos Monkey というインスタンスとサービスを落すものから Chaos Gorilla、Kong という availability zone や region 単位で障害を発生させるものなどです。

その経験から Chaos Engineering というものが提唱されました。
Principles of Chaos Engineeringによれば

Chaos Engineering is the discipline of experimenting on a distributed system in order to build confidence in the system’s capability to withstand turbulent conditions in production.

と定義されています。
意訳すると「本番環境の分散システムが過酷な状況でも耐えれるとの確信を得るために、実験するという取り組み」とかでしょうか?

分散システムはマイクロサービスと置きかえるとイメージしやすいと思います。複数のマイクロサービスが相互に呼び出し、協調して動くシステムでは一つのサービスがクラッシュしただけでシステム全体が壊れるような事になっていてはいけません。そうなると、ユーザーに届けられる価値が減ってしまいビジネス的にも問題です。
もちろんそうならないように作るべきですが、それでも予想不可能な事は起こります。それを知るためにコントロールされた障害を投入し、知見を広げたり、確信を得たりするのです。

そのための実験は下記のステップで進めます。

  1. 正常な振る舞いをしているかどうかを測定可能な値として定義する。
  2. この正常な状態が通常時と障害エミュレート時の両方で継続することを仮定する。
  3. サーバクラッシュ、ディスク異常、ネットワークエラーなどの現実世界で起こりえる障害をエミュレートする。
  4. 1で定義した値を通常時と障害エミュレート時で比較し検証していく。

詳しくは上記論文やリンク先を見ていただくとして、凄くザックリと纏めてしまうと、
システムにコントロールされた障害をエミューレートし、それでも壊れない事を検証していく
と、思ってもらえれば良いかと思います。
元々、昔から似たような事をやっているサービスはありましたが Netflix がそこに Chaos Engineering と名前と付け原則などをわかりやすく纏めた感じですね。

必要になった背景

クックパッドでは 2014 年ころからマイクロサービスに取り組んで来ました。

そして個々の Web アプリケーションはコンテナ技術で仮想化し、コンテナオーケストレーションツールとして ECS を使い運用しています。

また、サービス間の通信に関してもサービスメッシュの導入などを行なっています。

その結果、今ではチーム数も増加し、開発規模も大きくなっています。結果として( 管理画面を提供するサービスなどを除いても) 大小 80 個近くのサービスがそれぞれお互いに緩くではありますが連携し動いています(僕も数を調べてビックリしました)。

さすがにこれだけのサービスが連携して動いているとどこかで発生した障害がどこまで影響するのか把握するのは容易ではありません。
A というモバイルアプリが叩いている B という API の裏で通信している C が必要としているデータ取得のための D のレスポンス時間が遅くなって、結果として A の応答が悪くなったのだが、原因が D だとは思っていなかった……的な事も発生します。

何故やるのか

上記ブログのマイクロサービス導入背景にもありますが、昔のようにひとつの巨大なアプリケーションを運用するようなスタイルではプロダクト開発の規模の拡大やスピードに限界があり、マイクロサービスアーキテクチャを採用するようになりました。分散システムとして Web サービスを実装する事により単一の複雑なアプリケーションからは開放されましたが個々のサービス間の連携は複雑になりました。

Chaos Engineering の導入によってサービスの耐障害性に自信を持てるようにします。日常的に障害をエミュレートする事によってサービスの耐障害性が高いことを開発者に要求します。
ソフトウェアテストなどの文脈では良く言われますが、不具合は発見が遅れれば遅れるほど、その不具合を修正するコストはかさむことになります。
可用性の高いシステムを作るために、不具合を早く発見し改善していくために Chaos Engineering を導入していきます。

付随して考えている事

人は自分が想像出来るものにしか対処出来ません。バグというのは大体が想定外の入力によって発生します。そこでもっと想像力を働かせろ的な精神論を言っていても良くはなりません。Chaos Engineering のように実際にそういった状況を作る事が大事だと思っています。
例えばクックパッドでは各サービスがどんどん AWS の Spot インスタンスで動くように移行していっています。これはサーバはいつ落ちても良いようにしておかなくてはいけないし、バックグラウンドジョブは落ちたら再実行出来るようにしておかなくてはいけない事を開発者に強制します。
でも、それらは実は Spot インスタンスで動くからやらなければいけないものではありません。耐障害性の高いシステムを作るためにはやらなければいけない事を Spot インスタンスの環境にする事によって強制するようになっただけです。
さきほども書いたように人は自分が想像出来るものにしか対処出来ません。Chad Fowler が Immutable Infrastructure を提唱したけれども、開発者がそれを真に実行出来るようになったのは Docker という環境のおかげというのと同じです。

最後に

現在、クックパッドでは Hako や ECS を使ったコンテナ環境の整備が進み、サービスメッシュの導入によりサービス間の通信を集中管理出来るようになりました。これにより、Envoy proxy を利用してサービス間通信で障害をエミュレートしたり、それらの設定を hako で行えるようになったりと環境は整いました。
まだまだクックパッドのマイクロサービス群の正常な状態( steady-state )をどう定義していくかなどやらなければいけない事は色々あります。
個人的にはこの規模のマイクロサービス群を扱っていく環境は国内ではそんなに多くはなく大変面白い環境だと思ってます。
クックパッドでは一緒に Chaos Engineering を導入していく仲間を募集しています
このエントリを読んでご興味をお持ちいただけた方は、ぜひともご応募ください。

【開催レポ】Cookpad Tech Kitchen #16 コメルコテックバナシ〜新規事業開発のリアル〜

$
0
0

こんにちは。広報部のとくなり餃子大好き( id:tokunarigyozadaisuki)です。

2018年7月19日に、Cookpad Tech Kitchen #16 コメルコテックバナシ〜新規事業開発のリアル〜を開催いたしました。クックパッドでは、Cookpad Tech Kitchenを通して、技術やサービス開発に関する知見を定期的に発信しています。

f:id:tokunarigyozadaisuki:20180802173248j:plain
ホワイトボードと発表練習中の星川

第16回は2018年6月26日にリリースいたしました、料理が楽しくなるマルシェアプリ「Komerco-コメルコ-」の開発裏話をテーマとし、Firebase, Algolia など利用している技術の話はもちろん、新規事業のサービス開発について、デザインの観点からもお話をさせていただきました。 本ブログを通してご来場いただけなかったみなさまにも、当日の様子をお届けしたいと思います!

料理が楽しくなるマルシェアプリ「Komerco-コメルコ-」

f:id:tokunarigyozadaisuki:20180802173546p:plain

「モノとの出会いで、料理はもっと楽しくなる」

「Komerco-コメルコ-」は、料理道具、うつわ、カトラリー、リネン雑貨などの“料理が楽しくなるモノ”が買えるマルシェアプリです。自身の手で創るモノで「料理を楽しんで欲しい」と願うクリエイターさんが出品した作品を、スマートフォンアプリから直接購入することができます。また、作品のこだわりやストーリーを紹介する「コメルコバナシ」などの記事コンテンツも提供しております。 みなさんもぜひアプリをダウンロードして、とっておきのモノを見つけてみてくださいね!

Komerco - コメルコ - by クックパッド

Komerco - コメルコ - by クックパッド

  • Cookpad Inc.
  • ショッピング
  • 無料

発表プログラム

「Komerco-コメルコ-を支える技術」

はじめに、2017年に中途入社したiOSエンジニアの星川より「Komerco-コメルコ-を支える技術」というタイトルで、「Komerco-コメルコ-」で利用している技術についてお話しいたしました。

speakerdeck.com

「Firestore と Cloud Storage を用いたアプリでの画像の扱い方」

2017年に新卒でクックパッドへ入社した三浦からは、「Komerco-コメルコ-」でのユーザーから投稿される画像の圧縮やリサイズに関して、サンプルアプリを用いながら画像の投稿、取得フローについてご紹介しました。

speakerdeck.com

「Effective Firestore Security」

2017年に中途入社したiOSエンジニアの岸本からは、Firebase Cloud Firestoreの "セキュリティ"に焦点を当てて、問題となるケースの紹介、セキュリティを意識したモデル設計、セキュリティルールの実践的な書き方をお話させていただきました。 speakerdeck.com

「ゼロからはじめるサービスのデザイン」

2017年から新卒でクックパッドへ入社し、現在「Komerco-コメルコ-」のリードデザイナーとしてブランディングからサービスの体験、Web・アプリのUIなどデザイン全般と開発を担う藤井からは立ち上げからリリースまで、サービス全体のデザインを見るにあたって取り組んだことや、仕組みづくり、デザインの観点からサービス開発のリアルをお話いたしました。

speakerdeck.com

付箋形式でお答えするQ&Aディスカッション

Cookpad Tech Kitchenでは参加者のみなさまからの質問を付箋で集めています。 ほんの一部ではありますが、当日は下記のような質問に回答いたしました。

f:id:tokunarigyozadaisuki:20180802173252j:plain
たくさんのご質問ありがとうございました!

Q: Firestoreのデメリットは? A: スキーマが無い。エクスポート出来ない(分析の為にスクリプトで対応している)

Q: Cloud Functionsでの失敗をどの様にハンドリングしている? A: 不整合のあるデータを集めてきて一括でパッチするスクリプトがある

Q: rulesを書きはじめるタイミングは? A: 最初は緩かった。リリースする数ヶ月前ぐらいからでもよいのでは。とはいえ最初から書けるなら書いた方が良い

Q: rulesを書けなかったのはどういう時? A: 制限がある。リスト、マップの中の値が何かをチェック出来なかった。他のコレクション・ドキュメントを参照したり出来なかった。お金に関する部分はCloud Functionsに寄せている

Q: UIから直接モデル(Firestore)を操作していましたが、テストはどうしていますか? A: 外部のモデルフレームワークに則っているからそっちで任せている。モデルに実装を寄せると自由度が落ちる

f:id:tokunarigyozadaisuki:20180802173259j:plain

シェフの手作り料理

Cookpad Tech Kitchen ではイベントに参加してくださったみなさまにおもてなしの気持ちを込めて、シェフ手作りのごはんをご用意しております!食べながら飲みながらカジュアルに発表を聞いていただけるように工夫しています。今回お越しいただけなかった方も、ぜひ次のイベントはご参加くださいね。

f:id:tokunarigyozadaisuki:20180802174125j:plainf:id:tokunarigyozadaisuki:20180802234907j:plain
オリジナルロゴ寿司ケーキとシェフ特製の料理

f:id:tokunarigyozadaisuki:20180803080331j:plain
乾杯の様子

おわりに

クックパッドではKomerco事業部はもちろん、その他新規事業、レシピサービス事業などに携わる新しい仲間を募集しています。ご興味がある方はぜひご応募ください!お待ちしています。

www.wantedly.com

http:// https://info.cookpad.com/careers

次回のCookpad Tech Kitchen のテーマは「北欧で最新のインタラクションデザインを学んできた話」。8月22日 (水)に開催予定です! クックパッドでは他にも様々なイベントを企画しておりますため、今後のイベント情報についてご興味がある方は、ぜひConnpassのメンバー登録をポチっとお願いいたします!みなさまにお会いできることを楽しみにしております。

cookpad.connpass.com

iOSDC Japan 2018 に2名が登壇&ブースでお待ちしております!

$
0
0

こんにちは!広報部のとくなり餃子大好き( id:tokunarigyozadaisuki)です。

毎日異常気象が続いていますね。猛暑に豪雨…みなさん、体調管理には十分気をつけてくださいね。

さて、iOSと周辺技術を題材としたカンファレンス、iOSDC Japan 2018が今年も8月30日(木)〜9月2日(日)に開催されますね!

クックパッドは、昨年同様プラチナスポンサーをさせていただいておりますので、ブースを出展いたします。また、弊社エンジニア@giginet@slightairが登壇し、@sgr-ksmtが当日スタッフとして関わってくれます。カンファレンスには、他にも多くの社員が参加いたしますので、会場でクックパッド社員をお見かけの際には、お声がけいただけますと嬉しいです。

登壇スケジュール

クックパッドの社員2名は、カンファレンスの3日目と4日目に登壇いたします。 以下、スケジュールと登壇内容のご紹介です。

3日目 9月1日(土)

11:20〜 Track A 三木 康暉(@giginet):詳解Fastfile

プロジェクトが大規模化していくと、さまざまな業務を自動化したくなってきます。同時にロジックが増え、特定の人しかメンテできなくなったFastfileにお悩みの方も多いでしょう。 このトークでは、実際の活用事例を交えながら、大規模プロジェクトにおける効果的なFastfileの書き方、プロジェクトの雑務自動化についてをお話しします。 そのほか、fastlaneコミッターによる明日から使える実践的なtipsも数多くお伝えします。

コメント

Fastfileについて30分も何を話すんだ、と我ながら不安ですが、なかなか他所で聞けない知見を盛りだくさんにしたいと考えていますので、fastlaneを運用している方だけではなく、業務改善に興味がある方全てに役立つ内容にしたいと思っています!

16:20〜 Track C 茂呂 智大(@slightair):動作確認のための社内アプリ配信サービスを新たに作った話

アプリの開発中にビルドしたアプリをCrashlyticsBetaやDeployGateなどにアップロードし、手元で動作確認できるようにしているチームは多いと思います。 僕たちもそういったサービスを使ってきましたが、様々な課題が出てきたため自分たちの使い方にあったシステムを新しく作りました。 どういった課題がありどういうツールを用意したのか、そしてどうリリースフローが改善されたか話します。

コメント

組織に合わせてどういう仕組みやツールを用意したのか、それによってどう開発環境を改善できたかという話ができればと思っています。がんばります。

4日目 9月2日(日)

16:10〜 Track A 三木康暉(@giginet):🀄

Swiftの様々な言語機能を使って麻雀を遊んでみましょう! Swiftyな麻雀ライブラリの実装や、和了判定のアルゴリズムなどについてお話しします。

コメント

最終日の最後で、皆様お疲れだと思うので、頭を使わずに聞けるLTになると良いなと思います。僕はすでに準備で疲れています。

ブース

iOSDC Japan 2018 では、ブースの出展をいたします。グッズの配布はもちろんですが、今回は、みなさんに楽しんでいただける特別プログラムを予定しておりますので、乞うご期待……! ぜひ、お立ち寄りくださいね。

おわりに

発表内容へのご質問やクックパッドにご興味をお持ちの方は、お気軽にブースまでお越しください。みなさまにお会いできることを楽しみにしております。

機械学習を用いてユーザーのご意見分類業務を効率化した話

$
0
0

こんにちは。研究開発部の @vanhuyzです。機械学習・自然言語処理を中心に研究開発しています。 今回は機械学習を活用してユーザーからのご意見を 81 のカテゴリーに自動分類し、ユーザーサポートスタッフによる手動分類の工数を半分にできた話を紹介したいと思います。

背景

クックパッドは現在約 5,500 万人の国内月間ユーザーがあり、日々ユーザーからたくさんのご意見やご要望を頂いています。創業してからユーザーの声を大事に扱う文化があり、どのご意見も一度目を通すようにユーザーサポートスタッフが努力しています。ご意見はスタッフによってさらに分類され、必要に応じてディレクターやエンジニアに振り分けられています。

例えば、こんな感じのご意見が来ています。「このレシピは簡単なので、子供とやってみました。楽しかったです」や「機種変更して、ログイン出来ません」や「もっと具体的な内容でも検索できるようにしてほしいです。例えば、冷蔵庫の余りもので作りたいときに何種類かの食材でも検索できると助かります。」などです。

現在、ご意見に対して 100 以上のカテゴリーが登録されています。例えば、ポジティブ・ネガティブ・検索関連・ログイン関連・つくれぽ・不具合などです。

f:id:vanhuyz:20180808154458p:plain

ユーザーサポートスタッフが社内システムを使い、ご意見を読んでカテゴリーリストから適切なカテゴリーを選んだ上で、システムが Slack の適切なチャネルに転送してくれます。これにより、関係するディレクター・エンジニアはご意見を読むことができ、すぐその場で議論します。 ただし、ご意見の数が多く、カテゴリーも多いため、毎日の分類作業が大変で、ボトルネックとなっていました。そこで、自動分類の仕組みを導入したいと考えました。

ご意見分類におけるチャレンジ

ご意見分類をパッと見ると簡単そうかもしれませんが、実はそうではありません。

  • まず、カテゴリー数が多いです。現在は 100 カテゴリー以上があり、今後もどんどん増えていきます (open set problem)。

  • 次に、1つのご意見が複数カテゴリーに所属することがあります (multi-label problem)。例えば、「つくれぽを投稿するのは楽しいし、もらえるのも嬉しいですね。」というご意見は「ポジティブ」と「つくれぽ」に入っています。

  • また、蓄積されたデータは不均衡 (imbalanced data) です。汎用的なカテゴリー(例えば、ポジティブなご意見・不具合報告)のご意見が非常に多いのに対し、最近リリースしたばかりのサービス(Amazon Echo Skill, storeTV など)のご意見はまだ多くはありません。また、クローズされたサービスも多数存在し、それらに関するカテゴリーはいらなくなります。

取り組みについて

以上の3つのチャレンジに向けて、どのように解決したのかを簡単に説明します。今回は Google の Rules of Machine Learningを参考にしながらプロジェクトを進めました。43 ルールもありますが、その中で以下の 3 つを気にして進めました。

1. Don’t be afraid to launch a product without machine learning.
(機械学習を使わないことを恐れるな!)

辞書ベースの手法を試しました。例えば、「探、調べ、検索、キーワード」という単語が含まれるご意見は「検索関連」と判断されます。この方法だと、かなり高い精度が得られますが、再現率が低いです。そして、100 以上のカテゴリーの一つずつに辞書を作るのがあまり現実的ではないので、他の方法を試すことになりました。

2. Choose machine learning over a complex heuristic.
(複雑なルールになってきたら機械学習を選択!)

multi-label problem 対応で、1つのカテゴリーに相応する1つのバイナリ分類器を作りました。例えば、「ポジティブ」カテゴリーは ポジティブ-or-not 分類器になります。最終的にデータが少なすぎるカテゴリーを除いて合計 81 の分類器を作成しました。将来もしカテゴリーが増えたら、そのカテゴリーに相応する分類器を作れば良いので、open set problem も対処できます。 imbalanced data problem (負例が正例より圧倒的に多い)に関しては、正例はそのままにし、負例は正例と同数になるように無作為に選びました。

f:id:vanhuyz:20180808154522p:plain

3. Keep the first model simple and get the infrastructure right.
(シンプルなモデルを保ち、インフラを適切に保つ)

今回はサポートベクターマシン (SVM) を採用し、scikit-learn で実装しました。F1 Score を 85% 以上になるまでチューニングしました。このぐらいの精度でも十分なので、早い段階でインフラを整備し、本番環境に導入しました。もちろん100% の性能を達成することは難しいため、完全に自動分類で手作業をなくすことではなく、分類業務へサポートという形で導入しています。

結果的にご意見サジェストという形で、以下は導入後の管理画面のイメージです。

f:id:vanhuyz:20180808154629p:plain

導入前はスタッフが 100 以上のカテゴリーの中から探さないといけないのに対し、導入後は数少ない「もしかしてカテゴリー」の中だけ選択すれば良いので、非常に楽になりました。(「もしかしてカテゴリー」になかった場合は、従来通り、全カテゴリーの中から探すことです。)実際に計測したら工数を半分まで減らすことができたという結果になりました。 また、分類するスタッフに聞いたところ、昔はメンバーによって判断のばらつきがありましたが、今は直感的に分類精度が上がったという話もありました。

また、学習モデルの精度を上げるために、再学習の仕組みを導入しました。選択されていない「もしかしてカテゴリー」は学習の負例となり、Rundeck/Jenkins で毎週定期的に training コンテナを起動し、モデルを再学習しています。新しいモデルができたら、inference コンテナがそのモデルに切り替えるように設定します。仕組みは以下のイメージです。

f:id:vanhuyz:20180808154645p:plain

これによって、状況が変わっても、モデルを劣化なく長期的に使うことができます。これからしばらく運用し、いつか完全に自動分類できるかもしれません。

最後に

今回は機械学習を用いてご意見分類業務を効率化した話を紹介しました。 クックパッドでは機械学習を活用したくさんの問題を解決しています。 このエントリを読んでご興味をお持ちいただけた方は、ぜひとも採用ページからご応募ください。


リリース間近の新規事業「クックパッドマート」の立ち上げの話

$
0
0

こんにちは、買物事業部のデザイナー兼エンジニアの長野です。

現在買物事業部では、クックパッドマートという新規サービスの開発を進めています。この夏にいよいよリリースを予定しており、先日 プレスリリースを発表しました。

クックパッドマートは、今年の1月に私を含めて3名の小さなチームでサービスづくりを開始しました(8/13現在:10名)。チーム発足から半年をかけて様々な検証を行い、サービスを形にしてきたので、本記事ではそのプロセスの一部を下記の流れでご紹介したいと思います。

  1. クックパッドマートとは
  2. サービスが解決したい課題
  3. サービスが提供する価値
  4. 価値仮説に至るまでのプロセス

1. クックパッドマートとは

クックパッドマートは、料理が楽しみになるような食材を、スマホアプリから簡単に注文することができる、生鮮食品のECサービスです。

地域の精肉店や鮮魚店、野菜農家、ベーカリーなどの「こだわり食材」をアプリでまとめて注文できます。 「焼きたてパン」や「朝採れ野菜」などの新鮮な食材を、販売店から集荷した当日に受け取ることができ、1品からでも送料は無料。毎回必要な分だけを手軽に購入することができます。

商品の受け取りは、地域の様々な店舗・施設等に設置された「受け取り場所」の中から好きな場所を選び、好きな時間に受け取ることが可能です。そのため、日中忙しくて買い物をする時間がない方でも、新鮮なこだわり食材を手軽に入手することができるサービスです。

ティザーサイトはこちら

2. サービスが解決したい課題

クックパッドマートは、私たちが普段の買い物に抱える下記のような課題を解決しようとしています。

おいしい食材を新鮮な状態で手に入れることの難しさ

世の中の大半の人は普段の食材はスーパーで購入することが多いと思います。何でも揃うスーパーはとても便利ですが、一方で、スーパーの品揃え次第で食生活が決まってしまうのも事実です。 街にはこだわりを持って厳選された食材を扱う専門店もありますが、多くの人がその存在を知らない、もしくは知っていてもそれらの店舗を回る手間や時間をかけられない、というのが実情だと思います。

まとめ買いせざるを得ない環境と仕組み

仕事をしていたり忙しい人ほど、普段から買い物にかけられる時間は少なく、週末に1週間分をまとめ買いする人も多いのではないでしょうか。 また、ECサービスを利用する場合も、配送コストをカバーするための「最低注文金額」が設定されていることがほとんどです。 そのため、たとえ新鮮な状態で購入したとしても、結果的に家庭の冷蔵庫で鮮度を落としてしまうのが現実です。

3. サービスが提供する価値

クックパッドマートは、これらの課題を解決するため、以下の3つの軸で価値を実現しようとしています。

提供する3つの価値
クックパッドマートが提供する3つの価値

品質が良いこと

おいしい食材は食卓を豊かにし、毎日の楽しみを増やすことができるというのは、あまり疑いようがないのではないでしょうか。 クックパッドマートは、こだわりを持っておいしい食材を届けたいと考えている販売店や生産者の方にご協力をいただき、自信を持っておすすめできる食材だけを商品として扱います。 また、おいしいものをおいしいうちに食べることも大切です。出荷当日に配送し、数日で使いきれる量だけを注文できるサービスとすることで、それを実現しようとしています。

調達コストが低いこと

クックパッドマートはアプリで手軽に注文でき、1品でも送料は無料。受け取りは自宅ではなく、近所の配送拠点を選択する形をとります。 使いきれる量の食材を、いつもの帰り道で好きな時間に受け取れることで、食材の調達にかかるコスト(時間や手間)を減らします。

調理コストが低いこと

献立を考えながら買い物をするのは、時間もかかり、毎日のこととなればストレスを感じる人も多いです。クックパッドマートは、実際の調理例を見ながら食材を注文でき、献立決めと買い物が一度に完結する体験を提供します。 また、新鮮な食材はシンプルな調理だけで十分においしいので、良い食材が手に入れば結果的に調理は楽になるとも考えています。

4. 価値仮説に至るまでのプロセス

上記の「サービスが提供する価値」の仮説に至るまでに繰り返してきた様々な検証のプロセスをご紹介します。

スプレッドシートによる買物代行テスト

チーム発足当初に描いていたサービスイメージは、買い物の手間やストレスを減らすことを目的とした、シンプルな買物代行サービスでした。 そこで、普段の買い物を誰かに代行してもらえることにどれだけの価値を感じられるのか、またそれを実現するにはどのような課題があるのかを検証するため、まずは実際にチームメンバーで社員の普段の買い物を代行するテストを実施することから、プロジェクトを始めました。

最初のテストで用意したシステムはごくシンプルです。Googleスプレッドシートで作った発注書と、全ての注文をまとめた買い出しリストの2つで、実装期間は約3日ほど。注文したい人は、発注書のテンプレートをコピーし、欲しい食材の注文数を記入後、チームのメーリングリストに共有する、というものでした。

スプレッドシートの発注書と買い出しリスト
実際に使用したスプレッドシートの発注書と買い出しリスト

目的の価値検証ができる最も簡単な方法を選んだことで、チーム発足後わずか2週間で最初のテストを実施することができました。

結果どうだったか

初日だけで20名以上の社員から注文が入り、レジ袋20袋以上の量の食材をチームメンバー3人で買い出しに行きました。

このようなテストを1回実施してみただけでも、本当に山ほどの課題とサービス改善のヒントが得られました。その一部を以下に紹介します。(実際はこの形式のテストを数回実施しました)

  • 普段買っているスーパーの商品と同等のものを届けても、あまり有り難みを感じてもらえない
  • 1:1の買い物代行はスケールが難しい上に、コストが見合わなそう
  • 複数店舗での買い出しと、それらの店舗を巡る配送を分ける仕組みは良さそう
  • まとめて買い出した後の個別仕分けは恐ろしく大変で効率化が必須
  • 注文時に量感がわからず、持ち帰れないほどの量を頼んでしまう人がいる
  • 鮮魚はその日のオススメをおまかせで選び、食べ方まで提案したところ、とても評判が良かった
  • おいしい!と実感できた人から、自然発生的にレビューが届いた(チームのSlackチャンネルに写真と感想が寄せられた)

買い出しテスト後の仕分けの様子
最初の買い出し後の仕分けの様子(カオスです。。。)

このテストを経て、

  • おいしさに自信を持っておすすめできる商品だけを届ける
  • 1:1の買い物代行ではなく、1:Nのルート配送の仕組みを構築する
  • おいしい食べ方の提案まで含めた、買っただけで終わらない買い物体験を作る

といったサービスのコンセプトが明確になりました。

プロトタイプアプリによるテスト

上記のスプレッドシートによるテストは単発で行ったため、使う社員も少しイベントごとのような意識になってしまうという課題がありました。そこで、

  • 定常的に注文を受け付けられる仕組みの構築
  • 習慣的に使ってもらうための課題の洗い出し

を次の検証のステップとしました。

習慣的に利用してもらうために、スプレッドシートを手動で管理する形式を卒業し、簡単なWebアプリケーションを構築しました。この時も、機能は最低限に絞り、デザインも凝り過ぎずに、最短でテストを開始できる方法を意識して開発を進めました。アプリケーションの実装は約1週間ほど、コンテンツの準備などを含めるとリリースまで2週間ほどの期間でテストを開始しました。

Webアプリケーションの画面
実際のWebアプリケーションの画面

結果どうだったか

このタイミングから、社員はいつでもクックパッドマートで食材を注文できるようになり、毎週火・木の2回、注文された食材をオフィスに配送するテストが今現在も続けられています。

日常的に配送が行われることで、利用する社員も普段の買い物の手段の一つとしてクックパッドマートを使ってくれるようになりました。初回注文からのリピート率は約7割で、ほぼ毎回注文をしてくれるヘビーユーザーも出てきています。日常使いするほど、アンケートで厳しめのご意見をもらえることも多く、サービス改善に必要なフィードバックを常に得られる状況を作ることができました。

また、定常的に配送オペレーションを回すことで、配送の度にオペレーション上の新たな課題が見つかり、日々改善が進められています。社内テストを繰り返すことによって磨き上げが進んだオペレーションの例をいくつかご紹介します。

  • チームメンバー自ら買い出しに出向いていたものが、徐々に社外に委託することができるようになり、一部では配送員が直接店舗から商品を受け取る仕組みもできてきた
  • 商品に貼り付けるラベルに記載する情報の改善により、仕分けミスや受け取り間違いが起こりにくいシステム構築が進んだ
  • 当初ユーザーの不安要素に多く上がった保冷の問題も、配送時に常に温度計測を行い、検証を繰り返すことにより、食材に適した温度を維持しての配送が実現できてきた

チームMTGの様子
オペレーション改善を行うチームの配送振り返りMTGの様子

配送オペレーションのように複雑性の高いものは、トライを繰り返せる環境が常にあることが改善スピードに直結します。常時社員から注文が入る状態を作れていることで、配送の仕組みもどんどんブラッシュアップされ、サービス全体としての品質向上を進めることができています。

社外ユーザーテスト

上記の社内テストを日常的に実施するのと並行して、社外のターゲットユーザー層の方にご協力いただくユーザーテストも繰り返し実施してきました。 目的は、社員としてのバイアスが無い、より一般的なユーザーの率直な反応をみることです。

ユーザーテストは、毎回下記のような形式で実施しています。

  • ターゲットに近いユーザーのリクルーティング
  • 社内テストで運用中のプロトタイプアプリを使ってもらう
  • 参加できるチームメンバー全員、別室でユーザーテストの様子を観察する(参加できなかったメンバーは録画でキャッチアップする)
  • チーム全員で分析・振り返りをする

インタビュー観察部屋の様子
インタビュー観察部屋の様子

インタビュー後の分析の様子
インタビュー後の分析の様子

結果どうだったか

社内テストで使っているシステムと同じものを見せた場合でも、前提知識やバックグラウンドが異なる社外ユーザーからは、やはり社員とは異なる反応がたくさん得られました。その例をいくつかご紹介すると、

  • オフィス配送のイメージが持てない環境にいるユーザーも多い

    • 比較的近距離に住む社員が多いクックパッドに比べ、通勤時間が長く、持ち帰りがより現実的でない
    • 勤務先がオフィスビルとは限らず、小規模な店舗勤務など、配送拠点になるイメージが持てない
    • 会社の雰囲気的に買い物をしている姿を他の人に見られたくないという感覚の人も多い
  • 商品の品質(おいしさ)がアプリ上では判断できない

    • クックパッド社員は背景知識やクチコミにより初回注文のハードルがそこまで高くなかったが、一般ユーザーがサービスの品質を信頼して初めて注文するハードルはその何倍も高そう

このような結果はメンバー全員で受け止め、オフィスを配送先のメインとして考えていた方針を転換したり、おいしさを伝えるための方法をアプリ内に留まらずに広くアイデア出しをして検討したりと、プロジェクト全体の方向性修正に役立てています。

モバイルアプリへの移行

このように様々な検証を経て、徐々に社外リリースの方向性や価値仮説が定まってきたところで、これまでの知見を盛り込んだリリース版iOSアプリの開発を開始しました。

これまでのプロトタイプアプリのコードやデザインは捨てて、システム設計からすべてリニューアルする形式で開発を進めています。テストと改善をスピード感を持って繰り返してきたプロトタイプアプリは、どうしても不要なコードや現在の要件に合わない設計が残っているので、リリース要件が固まったこのタイミングで刷新する判断をしました。

今現在は社内テストも全てリリース版のiOSアプリに移行しており、社外リリースに向けた磨き込みを進めています。

まとめ

このように、ミニマムなテストで価値検証を繰り返すプロセスを愚直に続け、ようやく社外にリリースできる形のサービスとしてまとまってきたのが今です。

サービスの登場人物が多く(注文ユーザー・販売店・配送員・受け取り拠点)、食材というリアルなモノが介在する複雑性の高いサービスなので、実際にテストを回しながら品質を上げていくプロセスの有用性を強く実感しています。 またこのようなプロセスは、インターネット上にとどまらない買い物体験全体をデザインしている感覚が強く、サービス開発者としてとても刺激的で面白いです。

社外リリース後も、まだまだ価値検証と改善のプロセスは続きます。もしあなたの街にクックパッドマートの受け取り場所ができたら、ぜひ利用して、フィードバックをください。(東京都渋谷区・目黒区・世田谷区の一部エリアから順次リリース予定)

また、この記事を通して、クックパッドマートのサービス開発にご興味を持っていただけた方がいらっしゃいましたら、ぜひ一緒にサービスを作りましょう! エンジニア・デザイナーはもちろん、様々な職種で、一緒にサービスを作り上げる仲間を募集しています(募集要項: エンジニアオープンポジション)。ご応募おまちしております。

自作キーボード沼 自由研究ノート

$
0
0

こんにちは!広報部のとくなり餃子大好き( id:tokunarigyozadaisuki)です。

クックパッドのSlackには無数のオープンチャンネルが存在していますが、最近盛り上がりを見せているのが「#keyboards」というチャンネル。先週末コミックマーケットが開催されていたためここ最近はその話でもちきりの様子でしたが、普段から大事な仕事道具であるキーボードにこだわりを持った社員が日々情報交換をしています。興味本位で社員のキーボードをのぞいでみると、ピカピカ光るものから、カチカチッと音がなるもの、アルファベットも数字も書いていないもの……その多彩さにびっくりします。 そこで、クックパッドエンジニアの最近のキーボード事情を調査してみました! 

なお、HHKBやRealforceはクックパッドでは当たり前だったので、紹介は割愛いたします。

クックパッドエンジニアのキーボード

@takai

f:id:tokunarigyozadaisuki:20180814150420j:plain

キーボード概要

名称:Keebio Fourier
スイッチ:Cherry MX 茶軸(45g)
レイアウト :40%スプリットキーボード 

なぜ今のキーボードにしたのか

自作するならスプリットキーボードで、かつコンパクトな感じに仕上げたいと思っていたところ、Keebio の Fourier を見つけて、「ミニマムでストイック、まさに自分のためのキーボードだ」と思いました。

気に入っているポイント

40%キーボードって、ほどよく不便で楽しいじゃないですか。数字キーが無いわけですから、使うにあたって工夫する必要があります。そこを自分好みにカスタマイズして使いこなしてると「俺すごい」という気持ちになれるんです。キーキャップのカラーリングもこだわりのポイントで、Signature Plastics社のGRANIT KEYSETにインスパイアされました。好みのレイアウトだと、キーキャップのセットが売っていなかったので、キーキャップ単位で購入したりと、そこは妥協せずに頑張りました。

@slightair

f:id:tokunarigyozadaisuki:20180814150437j:plain

キーボード概要

名称:Ergo42
スイッチ:Cherry MX 赤軸(45g)
レイアウト:7x4格子配列スプリットキーボード

なぜ今のキーボードにしたのか

少し前にスプリットキーボードに挑戦してみよう、でもキーが減るのはちょっと怖いなと思い、比較的キーが多いViterbi Keyboard(7x5格子配列)を作って使い始めたのですが、想像と違ってキーを余らせてしまっていました。 また同じキー配置で文字を打ちたいので、自宅と会社の間で持ち歩いていたのですが、面倒くさくなってきてもう一台作りたいなと考えていました。 そんなところに一行少ないだけでちょうどよく使えそうなErgo42の開発キットの販売がはじまったので飛びついてしまいました。国産なので注文してすぐ届きました。

気に入っているポイント

はんだ付けは大変ですが、キーキャップやキーマップなど、自分好みにいじることができるのが楽しいですね。特に、基板の底にLEDを配置して光らせるUnderglow(アンダーグロウ)が気に入っています。キーキャップの色は黒を基本に、特殊キーなどを青系にしていて、光の色もそれに合わせています。みんなから声かけられるようになったし、目を引く、やったぜ! という気持ちです。

@eisuke

f:id:tokunarigyozadaisuki:20180814150442j:plain

キーボード概要

名称:TMK Alps64
スイッチ:Alps SKCM SALMON
レイアウト:60%キーボード

なぜ今のキーボードにしたのか

ビンテージキーボードが好きで、以前はIBMマシンで使用されていたバックスプリング式キーボードを使っていました。60%くらいのコンパクトさで自分好みのキータッチのキーボードを作ろうと思い、CHERRY軸を触ってみたのですが自分にはしっくりこなかったので別のものを探していました。その中で、80〜90年代に生産された多くのキーボードに使用されていたAlps軸が気になりました。特にサーモン(ピンク)軸が気になったので、サーモン軸を使っている Apple Extended Keyboard をオークションで購入し、解体してキースイッチを取り出し自作してみました。

気に入っているポイント

タクティカルキーボードの少しだけカチッとくる、この独特で軽めな押し心地がいいですね。Alps軸の古代パーツ感も気に入っています。 

@ragi256

f:id:tokunarigyozadaisuki:20180814150453j:plain

キーボード概要

名称:Helix
スイッチ:Kailhロープロファイル 赤軸(45g)とKailhロープロファイル 茶軸(45g)の併用
レイアウト:6x5格子配列スプリットキーボード

なぜ今のキーボードにしたのか

昔から特殊な形状のキーボードが好きで、Dactylキーボードを作っているブログ記事を見たときに自分でもDactylを作ってみたいと思いました。しかし、はんだ付けもやったことがない自分には難しそうだったので、一旦難易度を下げて簡単なキーボードを作ることにしました。Helixに決めた理由は、以前から一度試してみたかったKailhのロープロファイルスイッチが使えるからです。

気に入っているポイント

親指を使うキーだけ押し心地を変えたかったので、親指周りだけを茶軸にして他は赤軸にしました。キーキャップは、デフォルトの刻印セットに加えて無地の白と黒を見た目で覚えやすいように配置しています。スプリットキーボードなので、椅子の肘掛けに肘先を置いて使えるのもいいですね。

@uzzu

f:id:tokunarigyozadaisuki:20180814171034p:plain

キーボード概要

名称:NIZ keyboard Plum 75 EC Keyboard
スイッチ:静電容量無接点方式(35g)
レイアウト:75%キーボード

なぜ今のキーボードにしたのか

社会人になってから、主にRealforce 86Uを8年くらい使っていました。特に困ってもいなかったのですが気分転換をしたくなり、Realforceの打鍵感が好きだったのでそこはあまり変わらず、それでいて、いじりやすくて不要なキーが無いコンパクトなものを探していて、NIZ keyboardにしました。他の皆さんのキーボードとは違い、組まれた状態で売られているので自作とは言えないですね……。

気に入っているポイント

75%である事(ファンクションキーは欲しいけど十字キーとハードウェアキーと操作キーはいらない)、さらに右側の特殊キーを十字キーに変えられるというのが、まさに自分の需要に合っていて気に入っています。加えて、静電容量無接点スイッチなのにCherry MX互換のキーキャップが採用されているので、他の自作キーボードと同様にキーキャップも変えられますし、バネを付けることでキーの重さが変えられるんです。Realforceの偏荷重モデルの重さを参考にしつつ、気になる所の重さを調整して自己最適化しています。今後、キーキャップは変更していきたいと思っています。

@ayemos

f:id:tokunarigyozadaisuki:20180814150747j:plain

キーボード概要

名称:WASD Keyboard
スイッチ:Cherry MX 青軸(50g)
レイアウト:60%キーボード

なぜ今のキーボードにしたのか

大学生の時HHKBを使っていたのですが、ErgoDoxで組まれた自作キーボードを見たとき、自分好みのキーキャップに変えられるものがほしいと思いました。

気に入っているポイント

深夜に酔った勢いで作ったので気に入ってるとかこだわりとかはないですね。漢字を使ったキーキャップデザインを自作しました。

最後に

いかがでしたでしょうか。以前はHHKBシリーズを使っていて、そこから自作キーボードの門を叩いたという社員が多いように感じました。自分にとって最適なキーボードを求めて細かいところから自作する人、見た目の可愛さを求めて工夫する人と、こだわりは様々。インタビューしていてとても楽しかったです。徐々に詳しくなってきましたよ! Gherkinってキーボードをつくってみたいと思っています。

今回紹介しきれなかった社員の自作キーボードについてはまた次回。お楽しみに! 

Cloud Firestoreのrulesをテストする

$
0
0

Komerco事業部エンジニアの岸本(id: sgrksmt)です。今日でちょうど入社1年が経ち、現在Komerco -コメルコ-(以下、Komerco)の開発を担当しています。
入社前はお世話になっていたこの技術ブログに自分が投稿する日がくるとは...。

Komercoは、「料理が楽しくなるマルシェアプリ」というコンセプトの元、料理が楽しくなる器やカトラリー、リネン雑貨等を出品/購入できるサービスで、現在はiOS版のアプリケーションを提供しています。

今年2月のCookpad Tech Conf2018や先日催したCookpad Tech Kitchen#16などでもお伝えしてきていますが、現在KomercoではバックエンドでFirebaseを活用しています。
その中で、最近僕が仕組みづくりとして取り組んでいるCloud Firestoreのセキュリティルールのテストの方法についてご紹介します。

Cloud Firestoreのrules

Cloud Firestore(以下、Firestore)では、各プラットフォームで提供しているSDKやREST API経由でデータを安全に読み書きできるよう、セキュリティルールを記述することができます。
主にFirebase Authenticationでの認証を活用しつつ、どのような条件下でドキュメントを読み取ることができるか、書き込むことができるかを設定し、ユーザーのデータを保護します。
また余談にはなりますが、Realtime DBとCloud Storageにもセキュリティルールがあります。

セキュリティルールをしっかり設定しないと、本来読み取られてはいけないデータが悪意のある第三者に読み取られてしまったり、特定のフィールドを書き換えられてしまうといったことが起こります。
一方で、それを防ぐために全てのルールを閉じて、API経由でのみ読み書きできるようにして堅牢にすることも可能ですが、そうするとCloud Firestoreの次のような利点を享受しにくくなります。

  • リアルタイムでの情報の取得
  • オフラインから復帰したときに、ローカルでの変更を書き込む

なので、実際にアプリケーション開発をしていく場合は全ルールを閉じる運用よりは、ルールを適切に設定して運用していくことが多くなるかと思います。

Firestore rulesの確認が大変

Firestoreのrulesを書いて、期待通りに動作するのかを確かめつつ開発をしていきますが、これが非常に 大変かつ面倒だったりします。 変更をした後、毎回デプロイして、浸透するまで数分待って、正しく動作するかクライアント側で動作させて確かめるのはかなり非効率ですし、 エラーの内容も「permission-denied」程度のものしかないので何が原因か掴みづらいです。
一応、rulesの構文自体が間違っているかどうかはデプロイ前に検出してくれるので、構文がおかしくなっている場合は気づけますが、typoなんてしてしまった日にはなかなか気づきづらく、デプロイと確認だけで日が暮れてしまうこともありえます。
"もっと効率的にrulesを書いて確認したい..."

そう思いながら今年の前半を過ごしていたのですが、2018年の5月末頃にFirestore rulesの動作を書き込む前に確認できる「シミュレータ」の機能がFirebaseのコンソール上に搭載されました。

rulesのシミュレーター

Firestoreのrulesシミュレーターは、FirestoreのProjectからDatabase→Firestore→ルールとたどり、この部分をクリックすることで利用することができます。

f:id:sgrksmt:20180815180611p:plain

開くと、このような画面になっており、指定したドキュメントのパスに対して、readやwriteのルールを記述されたルールを基にシミュレートして確認することができます。

f:id:sgrksmt:20180815180716p:plain

また、認証情報も指定したテストができるので、認証ありきの条件もシミュレートすることが可能です。
以前私がQiitaに掲載した記事でも操作方法など書いていますのでよければ併せて御覧ください。

シミュレータを用いると以下のメリットがあります。

  • 公開前に条件をシミュレートして確認ができるので、誤った条件のものをデプロイしてしまう心配がない
  • すぐにシミュレートして試せるので、デプロイしてから数分待って、、といった具合に時間をロスすることがない
  • 失敗した場合に、何が原因でどこで失敗しているのか指摘してくれるので、原因がわかりやすい
  • 書き込みに関するシミュレートの場合、実際のDBに書き込みが行われるわけではないので、DBを汚してしまうことがない

ただ、シミュレータでのrulesの動作の確認はコンソール上で簡単に確認ができる反面、以下のデメリットがあります

  • listのオペレーションに関して、queryに関する条件の確認ができない
  • getAfter関数を用いた条件の確認ができない
  • 2つ以上のドキュメントの書き込みに関するテストができない
  • 数百行になってくるとコンソールがやや重たくなるので編集時にストレスがかかる

今後開発を進めていく上で、継続的にrulesが正しく動作するかを確かめられる環境がないと、新機能の追加や大幅な改修、リファクタリングに耐えられないので、
Firestoreのrulesが正しく動くかどうかのテストを構築していくことにしました。


Firestore rulesのテスト

ここからが本題になります。大まかな流れとしては

  • 開発環境、本番環境とは別の、 テスト環境用のFirebase Projectを準備する
  • テスト環境にfirestore.rulesファイルをにデプロイする。
  • テストを書き、実際にテスト環境に対してドキュメントの読み書きを行い、Firestore rulesが期待通りの動作をするかをテストする
  • 手元でテストを実行できる他、CI経由でも継続的にテストが行えるようにする

となります。順に、簡単なテストの例も交えつつ説明していきます。

構成

テストの構成としては、次のようになっています。

f:id:sgrksmt:20180815180802p:plain

普段のアプリケーション開発では開発環境用に必要なものをデプロイしたり、DBの読み書きを行っています。
テストのときは、 テスト用のFirebase Projectを準備し、そこにrulesをデプロイしています。
そして、テストを実行するときは、向き先をテスト環境のFirebase Projectにし、その環境のFirestoreのDBに対して読み書きを実行し、rulesのテストをします。
また、テストは手元からCLI経由で実行する他に、GitHub上にPullRequestが作成された時や、materブランチにマージされたタイミングで、CI経由でテストを実行するようにしています。

また、この記事では詳しく触れませんが、Komercoでは同様に、CloudFunctionsに関連するテストも同様に、テスト環境のFirebase Project上で行っています。

準備

テストを書くにあたり、jestを利用しているので、npmもしくはyarnにて追加します。
jestは、Facebook社がOSSとして提供している、JavaScriptでユニットテストを行うためのフレームワークです。RSpecのような記述が可能となります。
今回はjestを使った場合でのテストの紹介となりますが、テストのフレームワークは任意のものでも構いません。

また、テストに関連するファイルは、 test/ディレクトリ以下に配置していきます。 KomercoではCloud Functionsのテストもあるので、 test/rules/ディレクトリ以下に配置しています。

Firebaseの初期化をする

テストで使うFirebaseの初期化をします。
Cloud Functions等でFirebaseを扱うときは、adminSDKが使えるようadmin権限での初期化をすることが多いのですが、
admin権限でFirebaseを初期化してしまうと、設定したrulesに関係なく管理者権限にて読み書きが可能となってしまうので、Webアプリケーションと同様の初期化を行います。

webhelper.tsを任意のテストディレクトリ以下に配置し、以下のように記述します。

import * as firebase from'firebase'const config ={
  apiKey: 'API_KEY',
  authDomain: 'AUTH_DOMAIN',
  databaseURL: 'DATABASE_URL',
  projectId: 'PROJECT_ID',
  storageBucket: 'STORAGE_BUCKET',
  messagingSenderId: 'SERNDER_ID'}

firebase.initializeApp(config)const auth = firebase.auth()const firestore = firebase.firestore()const settings ={ timestampsInSnapshots: true}
firestore.settings(settings)export{ firebase, auth, firestore }

各種configの変数はご自身のテスト環境用のプロジェクトのIDを指定してください。(基本的には.env等から参照することになると思います。)
これら初期化した変数は次のようにテストファイルでimportして使用します。

import * as WebHelper from'./helper/webhelper'const postRef = WebHelper.firestore.collection('post').doc()

モデルを定義する

ドキュメントのモデル定義をします。
今回は例として、Postドキュメントの定義をします。

exportenum Path {
  Post ='/posts'}exportinterface Post {
  title: string,
  body: string,
  authorID: string,
  isPublished: boolean}

次項では、このPostドキュメントのcreateオペレーションに関するテストの例をご紹介します。
(すべてのオペレーションの例を紹介したいのですが、長くなるので割愛します。)

テストを書いていく

前提条件として

  • Firebase Authenticationにて認証されたユーザー
  • 定義したパラメータを全て有している
  • post.authorIDが、認証ユーザーのuidと一致する

という条件のもと、書き込みが正常に行われるのを期待するケースと、失敗し、permission-deniedがエラーとして返却されるのを期待するケースを記述します。
(失敗するケースは複数想定されますが、ここでは1つのケースに絞ります。)
posts.test.tsを作成し、次のように記述します。

import * as WebHelper from'./helper/webhelper'enum Path {
  Post ='/posts'}interface Post {
  title: string,
  body: string,
  authorID: string,
  isPublished: boolean}const makePostDocument =(authorID: string)=>{return<Post>{
    title: 'test post',
    body: 'test post body',
    authorID: authorID,
    isPublished: true}}const permissionDeniedError ={ code: 'permission-denied'}

describe('post document rules',()=>{
  jest.setTimeout(10000)let postCollectionRef: WebHelper.firebase.firestore.CollectionReference
  beforeAll(()=>{
    postCollectionRef = WebHelper.firestore.collection(Path.Post)})

  describe('write',()=>{
    describe('create',async()=>{let authUser: any

      beforeEach(async()=>{
        authUser =await WebHelper.auth.signInAnonymously()})

      afterEach(async()=>{await WebHelper.auth.signOut()})

      describe('when authorID is equal to auth.uid',()=>{
        test('should be succeeded',async()=>{const post = makePostDocument(authUser.user.uid)await expect(postCollectionRef.doc().set(post)).resolves.toBeUndefined()})})

      describe('when authorID is not equal to auth.uid',()=>{
        test('should be failed',async()=>{
          expect.assertions(1)const post = makePostDocument('xxxxxxxxxxx')await expect(postCollectionRef.doc().set(post)).rejects.toMatchObject(permissionDeniedError)})})})})})

(モックデータの作成に関する処理、モデル定義等は別途別のファイルに分割するほうが良いですが、今回は例を示すために同一のファイル内に置いています。) ここまでで実行すると、rulesがまだ書けていない場合はテストが通らないと思います。
次にこのテストを通過するための正しいrulesを記述します。

service cloud.firestore {
  match /databases/{database}/documents {function isAuthenticated(){return request.auth !=null;}function incomingData(){return request.resource.data;}

    match /posts/{postID}{
      allow create: if isAuthenticated()&& incomingData().keys().hasAll(requiredFields())&& incomingData().authorID == request.auth.uid;function requiredFields(){return['title','body','authorID','isPublished'];}}}}

これをテスト環境にデプロイし、再度テストを実行することで、テストが通るようになります。
(注: 紹介のため割愛しているテストケースがありますので、実際にはもう少しテストケースが多く、厚いものになると思います。また、rules自体も、各種フィールドのvalidationを行ったりと複雑になるかと思います)

事前にテストデータを作成する

テストを書いていると、セキュリティルールの制約を受けずにテストデータを作成したり、
CloudFunctionsで生成したり、事前にDBに格納しているのが前提で、読み出すだけのデータを準備したいケースがでてきます。
その場合はadmin権限でFirebaseを初期化したものを別途用意し、rulesの息のかからないところでテストデータを作成します。
adminhelper.tsというファイルを作成し、そこでAdmin SDKの初期化を行います。

import * as admin from'firebase-admin'

admin.initializeApp({ credential: admin.credential.cert(require('admin sdk jsonのpath'))})const auth = admin.auth()const firestore = admin.firestore()const settings ={ timestampsInSnapshots: true}
firestore.settings(settings)export{ admin, auth, firestore }

(注: admin用のservice accountの取扱には注意です。 外部に漏れぬように対策する必要があります)
参考: サーバーに Firebase Admin SDK を追加する

これにより、事前にPostドキュメントを作成し、そのPostドキュメントが取得可能かどうかのテストが次のように記述できます。

import * as WebHelper from'./helper/webhelper'import * as AdminHelper from'./helper/adminhelper'enum Path {
  Post ='/posts'}interface Post {
  title: string,
  body: string,
  authorID: string,
  isPublished: boolean}const permissionDeniedError ={ code: 'permission-denied'}const savePostDocument =async(isPublished: boolean)=>{const postRef = AdminHelper.firestore.collection(Path.Post).doc()await postRef.set({
    title: 'test post',
    body: 'test post body',
    authorID: 'xxxxxxxx',
    isPublished: isPublished
  })return postRef
}

describe('post document rules',()=>{
  jest.setTimeout(10000)let postCollectionRef: WebHelper.firebase.firestore.CollectionReference
  beforeAll(()=>{
    postCollectionRef = WebHelper.firestore.collection(Path.Post)})

  describe('read',()=>{
    describe('get',()=>{
      afterEach(async()=>{await WebHelper.auth.signOut()})

      describe('when user is authenticated',()=>{
        describe('try to get valid document',()=>{
          test('should be succeeded',async()=>{await WebHelper.auth.signInAnonymously()const mockPostRef =await savePostDocument(true)await expect(postCollectionRef.doc(mockPostRef.id).get()).resolves.toBeDefined()})})

        describe('try to get invalid document',()=>{
          test('should be failed',async()=>{
            expect.assertions(1)await WebHelper.auth.signInAnonymously()const mockPostRef =await savePostDocument(false)await expect(postCollectionRef.doc(mockPostRef.id).get()).rejects.toMatchObject(permissionDeniedError)})})})

      describe('when user is not authenticated',()=>{
        test('should be failed',async()=>{
          expect.assertions(1)const mockPostRef =await savePostDocument(true)await expect(postCollectionRef.doc(mockPostRef.id).get()).rejects.toMatchObject(permissionDeniedError)})})})})// ...writeのrule})

今回追加した、postドキュメントのgetに関するルールを次のように追加することで、上記テストが通るようになります。

service cloud.firestore {
  match /databases/{database}/documents {function isAuthenticated(){return request.auth !=null;}function incomingData(){return request.resource.data;}function existingData(){return resource.data;}

    match /posts/{postID}{
      allow get: if isAuthenticated()&& existingData().isPublished;
      allow create: if isAuthenticated()&& incomingData().keys().hasAll(requiredFields())&& incomingData().authorID == request.auth.uid;function requiredFields(){return['title','body','authorID','isPublished'];}}}}

料金面はどうか

実際にテストデータを作って、読み書きを行うので、コスト(料金)がかかるのでは、、と思われるかもしれませんが、
Komercoではrulesのテストに加え、Cloud Functionsのテストも実施しています。Pull Requestが作成される、あるいはmasterブランチが作成されたときにテストを実施していますが、 料金はほとんどそこまでかかっていません。

また、先にも触れましたが、テストを実行する環境と、普段開発している環境を分けているので、開発環境のDBが汚染されたりすることもありません。

Firestoreのルールのテストを書くことでのメリット

テストを書くことによるメリットとしては

  • アプリケーションを手動で動かしたり、シミュレータに頼ることなく(継続的に)rulesが正しく動作するか確かめることができる
  • rulesの変更が容易になる(書き換えた結果、正しくない場合にテストがあることで検知することができる。)

が挙げられます。普段のアプリケーション開発と同様で、テストがあることで、後の変更にも強くなり、rulesを変更することに臆することもなく、また高速に動作検証が行えるようになります。
また、事前にセキュリティを意識したドキュメントの設計がしやすくなり、見通しも良くなるので最近の開発では新機能の追加や改善に伴ってドキュメントの設計をするときは、同時にrulesのテストも記述し開発をしています。

さいごに

Firestoreのrulesに関して、より継続的に確認が行えるためのテストについてご紹介しました。
今回ご紹介したサンプルコードをGitHubにて公開していますので、よければ併せて御覧ください。

今後公式からテストする手段が提供される可能性もありますし、オフライン(ローカル)でのテストが提供される可能性もありますが、
現状Komercoではオンラインテストにて実施して、Firestore rulesの継続的なテストを行えるようにしています。
また、こうした事例に関わらず、Komerco開発チームではFirebaseと向き合って、その上でサービス提供ができるよう努めていきます。


クックパッドでは、Firebaseなど最新技術に興味がある!技術的に挑戦してサービスをより良くしたい!というエンジニアを募集しています。

builderscon tokyo 2018 にクックパッド社員が1名登壇いたします! 

$
0
0

こんにちは! 広報部のとくなり餃子大好き( id:tokunarigyozadaisuki)です。

朝晩を中心に少しずつ秋の気配を感じられるようになりましたが、夏日かと思うと台風が2つも。雨の被害がこれ以上、拡がりませんように。みなさんも寒暖の差で体調など崩されないようお気をつけください。

さて、2018年9月6日(木)〜2018年9月8日(土)は「知らなかった、を聞く」をテーマとした技術を愛する全てのギーク達のお祭り、「builderscon tokyo 2018」が開催されますね! 

クックパッドからは、技術部の小野 大器(@taiki45)が登壇させていただきます。セッションの内容は公式ページにて英語で紹介しておりますが、本ブログでは本人のコメント付きで、日本語にてご紹介いたします。 講演は日本語で行いますので、ご興味のある方はぜひご参加ください。

セッション情報

2日目 9月7日(金)12:30〜 イベントホール

Building and operating a service mesh at mid-size company

概要説明 クックパッドでのサービスメッシュの導入事例をお話します。大規模な環境ではなくても、サービスメッシュはマイクロサービス周辺の技術的課題の解決に対して効果があります。Envoy proxy を利用してスモールスタートで構築・導入する事例を基に、サービスメッシュに関係する知見や手法、導入後の効果について紹介する予定です。

コメント

マイクロサービス環境において、通信の失敗のハンドリングや障害発生時の対応等、サービス間の通信で悩んだ人は多いのではないでしょうか。このトークでは、以前とは違うアプローチでサービス間の通信周りの課題に対応するサービスメッシュについてお話します。 本セッションの前にはなんと Envoy proxy の作者である Matt のトークもあるので、そちらと一緒に本セッションも楽しんでもらえるとうれしいです。マイクロサービス間の通信に悩んでいる方やこれから悩みそうな方にぜひ。


登壇時の発表内容等に関してご質問がある方は小野に、他にも数名参加いたしますので、会場でクックパッド社員をお見かけの際には、ぜひお声がけくださいね! 

builderscon tokyo 2018 にクックパッド社員が1名登壇いたします!

$
0
0

こんにちは! 広報部のとくなり餃子大好き( id:tokunarigyozadaisuki)です。

朝晩を中心に少しずつ秋の気配を感じられるようになりましたが、夏日かと思うと台風が2つも。雨の被害がこれ以上、拡がりませんように。みなさんも寒暖の差で体調など崩されないようお気をつけください。

さて、2018年9月6日(木)〜2018年9月8日(土)は「知らなかった、を聞く」をテーマとした技術を愛する全てのギーク達のお祭り、「builderscon tokyo 2018」が開催されますね! 

クックパッドからは、技術部の小野 大器(@taiki45)が登壇させていただきます。セッションの内容は公式ページにて英語で紹介しておりますが、本ブログでは本人のコメント付きで、日本語にてご紹介いたします。 講演は日本語で行いますので、ご興味のある方はぜひご参加ください。

セッション情報

2日目 9月7日(金)12:30〜 イベントホール

Building and operating a service mesh at mid-size company

概要説明 クックパッドでのサービスメッシュの導入事例をお話します。大規模な環境ではなくても、サービスメッシュはマイクロサービス周辺の技術的課題の解決に対して効果があります。Envoy proxy を利用してスモールスタートで構築・導入する事例を基に、サービスメッシュに関係する知見や手法、導入後の効果について紹介する予定です。

コメント

マイクロサービス環境において、通信の失敗のハンドリングや障害発生時の対応等、サービス間の通信で悩んだ人は多いのではないでしょうか。このトークでは、以前とは違うアプローチでサービス間の通信周りの課題に対応するサービスメッシュについてお話します。 本セッションの前にはなんと Envoy proxy の作者である Matt のトークもあるので、そちらと一緒に本セッションも楽しんでもらえるとうれしいです。マイクロサービス間の通信に悩んでいる方やこれから悩みそうな方にぜひ。


登壇時の発表内容等に関してご質問がある方は小野に、他にも数名参加いたしますので、会場でクックパッド社員をお見かけの際には、ぜひお声がけくださいね! 

Viewing all 801 articles
Browse latest View live